Files
OldThink/Content.Server/Database/ServerDbManager.cs
Remuchi 34d4c529fa [Upstream] Upstream (#109)
* Automatic changelog update

* atlas update (#25071)

* atlas update

* untroll

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>

* box update (#25074)

* fland update (#25075)

* marathon update (#25076)

* meta update (#25077)

* Syndicate key grammar fix (#25085)

* Syndicate encryption key grammar fix

An encryption key used by... wait... Who is owner of this chip? > An encryption key used by... wait... Who is the owner of this chip?

* Syndicate key grammar fix

An encryption key used by... wait... Who is owner of this chip? > An encryption key used by... wait... Who is the owner of this chip?

* Fix hybridization seedless probability (#25084)

Fix comparison

Hybrids (different plants being crossed) are supposed to have a high
chance of becoming seedless to balance overpowered plants.

However, a logic error in the comparison gave seedless to plants when
they were from the same seed (not hybrids) rather than the other way
around.

Reported by:    @genderGeometries

* Added Tourniquets to stop bleeds (#23198)

* Fixed Error with RobustToolbox Edits. Removed the addition of secbelt tag from assault belt.

* Resprite of the monstrosity

* Resprite of the tourniquet NO LONGER PHALLUS SHAPED

* too bright, now darker and edgier

* Tourniquet resprite

* metajson yay

* Update Skelly Vs The Rev lobby art (#25088)

Someone said the original was a bit low res
and I agreed.

This is a 1440p (from an 8k base image) rendition
of the same scene, with a new dramatic forced
perspective look, and generally cleaner art throughout.

Also now there's two mothroaches.

* Automatic changelog update

* Minor airlock assembly clean up (#25073)

1

* Update README.md (#25067)

* Update README.md

* emo review

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

---------

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

* prevent opening debug menus without perms (#25091)

prevent people without permissions from opening the tile, entityspawn, or decal menus

* Remove battery from crew observation kit, fix description (#25000)

-Remove battery from crew observation kit, fix description

* Laughin' Peas (#25089)

* laughter

* make clownmobs bleed laughter

* laughin syrup and laughter recipe

* add laughin peas

* Automatic changelog update

* QM drip DLC (#24477)

* New Drip for the QM

QM beret and QM formal uniform

* Asd

Asd

* Adds the new clothing to the uniform printer

what it says on the tin

* I always forget to update the copyright RAAAAAH

yup

* EMT Belt Part 2 (#24289)

* add

* fix

* aaaa

* Flipped caps real (#24961)

* Flipped caps real

* oops

* whoops

* flip not fold

* fix formatting

* cargosoft formatting

* Automatic changelog update

* Nerf Beanbags (#24653)

Lowers beanbag damage from 55 stam to 30 stam

* Automatic changelog update

* Fix screenspace popups (#24987)

* Fix screenspace popups

Never got around to it earlier but need to draw it above UI controls.

* Minor null change

* Automatic changelog update

* Fix crew manifest department bugs (#24975)

* Automatic changelog update

* Changed door remote to trigger based on vision occlusion(#25093)

Changed door remote to trigger based on vision occlusion rather than opaque collision targeting check. Ian's butt will no longer absorb your 5G signals.

Co-authored-by: Plykiya <plykiya@protonmail.com>

* GPS In Paramed Locker (#25096)

GPS in paramed locker

* Remove 'travis scott day' from the game (#25106)

* remove travis scott from the game

* KILL TRAVIS EVEN MORE

* Automatic changelog update

* Remove erroneous changelog (#25107)

* fixed fland cargo shuttle not having tiny fans (sorry) (#25095)

* Glass box for antique laser pistol (#25104)

* glassbox

* fix

* Gibbing refactor (Per-part gibbing and giblet throwing!) (#24989)

* Moving Gibbing rework out from medrefactor into it's own PR

* Re-enabled warning for missing gibbable on TryGibEntity

* Implemented better logic for gibbing failover and better logging

* Allowing audio params and drop scattering customization per component. Created UnGibbable organ base types and made brains ungibbable.
Removed delete brain from gibBody function. Artifact crusher does not destroy brains anymore. It only destroyed brains before not other organs which was wierd.

* Update Content.Shared/Body/Systems/SharedBodySystem.Body.cs

Fixing space for multiplication

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Added event raised when attempting to gib contained entities to allow modification of allowed and excluded container ids

* removing audioParams var from component (sound specifier includes it)

* Fixing signature

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Automatic changelog update

* Gibbing contents hotfix (#25114)

Fixing gibbing contents not respecting Include/Exclude container lists. This is currently unused

* Predict two-way levers (#25043)

* Predict two-way levers

Annoys me the rare occasions I touch cargo. Doesn't predict the signal but at least the lever responds immediately.

* space

* a

* Replace Romerol with Ambuzol in chemist guidebook (#25108)

replace romerol with ambuzol

* Fix borgs being able to emag themselves (#24748)

* Fix self emagging borgs

* Add popup on self emag failure.

* Ectoplasm is grindable into Necrosol (#25053)

add

* Automatic changelog update

* Automatic changelog update

* Roundstart Food Service research (#25046)

add

* Automatic changelog update

* Add option for character name colors in chat & move coloration to clientside (#24625)

* Adds option to disable character names in chat/speechbubbles

* Moved the coloring of names to clientside

* Move string functions to SharedChatSystem to avoid duplicate code in SpeechBubble.cs

* Changed to be put under Accessibility section

* Cache CVar

* Automatic changelog update

* Update Credits (#25115)

Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>

* Replace fixed drink glasses with metamorphic versions (#25134)

* Replaced fixed drink glasses with metamorphic versions

* Fine, no milkshake then

* ambuzol beef (#25119)

* Automatic changelog update

* Shuttle floor resprite (#25127)

* resprites shuttle tiles

* resprites shuttle floor tiles & adds grey/black shuttle tiles

* attributions.yml update

* Adding a period to an object description (#25138)

Added a period to an object description.

Adds a period to silk's description. This is my first and last pull request.

* Door Remote Changelog Entry (#25144)

I'm so dumb.

Co-authored-by: Plykiya <plykiya@protonmail.com>

* Automatic changelog update

* Lowered Ion Storm Reoccurence Delay to 20 (#25135)

* Lowered reoccurencedelay to 45

* Lowered Further down to 20

* Re-added shivs to crafting menus (#25094)

Added a recipe for crafting menu

* Automatic changelog update

* Color Tipped Ammo (#25103)

* Tipped .35 ammo

* used layers instead of new sprites

* remove the useless old sprites

* changed the green slightly

* Automatic changelog update

* Add overlay decals for mini tiles and bricks (#24949)

Add minitile and brick decals overlay

* Update Core (#24862)

* add

* Update Resources/Textures/Parallaxes/attributions.yml

* sprite change

* address review

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Buyable Jani Trolley (#25139)

Trolley

* Added display for amount of hits left in stun batons/stun prods. (#25141)

Added display for amount of hits left in stun batons/stunprods.

Co-authored-by: Plykiya <plykiya@protonmail.com>

* Automatic changelog update

* autolatheable air tanks (#25130)

* gastankening

* fix price

* Automatic changelog update

* Fix pointing arrow trajectory (#25061)

Initial commit

* make linking logic gates 1000% better (#25041)

* make door status use SendSignal

* LastSignals and logic, add ClearSignal api too

* make everything outputting a logic signal default to false

* refactor ops

* :trollface:

* :trollface:

* protoid for LastSignals

* oop

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Buff emergency toolbox fill (#24225)

Resolves issue #23059, for which the submitter wrote:

Emergency toolboxes contain a crowbar, two flashlights (sometimes one), two breath masks, and two chocolate. With the addition of water bottles to survival boxes and emergency nitrogen tanks, I think emergency toolboxes should be updated to include said water bottles and emergency tanks. Would make these just a little bit more useful. It feels weird they don't have oxygen tanks when they have two breath masks, and since water bottles are now commonplace it would be a good idea to put them in a place where emergency food is stored as well.

* Void jetpack resprite (#25150)

add

* Automatic changelog update

* Allow configuring gen_build_info.py through environment variables (#25162)

This makes the life of forks slightly easier by letting you pass an
environment variable instead of having to maintain this file yourself.

* Significantly nerf Deathnettles (#25068)

* Balancing my beloved

Significantly nerfs deathnettles so botanists can't just take down jug's like it's no issue, we have guns, we should be using them

* Additional Balancing Changes.

* Losing my mind

* Automatic changelog update

* Fix spelling errors in mechs.yml (#25168)

* fix showhealthbars perms (#25157)

* Allow inspecting ID's and Health of people behind glass (#25163)

Hops will love me

* Automatic changelog update

* Fix decal error spam (#25172)

* Restore MonoOverlay (#25170)

https://github.com/space-wizards/space-station-14/pull/24949 nuked it.

* Automatic changelog update

* Added "wink" and "tearfully smiles" emotes + more cry emote variations (#25129)

Added wink and tearfully smiles emote + more cries  variations

* Automatic changelog update

* Update submodule to 210.0.0 (#25175)

* Update submodule to 210.0.0

* 210.0.1 instead, the previous one was broken

* 210.0.3 instead, the previous one was ALSO broken

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>

* The medical beret is real (#25176)

* 1

* 2

* button is real.

* Automatic changelog update

* Fixed Tipped ammo not being Spent (#25167)

Fix Tipped Ammo not being Spent

* Automatic changelog update

* Hoods and some chaplain's hats now hides hair (#25142)

* Hoods now hide hair

* additional

plague hat and witch hat now hide hair

* fixing some tags in hats

* hoods tag fix

* Glassbox shatter resprite (#25136)

* Automatic changelog update

* Random spontaneous cleanup PR (#25131)

* Use new Subs.CVar helper

Removes manual config OnValueChanged calls, removes need to remember to manually unsubscribe.

This both reduces boilerplate and fixes many issues where subscriptions weren't removed on entity system shutdown.

* Fix a bunch of warnings

* More warning fixes

* Use new DateTime serializer to get rid of ISerializationHooks in changelog code.

* Get rid of some more ISerializationHooks for enums

* And a little more

* Apply suggestions from code review

Co-authored-by: 0x6273 <0x40@keemail.me>

---------

Co-authored-by: 0x6273 <0x40@keemail.me>

* Decrease the chemical cost of regen mesh and sutures and move them to their own file (#24948)

* WHYWEREMEDSINMEALRECIPES

* 20chem

* didiforgettosavethis

* Split slime marking leg gradient (#24928)

* Split slime marking leg gradient

* up markings to 4

* Automatic changelog update

* Require plants to be harvestable before sampling (#24851)

* Add verbs to Open/Close Openable containers, and add optional seals (#24780)

* Implement closing; add open/close verbs

* Add breakable seals

* Allow custom verb names; make condiment bottles closeable

* Remove pointless VV annotations and false defaults

* Split Sealable off into a new component

* Should have a Closed event too

* Oh hey, there are icons I could use

* Ternary operator

* Add support for seal visualizers

* Moved Sealable to Shared, added networking

* Replaced bottle_close1.ogg

* Automatic changelog update

* Shadow anomaly returns (#24629)

* content

* add cat

* ambient

* I FORGOT HEARTS!

* fix ambient

* some fixes

* canCollide: false

* connect to damageable

* pi

* remove fx

* some fixes

* *sad bruh*

* hazed

* Update base_shadow.yml

* Automatic changelog update

* Microwave UX enhancements  (#24547)

* Facelift Microwave UI

Includes new background light in UI, Uses predictive input, UI now properly disables buttons when microwave is active

* Microwave now shows Elapsed time

* Fixed bad formatting

* Added new term for "BottomMargin"

* Change yellow color

* Update StyleNano.cs

just spacing fixed

* Cook time countdown now detached from server


Instead of the server constantly sending out messages for the cook countdown, it is now predicted client side using TimeSpan

* Update MicrowaveMenu.xaml

forgot to re-add item space

* Automatic changelog update

* Additional damage visualisers (#24618)

* brute

* add

* Anomaly Synchronizer + Signallers tweaks (#24461)

* content

* nerf

* fix rsi

* Automatic changelog update

* saltern update (#25182)

Co-authored-by: deltanedas <@deltanedas:kde.org>

* Allow players to run saveconfig command. (#25200)

Benign client-side command.

* Revert "Allow configuring gen_build_info.py through environment variables" (#25201)

Revert "Allow configuring gen_build_info.py through environment variables (#2…"

This reverts commit 163e6d2f89.

* Fax machines can print from text file (#23262)

* added

* checks tweaking

* fixed what sloth wanted

* fixed?

* dialog diposing fix

* checks tweaking

* more changes

* dispose streamreader

* Update Content.Client/Fax/UI/FaxBoundUi.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Update Content.Server/Fax/FaxSystem.cs

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* fix minor typo

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Automatic changelog update

* Vox names, species prototype cleanup, some cosmetic changes (#24994)

* voxnames

* New generator parameters, names are more readable

* bunch of missing vox stuff

* more names

* sad

* Balanced ChemVend Stock (#25207)

* Balanced ChemVend Stock

ChemVend needs more balanced stock for what actually gets used

* +1 Sugar

* Ranged Holosigns (#25120)

* Changed holo signs to be ranged and used on click rather than Z.

* Updated comments

* Failed attempt at ignoring walls

* Getting rid of unused libraries

---------

Co-authored-by: Plykiya <plykiya@protonmail.com>

* Automatic changelog update

* Minor test fixes (#25174)

Stuff that probably shoulda been wrapped ig but our test runner stinky.

* Add pun to diagnostic hud description (#25209)

Seaborgium is element number 106 and is presumably what lets these glasses "see" "borgs".

* Add events for TemperatureProtection and PressureProtection (#25165)

* Update criminal-records.ftl (#25229)

* Adds always powered variants of colored lights (#25185)

Co-authored-by: Jeff <velcroboy333@hotmail.com>

* Fixes silver bars being whole stacks (#25239)

Co-authored-by: Jeff <velcroboy333@hotmail.com>

* Makes clumsy not delete guns (#25243)

clumsy no longer deletes guns

Co-authored-by: Jessica M <jessica@maybe.sh>

* Predict Injector (syringes), cleanup (#25235)

At least the mode/transfer amount logic. Actual transfer logic needs Bloodstream which I didn't wanna move into shared.

* Updated disabler to have suitStorage tag under slots. (#25238)

* Automatic changelog update

* Updates to Origin (#24908)

Replaced medical's HM console with a CM console, added janitor equipment closet

* Fix spawn priority persistence on reconnect and restart (#25246)

Because of course I would forget one line

* Add French accent beret (#21430)

* Allow thermomachines to exchange with air instead of inlet (#25247)

Add purely atmospheric heat exchange to the gas thermomachine component (in preparation for space heaters).

* Fix: Holosigns can be stored again (#25249)

* Holosigns can be stored again

* TryComp to HasComp

---------

Co-authored-by: Plykiya <plykiya@protonmail.com>

* Death acidifier fix (#25251)

* Automatic changelog update

* Clarify stripping logs (#25190)

* Indicate whether pickpocketing is stealthy in logs, change :user to :actor, and clean up messages.

* Remove ugly whitespace

* Do the thing I should have done but didn't because I didn't want to think

* Fix spacing

* Fix disposals bins not automatically flushing after an object is inserted (#25233)

Fix disposals bins not automatically flushing after an object is inserted.

Because of Spaghetti Code™️, AfterInsert() in DisposalUnitSystem still handles insertion itself. Except in all cases except drag/drop insert, the object is already inserted so this check fails and the remaining logic doesn't happen anymore. Fixed now.

* Sec & greysec jumpskirt fix (#25269)

* "resprites" sec & greysec jumpskirts

* adjustments

* Automatic changelog update

* Reduce eshield hp (#25258)

reduce eshield hp

* Automatic changelog update

* Artifact hemoglobin trigger now accepts all sentient blood types (#25240)

* Artifact blood trigger now accepts all sentient blood types

* Update artifact-hints.ftl

* Update engine to v210.1.0 (#25288)

* Made ghost.role_time a server modifiable only cvar (#25292)

Fix

* Nuke fancification (#25297)

Actually use more icon states for deployed/armed/about to explode

Also unlit layer.

Also examine text

* Automatic changelog update

* Change copper blood from ferrous to metallic (#25217)

changed copper blood from ferrous to metallic

* Thindow glass dupe fix (#25304)

smite glass dupe off the face of the planet

eww nasty dupe exploits blehh

* Automatic changelog update

* Kill Seperated Mindshield Icons (#25303)

* Unghettoify mindshield icons

Adds support for layers in status icons, through the StatusIconLayer enum and the new "layer" datafield. Defaults to the Base layer where functionally remains unchanged.

* TG icon for shield

probably better than the shitty one I made in paint

* forgor meta.json

I forgor

* Emo review

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

---------

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

* Automatic changelog update

* MORE SUFFIXES (#25314)

Co-authored-by: Jeff <velcroboy333@hotmail.com>

* Fix spawning glass shard for each glass sheet in stack (#25308)

* fix: SpawnEntitiesBehavior now works with stacks

Fixed the issue of SpawnEntitiesBehavior not executing multiple times on
entities with stack conponent.

Fixes #25287

* fix: reduced dictionary iterations

* Automatic changelog update

* Adds atmospherics access to the fire fighting door remote. Feels like… (#25097)

Adds atmospherics access to the fire fighting door remote. Feels like an oversight for the atmos door remote to not have atmos access.

Co-authored-by: Plykiya <plykiya@protonmail.com>

* Automatic changelog update

* Re-organise main menu screen (#25173)

- The dummy control of 2px size has annoyed me for almost 5 years.
- Why is it in the top-right.
- Why is the server address not labelled.

* Allow t-ray to penetrate carpets and puddles (#25276)

* Allow t-ray to penetrate carpets and puddles

* handle edge cases

* Automatic changelog update

* Add sfx for writing on paper (#25257)

* Initial commit

* Moved params to sound

* Removed type tag

* Removed null check

* Forced default

* Automatic changelog update

* New sprites for guidebooks (#25232)

* added books to roles

* First pass

* removed yaml to split pull requests into resprite first, then giving the books to assistants

* new science

* Automatic changelog update

* LockVisualizer (#25224)

* LockVisualizer

* Fix state

* Clean some code

* Make it component, fix tests fail

* Fix for StateUnlocked

Now it is possible to manually set the unlocked state and it will work!

* Optimize LockVisualizer, add check for unlocked state

* No todo I guess

* Solution precision fixes (#25199)

* Add test for two chemistry issues

1. rounding issue with reaction processing when making chloral hydrate
2. reliable assert trip due to the ValidateSolution() heat capacity issue.

* Fix FixedPoint2 arithmetic

Fix internal floating point arithmetic in places where it could be avoided.

Fix incorrect rounding mode used in other places (it should always floor, like regular int arithmetic).

I had to add an explicit epsilon value for float -> FixedPoint2 because something like 1.05 is actually like 1.04999 and that'd cause it to be rounded down to 1.04.

This fixes reaction reagent processing in cases where the reagent inputs can't cleanly divide. Previously, when making 30u chloral hydrate by adding the chlorine in 10u increments you'd end up with 0.04 chlorine left over. This was caused by division in the reaction code rounding up in some cases. Changing division here to always round down fixes it.

* Attempt to fix heat capacity precision assert issues.

Fixes #22126

First, we just increase the tolerance of the assert. It was way too low.

Second, actually put a cap on float drift from one-off _heatCapacity changes.

* Fix float -> FixedPoint2 epsilon for negative number, fix tests.

* Fix DamageableTest

* Oh yeah I need to call CleanReturnAsync

* Automatic changelog update

* WebP lobby images (#25184)

* Allow webp in lobby background files

* Make lobby art webp images

Reduces folder from 10 MB to 2.5 MB without only slight quality loss.

* Update PutLobbyScreensHere.txt

* New lobby art : Blueprint (#25179)

* add

* replace image with webp version

waiting on #25184

* Automatic changelog update

* Diona Nymphs & Splitting (#24630)

* Porting & implementation

* Fix two stupid errors

* Human not humans

* fix audio path

* Fix test fails & update cooldown

* Work on reviews & test fail

* Rework nymph organ system.

* Make the nymph organs nospawn.

* IsDeadIC

* Automatic changelog update

* reform cooldown 10 minutes (#25328)

* Change plant clipping mechanics (#25326)

Make seeds from clipped plants inherit the decreased health from parents.
Also require one growth stage before clipping.

* Automatic changelog update

* Fix nymphs being deleted immediatly after spawning (#25344)

* nymphs now don't get deleted together with the body of the diona

* moved nymph system to server

* Automatic changelog update

* Fix: Grenades don't make trigger sound (#25321)

* Fix: Grenades don't make trigger sound

* transform instead of trycomp transform

---------

Co-authored-by: Plykiya <plykiya@protonmail.com>

* fixed the specific if statement called when plant age is under 0 (#25346)

* Save round information into replay_final.yml (#23013)

* Save round information into the replay

* Add round end text too

* This is way better

* Get actual job

* oop

* OK THERE

* Fake line endings to make life easier

* I was told this yaml is legal

* I just realised this will make my life easier

* REVIEWS BABY IM A PROGRAMMER MOMMY

* Live pjb reaction

* Live pjb reaction 2

* Reviews 2

* Dont need this

* Please no more have mercy on my soul

* Oh frick

* Adds a massban flag to the admin flags (#25327)

Adds a massban flag to the admin flags used on ss14 to ban large amounts of players rom a .tsv file

Co-authored-by: Geekyhobo <66805063+Ahlytlex@users.noreply.github.com>

* Automatic changelog update

* Fix missing line in nuke exploding sprite (#25351)

I could've sworn I corrected this before committing but guess not ???

* Added Evidence Markers for the Detective! (#25255)

* added evidence markers

* box tweak

* fixed a spelling mistake

* new sprites, tweaked yml too

* Add "tailed" hair (#25216)

* add

* yes

* Clean up scars.yml and add a new chest scar (#25215)

add

* Automatic changelog update

* Add new "OptionsVisualizer" (#25128)

This is a visualizer somewhat similar to the Generic. It allows configuring appearance info based on specific CVars the user has set. This allows YAML to easily configure alternatives for accessibility CVars like reduced motion.

* Suffix spelling mistake on seed vendor (#25352)

spelling error

* Update engine to v210.1.1 (#25354)

Important fixes from the UI PR

* Stop wagging tails on crit (#25323)

* Add Flammable Touch Reaction for liquid tritium

* Stop tail wagging action on crit

* Revert "Add Flammable Touch Reaction for liquid tritium"

This reverts commit 41be57b058a0cdee0cecfc51eb1c4a25631e62f3.

* Automatic changelog update

* EVA suit helmets now have (un)equip sounds (#25349)

add (un)equip sounds to EVA helms

* Automatic changelog update

* Newton Cradle Fix + Addition to Bureaucracy Crate (#25357)

fixes

makes the newton cradle not able to decimate ears while also adding it to the bureaucracy crate and lowering its volume and range a little bit

* Automatic changelog update

* Shadow anomaly respects "reduced motion" (#25355)

Enabling "reduced motion" now makes the smoke effects not animate. This helps some people with vision issues.

* Fixed directional window durability (#25259)

shit

* Very little cleanup (#25364)

* Origin Station Update 18.02.2024 (medbay update) (#25369)

* Update Credits (#25360)

Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>

* Add bio reactions (#25366)

* added more foodstuff breakdowns

* Calcium doesn't exist; moved phosphorus to protein

* tested reactions- some should be centrifuged

* more testing tweaks

* Automatic changelog update

* Increase printing price for neck clothes (#25375)

* Increase printing price

* Update clothing.yml

* Update Core (#25383)

add

* add icons for drinks in Solar's (#25301)

* add icons for drinks in Solar's

fixed colors of tea

* consistently use capitals for color codes

* don't add extra whitespace

* Automatic changelog update

* Small Nymph Changes (#25363)

* Emote sounds, accent & doorbump

* Oops

* Only non-brains can't talk

* Automatic changelog update

* Bump nixpkgs rev (#25361)

* Bump nixpkgs revision

* Update nix-direnv

* Remove extra nix functionalities

Both nix-direnv and legacy command nix-shell fail. Reverting to
flakes-only commands.

* Pin nixpkgs per suggestion

See https://github.com/space-wizards/space-station-14/pull/25361#discussion_r1494196038

* Revert nix-direnv removal

* Add python3 to shell.nix

* Adds guidebooks to the 4 learner roles (#25388)

added the books

* Automatic changelog update

* Add support for metamorphic fill levels (#25022)

* Added support for fill levels to metamorphic glasses

* Fix warnings and cleanup

* Don't break non-metamorphic fills!

* Vending UI facelift (#25377)

* Convert to fancy window + added footer + add list spacing

* margin add

* Automatic changelog update

* Make metamorphic glasses use last solution's empty sprite (#25322)

Make metamorphic glasses use empty sprite for the last solution they contained.

* Moths can eat plushies (#25382)

* Update toys.yml

* fix

* New sound of eating

* Increased eating time

* New sounds+sorting

* Automatic changelog update

* Make pills colorful and labeled (#25284)

* Make pills colorful and labeled

* Inherit pill sprites from parent

* Add Flammable Touch Reaction for liquid tritium (#25281)

* randomized sexless species now have epicene gender by default (#25282)

Fikss

* Automatic changelog update

* Mechanized treatment improvements (#25356)

Medical module merge

* Hotfix: Set round end information for replays back to null on round start instead of round end (#25394)

* Hotfix

* I dont think i need this now

* Fix action state handling bug (#25395)

* Rejig action state handling

* Fix entity arg

* Fix deserialization

* Automatic changelog update

* Fix admin notes and database time nonsense. (#25280)

God bloody christ. There's like three layers of shit here.

So firstly, apparently we were still using Npgsql.EnableLegacyTimestampBehavior. This means that time values (which are stored UTC in the database) were converted to local time when read out. This meant they were passed around as kind Local to clients (instead of UTC in the case of SQLite). That's easy enough to fix just turn off the flag and fix the couple spots we're passing a local DateTime ez.

Oh but it turns out there's a DIFFERENT problem with SQLite: See SQLite we definitely store the DateTimes as UTC, but when Microsoft.Data.Sqlite reads them it reads them as Kind Unspecified instead of Utc.

Why are these so bad? Because the admin notes system passes DateTime instances from EF Core straight to the rest of the game code. And that means it's a PAIN IN THE ASS to run the necessary conversions to fix the DateTime instances. GOD DAMNIT now I have to make a whole new set of "Record" entities so we avoid leaking the EF Core model entities. WAAAAAAA.

Fixes #19897

* PlayerListControl fixes. (#25248)

* PlayerListControl fixes.

Fix a button being selected by default always, which then can't be selected properly for real. This affected multiple admin UIs.

This broke due to upstream RT changes but ButtonGroup was always kinda busted so whatever. Uses the new IsNoneSetAllowed to implement everything properly.

Also make sure the selected player STAYS selected when filtering the list and stuff.

Also this PlayerInfo record has been changed to only do equality on the User ID because otherwise it'd need to compare each field individually which would be weird.

* Revert changes to ListContainer

This change was made default in the engine, no longer necessary here.

* Automatic changelog update

* Ore crab structural weakness (#25390)

* Attempt to change structural dmg of ore crab

* Made ore crabs susceptible to structural damage

* Automatic changelog update

* Set nav map icon textures to use bilinear filtering (#25411)

This just makes them look slightly better when zoomed.

* Fix PlayerListControl re-raising selection changed when repopulated. (#25412)

Now we just skip duplicate "item pressed" events from the ListContainer.

This caused the ahelp window to unfocus the message box after sending something. Flow is something like this: you send ahelp -> bwoink window refreshes player list due to new bwoink -> repopulated player list -> sent selection change -> repopulates right pane -> line edit gets unfocused.

* Train station (#24927)

* some content

* some next content

* fixes

* remove stained window

* return bot message?

* woah! Full map added!

* map update

* big update

* camera server

* Corvax playtest feedback

* add mail system and 30$ meteor shield

* update

* updatik

* hardwork

* pipip

* update

* remove from mappool

* fixes

* sentipode

* e

* Mindshield outline flashes, (#25409)

* Add animation support to status icons

Animated like any other entity. Change the png to have all frames, add delays in meta.json, and you're good to go.

* Dirty "fix" for the crashing.

Still have no idea why files cannot be read without changing their path in the yaml.

* Sloth review ig

I still have no idea why it wont work with /Textures/ missing as a prefix.

* Automatic changelog update

* Small Artifact Fixes (#25416)

* 20kw artifact fix

* Change that hopefully doesn't completely break storage artifacts without me realising

* Add roundid to replay_final.yml (#25398)

oopsy i forgor

* Resprite & hand position correction of Nettle & Death Nettle (#25421)

Resprite of Nettle & Death Nettle. Corrected R & L hand locations for all orientations of both plants.

* Automatic changelog update

* balance Explosive Technology (#25397)

* update arsenal yml

balance ExplosiveTechnology

* small changes in arsenal research

small changes in arsenal research

* Update arsenal.yml

* Automatic changelog update

* Tiny shove fix. (#25353)

* Remove second shove check.

* Change when popups and sounds are created.
Reduces phantom shoves that feel bad.

* why didn't i think of this i saw it earlier...

* Replaced Is fields with prefix

* remove some dependencies to fix tests???

* Automatic changelog update

* fix: пара фиксов апстрима

* Revert "Nuke fancification (#25297)"

This reverts commit 3a45d519dc.

# Conflicts:
#	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

* Revert "Shuttle floor resprite (#25127)"

This reverts commit a93466ac24.

# Conflicts:
#	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

* Revert "New lobby art : Blueprint (#25179)"

This reverts commit 90d02a5901.

# Conflicts:
#	Resources/Prototypes/lobbyscreens.yml

* Revert "Added Monitor Computer Boards to Observation Kit (#24979)"

This reverts commit c5027b51ec.

* Revert "Small t-ray scanner resprite (#25047)"

This reverts commit 601da0c3a3.

# Conflicts:
#	Resources/Textures/Objects/Tools/t-ray.rsi/tray-off.png
#	Resources/Textures/Objects/Tools/t-ray.rsi/tray-on.png

* add: переводы

* fix: фиксы под тесты

---------

Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>
Co-authored-by: Armok <155400926+ARMOKS@users.noreply.github.com>
Co-authored-by: Kevin Zheng <kevinz5000@gmail.com>
Co-authored-by: PoorMansDreams <150595537+PoorMansDreams@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com>
Co-authored-by: router <messagebus@vk.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Varen <ychwack@hotmail.it>
Co-authored-by: potato1234_x <79580518+potato1234x@users.noreply.github.com>
Co-authored-by: Hanz <41141796+Hanzdegloker@users.noreply.github.com>
Co-authored-by: Ubaser <134914314+UbaserB@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: Plykiya <plykiya@protonmail.com>
Co-authored-by: YuNii <benjamin@bhenrich.de>
Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: Mangohydra <156087924+Mangohydra@users.noreply.github.com>
Co-authored-by: Nim <128169402+Nimfar11@users.noreply.github.com>
Co-authored-by: Jezithyr <jezithyr@gmail.com>
Co-authored-by: Fluffiest Floofers <thebluewulf@gmail.com>
Co-authored-by: Jajsha <101492056+Zap527@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: forgotmyotheraccount <133569389+forgotmyotheraccount@users.noreply.github.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: Alex Nordlund <deep.alexander@gmail.com>
Co-authored-by: EdenTheLiznerd <138748328+EdenTheLiznerd@users.noreply.github.com>
Co-authored-by: deepdarkdepths <155149356+deepdarkdepths@users.noreply.github.com>
Co-authored-by: Genkail <50331122+Genkail@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: James Simonson <jamessimo89@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: Agoichi <92464780+Agoichi@users.noreply.github.com>
Co-authored-by: KREKS <132602258+xKREKSx@users.noreply.github.com>
Co-authored-by: 0x6273 <0x40@keemail.me>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Guilherme Ornel <86210200+joshepvodka@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: wafehling <wafehling@users.noreply.github.com>
Co-authored-by: Interrobang01 <113810873+Interrobang01@users.noreply.github.com>
Co-authored-by: k3yw <grenadiumdota@gmail.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Jeff <velcroboy333@hotmail.com>
Co-authored-by: Jessica M <jessica@jessicamaybe.com>
Co-authored-by: Jessica M <jessica@maybe.sh>
Co-authored-by: Zadeon <loldude9000@gmail.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Menshin <Menshin@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: liltenhead <104418166+liltenhead@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Daxxi3 <158596935+Daxxi3@users.noreply.github.com>
Co-authored-by: MACMAN2003 <macman2003c@gmail.com>
Co-authored-by: Golinth <amh2023@gmail.com>
Co-authored-by: Łukasz Mędrek <lukasz@lukaszm.xyz>
Co-authored-by: PotentiallyTom <67602105+PotentiallyTom@users.noreply.github.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: LankLTE <135308300+LankLTE@users.noreply.github.com>
Co-authored-by: Flesh <62557990+PolterTzi@users.noreply.github.com>
Co-authored-by: Arendian <137322659+Arendian@users.noreply.github.com>
Co-authored-by: Geekyhobo <66805063+Geekyhobo@users.noreply.github.com>
Co-authored-by: Geekyhobo <66805063+Ahlytlex@users.noreply.github.com>
Co-authored-by: Moomoobeef <62638182+Moomoobeef@users.noreply.github.com>
Co-authored-by: Peptide90 <78795277+Peptide90@users.noreply.github.com>
Co-authored-by: ArchPigeon <bookmaster3@gmail.com>
Co-authored-by: Killerqu00 <47712032+Killerqu00@users.noreply.github.com>
Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com>
Co-authored-by: Vigers Ray <60344369+VigersRay@users.noreply.github.com>
Co-authored-by: genderGeometries <159584039+genderGeometries@users.noreply.github.com>
Co-authored-by: hiucko <86206040+Hiucko@users.noreply.github.com>
Co-authored-by: Sybil <azurerosegarden@gmail.com>
Co-authored-by: Ioannis Eleftheriou <me@yath.xyz>
Co-authored-by: marboww <152051971+marboww@users.noreply.github.com>
Co-authored-by: veprolet <68151557+veprolet@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: Tonydatguy <154929293+Tonydatguy@users.noreply.github.com>
Co-authored-by: Gotimanga <127038462+Gotimanga@users.noreply.github.com>
Co-authored-by: MjrLandWhale <brandonemitch@gmail.com>
Co-authored-by: takemysoult <143123247+takemysoult@users.noreply.github.com>
Co-authored-by: Callmore <22885888+Callmore@users.noreply.github.com>
2024-02-23 16:05:48 +00:00

1142 lines
43 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);
Task MarkMessageAsSeen(int id);
#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)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.MarkMessageAsSeen(id));
}
// 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 : 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"));
}
}
private sealed class MSLogger : ILogger
{
private readonly ISawmill _sawmill;
public MSLogger(ISawmill sawmill)
{
_sawmill = sawmill;
}
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>
{
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));
}
private sealed class Enumerator : IAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
public Enumerator(IAsyncEnumerator<T> enumerator)
{
_enumerator = enumerator;
}
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;
}
}
}