# Conflicts:
#	Content.Client/Administration/Managers/ClientAdminManager.cs
#	Content.Client/Administration/Systems/BwoinkSystem.cs
#	Content.Client/Alerts/ClientAlertsSystem.cs
#	Content.Client/Audio/BackgroundAudioSystem.cs
#	Content.Client/CardboardBox/CardboardBoxSystem.cs
#	Content.Client/Chemistry/UI/InjectorStatusControl.cs
#	Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml
#	Content.Client/Chemistry/UI/ReagentDispenserWindow.xaml.cs
#	Content.Client/Clothing/ClientClothingSystem.cs
#	Content.Client/CriminalRecords/CriminalRecordsConsoleBoundUserInterface.cs
#	Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
#	Content.Client/Decals/Overlays/DecalOverlay.cs
#	Content.Client/DoAfter/DoAfterOverlay.cs
#	Content.Client/Doors/AirlockSystem.cs
#	Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
#	Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
#	Content.Client/Launcher/LauncherConnectingGui.xaml
#	Content.Client/Launcher/LauncherConnectingGui.xaml.cs
#	Content.Client/Lobby/LobbyState.cs
#	Content.Client/Lobby/UI/LobbyGui.xaml.cs
#	Content.Client/MainMenu/UI/MainMenuControl.xaml
#	Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml
#	Content.Client/MassMedia/Ui/MiniArticleCardControl.xaml.cs
#	Content.Client/MassMedia/Ui/NewsWriteMenu.xaml
#	Content.Client/MassMedia/Ui/NewsWriteMenu.xaml.cs
#	Content.Client/Options/UI/Tabs/MiscTab.xaml
#	Content.Client/Options/UI/Tabs/MiscTab.xaml.cs
#	Content.Client/Outline/InteractionOutlineSystem.cs
#	Content.Client/Overlays/ShowSecurityIconsSystem.cs
#	Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
#	Content.Client/Popups/PopupOverlay.cs
#	Content.Client/Popups/PopupSystem.cs
#	Content.Client/Preferences/ClientPreferencesManager.cs
#	Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
#	Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
#	Content.Client/StatusIcon/StatusIconOverlay.cs
#	Content.Client/Stylesheets/StyleNano.cs
#	Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
#	Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
#	Content.Server/Access/Systems/IdCardConsoleSystem.cs
#	Content.Server/Administration/Commands/BanCommand.cs
#	Content.Server/Administration/Notes/AdminMessageEui.cs
#	Content.Server/Administration/Notes/AdminNotesSystem.cs
#	Content.Server/Administration/Notes/IAdminNotesManager.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
#	Content.Server/Administration/Systems/BwoinkSystem.cs
#	Content.Server/Administration/UI/PermissionsEui.cs
#	Content.Server/Antag/AntagSelectionSystem.cs
#	Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
#	Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
#	Content.Server/Chat/Systems/ChatSystem.cs
#	Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
#	Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs
#	Content.Server/Connection/ConnectionManager.cs
#	Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs
#	Content.Server/Database/DatabaseRecords.cs
#	Content.Server/Database/ServerDbBase.cs
#	Content.Server/Database/ServerDbManager.cs
#	Content.Server/DeviceLinking/Systems/SignalTimerSystem.cs
#	Content.Server/Doors/Systems/DoorSystem.cs
#	Content.Server/Execution/ExecutionSystem.cs
#	Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
#	Content.Server/Fax/FaxSystem.cs
#	Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs
#	Content.Server/GameTicking/GameTicker.Replays.cs
#	Content.Server/GameTicking/GameTicker.RoundFlow.cs
#	Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs
#	Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
#	Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
#	Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
#	Content.Server/GameTicking/Rules/ThiefRuleSystem.cs
#	Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
#	Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
#	Content.Server/Hands/Systems/HandsSystem.cs
#	Content.Server/Holosign/HolosignSystem.cs
#	Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
#	Content.Server/Info/InfoSystem.cs
#	Content.Server/Kitchen/EntitySystems/SharpSystem.cs
#	Content.Server/Magic/MagicSystem.cs
#	Content.Server/MagicMirror/MagicMirrorSystem.cs
#	Content.Server/Mapping/MappingSystem.cs
#	Content.Server/MassMedia/Systems/NewsSystem.cs
#	Content.Server/Medical/Components/HealthAnalyzerComponent.cs
#	Content.Server/Medical/CrewMonitoring/CrewMonitoringServerSystem.cs
#	Content.Server/Medical/CryoPodSystem.cs
#	Content.Server/Medical/HealthAnalyzerSystem.cs
#	Content.Server/Nutrition/EntitySystems/OpenableSystem.cs
#	Content.Server/Preferences/Managers/ServerPreferencesManager.cs
#	Content.Server/Remotes/DoorRemoteSystem.cs
#	Content.Server/Resist/EscapeInventorySystem.cs
#	Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
#	Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
#	Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
#	Content.Server/Species/Systems/NymphSystem.cs
#	Content.Server/StationEvents/Components/GasLeakRuleComponent.cs
#	Content.Server/StationEvents/EventManagerSystem.cs
#	Content.Server/Store/Systems/StoreSystem.Ui.cs
#	Content.Server/Strip/StrippableSystem.cs
#	Content.Server/VendingMachines/VendingMachineSystem.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.cs
#	Content.Shared.Database/LogType.cs
#	Content.Shared/Actions/SharedActionsSystem.cs
#	Content.Shared/Administration/AdminFlags.cs
#	Content.Shared/Administration/SharedBwoinkSystem.cs
#	Content.Shared/Anomaly/SharedAnomalySystem.cs
#	Content.Shared/Bed/Sleep/SharedSleepingSystem.cs
#	Content.Shared/Buckle/SharedBuckleSystem.Strap.cs
#	Content.Shared/Chat/ChatChannel.cs
#	Content.Shared/Chemistry/Components/InjectorComponent.cs
#	Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs
#	Content.Shared/Chemistry/SharedReagentDispenser.cs
#	Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs
#	Content.Shared/CriminalRecords/Systems/SharedCriminalRecordsConsoleSystem.cs
#	Content.Shared/Cuffs/SharedCuffableSystem.cs
#	Content.Shared/Doors/Systems/SharedDoorSystem.cs
#	Content.Shared/Friction/TileFrictionController.cs
#	Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
#	Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
#	Content.Shared/Implants/SharedImplanterSystem.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/MedicalScanner/HealthAnalyzerScannedUserMessage.cs
#	Content.Shared/Nutrition/Components/OpenableComponent.cs
#	Content.Shared/Nutrition/EntitySystems/SharedOpenableSystem.cs
#	Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs
#	Content.Shared/Preferences/HumanoidCharacterProfile.cs
#	Content.Shared/Preferences/ICharacterProfile.cs
#	Content.Shared/Projectiles/SharedProjectileSystem.cs
#	Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs
#	Content.Shared/Security/SecurityStatus.cs
#	Content.Shared/Security/Systems/DeployableBarrierSystem.cs
#	Content.Shared/Slippery/SlipperySystem.cs
#	Content.Shared/Species/Systems/ReformSystem.cs
#	Content.Shared/Standing/StandingStateSystem.cs
#	Content.Shared/StatusIcon/StatusIconPrototype.cs
#	Content.Shared/Store/ListingPrototype.cs
#	Content.Shared/VendingMachines/SharedVendingMachineSystem.cs
#	Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
#	README.md
#	Resources/Audio/Ambience/Antag/pirate_start.ogg
#	Resources/Changelog/Changelog.yml
#	Resources/Credits/GitHub.txt
#	Resources/Locale/en-US/criminal-records/criminal-records.ftl
#	Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
#	Resources/Locale/en-US/medical/components/health-analyzer-component.ftl
#	Resources/Locale/en-US/reagents/meta/biological.ftl
#	Resources/Locale/en-US/reagents/meta/fun.ftl
#	Resources/Locale/en-US/reagents/meta/physical-desc.ftl
#	Resources/Locale/en-US/seeds/seeds.ftl
#	Resources/Locale/en-US/wires/wire-names.ftl
#	Resources/Maps/Shuttles/cargo_fland.yml
#	Resources/Maps/core.yml
#	Resources/Maps/europa.yml
#	Resources/Maps/fland.yml
#	Resources/Maps/origin.yml
#	Resources/Maps/saltern.yml
#	Resources/Maps/train.yml
#	Resources/Prototypes/Actions/diona.yml
#	Resources/Prototypes/Atmospherics/gases.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_emergency.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_engines.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_fun.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_security.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_service.yml
#	Resources/Prototypes/Catalog/Cargo/cargo_vending.yml
#	Resources/Prototypes/Catalog/Fills/Boxes/general.yml
#	Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml
#	Resources/Prototypes/Catalog/Fills/Crates/engines.yml
#	Resources/Prototypes/Catalog/Fills/Items/briefcases.yml
#	Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/dressers.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/bardrobe.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/clothesmate.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/lawdrobe.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/medidrobe.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/robotics.yml
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml
#	Resources/Prototypes/Catalog/uplink_catalog.yml
#	Resources/Prototypes/Datasets/Names/first_male.yml
#	Resources/Prototypes/Datasets/tips.yml
#	Resources/Prototypes/Entities/Clothing/Hands/colored.yml
#	Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
#	Resources/Prototypes/Entities/Clothing/Head/hats.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/coats.yml
#	Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
#	Resources/Prototypes/Entities/Mobs/Player/silicon.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Mobs/Species/slime.yml
#	Resources/Prototypes/Entities/Mobs/Species/vox.yml
#	Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml
#	Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml
#	Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml
#	Resources/Prototypes/Entities/Objects/Devices/Electronics/signaller.yml
#	Resources/Prototypes/Entities/Objects/Devices/nuke.yml
#	Resources/Prototypes/Entities/Objects/Devices/pda.yml
#	Resources/Prototypes/Entities/Objects/Fun/toys.yml
#	Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml
#	Resources/Prototypes/Entities/Objects/Materials/parts.yml
#	Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
#	Resources/Prototypes/Entities/Objects/Misc/implanters.yml
#	Resources/Prototypes/Entities/Objects/Misc/kudzu.yml
#	Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml
#	Resources/Prototypes/Entities/Objects/Shields/shields.yml
#	Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/healthanalyzer.yml
#	Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Bombs/firebomb.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml
#	Resources/Prototypes/Entities/Structures/Decoration/curtains.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml
#	Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml
#	Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
#	Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml
#	Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml
#	Resources/Prototypes/Entities/Structures/Storage/glass_box.yml
#	Resources/Prototypes/Entities/Structures/Wallmounts/fireaxe_cabinet.yml
#	Resources/Prototypes/Entities/Structures/Wallmounts/switch.yml
#	Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
#	Resources/Prototypes/Entities/Structures/Walls/grille.yml
#	Resources/Prototypes/Entities/Structures/Walls/walls.yml
#	Resources/Prototypes/Entities/Structures/Windows/window.yml
#	Resources/Prototypes/Entities/Structures/stairs.yml
#	Resources/Prototypes/GameRules/events.yml
#	Resources/Prototypes/Hydroponics/seeds.yml
#	Resources/Prototypes/Maps/saltern.yml
#	Resources/Prototypes/Reagents/biological.yml
#	Resources/Prototypes/Reagents/fun.yml
#	Resources/Prototypes/Recipes/Construction/Graphs/utilities/solarpanel.yml
#	Resources/Prototypes/Recipes/Construction/weapons.yml
#	Resources/Prototypes/Recipes/Crafting/improvised.yml
#	Resources/Prototypes/Recipes/Lathes/clothing.yml
#	Resources/Prototypes/Recipes/Reactions/fun.yml
#	Resources/Prototypes/Research/arsenal.yml
#	Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
#	Resources/Prototypes/Roles/Jobs/Fun/emergencyresponseteam.yml
#	Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
#	Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
#	Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
#	Resources/Prototypes/Roles/Jobs/Security/detective.yml
#	Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
#	Resources/Prototypes/Roles/Jobs/departments.yml
#	Resources/Prototypes/Species/human.yml
#	Resources/Prototypes/Species/vox.yml
#	Resources/Prototypes/Stacks/Materials/materials.yml
#	Resources/Prototypes/StatusEffects/health.yml
#	Resources/Prototypes/Tiles/plating.yml
#	Resources/Prototypes/Voice/speech_emote_sounds.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
#	Resources/Prototypes/explosion.yml
#	Resources/Prototypes/game_presets.yml
#	Resources/Prototypes/lobbyscreens.yml
#	Resources/Prototypes/secret_weights.yml
#	Resources/Prototypes/tags.yml
#	Resources/ServerInfo/Guidebook/Jobs.xml
#	Resources/ServerInfo/Guidebook/Science/ArtifactReports.xml
#	Resources/Textures/Clothing/Belt/emt.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hats/beret_medic.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hats/beret_qm.rsi/meta.json
#	Resources/Textures/Clothing/Head/Soft/bluesoft_flipped.rsi/meta.json
#	Resources/Textures/Clothing/Head/Soft/corpsoft_flipped.rsi/meta.json
#	Resources/Textures/Clothing/Head/Soft/greensoft_flipped.rsi/meta.json
#	Resources/Textures/Clothing/Head/Soft/greysoft_flipped.rsi/meta.json
#	Resources/Textures/Clothing/Head/Soft/paramedicsoft_flipped.rsi/meta.json
#	Resources/Textures/Clothing/Mask/neckgaiterred.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/slayer.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Hardsuits/syndicate.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Suits/syndicate.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/qmformal.rsi/meta.json
#	Resources/Textures/Decals/Overlays/greyscale.rsi/checkerNESW.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/checkerNWSE.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/fulltile_overlay.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/halftile_overlay.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/halftile_overlay_180.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/halftile_overlay_270.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/halftile_overlay_90.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/quartertile_overlay.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/quartertile_overlay_180.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/quartertile_overlay_270.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/quartertile_overlay_90.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/threequartertile_overlay.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/threequartertile_overlay_180.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/threequartertile_overlay_270.png
#	Resources/Textures/Decals/Overlays/greyscale.rsi/threequartertile_overlay_90.png
#	Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
#	Resources/Textures/Interface/emotions.svg.192dpi.png.yml
#	Resources/Textures/LobbyScreens/dead-in-space.png.yml
#	Resources/Textures/LobbyScreens/doomed.webp.yml
#	Resources/Textures/LobbyScreens/pharmacy.png.yml
#	Resources/Textures/LobbyScreens/pharmacy.webp.yml
#	Resources/Textures/LobbyScreens/robotics.webp.yml
#	Resources/Textures/LobbyScreens/supermatter.png.yml
#	Resources/Textures/LobbyScreens/susstation.png.yml
#	Resources/Textures/LobbyScreens/warden.png.yml
#	Resources/Textures/LobbyScreens/warden.webp.yml
#	Resources/Textures/Mobs/Customization/human_hair.rsi/meta.json
#	Resources/Textures/Mobs/Effects/brute_damage.rsi/LLeg_Brute_40.png
#	Resources/Textures/Mobs/Effects/brute_damage.rsi/RLeg_Brute_40.png
#	Resources/Textures/Objects/Devices/nuke.rsi/meta.json
#	Resources/Textures/Objects/Devices/nuke.rsi/nuclearbomb_deployed.png
#	Resources/Textures/Objects/Devices/nuke.rsi/nuclearbomb_exploding.png
#	Resources/Textures/Objects/Devices/nuke.rsi/nuclearbomb_timing.png
#	Resources/Textures/Objects/Misc/books.rsi/meta.json
#	Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-lawyer.png
#	Resources/Textures/Objects/Storage/boxes.rsi/meta.json
#	Resources/Textures/Objects/Tools/t-ray.rsi/tray-off.png
#	Resources/Textures/Objects/Tools/t-ray.rsi/tray-on.png
#	Resources/Textures/Objects/Weapons/Guns/Battery/mini-ebow.rsi/bolt.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/external.rsi/emergency_open_unlit.png
#	Resources/Textures/Structures/Doors/Airlocks/Standard/shuttle_syndicate.rsi/emergency_open_unlit.png
#	Resources/Textures/Structures/Doors/Windoors/plasma.rsi/emergency_unlit.png
#	Resources/Textures/Structures/Doors/Windoors/uranium.rsi/emergency_unlit.png
#	Resources/Textures/Structures/Power/Generation/Singularity/emitter.rsi/locked.png
#	Resources/Textures/Tiles/attributions.yml
#	Resources/Textures/Tiles/shuttleblue.png
#	Resources/Textures/Tiles/shuttleorange.png
#	Resources/Textures/Tiles/shuttlepurple.png
#	Resources/Textures/Tiles/shuttlered.png
#	Resources/Textures/Tiles/shuttlewhite.png
#	Resources/Textures/White/Fluff/DOOMMAX/cap_cap.rsi/meta.json
#	Resources/Textures/White/Fluff/HSKveez/hardsuit.rsi/meta.json
#	Resources/Textures/White/Fluff/Vtergot/strictgloves.rsi/meta.json
#	Resources/clientCommandPerms.yml
#	Resources/migration.yml
This commit is contained in:
Remuchi
2024-03-26 15:52:23 +07:00
2922 changed files with 137356 additions and 80742 deletions

View File

@@ -111,7 +111,8 @@ public sealed record AdminMessageRecord(
bool Deleted,
PlayerRecord? DeletedBy,
DateTimeOffset? DeletedAt,
bool Seen) : IAdminRemarksRecord;
bool Seen,
bool Dismissed) : IAdminRemarksRecord;
public sealed record PlayerRecord(

View File

@@ -31,7 +31,8 @@ namespace Content.Server.Database
_opsLog = opsLog;
}
#region Preferences
#region Preferences
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
{
await using var db = await GetDb();
@@ -138,7 +139,8 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
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)
@@ -158,10 +160,10 @@ namespace Content.Server.Database
.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)
@@ -256,6 +258,7 @@ namespace Content.Server.Database
{
markingStrings.Add(marking.ToString());
}
var markings = JsonSerializer.SerializeToDocument(markingStrings);
profile.CharacterName = humanoid.Name;
@@ -286,26 +289,28 @@ namespace Content.Server.Database
profile.Jobs.AddRange(
humanoid.JobPriorities
.Where(j => j.Value != JobPriority.Never)
.Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value})
.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})
.Select(a => new Antag { AntagName = a })
);
profile.Traits.Clear();
profile.Traits.AddRange(
humanoid.TraitPreferences
.Select(t => new Trait {TraitName = t})
.Select(t => new Trait { TraitName = t })
);
return profile;
}
#endregion
#region User Ids
#endregion
#region User Ids
public async Task<NetUserId?> GetAssignedUserIdAsync(string name)
{
await using var db = await GetDb();
@@ -326,9 +331,11 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
#endregion
#region Bans
#endregion
#region Bans
/*
* BAN STUFF
*/
@@ -373,15 +380,23 @@ namespace Content.Server.Database
string serverName = GlobalServerName);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
public abstract Task AddServerUnbanAsync(ServerUnbanDef serverUnban);
public async Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
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;
@@ -435,9 +450,10 @@ namespace Content.Server.Database
return flags ?? ServerBanExemptFlags.None;
}
#endregion
#endregion
#region Role Bans
#region Role Bans
/*
* ROLE BANS
*/
@@ -459,22 +475,31 @@ namespace Content.Server.Database
/// <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,
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned,
string serverName = GlobalServerName);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
public abstract Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban);
public async Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
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;
@@ -482,9 +507,11 @@ namespace Content.Server.Database
ban.LastEditedAt = editedAt.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
#endregion
#region Playtime
#endregion
#region Playtime
public async Task<List<PlayTime>> GetPlayTimes(Guid player)
{
await using var db = await GetDb();
@@ -535,9 +562,10 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
#endregion
#endregion
#region Player Records
#region Player Records
/*
* PLAYER RECORDS
*/
@@ -606,9 +634,10 @@ namespace Content.Server.Database
player.LastSeenHWId?.ToImmutableArray());
}
#endregion
#endregion
#region Connection Logs
#region Connection Logs
/*
* CONNECTION LOG
*/
@@ -635,9 +664,10 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync();
}
#endregion
#endregion
#region Admin Ranks
#region Admin Ranks
/*
* ADMIN RANKS
*/
@@ -688,7 +718,9 @@ namespace Content.Server.Database
{
await using var db = await GetDb();
var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel);
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;
@@ -761,8 +793,8 @@ namespace Content.Server.Database
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
""");
INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}) ON CONFLICT DO NOTHING
""");
}
await db.DbContext.SaveChangesAsync();
@@ -793,9 +825,10 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync(cancel);
}
#endregion
#region Admin Logs
#endregion
#region Admin Logs
public async Task<(Server, bool existed)> AddOrGetServer(string serverName)
{
@@ -898,7 +931,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
{
query = filter.DateOrder switch
{
DateOrder.Ascending => query.Where(log => log.Id > filter.LastLogId),
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}")
@@ -907,21 +940,14 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
query = filter.DateOrder switch
{
DateOrder.Ascending => query.OrderBy(log => log.Date),
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}")
};
const int hardLogLimit = 500_000;
if (filter.Limit != null)
{
query = query.Take(Math.Min(filter.Limit.Value, hardLogLimit));
}
else
{
query = query.Take(hardLogLimit);
}
query = query.Take(filter.Limit != null ? Math.Min(filter.Limit.Value, hardLogLimit) : hardLogLimit);
return query;
}
@@ -972,9 +998,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return await db.DbContext.AdminLog.CountAsync(log => log.RoundId == round);
}
#endregion
#endregion
#region Whitelist
#region Whitelist
public async Task<bool> GetWhitelistStatusAsync(NetUserId player)
{
@@ -1013,7 +1039,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
{
await using var db = await GetDb();
var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync();
var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player)
.SingleOrDefaultAsync();
if (dbPlayer == null)
{
return;
@@ -1023,15 +1051,17 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync();
}
#endregion
#endregion
#region Uploaded Resources Logs
#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 });
db.DbContext.UploadedResourceLog.Add(new UploadedResourceLog()
{ UserId = user, Date = date.UtcDateTime, Path = path, Data = data });
await db.DbContext.SaveChangesAsync();
}
@@ -1051,9 +1081,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync();
}
#endregion
#endregion
#region Admin Notes
#region Admin Notes
public virtual async Task<int> AddAdminNote(AdminNote note)
{
@@ -1163,7 +1193,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
entity.Deleted,
MakePlayerRecord(entity.DeletedBy),
NormalizeDatabaseTime(entity.DeletedAt),
entity.Seen);
entity.Seen,
entity.Dismissed);
}
public async Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
@@ -1222,8 +1253,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
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);
? null
: await db.DbContext.Player.SingleOrDefaultAsync(b => b.UserId == ban.Unban.UnbanningAdmin);
return new ServerRoleBanNoteRecord(
ban.Id,
@@ -1238,7 +1269,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
ban.LastEditedAt,
ban.ExpirationTime,
ban.Hidden,
new [] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
new[] { ban.RoleId.Replace(BanManager.JobPrefix, null) },
MakePlayerRecord(unbanningAdmin),
ban.Unban?.UnbanTime);
}
@@ -1250,8 +1281,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
notes.AddRange(
(await (from note in db.DbContext.AdminNotes
where note.PlayerUserId == player &&
!note.Deleted &&
(note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
!note.Deleted &&
(note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
select note)
.Include(note => note.Round)
.ThenInclude(r => r!.Server)
@@ -1259,13 +1290,22 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.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)
public async Task EditAdminNote(
int id,
string message,
NoteSeverity severity,
bool secret,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
@@ -1280,7 +1320,12 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync();
}
public async Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
public async Task EditAdminWatchlist(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
@@ -1293,7 +1338,12 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
await db.DbContext.SaveChangesAsync();
}
public async Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
public async Task EditAdminMessage(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
await using var db = await GetDb();
@@ -1378,16 +1428,19 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
notesCol.AddRange(
(await (from note in db.DbContext.AdminNotes
where note.PlayerUserId == player &&
!note.Secret &&
!note.Deleted &&
(note.ExpirationTime == null || DateTime.UtcNow < note.ExpirationTime)
!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;
}
@@ -1400,10 +1453,10 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
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)
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)
@@ -1416,7 +1469,11 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
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));
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)
@@ -1428,23 +1485,26 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
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();
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)
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();
}
@@ -1492,7 +1552,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return banNotes;
}
protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(DbGuard db, Guid user)
protected async Task<List<ServerRoleBanNoteRecord>> GetGroupedServerRoleBansAsNotesForUser(
DbGuard db,
Guid user)
{
// Server side query
var bansQuery = await db.DbContext.RoleBan
@@ -1507,9 +1569,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
// Client side query, as EF can't do groups yet
var bansEnumerable = bansQuery
.GroupBy(ban => new { ban.BanTime, ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
.Select(banGroup => banGroup)
.ToArray();
.GroupBy(ban => new { ban.BanTime, ban.CreatedBy, ban.Reason, Unbanned = ban.Unban == null })
.Select(banGroup => banGroup)
.ToArray();
List<ServerRoleBanNoteRecord> bans = new();
var player = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == user);
@@ -1519,7 +1581,11 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
Player? unbanningAdmin = null;
if (firstBan.Unban?.UnbanningAdmin is not null)
unbanningAdmin = await db.DbContext.Player.SingleOrDefaultAsync(p => p.UserId == firstBan.Unban.UnbanningAdmin.Value);
{
unbanningAdmin =
await db.DbContext.Player.SingleOrDefaultAsync(p =>
p.UserId == firstBan.Unban.UnbanningAdmin.Value);
}
bans.Add(new ServerRoleBanNoteRecord(
firstBan.Id,
@@ -1542,9 +1608,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return bans;
}
#endregion
#endregion
#region Player Reputation (WD edit)
#region Player Reputation (WD edit)
public async Task SetPlayerReputation(Guid player, float value)
{
@@ -1560,6 +1626,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
UserId = player,
Reputation = value
};
db.DbContext.PlayerReputations.Add(reputation);
}
else
@@ -1584,6 +1651,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
UserId = player,
Reputation = 0f + value
};
db.DbContext.PlayerReputations.Add(reputation);
}
else
@@ -1604,7 +1672,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return reputation?.Reputation ?? 0f;
}
#endregion
#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.
@@ -1616,6 +1684,12 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return time != null ? NormalizeDatabaseTime(time.Value) : time;
}
public async Task<bool> HasPendingModelChanges()
{
await using var db = await GetDb();
return db.DbContext.Database.HasPendingModelChanges();
}
protected abstract Task<DbGuard> GetDb([CallerMemberName] string? name = null);
protected void LogDbOp(string? name)
@@ -1630,4 +1704,4 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
public abstract ValueTask DisposeAsync();
}
}
}
}

View File

@@ -25,12 +25,15 @@ namespace Content.Server.Database
public interface IServerDbManager
{
const string GlobalServerName = "unknown";
void Init();
void Shutdown();
#region Preferences
#region Preferences
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
@@ -39,16 +42,22 @@ namespace Content.Server.Database
// Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
#endregion
#region User Ids
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
#endregion
#region User Ids
// Username assignment (for guest accounts, so they persist GUID)
Task AssignUserIdAsync(string name, NetUserId userId);
Task<NetUserId?> GetAssignedUserIdAsync(string name);
#endregion
#region Bans
Task<NetUserId?> GetAssignedUserIdAsync(string name);
#endregion
#region Bans
/// <summary>
/// Looks up a ban by id.
/// This will return a pardoned ban as well.
@@ -85,10 +94,11 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned=true,
bool includeUnbanned = true,
string serverName = GlobalServerName);
Task AddServerBanAsync(ServerBanDef serverBan);
Task AddServerUnbanAsync(ServerUnbanDef serverBan);
public Task EditServerBan(
@@ -115,9 +125,10 @@ namespace Content.Server.Database
/// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId);
#endregion
#endregion
#region Role Bans
#region Role Bans
/// <summary>
/// Looks up a role ban by id.
/// This will return a pardoned role ban as well.
@@ -144,6 +155,7 @@ namespace Content.Server.Database
string serverName = GlobalServerName);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
public Task EditServerRoleBan(
@@ -153,9 +165,10 @@ namespace Content.Server.Database
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt);
#endregion
#region Playtime
#endregion
#region Playtime
/// <summary>
/// Look up a player's role timers.
@@ -170,19 +183,24 @@ namespace Content.Server.Database
/// <param name="updates">The list of all updates to apply to the database.</param>
Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates);
#endregion
#endregion
#region Player Records
#region Player Records
Task UpdatePlayerRecordAsync(
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
#region Connection Logs
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
#region Connection Logs
/// <returns>ID of newly inserted connection log row.</returns>
Task<int> AddConnectionLogAsync(
NetUserId userId,
@@ -194,44 +212,58 @@ namespace Content.Server.Database
Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans);
#endregion
#endregion
#region Admin Ranks
#region Admin Ranks
Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default);
Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default);
Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel = default);
Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default);
Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
#endregion
#region Rounds
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
#endregion
#region Rounds
Task<int> AddNewRound(Server server, params Guid[] playerIds);
Task<Round> GetRound(int id);
Task AddRoundPlayers(int id, params Guid[] playerIds);
#endregion
#endregion
#region Admin Logs
#region Admin Logs
Task<Server> AddOrGetServer(string serverName);
Task AddAdminLogs(List<AdminLog> logs);
IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null);
IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null);
IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null);
Task<int> CountAdminLogs(int round);
#endregion
#endregion
#region Whitelist
#region Whitelist
Task<bool> GetWhitelistStatusAsync(NetUserId player);
@@ -239,50 +271,118 @@ namespace Content.Server.Database
Task RemoveFromWhitelistAsync(NetUserId player);
#endregion
#endregion
#region Uploaded Resources Logs
#region Uploaded Resources Logs
Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
Task PurgeUploadedResourceLogAsync(int days);
#endregion
#endregion
#region Rules
#region Rules
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
#endregion
#endregion
#region Admin Notes
#region Admin Notes
Task<int> AddAdminNote(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
NoteSeverity severity,
bool secret,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime);
Task<int> AddAdminWatchlist(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime);
Task<int> AddAdminMessage(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime);
Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
Task<AdminNoteRecord?> GetAdminNote(int id);
Task<AdminWatchlistRecord?> GetAdminWatchlist(int id);
Task<AdminMessageRecord?> GetAdminMessage(int id);
Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id);
Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id);
Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player);
Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player);
Task<List<AdminMessageRecord>> GetMessages(Guid player);
Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
Task EditAdminNote(
int id,
string message,
NoteSeverity severity,
bool secret,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime);
Task EditAdminWatchlist(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime);
Task EditAdminMessage(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime);
Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
Task MarkMessageAsSeen(int id);
#endregion
/// <summary>
/// Mark an admin message as being seen by the target player.
/// </summary>
/// <param name="id">The database ID of the admin message.</param>
/// <param name="dismissedToo">
/// If true, the message is "permanently dismissed" and will not be shown to the player again when they join.
/// </param>
Task MarkMessageAsSeen(int id, bool dismissedToo);
#region Player Reputation (WD edit)
#endregion
#region Player Reputation (WD edit)
/// <summary>
/// Set player's reputation to the certain value.
@@ -305,7 +405,7 @@ namespace Content.Server.Database
/// <returns>Value of player's reputation.</returns>
Task<float> GetPlayerReputation(Guid player);
#endregion
#endregion
}
public sealed class ServerDbManager : IServerDbManager
@@ -333,6 +433,7 @@ namespace Content.Server.Database
private ILoggerFactory _msLoggerFactory = default!;
private bool _synchronous;
// When running in integration tests, we'll use a single in-memory SQLite database connection.
// This is that connection, close it when we shut down.
private SqliteConnection? _sqliteInMemoryConnection;
@@ -437,7 +538,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned=true,
bool includeUnbanned = true,
string serverName = GlobalServerName)
{
DbReadOpsMetric.Inc();
@@ -456,7 +557,13 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban));
}
public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
public Task EditServerBan(
int id,
string reason,
NoteSeverity severity,
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt));
@@ -474,7 +581,8 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetBanExemption(userId));
}
#region Role Ban
#region Role Ban
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
DbReadOpsMetric.Inc();
@@ -485,7 +593,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned=true,
bool includeUnbanned = true,
string serverName = GlobalServerName)
{
DbReadOpsMetric.Inc();
@@ -504,14 +612,21 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban));
}
public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
public Task EditServerRoleBan(
int id,
string reason,
NoteSeverity severity,
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt));
}
#endregion
#region Playtime
#endregion
#region Playtime
public Task<List<PlayTime>> GetPlayTimes(Guid player)
{
@@ -525,9 +640,9 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.UpdatePlayTimes(updates));
}
#endregion
#endregion
#region Player Reputation (WD edit)
#region Player Reputation (WD edit)
public Task SetPlayerReputation(Guid player, float value)
{
@@ -547,7 +662,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetPlayerReputation(player));
}
#endregion
#endregion
public Task UpdatePlayerRecordAsync(
NetUserId userId,
@@ -745,7 +860,16 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.SetLastReadRules(player, time));
}
public Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
public Task<int> AddAdminNote(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
NoteSeverity severity,
bool secret,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
var note = new AdminNote
@@ -766,7 +890,14 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.AddAdminNote(note));
}
public Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
public Task<int> AddAdminWatchlist(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
var note = new AdminWatchlist
@@ -785,7 +916,14 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.AddAdminWatchlist(note));
}
public Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
public Task<int> AddAdminMessage(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
var note = new AdminMessage
@@ -809,11 +947,13 @@ namespace Content.Server.Database
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminNote(id));
}
public Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminWatchlist(id));
}
public Task<AdminMessageRecord?> GetAdminMessage(int id)
{
DbReadOpsMetric.Inc();
@@ -832,7 +972,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
}
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllAdminRemarks(player));
@@ -855,19 +995,37 @@ namespace Content.Server.Database
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetMessages(player));
}
public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
public Task EditAdminNote(
int id,
string message,
NoteSeverity severity,
bool secret,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditAdminNote(id, message, severity, secret, editedBy, editedAt, expiryTime));
}
public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
public Task EditAdminWatchlist(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, expiryTime));
}
public Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
public Task EditAdminMessage(
int id,
string message,
Guid editedBy,
DateTimeOffset editedAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditAdminMessage(id, message, editedBy, editedAt, expiryTime));
@@ -903,10 +1061,10 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt));
}
public Task MarkMessageAsSeen(int id)
public Task MarkMessageAsSeen(int id, bool dismissedToo)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.MarkMessageAsSeen(id));
return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
}
// Wrapper functions to run DB commands from the thread pool.
@@ -1032,35 +1190,25 @@ namespace Content.Server.Database
builder.UseLoggerFactory(_msLoggerFactory);
}
private sealed class LoggingProvider : ILoggerProvider
private sealed class LoggingProvider(ILogManager logManager) : ILoggerProvider
{
private readonly ILogManager _logManager;
public LoggingProvider(ILogManager logManager)
{
_logManager = logManager;
}
public void Dispose()
{
}
public ILogger CreateLogger(string categoryName)
{
return new MSLogger(_logManager.GetSawmill("db.ef"));
return new MSLogger(logManager.GetSawmill("db.ef"));
}
}
private sealed class MSLogger : ILogger
private sealed class MSLogger(ISawmill sawmill) : ILogger
{
private readonly ISawmill _sawmill;
public MSLogger(ISawmill sawmill)
{
_sawmill = sawmill;
}
public void Log<TState>(MSLogLevel logLevel, EventId eventId, TState state, Exception? exception,
public void Log<TState>(
MSLogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var lvl = logLevel switch
@@ -1069,14 +1217,14 @@ namespace Content.Server.Database
MSLogLevel.Debug => LogLevel.Debug,
// EFCore feels the need to log individual DB commands as "Information" so I'm slapping debug on it.
MSLogLevel.Information => LogLevel.Debug,
MSLogLevel.Warning => LogLevel.Warning,
MSLogLevel.Error => LogLevel.Error,
MSLogLevel.Critical => LogLevel.Fatal,
MSLogLevel.None => LogLevel.Debug,
_ => LogLevel.Debug
MSLogLevel.Warning => LogLevel.Warning,
MSLogLevel.Error => LogLevel.Error,
MSLogLevel.Critical => LogLevel.Fatal,
MSLogLevel.None => LogLevel.Debug,
_ => LogLevel.Debug
};
_sawmill.Log(lvl, formatter(state, exception));
sawmill.Log(lvl, formatter(state, exception));
}
public bool IsEnabled(MSLogLevel logLevel)
@@ -1094,32 +1242,18 @@ namespace Content.Server.Database
public sealed record PlayTimeUpdate(NetUserId User, string Tracker, TimeSpan Time);
internal sealed class SyncAsyncEnumerable<T> : IAsyncEnumerable<T>
internal sealed class SyncAsyncEnumerable<T>(IAsyncEnumerable<T> enumerable) : IAsyncEnumerable<T>
{
private readonly IAsyncEnumerable<T> _enumerable;
public SyncAsyncEnumerable(IAsyncEnumerable<T> enumerable)
{
_enumerable = enumerable;
}
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new Enumerator(_enumerable.GetAsyncEnumerator(cancellationToken));
return new Enumerator(enumerable.GetAsyncEnumerator(cancellationToken));
}
private sealed class Enumerator : IAsyncEnumerator<T>
private sealed class Enumerator(IAsyncEnumerator<T> enumerator) : IAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
public Enumerator(IAsyncEnumerator<T> enumerator)
{
_enumerator = enumerator;
}
public ValueTask DisposeAsync()
{
var task = _enumerator.DisposeAsync();
var task = enumerator.DisposeAsync();
if (!task.IsCompleted)
throw new InvalidOperationException("DisposeAsync did not complete synchronously.");
@@ -1128,14 +1262,14 @@ namespace Content.Server.Database
public ValueTask<bool> MoveNextAsync()
{
var task = _enumerator.MoveNextAsync();
var task = enumerator.MoveNextAsync();
if (!task.IsCompleted)
throw new InvalidOperationException("MoveNextAsync did not complete synchronously.");
return task;
}
public T Current => _enumerator.Current;
public T Current => enumerator.Current;
}
}
}
}