Files
OldThink/Content.Server/Database/ServerDbManager.cs
Remuchi 5e74f6ae53 Merge branch 'master' of https://github.com/space-wizards/space-station-14 into upstream
# 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
2024-03-26 15:52:23 +07:00

1275 lines
44 KiB
C#

using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Preferences;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;
using Prometheus;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Network;
using LogLevel = Robust.Shared.Log.LogLevel;
using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Content.Server.Database
{
public interface IServerDbManager
{
const string GlobalServerName = "unknown";
void Init();
void Shutdown();
#region Preferences
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
// Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
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
/// <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>
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>
/// <param name="hwId">The hardware ID of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
string serverName = GlobalServerName);
/// <summary>
/// Looks up an user's ban history.
/// 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>
/// <param name="hwId">The HWId of the user.</param>
/// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
/// <returns>The user's ban history.</returns>
Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true,
string serverName = GlobalServerName);
Task AddServerBanAsync(ServerBanDef serverBan);
Task AddServerUnbanAsync(ServerUnbanDef serverBan);
public Task EditServerBan(
int id,
string reason,
NoteSeverity severity,
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt);
/// <summary>
/// Update ban exemption information for a player.
/// </summary>
/// <remarks>
/// Database rows are automatically created and removed when appropriate.
/// </remarks>
/// <param name="userId">The user to update</param>
/// <param name="flags">The new ban exemption flags.</param>
Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags);
/// <summary>
/// Get current ban exemption flags for a user
/// </summary>
/// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId);
#endregion
#region 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>
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>
Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true,
string serverName = GlobalServerName);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
public Task EditServerRoleBan(
int id,
string reason,
NoteSeverity severity,
DateTimeOffset? expiration,
Guid editedBy,
DateTimeOffset editedAt);
#endregion
#region Playtime
/// <summary>
/// Look up a player's role timers.
/// </summary>
/// <param name="player">The player to get the role timer information from.</param>
/// <returns>All role timers belonging to the player.</returns>
Task<List<PlayTime>> GetPlayTimes(Guid player);
/// <summary>
/// Update play time information in bulk.
/// </summary>
/// <param name="updates">The list of all updates to apply to the database.</param>
Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates);
#endregion
#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
/// <returns>ID of newly inserted connection log row.</returns>
Task<int> AddConnectionLogAsync(
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ConnectionDenyReason? denied,
int serverId);
Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans);
#endregion
#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<int> AddNewRound(Server server, params Guid[] playerIds);
Task<Round> GetRound(int id);
Task AddRoundPlayers(int id, params Guid[] playerIds);
#endregion
#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
#region Whitelist
Task<bool> GetWhitelistStatusAsync(NetUserId player);
Task AddToWhitelistAsync(NetUserId player);
Task RemoveFromWhitelistAsync(NetUserId player);
#endregion
#region Uploaded Resources Logs
Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
Task PurgeUploadedResourceLogAsync(int days);
#endregion
#region Rules
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
#endregion
#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<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 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);
/// <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);
#endregion
#region Player Reputation (WD edit)
/// <summary>
/// Set player's reputation to the certain value.
/// </summary>
/// <param name="player">Guid of the player to set the value.</param>
/// <param name="value">Value to set.</param>
Task SetPlayerReputation(Guid player, float value);
/// <summary>
/// Modify player's reputation by adding value (currentValue + value).
/// </summary>
/// <param name="player">Guid of the player to modify the value.</param>
/// <param name="value">Value to add.</param>
Task ModifyPlayerReputation(Guid player, float value);
/// <summary>
/// Gets value of player reputation.
/// </summary>
/// <param name="player">Guid of the player to get the value.</param>
/// <returns>Value of player's reputation.</returns>
Task<float> GetPlayerReputation(Guid player);
#endregion
}
public sealed class ServerDbManager : IServerDbManager
{
private const string GlobalServerName = "unknown";
public static readonly Counter DbReadOpsMetric = Metrics.CreateCounter(
"db_read_ops",
"Amount of read operations processed by the database manager.");
public static readonly Counter DbWriteOpsMetric = Metrics.CreateCounter(
"db_write_ops",
"Amount of write operations processed by the database manager.");
public static readonly Gauge DbActiveOps = Metrics.CreateGauge(
"db_executing_ops",
"Amount of active database operations. Note that some operations may be waiting for a database connection.");
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _logMgr = default!;
private ServerDbBase _db = default!;
private LoggingProvider _msLogProvider = default!;
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;
public void Init()
{
_msLogProvider = new LoggingProvider(_logMgr);
_msLoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddProvider(_msLogProvider);
});
_synchronous = _cfg.GetCVar(CCVars.DatabaseSynchronous);
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
var opsLog = _logMgr.GetSawmill("db.op");
switch (engine)
{
case "sqlite":
SetupSqlite(out var contextFunc, out var inMemory);
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
break;
case "postgres":
var pgOptions = CreatePostgresOptions();
_db = new ServerDbPostgres(pgOptions, _cfg, opsLog);
break;
default:
throw new InvalidDataException($"Unknown database engine {engine}.");
}
}
public void Shutdown()
{
_sqliteInMemoryConnection?.Dispose();
}
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.InitPrefsAsync(userId, defaultProfile));
}
public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SaveSelectedCharacterIndexAsync(userId, index));
}
public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SaveCharacterSlotAsync(userId, profile, slot));
}
public Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.DeleteSlotAndSetSelectedIndex(userId, deleteSlot, newSlot));
}
public Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
}
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId));
}
public Task AssignUserIdAsync(string name, NetUserId userId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AssignUserIdAsync(name, userId));
}
public Task<NetUserId?> GetAssignedUserIdAsync(string name)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAssignedUserIdAsync(name));
}
public Task<ServerBanDef?> GetServerBanAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(id));
}
public Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
string serverName = GlobalServerName)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, serverName));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true,
string serverName = GlobalServerName)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned, serverName));
}
public Task AddServerBanAsync(ServerBanDef serverBan)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerBanAsync(serverBan));
}
public Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban));
}
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));
}
public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
}
public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetBanExemption(userId));
}
#region Role Ban
public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBanAsync(id));
}
public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
bool includeUnbanned = true,
string serverName = GlobalServerName)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned, serverName));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan));
}
public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban));
}
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
public Task<List<PlayTime>> GetPlayTimes(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayTimes(player));
}
public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayTimes(updates));
}
#endregion
#region Player Reputation (WD edit)
public Task SetPlayerReputation(Guid player, float value)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetPlayerReputation(player, value));
}
public Task ModifyPlayerReputation(Guid player, float value)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.ModifyPlayerReputation(player, value));
}
public Task<float> GetPlayerReputation(Guid player)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerReputation(player));
}
#endregion
public Task UpdatePlayerRecordAsync(
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
}
public Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerRecordByUserName(userName, cancel));
}
public Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerRecordByUserId(userId, cancel));
}
public Task<int> AddConnectionLogAsync(
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ConnectionDenyReason? denied,
int serverId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId));
}
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans));
}
public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminDataForAsync(userId, cancel));
}
public Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminRankDataForAsync(id, cancel));
}
public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllAdminAndRanksAsync(cancel));
}
public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.RemoveAdminAsync(userId, cancel));
}
public Task AddAdminAsync(Admin admin, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddAdminAsync(admin, cancel));
}
public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
}
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.RemoveAdminRankAsync(rankId, cancel));
}
public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddAdminRankAsync(rank, cancel));
}
public Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddNewRound(server, playerIds));
}
public Task<Round> GetRound(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetRound(id));
}
public Task AddRoundPlayers(int id, params Guid[] playerIds)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddRoundPlayers(id, playerIds));
}
public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdateAdminRankAsync(rank, cancel));
}
public async Task<Server> AddOrGetServer(string serverName)
{
var (server, existed) = await RunDbCommand(() => _db.AddOrGetServer(serverName));
if (existed)
DbReadOpsMetric.Inc();
else
DbWriteOpsMetric.Inc();
return server;
}
public Task AddAdminLogs(List<AdminLog> logs)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddAdminLogs(logs));
}
public IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminLogMessages(filter));
}
public IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminLogs(filter));
}
public IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAdminLogsJson(filter));
}
public Task<int> CountAdminLogs(int round)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.CountAdminLogs(round));
}
public Task<bool> GetWhitelistStatusAsync(NetUserId player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetWhitelistStatusAsync(player));
}
public Task AddToWhitelistAsync(NetUserId player)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddToWhitelistAsync(player));
}
public Task RemoveFromWhitelistAsync(NetUserId player)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player));
}
public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddUploadedResourceLogAsync(user, date, path, data));
}
public Task PurgeUploadedResourceLogAsync(int days)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days));
}
public Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetLastReadRules(player));
}
public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
{
DbWriteOpsMetric.Inc();
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)
{
DbWriteOpsMetric.Inc();
var note = new AdminNote
{
RoundId = roundId,
CreatedById = createdBy,
LastEditedById = createdBy,
PlayerUserId = player,
PlaytimeAtNote = playtimeAtNote,
Message = message,
Severity = severity,
Secret = secret,
CreatedAt = createdAt.UtcDateTime,
LastEditedAt = createdAt.UtcDateTime,
ExpirationTime = expiryTime?.UtcDateTime
};
return RunDbCommand(() => _db.AddAdminNote(note));
}
public Task<int> AddAdminWatchlist(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
var note = new AdminWatchlist
{
RoundId = roundId,
CreatedById = createdBy,
LastEditedById = createdBy,
PlayerUserId = player,
PlaytimeAtNote = playtimeAtNote,
Message = message,
CreatedAt = createdAt.UtcDateTime,
LastEditedAt = createdAt.UtcDateTime,
ExpirationTime = expiryTime?.UtcDateTime
};
return RunDbCommand(() => _db.AddAdminWatchlist(note));
}
public Task<int> AddAdminMessage(
int? roundId,
Guid player,
TimeSpan playtimeAtNote,
string message,
Guid createdBy,
DateTimeOffset createdAt,
DateTimeOffset? expiryTime)
{
DbWriteOpsMetric.Inc();
var note = new AdminMessage
{
RoundId = roundId,
CreatedById = createdBy,
LastEditedById = createdBy,
PlayerUserId = player,
PlaytimeAtNote = playtimeAtNote,
Message = message,
CreatedAt = createdAt.UtcDateTime,
LastEditedAt = createdAt.UtcDateTime,
ExpirationTime = expiryTime?.UtcDateTime
};
return RunDbCommand(() => _db.AddAdminMessage(note));
}
public Task<AdminNoteRecord?> GetAdminNote(int id)
{
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();
return RunDbCommand(() => _db.GetAdminMessage(id));
}
public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id));
}
public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
}
public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllAdminRemarks(player));
}
public Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetVisibleAdminRemarks(player));
}
public Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetActiveWatchlists(player));
}
public Task<List<AdminMessageRecord>> GetMessages(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetMessages(player));
}
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)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, 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));
}
public Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.DeleteAdminNote(id, deletedBy, deletedAt));
}
public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.DeleteAdminWatchlist(id, deletedBy, deletedAt));
}
public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt));
}
public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt));
}
public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt));
}
public Task MarkMessageAsSeen(int id, bool dismissedToo)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
}
// Wrapper functions to run DB commands from the thread pool.
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
// For SQLite, this will also enable read parallelization (within limits).
//
// If we're configured to be synchronous (for integration tests) we shouldn't thread pool it,
// as that would make things very random and undeterministic.
// That only works on SQLite though, since SQLite is internally synchronous anyways.
private async Task<T> RunDbCommand<T>(Func<Task<T>> command)
{
using var _ = DbActiveOps.TrackInProgress();
if (_synchronous)
return await RunDbCommandCoreSync(command);
return await Task.Run(command);
}
private async Task RunDbCommand(Func<Task> command)
{
using var _ = DbActiveOps.TrackInProgress();
if (_synchronous)
{
await RunDbCommandCoreSync(command);
return;
}
await Task.Run(command);
}
private static T RunDbCommandCoreSync<T>(Func<T> command) where T : IAsyncResult
{
var task = command();
if (!task.IsCompleted)
{
// We can't just do BlockWaitOnTask here, because that could cause deadlocks.
// This flag is only intended for integration tests. If we trip this, it's a bug.
throw new InvalidOperationException(
"Database task is running asynchronously. " +
"This should be impossible when the database is set to synchronous.");
}
return task;
}
private IAsyncEnumerable<T> RunDbCommand<T>(Func<IAsyncEnumerable<T>> command)
{
var enumerable = command();
if (_synchronous)
return new SyncAsyncEnumerable<T>(enumerable);
return enumerable;
}
private DbContextOptions<PostgresServerDbContext> CreatePostgresOptions()
{
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
var db = _cfg.GetCVar(CCVars.DatabasePgDatabase);
var user = _cfg.GetCVar(CCVars.DatabasePgUsername);
var pass = _cfg.GetCVar(CCVars.DatabasePgPassword);
var builder = new DbContextOptionsBuilder<PostgresServerDbContext>();
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = host,
Port = port,
Database = db,
Username = user,
Password = pass
}.ConnectionString;
Logger.DebugS("db.manager", $"Using Postgres \"{host}:{port}/{db}\"");
builder.UseNpgsql(connectionString);
SetupLogging(builder);
return builder.Options;
}
private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory)
{
#if USE_SYSTEM_SQLITE
SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
#endif
// Can't re-use the SqliteConnection across multiple threads, so we have to make it every time.
Func<SqliteConnection> getConnection;
var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath);
inMemory = _res.UserData.RootDir == null;
if (!inMemory)
{
var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath);
Logger.DebugS("db.manager", $"Using SQLite DB \"{finalPreferencesDbPath}\"");
getConnection = () => new SqliteConnection($"Data Source={finalPreferencesDbPath}");
}
else
{
Logger.DebugS("db.manager", "Using in-memory SQLite DB");
_sqliteInMemoryConnection = new SqliteConnection("Data Source=:memory:");
// When using an in-memory DB we have to open it manually
// so EFCore doesn't open, close and wipe it every operation.
_sqliteInMemoryConnection.Open();
getConnection = () => _sqliteInMemoryConnection;
}
contextFunc = () =>
{
var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
builder.UseSqlite(getConnection());
SetupLogging(builder);
return builder.Options;
};
}
private void SetupLogging(DbContextOptionsBuilder builder)
{
builder.UseLoggerFactory(_msLoggerFactory);
}
private sealed class LoggingProvider(ILogManager logManager) : ILoggerProvider
{
public void Dispose()
{
}
public ILogger CreateLogger(string categoryName)
{
return new MSLogger(logManager.GetSawmill("db.ef"));
}
}
private sealed class MSLogger(ISawmill sawmill) : ILogger
{
public void Log<TState>(
MSLogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
var lvl = logLevel switch
{
MSLogLevel.Trace => LogLevel.Debug,
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
};
sawmill.Log(lvl, formatter(state, exception));
}
public bool IsEnabled(MSLogLevel logLevel)
{
return true;
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
// TODO: this
return null;
}
}
}
public sealed record PlayTimeUpdate(NetUserId User, string Tracker, TimeSpan Time);
internal sealed class SyncAsyncEnumerable<T>(IAsyncEnumerable<T> enumerable) : IAsyncEnumerable<T>
{
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new Enumerator(enumerable.GetAsyncEnumerator(cancellationToken));
}
private sealed class Enumerator(IAsyncEnumerator<T> enumerator) : IAsyncEnumerator<T>
{
public ValueTask DisposeAsync()
{
var task = enumerator.DisposeAsync();
if (!task.IsCompleted)
throw new InvalidOperationException("DisposeAsync did not complete synchronously.");
return task;
}
public ValueTask<bool> MoveNextAsync()
{
var task = enumerator.MoveNextAsync();
if (!task.IsCompleted)
throw new InvalidOperationException("MoveNextAsync did not complete synchronously.");
return task;
}
public T Current => enumerator.Current;
}
}
}