Upstream sync (#65)

* or was it

* S: Awaiting Changes

* Temporarily make singularity a bit harder to loose as non-antag

* Fix Fluent string ID copypaste fail

* Fix the component defaults

* Changes + Cleanup

* Reduced cost of coloured light fixtures

* Bump the failsafe timer down

* Increase softcap back to 80 (#33400)

* Toggle clothing fix (#32826)

* toggle clothing fix

* some adding

* Automatic changelog update

* .NET 9 forward compatibility changes (#33421)

This doesn't switch the projects over to .NET 9, but it does make them work on .NET 9 when we decide to switch in the future.

* Fix security riot crate (#33415)

* move riot crate from security to armory category

* Move riot crate to armory, actually make it require armory access to unlock

* Localize planet dataset names (#33398)

* Localize planet names (borer)

* DatasetPrototype -> LocalizedDatasetPrototype

* Apply requested changes

* Automatic changelog update

* Remove drag & drop dropping items from containers (#32706)

* Initial commit

* Update based on maintainer discussion

* Forgot to remove this woops

* Automatic changelog update

* Automatically add "Approved" to maintainer PRs (#33337)

* Add an Approved labeler for maintainer PRs

* Be extra safe with conditions

* Crew monitoring crate updated to contain flatpacks, science access instead of engi (#33417)

* Make a crew monitoring crate with flatpacks

* fix image

* migration

* Automatic changelog update

* Add emag functionality

* Move some of the new singularity code into shared

Hopefully without explosions yay

* Fix toggle verbs (#32138)

First commit

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

* Construction menu grid view (#32577)

* button

* implement populate grid view

* tweak min width

* Make grid button toggle visible

* tweak min window size

* fix missing recipe button when mirroring item

* make grid buttons toggleable

* align button texture vertically

* selected grid item has plain color background

* tweak window width so all buttons look good

* rename select method, defer colouring

* get icon better

* whoops

* simpler button toggle

* spritesys frame0, move spritesys

* delete old sprite system refs

* Automatic changelog update

* Automatic changelog update

* Automatic changelog update

* Actually make the emagging popup work properly

* Move PlayerBeforeSpawnEvent and PlayerSpawnCompleteEvent to Shared

* Fix imports

* Automatic changelog update

* Automatic changelog update

* Automatic changelog update

* Set airlock unlit layers as invisible (#32484)

Doesn't really affect anything due to appearance bulldozing this but this aligns with their actual normal states so.

* Update salvage.yml

* Update cargo_vending.yml

* Update salvage.yml

* Update salvage.yml

* Update to Robust v237.2.0

* Syndicate item fix ups (#33435)

tweaks and fixes

* Coal presents and chrimmas tree options. Presents no longer itemify (#33147)

* Dont ensure ItemComp because it could lead to weirds, and also PickupOrDrop handles non-items already.

* presents and tree

* woops

* reviews a

* Automatic changelog update

* Fix approval labeler (#33440)

* Fix approval labeler

* Update labeler-review.yml

* Update labeler-review.yml

* electrification hud

* Fix admin ghosts not being able to see items in pockets or interact with them (#31076)

* Fix admin ghosts not being able to see items in pouches or interact with them

* fix

* oops

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* Gas pipe sensors (#33128)

* Initial commit

* Monitored pipe node is now referenced by name

* Review changes

* Simplified construction

* Tweaked deconstruction to match other binary atmos devices

* Helper function removal

* Updated attribution

* Automatic changelog update

* Add telegram to the server info-links (#33459)

* Fix sandbox error with new HWID code. (#33461)

Oops

* cleanup

* rename

* another rename

* baby proof the terminal (#33281)

* baby proof the terminal

* Make a couple exceptions for items that you might take with you.

* alwayspoweredlights

* Uncuttable cables since cablecomp is a snowflake construction system

* chairs and vendors

* rerun heisentests

* rerun tests again

* Automatic changelog update

* Automatic changelog update

* Automatic changelog update

* Disable submit admin note button on switch to note (#33456)

Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>

* Automatic changelog update

* Fix startingGear storage (#33394)

* fix starting gear storage

* removal of unused

---------

Co-authored-by: MetalSage <metalsage.official@gmail.com>

* Minor improvements & fixes to Shuttle Console UI (#31623)

* Fix grids and docks being culled from display prematurely

* Fix inconsistent disabling of "Undock" buttons

* Add a radar icon to indicate where the controlling console is

* Tidy up math

Remove lots of sketchy transforms-of-transforms, which should have been
as single matrix multiply. Assign proper names to matrices. Remove some
redundant calculations.

* Feedback

* Fix door animations mispredicting if closing is interrupted (#33481)

* Fix door animations mispredicting if closing is interrupted

On master it will flicker states a little bit partially due to it not being predicted.

Instead we'll just set it straight back to opening (no animation is ever played anyway).

* no log

* Automatic changelog update

* Don't show drag-drop outline if climbing (#33477)

It won't actually do anything.

* Automatic changelog update

* Ensure wires can always be cut (#32447)

ensure wires are always cut

* Automatic changelog update

* babyproof arrivals shuttle (#33284)

* babyproof arrivals shuttle

* always powered lights

* uncuttable cables from terminal PR.

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* Add delay to AutoOrient (#33479)

It functions identically to how V1 of orientation worked and it's incredibly annoying.

* Automatic changelog update

* Update Credits (#33503)

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

* Draw muzzle flash below mobs (#33465)

* Draw muzzle flash below mobs

* Better naming

---------

Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>

* Automatic changelog update

* Allow shuttles on planets to make FTL jump (#33507)

This check conflicts with an attempt to FTL from the planet before expedition ends

* Automatic changelog update

* fix exped caves generation (#32890)

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

* Automatic changelog update

* various material & ore inhands (#33342)

* begin

* bones + pyrotten + goliath hide inhands

* Update Resources/Prototypes/Entities/Objects/Materials/materials.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Textures/Objects/Materials/materials.rsi/meta.json

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Textures/Objects/Materials/materials.rsi/meta.json

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Textures/Objects/Materials/materials.rsi/meta.json

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* pyrottOn

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Delete HOS headset from warden's locker (#33234)

* add headset

* Add icons

* Meta change

* fix

* Revert + delete headset from locker

* RCD icons resprite (#2800)

* fix airlocks inconsistently auto-closing after unbolting (#33524)

fix door auto close timer

* Automatic changelog update

* baby proof the terminal (#33281)

* baby proof the terminal

* Make a couple exceptions for items that you might take with you.

* alwayspoweredlights

* Uncuttable cables since cablecomp is a snowflake construction system

* chairs and vendors

* rerun heisentests

* rerun tests again

* babyproof arrivals shuttle (#33284)

* babyproof arrivals shuttle

* always powered lights

* uncuttable cables from terminal PR.

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* [BUGFIX] "Ghost" in the lobby lets you see the whole chat (#33529)

* fix bug, in ghost command lobby

* fix

* Fix build

* Automatic changelog update

* removed obsolete netmessage creator (#33542)

removed opsolete netmessage createor

* Fix RA0003 warning for ChatBox (#33531)

* Shark plushies now goes rawr on hit. (#33540)

Shark goes rawr more

* Automatic changelog update

* Turn off PointLights on VendingMachines when broken or off. (#33513)

The light itself should already turn off due to `LitOnPowered`
component, but the broken state of a VendingMachine did not.

Fixes  #33382

* Automatic changelog update

* Adds more diona names (#33066)

* adds more diona names

* more stuff

* AHHHHHHHHHHHHHHHHHHHHHHH

* further additions

* removes depression + adds comment

* fixes + remove some weird stuff + more stuff

* remove haste

* minor AI cleanup (#33555)

* minor cleanup

* to

* Simplify separated screen top menu (#33047)

* Automatic changelog update

* [Maps] Maus Winter Update (#2803)

* [MAPS] Paper tweak (#2804)

* Automatic changelog update

* Ghost role drop-down alignment (#33397)

* dropdown shares margin width with children

* removed dependency that rider added for some reason

* reduced vertical margin from 8 to 2

* Greytide Virus station event (#33547)

* proof of concept

* full implementation

* I commited a crime

* t

* min players increase

* Make shuttle airlocks not snapcardinals (#33557)

* Make shuttle airlocks not snapcardinals

* Update Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

---------

Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* [MAPS] Avrite winter (#2806)

* Added two N2 lockers to Reach (#33409)

Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~>

* Hotfix the randomly occurring DeleteAllThenGhost test failures (#33582)

* clear mindrole on component shutdown

* let it go

* Fix space ambient music (#33594)

* Automatic changelog update

* Fix windoor and high security door not showing electrocution HUD (#33551)

* Automatic changelog update

* [Maps] Awesome new god update (#2811)

* [Maps] Avrite minor change (#2810)

* Automatic changelog update

* Removes burnt tiles (#33422)

remove burnt tiles

* Parallax pack 2 (#2813)

* Added Oppenhopper poster to the game. (#33588)

* o7

* -

* oppenhopper v2

* Update Resources/Prototypes/Entities/Structures/Wallmounts/Signs/posters.yml

* Update Resources/Prototypes/Entities/Structures/Wallmounts/Signs/posters.yml

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Fix version for electril_grill meta.json (#33611)

fix

fixing the version thing in meta json for electril grill textures

Co-authored-by: Arthur Kustenko <arthur.kustenko@learnet.se>

* Fix gauze eyepatch flying pixel (#33564)

boo

* Fix swapped uniform printer east/west sprites (#33442)

Fix uniform printer sprites rotating the wrong way

* Makes knives fly straight when thrown (#33615)

Gives knives a thrown angle

* Light verb is now predicted (#33622)

Fix

* Automatic changelog update

* Update `Content.PatreonParser` to use `net8.0` `TargetFramework` (#33559)

Update Content.PatreonParse to use net8.0 targetframework

* Approval labeler fix electric boogaloo (#33633)

Nik told me to not name this "nya"

* Fix gender, maybe (#33631)

Co-authored-by: Alpha-Two <alpha2.5232@gmail.com>

* Display GPS coordinates on their own line (#33625)

* Automatic changelog update

* Remove grasshopper from the panic bunker message (#33638)

Can be retargetted to stable if preferred, which will then be a hotfix

* Add admin log for ghost warping (#33636)

* Automatic changelog update

* Rename nitrogen internals crate (#33545)

* rename nitrogen internals crate

* nitrogen internals crate description

* migrate CrateNitrogenInternals ID

* Automatic changelog update

* Cleanup some Client atmos systems (#33634)

* Cleanup `ScrubberControl.xaml.cs`

* Minor cleanups

* Another pile of minor cleanups

* Apply requested changes

* Rename "which" into "bound". Add whitespace after "if"

* Fix for arrivals deleting nuke (#33659)

Adds FTLSmashImmune to nuke prototype to stop it from arrival smashing
Adds a comment in FasterThanLight to indicate where the FTLSmashImmuneComponent is checked

Co-authored-by: aa5g21 <aa5g21@soton.ac.uk>

* Automatic changelog update

* add locale to Shuttle Console Map tab (#33651)

fixed

* Automatic changelog update

* fix PermanentBlindnessComponent to be not so permanent (#33292)

* adjust min blindness back to 0 when PermanentBlindnessComponent is removed

* mapinit changes

* remove OnRemove, move changes to OnShutdown

* goodbye event

* dependency removal

* final adjustment

---------

Co-authored-by: lunarcomets <luanrcomets2@gmail,com>

* Cog/Marathon: Airlock fixes (#33621)

* Fixes the TEG airlock on marathon

* Fixes the atmos external airlock on cog

* Automatic changelog update

* Revert "Rebalancing zombie mode chances" (#2818)

* Automatic changelog update

* [Maps] Silly Winter Update (#2809)

* [Maps] Pilgrim update #7 — Winter (#2821)

* Fix for handcuffing someone more than once (#33646)

* Fix for over-cuffing someone

* comment

* Automatic changelog update

* Fix for inspecting entities in hand (#33642)

Actually inspect entity in hands

* Automatic changelog update

* Fix for inspecting entities in the stripping window (#33644)

Handle it

* Automatic changelog update

* Fix borgs not getting names on roundstart (#33578)

* Fix borgs not getting names on roundstart

* Glory to the NT

* Allow riggable to take in multiple reagents

* Revert

---------

Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>

* Automatic changelog update

* Require Wield To Activate Double-bladed ESword (#32869)

* Add MeleeRequiresWield component

* Prevent world activation

* Automatic changelog update

* Fix makeghostroleraffle command where 4 arguments (#31836)

Fix makeghostroleraffle command with 4 arguments

* Automatic changelog update

* Update Credits (#33670)

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

* Fix formatting IDE0055 warnings in VS Code (#33669)

* Replace obsolete GetTilesIntersecting methods (#32455)

* Remove usage of obsolete GetTilesIntersecting round 1

* Oop wrong uids

* Remove usage of obsolete GetLocalTilesIntersecting round 2

* Remove usage of obsolete GetLocalTilesIntersecting final round

* weh

* Fix using `SharedMapSystem` in `StencilOverlay`

* delta winter (#2824)

* Update Snowasis (#33364)

* Update Snowasis

* Appease test gods

* Woops had the leftward animation backwards. Fixed.

* add santa suits and envelopes to chapel

* It looks worse unscaled but whatever.

* fix ghost role

* examines

* Automatic changelog update

* [MAPS] Paper Cristmass update (#2822)

* Fix wrong system usage (#33679)

Fix wrong system using

* resprite new year nuke (#2825)

* Fix BuckleSystem always marking InteractHandEvent as Handled (#33602)

* Add check before marking event as handled

* Update Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Cleanup

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>

* Automatic changelog update

* Touching up Bagel Security Brig (#33680)

* Touching up Bagel Security Brig

* Added a few missing door names

* Last door name I promise

* Address feedback

* Forgot to delete old shutter button

---------

Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~>

* [Maps] Tushkan update #3 — Winter (#2826)

* Map pool tweak (#2827)

* Fixed ghost role rules for some syndicate familiars (#32457)

* fix ghost role rules for some syndicate familiars

* change from monkey rules to Team Antag rules.

* Resolve reviews + Fix rules for LoneOp and Xenos

* ghostrole rules

---------

Co-authored-by: JIPDawg <JIPDawg93@gmail.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>

* Automatic changelog update

* Fix lobby countdown not showing hours (#33685)

* New Low-Mid Pop Station - Amber Station  (#33441)

* Initial Commit with two departments done

* Checkpointing work

* Added most of service and science

* Started work on medbay

* Modified TEG setup and set up atmos pipes for engineering

* Re-added medbay, added chapel and janitorial, started routing disposals and generally the final layout of the map is coming together

* Plenty of additions, too many to list in a commit message

* Major update

* Nearly done with the map, just have to do decals and emergency lights

* Added all decals, only thing missing is department signs

* Toned back the dirt decals a bunch, added hallway signs and other decorations

* Finishing touches on the map before testing.

* Fixed invalids

* Renamed the station to Amber, and made a couple adjustments

* Completely redid cargo, added maints around sec

* Added Amber to the map post init integration test

* Many small updates addressing issues.

* Ran a script to update all the camera names, also ran fixgridatmos, fixrotations, tilewalls, and variantize

* Started addressing some of the issues brough up by Emisse and others

* Addressed all issues mentioned by reviewers. Added mail system.

* Wrapped up meeting all the issues raised by reviewers, also did a bunch of testing and resolved issues found during those tests

* Addressed additional requested changes, and nabbed some ship designs from Frontier. Redesigned south east maints and surrounding meteor nets based off of those designs

* Making a couple more requested changes

* Reduced the amount of Nitrogen Closets, made several small adjustments. Ready for review again!

* Removed invalids!

* web edit lmao

---------

Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>

* Automatic changelog update

* Makes kukri not use combat knife's storage sprite (#33661)

* Adds a morgue locked maints airlock (#33693)

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

* Juiceable slimeballs (#33660)

adds extractable component to slimeball prototype

* Add missing nacho recipes (#33637)

* Nacho recipes, nutrition, and trash

In meal_recipes.yml: Add recipes for Nachos, Cheesy Nachos, and Cuban Nachos.

In meals.yml: Add a solutions container to regular nachos so it lines up with the others, and add a small plate as trash to each one.

* Volumes, nutritional tweaks, and flavor

In meals.yml: Lowered maxVol on nachos to leave 5 units of space. Nachos nutriment and vitamin lowered for costing so little. A cheese wedge is 3.75 nutriment, so cheesy having 4 more total nutritional value lines up nicely to me.

Cuban nachos had too little volume to fit its reagents in the first place, so increased maxVol by 10. Chili peppers are 4 nutri / 4 vitamin and ketchup has a touch of tomato, so bumped to 8 and 5. Capsaicin lowered partly to make it an even 20u. Ketchup dilutes I guess. Also they don't have cheese so I changed the flavor profile to tomato instead.

In meal_recipes.yml: Cuban Nachos recipe tweaked to require one less pepper. Given above numbers, this fits the nutritional value imo.

* Automatic changelog update

* Amber Station Seasonal Update (#33698)

Made some modifications requested by reviewers prior to and after map merge, also added holiday decorations!

* [Maps] Pilgrim update #8 & Silly tweak (#2828)

* Amber Improvements (#33707)

* Various changes (see PR)

* variantized, fixedgridatmos, tiledwalls

* Fix sinks and toilets not draining (#33691)

* Fix AutoDrain

Per the system comments, AutoDrain is designed to automatically move
puddles into the drain (like a floor drain). Drains without AutoDrain
are still supposed to gradually empty the buffer, but not remove puddles
(like sinks and toilets).

However, a logic error in the original implementation causes drains with
AutoDrain set to false to simply not work. Hence sinks never emptied.

* Update documentation

* Automatic changelog update

* Fix layout on wires UI (#33714)

Layout would break for machines with >6 lights because the column count was hardcoded. Uncap the UI width and fix the rows count instead.

Lights with less than 4 characters of text weren't aligned right, now they are.

* Automatic changelog update

* Fix for towels not having a cooldown for cleaning (#33700)

Added a delay to cleaning with the towel

* Add cooldown to buttons in borg's laws UI (#31490)

* Adds Store on Collide and Wand of the Locker (#33710)

* Adds wand of locker and locker projectile

* Adds IsOpen method to check if storage is open

* Adds store on collide

* Adds Store On Collide to Wizard Locker

* Adds Lock API

* Adds locking support

* Adds resist override and custom visual layers

* Fixes decursed states, adds comment for a future visualizer

* adds locker wand visuals and descriptions

* shrinks locker radius, moves TODO for throw support

* Adds whitelist and moves storage and lock logic into their own methods

* Adds support to disable store on collide after the first open. Fixes prediction issues with disabling.

* Adds wand of locker to the grimoire

* Adds wizard access prototype

* Adds Wizard to universal access

* Moves Lock on collide to on collide method

* Comments

* Changes layer order

* Fixes prediction issues when locking.

* Adds Wiz access to universal ID

* Automatic changelog update

* Update locale

* Fix upstream

* update server configs (#2799)

* Fix displacements map for female vulp and reptilian (#2832)

Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>

* Translate Upstream #2748 (#2837)

Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com>

* Перевод Upstream #2830  (#2836)

Co-authored-by: lapatison <lapatisonsocial@gmail.com>
Co-authored-by: NotSoDamn <75203942+NotSoDana@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: cfif126 <94059374+cfif126@users.noreply.github.com>

* translate borer names (#2835)

* [Localize] Poster 52 (#2840)

* Translate new diona names (#2839)

Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com>

* [Resprite] Medical uniforms & rollerbeds (#2802)

* Automatic changelog update

* Revert "new year lobbyscreens removed" (#2831)

* Surgeon spawnpoint (#2841)

Added to support map in corvax next
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>

* fix nukedisk chances (#2745)

* Adds surgeon spawnpoint on maps  (#2845)

* Winter stuff tweak (#2833)

* [MAPS] Silly Christmass Date  (#2854)

* [MAPS] Paper Christmass Date (#2853)

* [MAPS] Avrite New Year update (#2852)

* [Maps] Tushkan new year update (#2851)

* [Maps] Pilgrim new year update (#2850)

* [Maps] Maus new year update (#2849)

* [Maps] Awesome new year update (#2848)

* [Maps] Outpost return (#2847)

Co-authored-by: kvant8 <ar4477023rr.@gmail.com>
Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com>

* [Maps] Astra new year update (#2844)

* Update corvax_delta.yml (#2856)

* [Maps] Pilgrim hotfix #2 (#2860)

* Resprite botany food (#2858)

* Fix spacelaw loadout (#2861)

* Make Jolene TTS voice female (#2834)

* Update configs (#2855)

* New Year Music (#2829)

* Revert "Удаление лимита времени для доступа к стартовым ролям." (#2652)

* Automatic changelog update

* Automatic changelog update

* [Maps] Glacier new year update (#2864)

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Saphire <lattice@saphi.re>
Co-authored-by: Justice League <the.justice.league.of.canada@gmail.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ArZarLordOfMango <96249677+ArZarLordOfMango@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: qwerltaz <69696513+qwerltaz@users.noreply.github.com>
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: ThatGuyUSA <thatguyusa123@gmail.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: c4llv07e <igor@c4llv07e.xyz>
Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com>
Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>
Co-authored-by: MetalSage <74924875+MetalSage@users.noreply.github.com>
Co-authored-by: MetalSage <metalsage.official@gmail.com>
Co-authored-by: eoineoineoin <github@eoinrul.es>
Co-authored-by: goet <6637097+goet@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: mubururu_ <139181059+muburu@users.noreply.github.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: SonicHDC <100022571+SonicHDC@users.noreply.github.com>
Co-authored-by: Schrödinger <132720404+Schrodinger71@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Ben <benjaminevanownby@gmail.com>
Co-authored-by: Nikolai Korolev <CrafterKolyan@mail.ru>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Niels Huylebroeck <red15@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Meguneri <163569304+Meguneri@users.noreply.github.com>
Co-authored-by: TiFeRi <57865696+XsenonDash@users.noreply.github.com>
Co-authored-by: IanComradeBot <96892333+IanComradeBot@users.noreply.github.com>
Co-authored-by: Intoxicating-Innocence <188202277+Intoxicating-Innocence@users.noreply.github.com>
Co-authored-by: CaptainMaru <124701378+CaptainMaru@users.noreply.github.com>
Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com>
Co-authored-by: dylanstrategie <188926747+dylanstrategie@users.noreply.github.com~>
Co-authored-by: Stubaretka24 <143966631+Stubaretka24@users.noreply.github.com>
Co-authored-by: AlexUm <159550239+AlexUm418@users.noreply.github.com>
Co-authored-by: Arthur Kustenko <arthur.kustenko@learnet.se>
Co-authored-by: Mifia <xzx._@outlook.com>
Co-authored-by: SpaceManiac <tad@platymuus.com>
Co-authored-by: SpaceRox1244 <138547931+SpaceRox1244@users.noreply.github.com>
Co-authored-by: Alpha-Two <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Alpha-Two <alpha2.5232@gmail.com>
Co-authored-by: Alice "Arimah" Heurlin <30327355+arimah@users.noreply.github.com>
Co-authored-by: MossyGreySlope <mossygreyslope@gmail.com>
Co-authored-by: Gansu <68031780+GansuLalan@users.noreply.github.com>
Co-authored-by: aa5g21 <aa5g21@soton.ac.uk>
Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com>
Co-authored-by: lunarcomets <140772713+lunarcomets@users.noreply.github.com>
Co-authored-by: lunarcomets <luanrcomets2@gmail,com>
Co-authored-by: compilatron <40789662+jbox144@users.noreply.github.com>
Co-authored-by: Kill_Me_I_Noobs <118206719+Vonsant@users.noreply.github.com>
Co-authored-by: Doctorjakes <122163951+Doctorjakes@users.noreply.github.com>
Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com>
Co-authored-by: Preston Smith <92108534+thetolbean@users.noreply.github.com>
Co-authored-by: Token <56667933+TokenStyle@users.noreply.github.com>
Co-authored-by: Nikolai Korolev <korolevns98@gmail.com>
Co-authored-by: Bloodcanis <113198922+Bloodcanis@users.noreply.github.com>
Co-authored-by: JIPDawg <51352440+JIPDawg@users.noreply.github.com>
Co-authored-by: JIPDawg <JIPDawg93@gmail.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: Luiz Costa <33888056+luizwritescode@users.noreply.github.com>
Co-authored-by: CheddaCheez <cheddacheezy@gmail.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: Tap <tapiocaphobe@gmail.com>
Co-authored-by: Hreno <hrenor@gmail.com>
Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com>
Co-authored-by: Morb0 <14136326+Morb0@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Litogin <139079015+Litogin@users.noreply.github.com>
Co-authored-by: lapatison <100279397+lapatison@users.noreply.github.com>
Co-authored-by: lapatison <lapatisonsocial@gmail.com>
Co-authored-by: NotSoDamn <75203942+NotSoDana@users.noreply.github.com>
Co-authored-by: cfif126 <94059374+cfif126@users.noreply.github.com>
Co-authored-by: MureixloI <132683811+MureixloI@users.noreply.github.com>
Co-authored-by: kvant8 <163752943+kvant8@users.noreply.github.com>
Co-authored-by: kvant8 <ar4477023rr.@gmail.com>
Co-authored-by: lastPechkin <mevlyutov1958@gmail.com>
Co-authored-by: AwareFoxy <135021509+AwareFoxy@users.noreply.github.com>
This commit is contained in:
mosleyos
2024-12-17 22:37:21 +03:00
committed by GitHub
parent 7470af64de
commit 049ba95a0d
674 changed files with 599224 additions and 281329 deletions

23
.github/workflows/labeler-review.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: "Labels: Approved"
on:
pull_request_review:
types: [submitted]
jobs:
add_label:
# Change the repository name after you've made sure the team name is correct for your fork!
if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }}
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: tspascoal/get-user-teams-membership@v3
id: checkUserMember
with:
username: ${{ github.actor }}
team: "content-maintainers,junior-maintainers"
GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
- if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
uses: actions-ecosystem/action-add-labels@v1
with:
labels: "S: Approved"

View File

@@ -258,13 +258,13 @@ namespace Content.Client.Actions
public void LinkAllActions(ActionsComponent? actions = null)
{
if (_playerManager.LocalEntity is not { } user ||
!Resolve(user, ref actions, false))
{
return;
}
if (_playerManager.LocalEntity is not { } user ||
!Resolve(user, ref actions, false))
{
return;
}
LinkActions?.Invoke(actions);
LinkActions?.Invoke(actions);
}
public override void Shutdown()

View File

@@ -22,11 +22,11 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow
{
public event Action<string?, (IPAddress, int)?, bool, byte[]?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<string>? PlayerChanged;
private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; }
private byte[]? Hwid { get; set; }
private ImmutableTypedHwid? Hwid { get; set; }
private double TimeEntered { get; set; }
private uint Multiplier { get; set; }
private bool HasBanFlag { get; set; }
@@ -371,9 +371,8 @@ public sealed partial class BanPanel : DefaultWindow
private void OnHwidChanged()
{
var hwidString = HwidLine.Text;
var length = 3 * (hwidString.Length / 4) - hwidString.TakeLast(2).Count(c => c == '=');
Hwid = new byte[length];
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !Convert.TryFromBase64String(hwidString, Hwid, out _))
ImmutableTypedHwid? hwid = null;
if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !ImmutableTypedHwid.TryParse(hwidString, out hwid))
{
ErrorLevel |= ErrorLevelEnum.Hwid;
HwidLine.ModulateSelfOverride = Color.Red;
@@ -390,7 +389,7 @@ public sealed partial class BanPanel : DefaultWindow
Hwid = null;
return;
}
Hwid = Convert.FromHexString(hwidString);
Hwid = hwid;
}
private void OnTypeChanged()

View File

@@ -159,6 +159,7 @@ public sealed partial class NoteEdit : FancyWindow
SecretCheckBox.Pressed = false;
SeverityOption.Disabled = false;
PermanentCheckBox.Pressed = true;
SubmitButton.Disabled = true;
UpdatePermanentCheckboxFields();
break;
case (int) NoteType.Message: // Message: these are shown to the player when they log on

View File

@@ -1,8 +1,4 @@
using Robust.Shared.GameObjects;
namespace Content.Client.Atmos.Components;
[RegisterComponent]
public sealed partial class PipeColorVisualsComponent : Component
{
}
public sealed partial class PipeColorVisualsComponent : Component;

View File

@@ -1,6 +1,5 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:Content.Client.Stylesheets"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical" HorizontalExpand ="True" Margin="0 0 0 3">
@@ -62,7 +61,7 @@
</PanelContainer>
</BoxContainer>
<!-- If the alarm is inactive, this is label is diplayed instead -->
<!-- If the alarm is inactive, this is label is displayed instead -->
<Label Name="NoDataLabel" Text="{Loc 'atmos-alerts-window-no-data-available'}" HorizontalAlignment="Center" Margin="0 15" FontColorOverride="#a9a9a9" ReservesSpace="False" Visible="False"></Label>
<!-- Silencing progress bar -->

View File

@@ -136,8 +136,9 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
GasGridContainer.RemoveAllChildren();
var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen);
var keyValuePairs = gasData.ToList();
if (gasData.Count() == 0)
if (keyValuePairs.Count == 0)
{
// No other gases
var gasLabel = new Label()
@@ -158,13 +159,11 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
else
{
// Add an entry for each gas
foreach ((var gas, (var mol, var percent, var alert)) in gasData)
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
var gasPercent = (FixedPoint2)0f;
gasPercent = percent * 100f;
FixedPoint2 gasPercent = percent * 100f;
if (!_gasShorthands.TryGetValue(gas, out var gasShorthand))
gasShorthand = "X";
var gasShorthand = _gasShorthands.GetValueOrDefault(gas, "X");
var gasLabel = new Label()
{

View File

@@ -14,8 +14,6 @@ public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
_menu = new AtmosAlertsComputerWindow(this, Owner);
_menu.OpenCentered();
_menu.OnClose += Close;
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -24,9 +22,6 @@ public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
var castState = (AtmosAlertsComputerBoundInterfaceState) state;
if (castState == null)
return;
EntMan.TryGetComponent<TransformComponent>(Owner, out var xform);
_menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData);
}

View File

@@ -1,7 +1,6 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'atmos-alerts-window-title'}"
Resizable="False"
SetSize="1120 750"

View File

@@ -4,8 +4,6 @@ using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Client.Atmos.EntitySystems;
@@ -19,7 +17,7 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<PipeAppearanceComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PipeAppearanceComponent, AppearanceChangeEvent>(OnAppearanceChanged, after: new[] { typeof(SubFloorHideSystem) });
SubscribeLocalEvent<PipeAppearanceComponent, AppearanceChangeEvent>(OnAppearanceChanged, after: [typeof(SubFloorHideSystem)]);
}
private void OnInit(EntityUid uid, PipeAppearanceComponent component, ComponentInit args)
@@ -84,7 +82,8 @@ public sealed class AtmosPipeAppearanceSystem : EntitySystem
layer.Visible &= visible;
if (!visible) continue;
if (!visible)
continue;
layer.Color = color;
}

View File

@@ -1,12 +1,7 @@
using System.Collections.Generic;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Power;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Atmos.Monitor;
@@ -27,7 +22,7 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
{
foreach (var visLayer in component.HideOnDepowered)
{
if (args.Sprite.LayerMapTryGet(visLayer, out int powerVisibilityLayer))
if (args.Sprite.LayerMapTryGet(visLayer, out var powerVisibilityLayer))
args.Sprite.LayerSetVisible(powerVisibilityLayer, powered);
}
}
@@ -36,7 +31,7 @@ public sealed class AtmosAlarmableVisualsSystem : VisualizerSystem<AtmosAlarmabl
{
foreach (var (setLayer, powerState) in component.SetOnDepowered)
{
if (args.Sprite.LayerMapTryGet(setLayer, out int setStateLayer))
if (args.Sprite.LayerMapTryGet(setLayer, out var setStateLayer))
args.Sprite.LayerSetState(setStateLayer, new RSI.StateId(powerState));
}
}

View File

@@ -1,11 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Content.Client.Atmos.Monitor.UI;
@@ -78,6 +74,7 @@ public sealed class AirAlarmBoundUserInterface : BoundUserInterface
{
base.Dispose(disposing);
if (disposing) _window?.Dispose();
if (disposing)
_window?.Dispose();
}
}

View File

@@ -8,7 +8,6 @@ using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -59,7 +58,7 @@ public sealed partial class AirAlarmWindow : FancyWindow
AirAlarmMode.Fill => "air-alarm-ui-mode-fill",
AirAlarmMode.Panic => "air-alarm-ui-mode-panic",
AirAlarmMode.None => "air-alarm-ui-mode-none",
_ => "error"
_ => "error",
};
_modes.AddItem(Loc.GetString(text));
}
@@ -70,7 +69,7 @@ public sealed partial class AirAlarmWindow : FancyWindow
AirAlarmModeChanged!.Invoke((AirAlarmMode) args.Id);
};
_autoMode.OnToggled += args =>
_autoMode.OnToggled += _ =>
{
AutoModeChanged!.Invoke(_autoMode.Pressed);
};
@@ -176,22 +175,18 @@ public sealed partial class AirAlarmWindow : FancyWindow
public static Color ColorForThreshold(float amount, AtmosAlarmThreshold threshold)
{
threshold.CheckThreshold(amount, out AtmosAlarmType curAlarm);
threshold.CheckThreshold(amount, out var curAlarm);
return ColorForAlarm(curAlarm);
}
public static Color ColorForAlarm(AtmosAlarmType curAlarm)
{
if(curAlarm == AtmosAlarmType.Danger)
return curAlarm switch
{
return StyleNano.DangerousRedFore;
}
else if(curAlarm == AtmosAlarmType.Warning)
{
return StyleNano.ConcerningOrangeFore;
}
return StyleNano.GoodGreenFore;
AtmosAlarmType.Danger => StyleNano.DangerousRedFore,
AtmosAlarmType.Warning => StyleNano.ConcerningOrangeFore,
_ => StyleNano.GoodGreenFore,
};
}

View File

@@ -1,12 +1,8 @@
using System;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets;
@@ -25,7 +21,7 @@ public sealed partial class PumpControl : BoxContainer
private OptionButton _pressureCheck => CPressureCheck;
private FloatSpinBox _externalBound => CExternalBound;
private FloatSpinBox _internalBound => CInternalBound;
private Button _copySettings => CCopySettings;
private Button _copySettings => CCopySettings;
public PumpControl(GasVentPumpData data, string address)
{
@@ -86,7 +82,7 @@ public sealed partial class PumpControl : BoxContainer
_data.PressureChecks = (VentPressureBound) args.Id;
PumpDataChanged?.Invoke(_address, _data);
};
_copySettings.OnPressed += _ =>
{
PumpDataCopied?.Invoke(_data);

View File

@@ -1,15 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Atmos.Monitor.UI.Widgets;
@@ -27,7 +21,7 @@ public sealed partial class ScrubberControl : BoxContainer
private OptionButton _pumpDirection => CPumpDirection;
private FloatSpinBox _volumeRate => CVolumeRate;
private CheckBox _wideNet => CWideNet;
private Button _copySettings => CCopySettings;
private Button _copySettings => CCopySettings;
private GridContainer _gases => CGasContainer;
private Dictionary<Gas, Button> _gasControls = new();
@@ -77,7 +71,7 @@ public sealed partial class ScrubberControl : BoxContainer
_data.PumpDirection = (ScrubberPumpDirection) args.Id;
ScrubberDataChanged?.Invoke(_address, _data);
};
_copySettings.OnPressed += _ =>
{
ScrubberDataCopied?.Invoke(_data);

View File

@@ -43,7 +43,8 @@ public sealed partial class SensorInfo : BoxContainer
var label = new RichTextLabel();
var fractionGas = amount / data.TotalMoles;
label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator", ("gas", $"{gas}"),
label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator",
("gas", $"{gas}"),
("color", AirAlarmWindow.ColorForThreshold(fractionGas, data.GasThresholds[gas])),
("amount", $"{amount:0.####}"),
("percentage", $"{(100 * fractionGas):0.##}")));
@@ -53,9 +54,9 @@ public sealed partial class SensorInfo : BoxContainer
var threshold = data.GasThresholds[gas];
var gasThresholdControl = new ThresholdControl(Loc.GetString($"air-alarm-ui-thresholds-gas-title", ("gas", $"{gas}")), threshold, AtmosMonitorThresholdType.Gas, gas, 100);
gasThresholdControl.Margin = new Thickness(20, 2, 2, 2);
gasThresholdControl.ThresholdDataChanged += (type, threshold, arg3) =>
gasThresholdControl.ThresholdDataChanged += (type, alarmThreshold, arg3) =>
{
OnThresholdUpdate!(_address, type, threshold, arg3);
OnThresholdUpdate!(_address, type, alarmThreshold, arg3);
};
_gasThresholds.Add(gas, gasThresholdControl);
@@ -64,7 +65,8 @@ public sealed partial class SensorInfo : BoxContainer
_pressureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-pressure-title"), data.PressureThreshold, AtmosMonitorThresholdType.Pressure);
PressureThresholdContainer.AddChild(_pressureThreshold);
_temperatureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"), data.TemperatureThreshold,
_temperatureThreshold = new ThresholdControl(Loc.GetString("air-alarm-ui-thresholds-temperature-title"),
data.TemperatureThreshold,
AtmosMonitorThresholdType.Temperature);
TemperatureThresholdContainer.AddChild(_temperatureThreshold);
@@ -103,7 +105,8 @@ public sealed partial class SensorInfo : BoxContainer
}
var fractionGas = amount / data.TotalMoles;
label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator", ("gas", $"{gas}"),
label.SetMarkup(Loc.GetString("air-alarm-ui-gases-indicator",
("gas", $"{gas}"),
("color", AirAlarmWindow.ColorForThreshold(fractionGas, data.GasThresholds[gas])),
("amount", $"{amount:0.####}"),
("percentage", $"{(100 * fractionGas):0.##}")));

View File

@@ -1,7 +1,4 @@
using Content.Client.Message;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;

View File

@@ -1,12 +1,8 @@
using System;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
// holy FUCK
// this technically works because some of this you can *not* do in XAML but holy FUCK
@@ -115,29 +111,38 @@ public sealed partial class ThresholdControl : BoxContainer
_enabled.Pressed = !_threshold.Ignore;
}
private String LabelForBound(string boundType) //<todo.eoin Replace this with enums
private string LabelForBound(string boundType) //<todo.eoin Replace this with enums
{
return Loc.GetString($"air-alarm-ui-thresholds-{boundType}");
}
public void UpdateThresholdData(AtmosAlarmThreshold threshold, float currentAmount)
{
threshold.CheckThreshold(currentAmount, out AtmosAlarmType alarm, out AtmosMonitorThresholdBound which);
threshold.CheckThreshold(currentAmount, out var alarm, out var bound);
var upperDangerState = AtmosAlarmType.Normal;
var lowerDangerState = AtmosAlarmType.Normal;
var upperWarningState = AtmosAlarmType.Normal;
var lowerWarningState = AtmosAlarmType.Normal;
if(alarm == AtmosAlarmType.Danger)
switch (alarm)
{
if(which == AtmosMonitorThresholdBound.Upper) upperDangerState = alarm;
else lowerDangerState = alarm;
}
else if(alarm == AtmosAlarmType.Warning)
{
if(which == AtmosMonitorThresholdBound.Upper) upperWarningState = alarm;
else lowerWarningState = alarm;
case AtmosAlarmType.Danger:
{
if (bound == AtmosMonitorThresholdBound.Upper)
upperDangerState = alarm;
else
lowerDangerState = alarm;
break;
}
case AtmosAlarmType.Warning:
{
if (bound == AtmosMonitorThresholdBound.Upper)
upperWarningState = alarm;
else
lowerWarningState = alarm;
break;
}
}
_upperBoundControl.SetValue(threshold.UpperBound.Value);

View File

@@ -1,3 +1,4 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
using Content.Client.Atmos.EntitySystems;
@@ -101,14 +102,9 @@ public sealed class AtmosDebugOverlay : Overlay
else
{
// Red-Green-Blue interpolation
if (interp < 0.5f)
{
res = Color.InterpolateBetween(Color.Red, Color.LimeGreen, interp * 2);
}
else
{
res = Color.InterpolateBetween(Color.LimeGreen, Color.Blue, (interp - 0.5f) * 2);
}
res = interp < 0.5f
? Color.InterpolateBetween(Color.Red, Color.LimeGreen, interp * 2)
: Color.InterpolateBetween(Color.LimeGreen, Color.Blue, (interp - 0.5f) * 2);
}
res = res.WithAlpha(0.75f);
@@ -181,7 +177,10 @@ public sealed class AtmosDebugOverlay : Overlay
handle.DrawCircle(tileCentre, 0.05f, Color.Black);
}
private void CheckAndShowBlockDir(AtmosDebugOverlayData data, DrawingHandleWorld handle, AtmosDirection dir,
private void CheckAndShowBlockDir(
AtmosDebugOverlayData data,
DrawingHandleWorld handle,
AtmosDirection dir,
Vector2 tileCentre)
{
if (!data.BlockDirection.HasFlag(dir))
@@ -243,7 +242,7 @@ public sealed class AtmosDebugOverlay : Overlay
var moles = data.Moles == null
? "No Air"
: data.Moles.Sum().ToString();
: data.Moles.Sum().ToString(CultureInfo.InvariantCulture);
handle.DrawString(_font, pos, $"Moles: {moles}");
pos += offset;
@@ -263,7 +262,12 @@ public sealed class AtmosDebugOverlay : Overlay
private void GetGrids(MapId mapId, Box2Rotated box)
{
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, box, ref _grids, (EntityUid uid, MapGridComponent grid,
_mapManager.FindGridsIntersecting(
mapId,
box,
ref _grids,
(EntityUid uid,
MapGridComponent grid,
ref List<(Entity<MapGridComponent>, DebugMessage)> state) =>
{
if (_system.TileData.TryGetValue(uid, out var data))

View File

@@ -7,7 +7,7 @@ using Robust.Client.GameObjects;
namespace Content.Client.Clothing;
public sealed class FlippableClothingVisualizerSystem : VisualizerSystem<FlippableClothingVisualsComponent>
public sealed class FlippableClothingVisualizerSystem : VisualizerSystem<FlippableClothingVisualsComponent>
{
[Dependency] private readonly SharedItemSystem _itemSys = default!;

View File

@@ -1,15 +1,20 @@
<DefaultWindow xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4" Margin="0 0 5 0">
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/>
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.6">
<Button Name="FavoriteButton" Visible="false" HorizontalExpand="False"
HorizontalAlignment="Right" Margin="0 0 0 15"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<Button Name="MenuGridViewButton" ToggleMode="True" Text="{Loc construction-menu-grid-view}"/>
<Button Name="FavoriteButton" Visible="false"/>
</BoxContainer>
<Control>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" Align="Center">

View File

@@ -25,11 +25,16 @@ namespace Content.Client.Construction.UI
OptionButton OptionCategories { get; }
bool EraseButtonPressed { get; set; }
bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
ItemList Recipes { get; }
ItemList RecipeStepList { get; }
ScrollContainer RecipesGridScrollContainer { get; }
GridContainer RecipesGrid { get; }
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler<ItemList.Item?> RecipeSelected;
event EventHandler RecipeFavorited;
@@ -72,9 +77,16 @@ namespace Content.Client.Construction.UI
set => EraseButton.Pressed = value;
}
public bool GridViewButtonPressed
{
get => MenuGridViewButton.Pressed;
set => MenuGridViewButton.Pressed = value;
}
public ConstructionMenu()
{
SetSize = MinSize = new Vector2(720, 320);
SetSize = new Vector2(560, 450);
MinSize = new Vector2(560, 320);
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
@@ -102,6 +114,9 @@ namespace Content.Client.Construction.UI
EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty);
MenuGridViewButton.OnPressed += _ =>
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
}
public event EventHandler? ClearAllGhosts;

View File

@@ -1,7 +1,8 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -11,7 +12,6 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.Graphics;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -33,10 +33,12 @@ namespace Content.Client.Construction.UI
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
private readonly SpriteSystem _spriteSystem;
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, TextureButton> _recipeButtons = new();
private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all";
@@ -85,6 +87,7 @@ namespace Content.Client.Construction.UI
IoCManager.InjectDependencies(this);
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System<EntityWhitelistSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem<ConstructionSystem>(out var constructionSystem))
@@ -150,12 +153,24 @@ namespace Content.Client.Construction.UI
PopulateInfo(_selected);
}
private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe)
{
if (recipe is null)
{
_selected = null;
_constructionView.ClearRecipeInfo();
return;
}
_selected = recipe;
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
PopulateInfo(_selected);
}
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{
var (search, category) = args;
var recipesList = _constructionView.Recipes;
recipesList.Clear();
var recipes = new List<ConstructionPrototype>();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
@@ -201,12 +216,73 @@ namespace Content.Client.Construction.UI
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
foreach (var recipe in recipes)
{
recipesList.Add(GetItem(recipe, recipesList));
}
var recipesList = _constructionView.Recipes;
recipesList.Clear();
// There is apparently no way to set which
var recipesGrid = _constructionView.RecipesGrid;
recipesGrid.RemoveAllChildren();
_constructionView.RecipesGridScrollContainer.Visible = _constructionView.GridViewButtonPressed;
_constructionView.Recipes.Visible = !_constructionView.GridViewButtonPressed;
if (_constructionView.GridViewButtonPressed)
{
foreach (var recipe in recipes)
{
var itemButton = new TextureButton
{
TextureNormal = _spriteSystem.Frame0(recipe.Icon),
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.Name,
ToolTip = recipe.Name,
Scale = new Vector2(1.35f),
ToggleMode = true,
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Name] = itemButton;
var isCurrentButtonSelected = _selected == recipe;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
}
else
{
foreach (var recipe in recipes)
{
recipesList.Add(GetItem(recipe, recipesList));
}
}
}
private void SelectGridButton(TextureButton button, bool select)
{
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Modulate = select ? Color.Green : Color.White;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
private void PopulateCategories(string? selectCategory = null)
@@ -257,11 +333,10 @@ namespace Content.Client.Construction.UI
private void PopulateInfo(ConstructionPrototype prototype)
{
var spriteSys = _systemManager.GetEntitySystem<SpriteSystem>();
_constructionView.ClearRecipeInfo();
_constructionView.SetRecipeInfo(
prototype.Name, prototype.Description, spriteSys.Frame0(prototype.Icon),
prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon),
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
@@ -274,7 +349,6 @@ namespace Content.Client.Construction.UI
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return;
var spriteSys = _systemManager.GetEntitySystem<SpriteSystem>();
foreach (var entry in guide.Entries)
{
@@ -290,20 +364,20 @@ namespace Content.Client.Construction.UI
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
text = text.PadLeft(text.Length + entry.Padding);
var icon = entry.Icon != null ? spriteSys.Frame0(entry.Icon) : Texture.Transparent;
var icon = entry.Icon != null ? _spriteSystem.Frame0(entry.Icon) : Texture.Transparent;
stepList.AddItem(text, icon, false);
}
}
private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = recipe.Name,
Icon = recipe.Icon.Frame0(),
Icon = _spriteSystem.Frame0(recipe.Icon),
TooltipEnabled = true,
TooltipText = recipe.Description
TooltipText = recipe.Description,
};
}

View File

@@ -0,0 +1,95 @@
using Content.Shared.Electrocution;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.Electrocution;
/// <summary>
/// Shows the Electrocution HUD to entities with the ShowElectrocutionHUDComponent.
/// </summary>
public sealed class ElectrocutionHUDVisualizerSystem : VisualizerSystem<ElectrocutionHUDVisualsComponent>
{
[Dependency] private readonly IPlayerManager _playerMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ShowElectrocutionHUDComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ShowElectrocutionHUDComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
}
private void OnPlayerAttached(Entity<ShowElectrocutionHUDComponent> ent, ref LocalPlayerAttachedEvent args)
{
ShowHUD();
}
private void OnPlayerDetached(Entity<ShowElectrocutionHUDComponent> ent, ref LocalPlayerDetachedEvent args)
{
RemoveHUD();
}
private void OnInit(Entity<ShowElectrocutionHUDComponent> ent, ref ComponentInit args)
{
if (_playerMan.LocalEntity == ent)
{
ShowHUD();
}
}
private void OnShutdown(Entity<ShowElectrocutionHUDComponent> ent, ref ComponentShutdown args)
{
if (_playerMan.LocalEntity == ent)
{
RemoveHUD();
}
}
// Show the HUD to the client.
// We have to look for all current entities that can be electrified and toggle the HUD layer on if they are.
private void ShowHUD()
{
var electrifiedQuery = AllEntityQuery<ElectrocutionHUDVisualsComponent, AppearanceComponent, SpriteComponent>();
while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
{
if (!AppearanceSystem.TryGetData<bool>(uid, ElectrifiedVisuals.IsElectrified, out var electrified, appearanceComp))
continue;
if (electrified)
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, true);
else
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}
// Remove the HUD from the client.
// Find all current entities that can be electrified and hide the HUD layer.
private void RemoveHUD()
{
var electrifiedQuery = AllEntityQuery<ElectrocutionHUDVisualsComponent, AppearanceComponent, SpriteComponent>();
while (electrifiedQuery.MoveNext(out var uid, out var _, out var appearanceComp, out var spriteComp))
{
spriteComp.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}
// Toggle the HUD layer if an entity becomes (de-)electrified
protected override void OnAppearanceChange(EntityUid uid, ElectrocutionHUDVisualsComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!AppearanceSystem.TryGetData<bool>(uid, ElectrifiedVisuals.IsElectrified, out var electrified, args.Component))
return;
var player = _playerMan.LocalEntity;
if (electrified && HasComp<ShowElectrocutionHUDComponent>(player))
args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, true);
else
args.Sprite.LayerSetVisible(ElectrifiedLayers.HUD, false);
}
}

View File

@@ -55,7 +55,7 @@ namespace Content.Client.Eui
/// </summary>
protected void SendMessage(EuiMessageBase msg)
{
var netMsg = _netManager.CreateNetMessage<MsgEuiMessage>();
var netMsg = new MsgEuiMessage();
netMsg.Id = Id;
netMsg.Message = msg;

View File

@@ -39,6 +39,7 @@ namespace Content.Client.Info
AddInfoButton("server-info-website-button", CCVars.InfoLinksWebsite);
AddInfoButton("server-info-wiki-button", CCVars.InfoLinksWiki);
AddInfoButton("server-info-forum-button", CCVars.InfoLinksForum);
AddInfoButton("server-info-telegram-button", CCVars.InfoLinksTelegram);
//WL-Changes-start
_playerSessionInfoUIController = UserInterfaceManager.GetUIController<PlayerSessionInfoUIController>();

View File

@@ -17,6 +17,7 @@ using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Strip.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
@@ -29,10 +30,13 @@ namespace Content.Client.Inventory
[UsedImplicitly]
public sealed class StrippableBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private readonly ExamineSystem _examine;
private readonly InventorySystem _inv;
private readonly SharedCuffableSystem _cuffable;
private readonly StrippableSystem _strippable;
[ViewVariables]
private const int ButtonSeparation = 4;
@@ -51,6 +55,8 @@ namespace Content.Client.Inventory
_examine = EntMan.System<ExamineSystem>();
_inv = EntMan.System<InventorySystem>();
_cuffable = EntMan.System<SharedCuffableSystem>();
_strippable = EntMan.System<StrippableSystem>();
_virtualHiddenEntity = EntMan.SpawnEntity(HiddenPocketEntityId, MapCoordinates.Nullspace);
}
@@ -185,9 +191,15 @@ namespace Content.Client.Inventory
return;
if (ev.Function == ContentKeyFunctions.ExamineEntity)
{
_examine.DoExamine(slot.Entity.Value);
ev.Handle();
}
else if (ev.Function == EngineKeyFunctions.UseSecondary)
{
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(slot.Entity.Value);
ev.Handle();
}
}
private void AddInventoryButton(EntityUid invUid, string slotId, InventoryComponent inv)
@@ -198,7 +210,8 @@ namespace Content.Client.Inventory
var entity = container.ContainedEntity;
// If this is a full pocket, obscure the real entity
if (entity != null && slotDef.StripHidden)
// this does not work for modified clients because they are still sent the real entity
if (entity != null && _strippable.IsStripHidden(slotDef, _player.LocalEntity))
entity = _virtualHiddenEntity;
var button = new SlotButton(new SlotData(slotDef, container));

View File

@@ -21,6 +21,22 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
SubscribeLocalEvent<HandheldLightComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
/// <remarks>
/// TODO: Not properly predicted yet. Don't call this function if you want a the actual return value!
/// </remarks>
public override bool TurnOff(Entity<HandheldLightComponent> ent, bool makeNoise = true)
{
return true;
}
/// <remarks>
/// TODO: Not properly predicted yet. Don't call this function if you want a the actual return value!
/// </remarks>
public override bool TurnOn(EntityUid user, Entity<HandheldLightComponent> uid)
{
return true;
}
private void OnAppearanceChange(EntityUid uid, HandheldLightComponent? component, ref AppearanceChangeEvent args)
{
if (!Resolve(uid, ref component))

View File

@@ -137,7 +137,7 @@ namespace Content.Client.Lobby
return;
}
Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-not-started");
string text;
if (_gameTicker.Paused)
@@ -157,6 +157,10 @@ namespace Content.Client.Lobby
{
text = Loc.GetString(seconds < -5 ? "lobby-state-right-now-question" : "lobby-state-right-now-confirmation");
}
else if (difference.TotalHours >= 1)
{
text = $"{Math.Floor(difference.TotalHours)}:{difference.Minutes:D2}:{difference.Seconds:D2}";
}
else
{
text = $"{difference.Minutes}:{difference.Seconds:D2}";

View File

@@ -35,7 +35,7 @@ public sealed partial class StencilOverlay
var matty = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(matty);
foreach (var tile in grid.Comp.GetTilesIntersecting(worldAABB))
foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB))
{
// Ignored tiles for stencil
if (_weather.CanWeatherAffect(grid.Owner, grid, tile))

View File

@@ -24,6 +24,7 @@ public sealed partial class StencilOverlay : Overlay
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly ParallaxSystem _parallax;
private readonly SharedTransformSystem _transform;
private readonly SharedMapSystem _map;
private readonly SpriteSystem _sprite;
private readonly WeatherSystem _weather;
@@ -33,11 +34,12 @@ public sealed partial class StencilOverlay : Overlay
private readonly ShaderInstance _shader;
public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SpriteSystem sprite, WeatherSystem weather)
public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SharedMapSystem map, SpriteSystem sprite, WeatherSystem weather)
{
ZIndex = ParallaxSystem.ParallaxZIndex + 1;
_parallax = parallax;
_transform = transform;
_map = map;
_sprite = sprite;
_weather = weather;
IoCManager.InjectDependencies(this);

View File

@@ -10,13 +10,14 @@ public sealed class StencilOverlaySystem : EntitySystem
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly ParallaxSystem _parallax = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly WeatherSystem _weather = default!;
public override void Initialize()
{
base.Initialize();
_overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _sprite, _weather));
_overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather));
}
public override void Shutdown()

View File

@@ -116,7 +116,7 @@ public partial class BaseShuttleControl : MapGridControl
}
}
protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 matrix, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
protected void DrawGrid(DrawingHandleScreen handle, Matrix3x2 gridToView, Entity<MapGridComponent> grid, Color color, float alpha = 0.01f)
{
var rator = Maps.GetAllTilesEnumerator(grid.Owner, grid.Comp);
var minimapScale = MinimapScale;
@@ -264,7 +264,7 @@ public partial class BaseShuttleControl : MapGridControl
Extensions.EnsureLength(ref _allVertices, totalData);
_drawJob.MidPoint = midpoint;
_drawJob.Matrix = matrix;
_drawJob.Matrix = gridToView;
_drawJob.MinimapScale = minimapScale;
_drawJob.Vertices = gridData.Vertices;
_drawJob.ScaledVertices = _allVertices;
@@ -286,7 +286,7 @@ public partial class BaseShuttleControl : MapGridControl
private record struct GridDrawJob : IParallelRobustJob
{
public int BatchSize => 16;
public int BatchSize => 64;
public float MinimapScale;
public Vector2 MidPoint;
@@ -297,12 +297,7 @@ public partial class BaseShuttleControl : MapGridControl
public void Execute(int index)
{
var vert = Vertices[index];
var adjustedVert = Vector2.Transform(vert, Matrix);
adjustedVert = adjustedVert with { Y = -adjustedVert.Y };
var scaledVert = ScalePosition(adjustedVert, MinimapScale, MidPoint);
ScaledVertices[index] = scaledVert;
ScaledVertices[index] = Vector2.Transform(Vertices[index], Matrix);
}
}
}

View File

@@ -15,6 +15,7 @@ public sealed partial class NavScreen : BoxContainer
[Dependency] private readonly IEntityManager _entManager = default!;
private SharedTransformSystem _xformSystem;
private EntityUid? _consoleEntity; // Entity of controlling console
private EntityUid? _shuttleEntity;
public NavScreen()
@@ -35,6 +36,12 @@ public sealed partial class NavScreen : BoxContainer
_shuttleEntity = shuttle;
}
public void SetConsole(EntityUid? console)
{
_consoleEntity = console;
NavRadar.SetConsole(console);
}
private void OnIFFTogglePressed(BaseButton.ButtonEventArgs args)
{
NavRadar.ShowIFF ^= true;

View File

@@ -138,6 +138,7 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
{
var coordinates = _entManager.GetCoordinates(cState.NavState.Coordinates);
NavContainer.SetShuttle(coordinates?.EntityId);
NavContainer.SetConsole(owner);
MapContainer.SetShuttle(coordinates?.EntityId);
MapContainer.SetConsole(owner);

View File

@@ -107,16 +107,19 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
DrawCircles(handle);
var gridNent = EntManager.GetNetEntity(GridEntity);
var mapPos = _xformSystem.ToMapCoordinates(_coordinates.Value);
var ourGridMatrix = _xformSystem.GetWorldMatrix(GridEntity.Value);
var dockMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero);
var worldFromDock = Matrix3x2.Multiply(dockMatrix, ourGridMatrix);
var ourGridToWorld = _xformSystem.GetWorldMatrix(GridEntity.Value);
var selectedDockToOurGrid = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, Angle.Zero);
var selectedDockToWorld = Matrix3x2.Multiply(selectedDockToOurGrid, ourGridToWorld);
Matrix3x2.Invert(worldFromDock, out var offsetMatrix);
Box2 viewBoundsWorld = Matrix3Helpers.TransformBox(selectedDockToWorld, new Box2(-WorldRangeVector, WorldRangeVector));
Matrix3x2.Invert(selectedDockToWorld, out var worldToSelectedDock);
var selectedDockToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
// Draw nearby grids
var controlBounds = PixelSizeBox;
_grids.Clear();
_mapManager.FindGridsIntersecting(gridXform.MapID, new Box2(mapPos.Position - WorldRangeVector, mapPos.Position + WorldRangeVector), ref _grids);
_mapManager.FindGridsIntersecting(gridXform.MapID, viewBoundsWorld, ref _grids);
// offset the dotted-line position to the bounds.
Vector2? viewedDockPos = _viewedState != null ? MidPointVector : null;
@@ -136,11 +139,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (grid.Owner != GridEntity && !_shuttles.CanDraw(grid.Owner, iffComp: iffComp))
continue;
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, offsetMatrix);
var curGridToWorld = _xformSystem.GetWorldMatrix(grid.Owner);
var curGridToView = curGridToWorld * worldToSelectedDock * selectedDockToView;
var color = _shuttles.GetIFFColor(grid.Owner, grid.Owner == GridEntity, component: iffComp);
DrawGrid(handle, matty, grid, color);
DrawGrid(handle, curGridToView, grid, color);
// Draw any docks on that grid
if (!DockState.Docks.TryGetValue(EntManager.GetNetEntity(grid), out var gridDocks))
@@ -151,23 +154,24 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
if (ViewedDock == dock.Entity)
continue;
var position = Vector2.Transform(dock.Coordinates.Position, matty);
var otherDockRotation = Matrix3Helpers.CreateRotation(dock.Angle);
var scaledPos = ScalePosition(position with {Y = -position.Y});
if (!controlBounds.Contains(scaledPos.Floored()))
// This box is the AABB of all the vertices we draw below.
var dockRenderBoundsLocal = new Box2(-0.5f, -0.7f, 0.5f, 0.5f);
var currentDockToCurGrid = Matrix3Helpers.CreateTransform(dock.Coordinates.Position, dock.Angle);
var currentDockToWorld = Matrix3x2.Multiply(currentDockToCurGrid, curGridToWorld);
var dockRenderBoundsWorld = Matrix3Helpers.TransformBox(currentDockToWorld, dockRenderBoundsLocal);
if (!viewBoundsWorld.Intersects(dockRenderBoundsWorld))
continue;
// Draw the dock's collision
var collisionBL = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), matty);
Vector2.Transform(new Vector2(-0.2f, -0.7f), otherDockRotation), curGridToView);
var collisionBR = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), matty);
Vector2.Transform(new Vector2(0.2f, -0.7f), otherDockRotation), curGridToView);
var collisionTR = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), matty);
Vector2.Transform(new Vector2(0.2f, -0.5f), otherDockRotation), curGridToView);
var collisionTL = Vector2.Transform(dock.Coordinates.Position +
Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), matty);
Vector2.Transform(new Vector2(-0.2f, -0.5f), otherDockRotation), curGridToView);
var verts = new[]
{
@@ -181,13 +185,6 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
collisionBL,
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
var collisionCenter = verts[0] + verts[1] + verts[3] + verts[5];
var otherDockConnection = Color.ToSrgb(Color.Pink);
@@ -195,10 +192,10 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, verts, otherDockConnection);
// Draw the dock itself
var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), matty);
var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), matty);
var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), matty);
var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), matty);
var dockBL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, -0.5f), curGridToView);
var dockBR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, -0.5f), curGridToView);
var dockTR = Vector2.Transform(dock.Coordinates.Position + new Vector2(0.5f, 0.5f), curGridToView);
var dockTL = Vector2.Transform(dock.Coordinates.Position + new Vector2(-0.5f, 0.5f), curGridToView);
verts = new[]
{
@@ -212,13 +209,6 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
dockBL
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
Color otherDockColor;
if (HighlightedDock == dock.Entity)
@@ -253,9 +243,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
collisionCenter /= 4;
var range = viewedDockPos.Value - collisionCenter;
if (range.Length() < SharedDockingSystem.DockingHiglightRange * MinimapScale)
var maxRange = SharedDockingSystem.DockingHiglightRange * MinimapScale;
var maxRangeSq = maxRange * maxRange;
if (range.LengthSquared() < maxRangeSq)
{
if (_viewedState?.GridDockedWith == null)
if (dock.GridDockedWith == null)
{
var coordsOne = EntManager.GetCoordinates(_viewedState!.Coordinates);
var coordsTwo = EntManager.GetCoordinates(dock.Coordinates);
@@ -265,10 +257,11 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
var rotA = _xformSystem.GetWorldRotation(coordsOne.EntityId) + _viewedState!.Angle;
var rotB = _xformSystem.GetWorldRotation(coordsTwo.EntityId) + dock.Angle;
var distance = (mapOne.Position - mapTwo.Position).Length();
var distanceSq = (mapOne.Position - mapTwo.Position).LengthSquared();
var inAlignment = _dockSystem.InAlignment(mapOne, rotA, mapTwo, rotB);
var canDock = distance < SharedDockingSystem.DockRange && inAlignment;
var maxDockDistSq = SharedDockingSystem.DockRange * SharedDockingSystem.DockRange;
var canDock = distanceSq < maxDockDistSq && inAlignment;
if (dockButton != null)
dockButton.Disabled = !canDock || !canDockChange;
@@ -297,7 +290,8 @@ public sealed partial class ShuttleDockControl : BaseShuttleControl
{
// Because it's being layed out top-down we have to arrange for first frame.
container.Arrange(PixelRect);
var containerPos = scaledPos / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
var dockPositionInView = Vector2.Transform(dock.Coordinates.Position, curGridToView);
var containerPos = dockPositionInView / UIScale - container.DesiredSize / 2 - new Vector2(0f, 0.75f) * MinimapScale;
SetPosition(container, containerPos);
}

View File

@@ -29,6 +29,11 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
/// </summary>
private EntityCoordinates? _coordinates;
/// <summary>
/// Entity of controlling console
/// </summary>
private EntityUid? _consoleEntity;
private Angle? _rotation;
private Dictionary<NetEntity, List<DockingPortState>> _docks = new();
@@ -57,6 +62,11 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
_rotation = angle;
}
public void SetConsole(EntityUid? consoleEntity)
{
_consoleEntity = consoleEntity;
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
@@ -139,40 +149,35 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
}
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
var offset = _coordinates.Value.Position;
var posMatrix = Matrix3Helpers.CreateTransform(offset, _rotation.Value);
var posMatrix = Matrix3Helpers.CreateTransform(_coordinates.Value.Position, _rotation.Value);
var ourEntRot = RotateWithEntity ? _transform.GetWorldRotation(xform) : _rotation.Value;
var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
var ourWorldMatrix = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(ourWorldMatrix, out var ourWorldMatrixInvert);
var shuttleToWorld = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(shuttleToWorld, out var worldToShuttle);
var shuttleToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
// Draw our grid in detail
var ourGridId = xform.GridUid;
if (EntManager.TryGetComponent<MapGridComponent>(ourGridId, out var ourGrid) &&
fixturesQuery.HasComponent(ourGridId.Value))
{
var ourGridMatrix = _transform.GetWorldMatrix(ourGridId.Value);
var matrix = Matrix3x2.Multiply(ourGridMatrix, ourWorldMatrixInvert);
var ourGridToWorld = _transform.GetWorldMatrix(ourGridId.Value);
var ourGridToShuttle = Matrix3x2.Multiply(ourGridToWorld, worldToShuttle);
var ourGridToView = ourGridToShuttle * shuttleToView;
var color = _shuttles.GetIFFColor(ourGridId.Value, self: true);
DrawGrid(handle, matrix, (ourGridId.Value, ourGrid), color);
DrawDocks(handle, ourGridId.Value, matrix);
DrawGrid(handle, ourGridToView, (ourGridId.Value, ourGrid), color);
DrawDocks(handle, ourGridId.Value, ourGridToView);
}
var invertedPosition = _coordinates.Value.Position - offset;
invertedPosition.Y = -invertedPosition.Y;
// Don't need to transform the InvWorldMatrix again as it's already offset to its position.
// Draw radar position on the station
var radarPos = invertedPosition;
const float radarVertRadius = 2f;
var radarPosVerts = new Vector2[]
{
ScalePosition(radarPos + new Vector2(0f, -radarVertRadius)),
ScalePosition(radarPos + new Vector2(radarVertRadius / 2f, 0f)),
ScalePosition(radarPos + new Vector2(0f, radarVertRadius)),
ScalePosition(radarPos + new Vector2(radarVertRadius / -2f, 0f)),
ScalePosition(new Vector2(0f, -radarVertRadius)),
ScalePosition(new Vector2(radarVertRadius / 2f, 0f)),
ScalePosition(new Vector2(0f, radarVertRadius)),
ScalePosition(new Vector2(radarVertRadius / -2f, 0f)),
};
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, radarPosVerts, Color.Lime);
@@ -197,8 +202,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
if (!_shuttles.CanDraw(gUid, gridBody, iff))
continue;
var gridMatrix = _transform.GetWorldMatrix(gUid);
var matty = Matrix3x2.Multiply(gridMatrix, ourWorldMatrixInvert);
var curGridToWorld = _transform.GetWorldMatrix(gUid);
var curGridToView = curGridToWorld * worldToShuttle * shuttleToView;
var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
var coordColor = new Color(labelColor.R * 0.8f, labelColor.G * 0.8f, labelColor.B * 0.8f, 0.5f);
@@ -213,8 +218,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
{
var gridBounds = grid.Comp.LocalAABB;
var gridCentre = Vector2.Transform(gridBody.LocalCenter, matty);
gridCentre.Y = -gridCentre.Y;
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
var distance = gridCentre.Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
@@ -230,9 +234,8 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
// y-offset the control to always render below the grid (vertically)
var yOffset = Math.Max(gridBounds.Height, gridBounds.Width) * MinimapScale / 1.8f;
// The actual position in the UI. We centre the label by offsetting the matrix position
// by half the label's width, plus the y-offset
var gridScaledPosition = ScalePosition(gridCentre) - new Vector2(0, -yOffset);
// The actual position in the UI.
var gridScaledPosition = gridCentre - new Vector2(0, -yOffset);
// Normalize the grid position if it exceeds the viewport bounds
// normalizing it instead of clamping it preserves the direction of the vector and prevents corner-hugging
@@ -264,18 +267,32 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
}
// Detailed view
var gridAABB = gridMatrix.TransformBox(grid.Comp.LocalAABB);
var gridAABB = curGridToWorld.TransformBox(grid.Comp.LocalAABB);
// Skip drawing if it's out of range.
if (!gridAABB.Intersects(viewAABB))
continue;
DrawGrid(handle, matty, grid, labelColor);
DrawDocks(handle, gUid, matty);
DrawGrid(handle, curGridToView, grid, labelColor);
DrawDocks(handle, gUid, curGridToView);
}
// If we've set the controlling console, and it's on a different grid
// to the shuttle itself, then draw an additional marker to help the
// player determine where they are relative to the shuttle.
if (_consoleEntity != null && xformQuery.TryGetComponent(_consoleEntity, out var consoleXform))
{
if (consoleXform.ParentUid != _coordinates.Value.EntityId)
{
var consolePositionWorld = _transform.GetWorldPosition((EntityUid)_consoleEntity);
var p = Vector2.Transform(consolePositionWorld, worldToShuttle * shuttleToView);
handle.DrawCircle(p, 5, Color.ToSrgb(Color.Cyan), true);
}
}
}
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 matrix)
private void DrawDocks(DrawingHandleScreen handle, EntityUid uid, Matrix3x2 gridToView)
{
if (!ShowDocks)
return;
@@ -283,33 +300,32 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
const float DockScale = 0.6f;
var nent = EntManager.GetNetEntity(uid);
const float sqrt2 = 1.41421356f;
const float dockRadius = DockScale * sqrt2;
// Worst-case bounds used to cull a dock:
Box2 viewBounds = new Box2(-dockRadius, -dockRadius, Size.X + dockRadius, Size.Y + dockRadius);
if (_docks.TryGetValue(nent, out var docks))
{
foreach (var state in docks)
{
var position = state.Coordinates.Position;
var uiPosition = Vector2.Transform(position, matrix);
if (uiPosition.Length() > (WorldRange * 2f) - DockScale)
var positionInView = Vector2.Transform(position, gridToView);
if (!viewBounds.Contains(positionInView))
{
continue;
}
var color = Color.ToSrgb(Color.Magenta);
var verts = new[]
{
Vector2.Transform(position + new Vector2(-DockScale, -DockScale), matrix),
Vector2.Transform(position + new Vector2(DockScale, -DockScale), matrix),
Vector2.Transform(position + new Vector2(DockScale, DockScale), matrix),
Vector2.Transform(position + new Vector2(-DockScale, DockScale), matrix),
Vector2.Transform(position + new Vector2(-DockScale, -DockScale), gridToView),
Vector2.Transform(position + new Vector2(DockScale, -DockScale), gridToView),
Vector2.Transform(position + new Vector2(DockScale, DockScale), gridToView),
Vector2.Transform(position + new Vector2(-DockScale, DockScale), gridToView),
};
for (var i = 0; i < verts.Length; i++)
{
var vert = verts[i];
vert.Y = -vert.Y;
verts[i] = ScalePosition(vert);
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, color.WithAlpha(0.8f));
handle.DrawPrimitives(DrawPrimitiveTopology.LineStrip, verts, color);
}

View File

@@ -9,6 +9,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Silicons.Laws.Ui;
@@ -18,8 +19,13 @@ public sealed partial class LawDisplay : Control
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
private static readonly TimeSpan PressCooldown = TimeSpan.FromSeconds(3);
private readonly Dictionary<Button, TimeSpan> _nextAllowedPress = new();
public LawDisplay(EntityUid uid, SiliconLaw law, HashSet<string>? radioChannels)
{
RobustXamlLoader.Load(this);
@@ -47,9 +53,12 @@ public sealed partial class LawDisplay : Control
MinWidth = 75,
};
_nextAllowedPress[localButton] = TimeSpan.Zero;
localButton.OnPressed += _ =>
{
_chatManager.SendMessage($"{lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Local);
_nextAllowedPress[localButton] = _timing.CurTime + PressCooldown;
};
LawAnnouncementButtons.AddChild(localButton);
@@ -71,6 +80,8 @@ public sealed partial class LawDisplay : Control
MinWidth = 75,
};
_nextAllowedPress[radioChannelButton] = TimeSpan.Zero;
radioChannelButton.OnPressed += _ =>
{
switch (radioChannel)
@@ -80,9 +91,21 @@ public sealed partial class LawDisplay : Control
default:
_chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio); break;
}
_nextAllowedPress[radioChannelButton] = _timing.CurTime + PressCooldown;
};
LawAnnouncementButtons.AddChild(radioChannelButton);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
var curTime = _timing.CurTime;
foreach (var (button, nextPress) in _nextAllowedPress)
{
button.Disabled = curTime < nextPress;
}
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
namespace Content.Client.Singularity.EntitySystems;
namespace Content.Client.Singularity.Systems;
/// <summary>
/// The client-side version of <see cref="SharedEventHorizonSystem"/>.

View File

@@ -0,0 +1,12 @@
using Content.Shared.Singularity.EntitySystems;
using Content.Shared.Singularity.Components;
namespace Content.Client.Singularity.Systems;
/// <summary>
/// The client-side version of <see cref="SharedSingularityGeneratorSystem"/>.
/// Manages <see cref="SingularityGeneratorComponent"/>s.
/// Exists to make relevant signal handlers (ie: <see cref="SharedSingularityGeneratorSystem.OnEmagged"/>) work on the client.
/// </summary>
public sealed class SingularityGeneratorSystem : SharedSingularityGeneratorSystem
{}

View File

@@ -5,7 +5,7 @@ using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Client.Singularity.EntitySystems;
namespace Content.Client.Singularity.Systems;
/// <summary>
/// The client-side version of <see cref="SharedSingularitySystem"/>.

View File

@@ -16,9 +16,8 @@ using static Robust.Client.UserInterface.Controls.LineEdit;
namespace Content.Client.UserInterface.Systems.Chat.Widgets;
[GenerateTypedNameReferences]
#pragma warning disable RA0003
[Virtual]
public partial class ChatBox : UIWidget
#pragma warning restore RA0003
{
private readonly ChatUIController _controller;
private readonly IEntityManager _entManager;

View File

@@ -71,6 +71,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
buttonHeading.AddStyleClass(ContainerButton.StyleClassButton);
buttonHeading.Label.HorizontalAlignment = HAlignment.Center;
buttonHeading.Label.HorizontalExpand = true;
buttonHeading.Margin = new Thickness(8, 0, 8, 2);
var body = new CollapsibleBody
{

View File

@@ -83,22 +83,27 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (args.Function == EngineKeyFunctions.UIClick)
{
_handsSystem.UIHandClick(_playerHandsComponent, hand.SlotName);
args.Handle();
}
else if (args.Function == EngineKeyFunctions.UseSecondary)
{
_handsSystem.UIHandOpenContextMenu(hand.SlotName);
args.Handle();
}
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
{
_handsSystem.UIHandActivate(hand.SlotName);
args.Handle();
}
else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
_handsSystem.UIHandAltActivateItem(hand.SlotName);
args.Handle();
}
else if (args.Function == ContentKeyFunctions.ExamineEntity)
{
_handsSystem.UIInventoryExamine(hand.SlotName);
args.Handle();
}
}

View File

@@ -11,17 +11,21 @@
Orientation="Horizontal"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SeparationOverride="5"
>
<GridContainer
Rows="2"
HSeparationOverride="5"
VSeparationOverride="5"
HorizontalExpand="True">
<ui:MenuButton
Name="EscapeButton"
Access="Internal"
Icon="{xe:Tex '/Textures/Interface/hamburger.svg.192dpi.png'}"
BoundKey = "{x:Static ic:EngineKeyFunctions.EscapeMenu}"
ToolTip="{Loc 'game-hud-open-escape-menu-button-tooltip'}"
MinSize="70 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenRight}"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
<ui:MenuButton
Name="GuidebookButton"
@@ -29,7 +33,7 @@
Icon="{xe:Tex '/Textures/Interface/VerbIcons/information.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-guide-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenGuidebook}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -39,7 +43,7 @@
Icon="{xe:Tex '/Textures/Interface/character.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-character-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCharacterMenu}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -49,7 +53,7 @@
Icon="{xe:Tex '/Textures/Interface/emotes.svg.192dpi.png'}"
ToolTip="{Loc 'game-hud-open-emotes-menu-button-tooltip'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenEmotesMenu}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -59,7 +63,7 @@
Icon="{xe:Tex '/Textures/Interface/hammer.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenCraftingMenu}"
ToolTip="{Loc 'game-hud-open-crafting-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -69,7 +73,7 @@
Icon="{xe:Tex '/Textures/Interface/fist.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenActionsMenu}"
ToolTip="{Loc 'game-hud-open-actions-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -79,7 +83,7 @@
Icon="{xe:Tex '/Textures/Interface/gavel.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAdminMenu}"
ToolTip="{Loc 'game-hud-open-admin-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -89,7 +93,7 @@
Icon="{xe:Tex '/Textures/Interface/sandbox.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenSandboxWindow}"
ToolTip="{Loc 'game-hud-open-sandbox-menu-button-tooltip'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
@@ -99,8 +103,9 @@
Icon="{xe:Tex '/Textures/Interface/info.svg.192dpi.png'}"
BoundKey = "{x:Static is:ContentKeyFunctions.OpenAHelp}"
ToolTip="{Loc 'ui-options-function-open-a-help'}"
MinSize="42 64"
MinSize="48 64"
HorizontalExpand="True"
AppendStyleClass="{x:Static style:StyleBase.ButtonOpenLeft}"
AppendStyleClass="{x:Static style:StyleBase.ButtonSquare}"
/>
</GridContainer>
</widgets:GameTopMenuBar>

View File

@@ -307,12 +307,6 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
_entity.GetNetEntity(storageEnt),
new ItemStorageLocation(DraggingRotation, position)));
}
else
{
_entity.RaisePredictiveEvent(new StorageRemoveItemEvent(
_entity.GetNetEntity(draggingGhost.Entity),
_entity.GetNetEntity(storageEnt)));
}
_menuDragHelper.EndDrag();
_container?.BuildItemPieces();

View File

@@ -206,8 +206,7 @@ namespace Content.Client.Wires.UI
(_statusContainer = new GridContainer
{
Margin = new Thickness(8, 4),
// TODO: automatically change columns count.
Columns = 3
Rows = 2
})
}
}
@@ -227,7 +226,8 @@ namespace Content.Client.Wires.UI
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#525252ff")}
});
CloseButton.OnPressed += _ => Close();
SetSize = new Vector2(320, 200);
SetHeight = 200;
MinWidth = 320;
}
@@ -503,6 +503,8 @@ namespace Content.Client.Wires.UI
public StatusLight(StatusLightData data, IResourceCache resourceCache)
{
HorizontalAlignment = HAlignment.Right;
var hsv = Color.ToHsv(data.Color);
hsv.Z /= 2;
var dimColor = Color.FromHsv(hsv);

View File

@@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands
// No bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
// Try to pardon a ban that does not exist
@@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Still no bans on record
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Is.Empty);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
});
var banReason = "test";
@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands
// Should have one ban on record now
Assert.Multiple(async () =>
{
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
await pair.RunTicksSync(5);
@@ -70,13 +70,13 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
// The existing ban is unaffected
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Not.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
var ban = await sDatabase.GetServerBanAsync(1);
Assert.Multiple(async () =>
{
Assert.That(ban, Is.Not.Null);
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
// Check that it matches
Assert.That(ban.Id, Is.EqualTo(1));
@@ -95,7 +95,7 @@ namespace Content.IntegrationTests.Tests.Commands
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
var pardonedBan = await sDatabase.GetServerBanAsync(1);
@@ -105,7 +105,7 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.That(pardonedBan, Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
Assert.That(pardonedBan.Id, Is.EqualTo(1));
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
@@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands
Assert.Multiple(async () =>
{
// No bans should be returned
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null), Is.Null);
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
// Direct id lookup returns a pardoned ban
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
// The list is still returned since that ignores pardons
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null), Has.Count.EqualTo(1));
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
});
// Reconnect client. Slightly faster than dirtying the pair.

View File

@@ -81,7 +81,8 @@ namespace Content.IntegrationTests.Tests
"Reach",
"Train",
"Oasis",
"Cog"
"Cog",
"Amber"
};
/// <summary>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "integer",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "integer",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "real",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View File

@@ -512,20 +512,6 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@@ -571,6 +557,19 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -589,10 +588,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("smallint")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
@@ -603,6 +598,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("real")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("uuid")
.HasColumnName("user_id");
@@ -811,10 +810,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("inet")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen_time");
@@ -1165,10 +1160,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@@ -1299,10 +1290,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("bytea")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
@@ -1744,6 +1731,34 @@ namespace Content.Server.Database.Migrations.Postgres
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("integer")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@@ -1783,6 +1798,37 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("integer")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@@ -1877,8 +1923,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("integer")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@@ -1926,8 +2000,36 @@ namespace Content.Server.Database.Migrations.Postgres
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("integer")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("bytea")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ModernHwid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_role_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "server_ban",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "last_seen_hwid_type",
table: "player",
type: "INTEGER",
nullable: true,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "hwid_type",
table: "connection_log",
type: "INTEGER",
nullable: true,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_role_ban");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "server_ban");
migrationBuilder.DropColumn(
name: "last_seen_hwid_type",
table: "player");
migrationBuilder.DropColumn(
name: "hwid_type",
table: "connection_log");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ConnectionTrust : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<float>(
name: "trust",
table: "connection_log",
type: "REAL",
nullable: false,
defaultValue: 0f);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "trust",
table: "connection_log");
}
}
}

View File

@@ -483,19 +483,6 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist",
b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string) null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
@@ -539,6 +526,19 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
{
b.Property<Guid>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("user_id");
b.HasKey("UserId")
.HasName("PK_blacklist");
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -555,10 +555,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("denied");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<int>("ServerId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -569,6 +565,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("time");
b.Property<float>("Trust")
.HasColumnType("REAL")
.HasColumnName("trust");
b.Property<Guid>("UserId")
.HasColumnType("TEXT")
.HasColumnName("user_id");
@@ -762,10 +762,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("last_seen_address");
b.Property<byte[]>("LastSeenHWId")
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b.Property<DateTime>("LastSeenTime")
.HasColumnType("TEXT")
.HasColumnName("last_seen_time");
@@ -1097,10 +1093,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@@ -1225,10 +1217,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("expiration_time");
b.Property<byte[]>("HWId")
.HasColumnType("BLOB")
.HasColumnName("hwid");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
@@ -1660,6 +1648,34 @@ namespace Content.Server.Database.Migrations.Sqlite
.IsRequired()
.HasConstraintName("FK_connection_log_server_server_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ConnectionLogId")
.HasColumnType("INTEGER")
.HasColumnName("connection_log_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ConnectionLogId");
b1.ToTable("connection_log");
b1.WithOwner()
.HasForeignKey("ConnectionLogId")
.HasConstraintName("FK_connection_log_connection_log_connection_log_id");
});
b.Navigation("HWId");
b.Navigation("Server");
});
@@ -1699,6 +1715,37 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Player", b =>
{
b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
{
b1.Property<int>("PlayerId")
.HasColumnType("INTEGER")
.HasColumnName("player_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("last_seen_hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("last_seen_hwid_type");
b1.HasKey("PlayerId");
b1.ToTable("player");
b1.WithOwner()
.HasForeignKey("PlayerId")
.HasConstraintName("FK_player_player_player_id");
});
b.Navigation("LastSeenHWId");
});
modelBuilder.Entity("Content.Server.Database.Profile", b =>
{
b.HasOne("Content.Server.Database.Preference", "Preference")
@@ -1793,8 +1840,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerBanId");
b1.ToTable("server_ban");
b1.WithOwner()
.HasForeignKey("ServerBanId")
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");
@@ -1842,8 +1917,36 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasForeignKey("RoundId")
.HasConstraintName("FK_server_role_ban_round_round_id");
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
{
b1.Property<int>("ServerRoleBanId")
.HasColumnType("INTEGER")
.HasColumnName("server_role_ban_id");
b1.Property<byte[]>("Hwid")
.IsRequired()
.HasColumnType("BLOB")
.HasColumnName("hwid");
b1.Property<int>("Type")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("hwid_type");
b1.HasKey("ServerRoleBanId");
b1.ToTable("server_role_ban");
b1.WithOwner()
.HasForeignKey("ServerRoleBanId")
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
});
b.Navigation("CreatedBy");
b.Navigation("HWId");
b.Navigation("LastEditedBy");
b.Navigation("Round");

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Text.Json;
@@ -357,6 +359,47 @@ namespace Content.Server.Database
.HasForeignKey(w => w.PlayerUserId)
.HasPrincipalKey(p => p.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Changes for modern HWID integration
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Hwid)
.HasColumnName("last_seen_hwid");
modelBuilder.Entity<Player>()
.OwnsOne(p => p.LastSeenHWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ServerRoleBan>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Hwid)
.HasColumnName("hwid");
modelBuilder.Entity<ConnectionLog>()
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
}
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
@@ -584,7 +627,7 @@ namespace Content.Server.Database
public string LastSeenUserName { get; set; } = null!;
public DateTime LastSeenTime { get; set; }
public IPAddress LastSeenAddress { get; set; } = null!;
public byte[]? LastSeenHWId { get; set; }
public TypedHwid? LastSeenHWId { get; set; }
// Data that changes with each round
public List<Round> Rounds { get; set; } = null!;
@@ -733,7 +776,7 @@ namespace Content.Server.Database
int Id { get; set; }
Guid? PlayerUserId { get; set; }
NpgsqlInet? Address { get; set; }
byte[]? HWId { get; set; }
TypedHwid? HWId { get; set; }
DateTime BanTime { get; set; }
DateTime? ExpirationTime { get; set; }
string Reason { get; set; }
@@ -818,7 +861,7 @@ namespace Content.Server.Database
/// <summary>
/// Hardware ID of the banned player.
/// </summary>
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
/// <summary>
/// The time when the ban was applied by an administrator.
@@ -956,7 +999,7 @@ namespace Content.Server.Database
public DateTime Time { get; set; }
public IPAddress Address { get; set; } = null!;
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public ConnectionDenyReason? Denied { get; set; }
@@ -973,6 +1016,8 @@ namespace Content.Server.Database
public List<ServerBanHit> BanHits { get; set; } = null!;
public Server Server { get; set; } = null!;
public float Trust { get; set; }
}
public enum ConnectionDenyReason : byte
@@ -1010,7 +1055,7 @@ namespace Content.Server.Database
public Guid? PlayerUserId { get; set; }
[Required] public TimeSpan PlaytimeAtNote { get; set; }
public NpgsqlInet? Address { get; set; }
public byte[]? HWId { get; set; }
public TypedHwid? HWId { get; set; }
public DateTime BanTime { get; set; }
@@ -1271,4 +1316,37 @@ namespace Content.Server.Database
/// <seealso cref="ServerBan.Hidden"/>
public bool Hidden { get; set; }
}
/// <summary>
/// A hardware ID value together with its <see cref="HwidType"/>.
/// </summary>
/// <seealso cref="ImmutableTypedHwid"/>
[Owned]
public sealed class TypedHwid
{
public byte[] Hwid { get; set; } = default!;
public HwidType Type { get; set; }
[return: NotNullIfNotNull(nameof(immutable))]
public static implicit operator TypedHwid?(ImmutableTypedHwid? immutable)
{
if (immutable == null)
return null;
return new TypedHwid
{
Hwid = immutable.Hwid.ToArray(),
Type = immutable.Type,
};
}
[return: NotNullIfNotNull(nameof(hwid))]
public static implicit operator ImmutableTypedHwid?(TypedHwid? hwid)
{
if (hwid == null)
return null;
return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
}
}
}

View File

@@ -82,7 +82,7 @@ namespace Content.Server.Database
}
}
public class SnakeCaseConvention :
public partial class SnakeCaseConvention :
IEntityTypeAddedConvention,
IEntityTypeAnnotationChangedConvention,
IPropertyAddedConvention,
@@ -99,22 +99,27 @@ namespace Content.Server.Database
public static string RewriteName(string name)
{
var regex = new Regex("[A-Z]+", RegexOptions.Compiled);
return regex.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
return UpperCaseLocator()
.Replace(
name,
(Match match) => {
if (match.Index == 0 && (match.Value == "FK" || match.Value == "PK" || match.Value == "IX")) {
return match.Value;
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
// Do not add a _ if there is already one before this. This happens with owned entities.
if (name[match.Index - 1] == '_')
return match.Value.ToLower();
return "_" + match.Value.ToLower();
}
if (match.Value == "HWI")
return (match.Index == 0 ? "" : "_") + "hwi";
if (match.Index == 0)
return match.Value.ToLower();
if (match.Length > 1)
return $"_{match.Value[..^1].ToLower()}_{match.Value[^1..^0].ToLower()}";
return "_" + match.Value.ToLower();
}
);
);
}
public virtual void ProcessEntityTypeAdded(
@@ -332,5 +337,8 @@ namespace Content.Server.Database
}
}
}
[GeneratedRegex("[A-Z]+", RegexOptions.Compiled)]
private static partial Regex UpperCaseLocator();
}
}

View File

@@ -54,7 +54,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -74,7 +74,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
Bans.Add(new SharedServerBan(
@@ -95,7 +95,7 @@ public sealed class BanListEui : BaseEui
private async Task LoadRoleBans(NetUserId userId)
{
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null))
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null))
{
SharedServerUnban? unban = null;
if (ban.Unban is { } unbanDef)
@@ -115,7 +115,7 @@ public sealed class BanListEui : BaseEui
? (address.address.ToString(), address.cidrMask)
: null;
hwid = ban.HWId == null ? null : Convert.ToBase64String(ban.HWId.Value.AsSpan());
hwid = ban.HWId?.ToString();
}
RoleBans.Add(new SharedServerRoleBan(
ban.Id,

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using System.Net.Sockets;
using Content.Server.Administration.Managers;
@@ -8,7 +7,6 @@ using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Robust.Server.Player;
using Robust.Shared.Network;
namespace Content.Server.Administration;
@@ -27,7 +25,7 @@ public sealed class BanPanelEui : BaseEui
private NetUserId? PlayerId { get; set; }
private string PlayerName { get; set; } = string.Empty;
private IPAddress? LastAddress { get; set; }
private ImmutableArray<byte>? LastHwid { get; set; }
private ImmutableTypedHwid? LastHwid { get; set; }
private const int Ipv4_CIDR = 32;
private const int Ipv6_CIDR = 64;
@@ -51,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg)
{
case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid?.ToImmutableArray(), r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername);
@@ -59,7 +57,7 @@ public sealed class BanPanelEui : BaseEui
}
}
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableArray<byte>? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
{
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
@@ -155,7 +153,7 @@ public sealed class BanPanelEui : BaseEui
ChangePlayer(located?.UserId, located?.Username ?? string.Empty, located?.LastAddress, located?.LastHWId);
}
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableArray<byte>? lastHwid)
public void ChangePlayer(NetUserId? playerId, string playerName, IPAddress? lastAddress, ImmutableTypedHwid? lastHwid)
{
PlayerId = playerId;
PlayerName = playerName;

View File

@@ -38,7 +38,7 @@ public sealed class BanListCommand : LocalizedCommands
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastHWId, false);
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
if (bans.Count == 0)
{

View File

@@ -48,7 +48,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
if (shell.Player is not { } player)
{
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastHWId, includeUnbanned);
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned);
if (bans.Count == 0)
{

View File

@@ -65,7 +65,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var netChannel = player.Channel;
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
var modernHwids = netChannel.UserData.ModernHWIds;
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
var userRoleBans = new List<ServerRoleBanDef>();
foreach (var ban in roleBans)
@@ -132,7 +133,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
}
#region Server Bans
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason)
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
{
DateTimeOffset? expires = null;
if (minutes > 0)
@@ -166,9 +167,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
var addressRangeString = addressRange != null
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
: "null";
var hwidString = hwid != null
? string.Concat(hwid.Value.Select(x => x.ToString("x2")))
: "null";
var hwidString = hwid?.ToString() ?? "null";
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
@@ -208,6 +207,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
UserId = player.UserId,
Address = player.Channel.RemoteEndPoint.Address,
HWId = player.Channel.UserData.HWId,
ModernHWIds = player.Channel.UserData.ModernHWIds,
// It's possible for the player to not have cached data loading yet due to coincidental timing.
// If this is the case, we assume they have all flags to avoid false-positives.
ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
@@ -228,7 +228,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
#region Job Bans
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
public async void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan)
{
if (!_prototypeManager.TryIndex(role, out JobPrototype? _))
{

View File

@@ -24,7 +24,7 @@ public interface IBanManager
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="severity">Severity of the resulting ban note</param>
/// <param name="reason">Reason for the ban</param>
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, uint? minutes, NoteSeverity severity, string reason);
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId);
@@ -37,7 +37,7 @@ public interface IBanManager
/// <param name="reason">Reason for the ban</param>
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableArray<byte>? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
public void CreateRoleBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, string role, uint? minutes, NoteSeverity severity, string reason, DateTimeOffset timeOfBan);
/// <summary>
/// Pardons a role ban for the specified target, username or GUID

View File

@@ -5,16 +5,42 @@ using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Connection;
using Content.Server.Database;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration
{
public sealed record LocatedPlayerData(NetUserId UserId, IPAddress? LastAddress, ImmutableArray<byte>? LastHWId, string Username);
/// <summary>
/// Contains data resolved via <see cref="IPlayerLocator"/>.
/// </summary>
/// <param name="UserId">The ID of the located user.</param>
/// <param name="LastAddress">The last known IP address that the user connected with.</param>
/// <param name="LastHWId">
/// The last known HWID that the user connected with.
/// This should be used for placing new records involving HWIDs, such as bans.
/// For looking up data based on HWID, use combined <see cref="LastLegacyHWId"/> and <see cref="LastModernHWIds"/>.
/// </param>
/// <param name="Username">The last known username for the user connected with.</param>
/// <param name="LastLegacyHWId">
/// The last known legacy HWID value this user connected with. Only use for old lookups!
/// </param>
/// <param name="LastModernHWIds">
/// The set of last known modern HWIDs the user connected with.
/// </param>
public sealed record LocatedPlayerData(
NetUserId UserId,
IPAddress? LastAddress,
ImmutableTypedHwid? LastHWId,
string Username,
ImmutableArray<byte>? LastLegacyHWId,
ImmutableArray<ImmutableArray<byte>> LastModernHWIds);
/// <summary>
/// Utilities for finding user IDs that extend to more than the server database.
@@ -67,63 +93,42 @@ namespace Content.Server.Administration
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionByUsername(playerName, out var session))
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserName(playerName, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/name?name={WebUtility.UrlEncode(playerName)}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
if (!resp.IsSuccessStatusCode)
{
_sawmill.Error("Auth server returned bad response {StatusCode}!", resp.StatusCode);
return null;
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return await HandleAuthServerResponse(resp, cancel);
}
public async Task<LocatedPlayerData?> LookupIdAsync(NetUserId userId, CancellationToken cancel = default)
{
// Check people currently on the server, the easiest case.
if (_playerManager.TryGetSessionById(userId, out var session))
{
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.HWId;
return new LocatedPlayerData(userId, address, hwId, session.Name);
}
return ReturnForSession(session);
// Check database for past players.
var record = await _db.GetPlayerRecordByUserId(userId, cancel);
if (record != null)
return new LocatedPlayerData(record.UserId, record.LastSeenAddress, record.HWId, record.LastSeenUserName);
return ReturnForPlayerRecord(record);
// If all else fails, ask the auth server.
var authServer = _configurationManager.GetCVar(CVars.AuthServer);
var requestUri = $"{authServer}api/query/userid?userid={WebUtility.UrlEncode(userId.UserId.ToString())}";
using var resp = await _httpClient.GetAsync(requestUri, cancel);
return await HandleAuthServerResponse(resp, cancel);
}
private async Task<LocatedPlayerData?> HandleAuthServerResponse(HttpResponseMessage resp, CancellationToken cancel)
{
if (resp.StatusCode == HttpStatusCode.NotFound)
return null;
@@ -134,14 +139,40 @@ namespace Content.Server.Administration
}
var responseData = await resp.Content.ReadFromJsonAsync<UserDataResponse>(cancellationToken: cancel);
if (responseData == null)
{
_sawmill.Error("Auth server returned null response!");
return null;
}
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName);
return new LocatedPlayerData(new NetUserId(responseData.UserId), null, null, responseData.UserName, null, []);
}
private static LocatedPlayerData ReturnForSession(ICommonSession session)
{
var userId = session.UserId;
var address = session.Channel.RemoteEndPoint.Address;
var hwId = session.Channel.UserData.GetModernHwid();
return new LocatedPlayerData(
userId,
address,
hwId,
session.Name,
session.Channel.UserData.HWId,
session.Channel.UserData.ModernHWIds);
}
private static LocatedPlayerData ReturnForPlayerRecord(PlayerRecord record)
{
var hwid = record.HWId;
return new LocatedPlayerData(
record.UserId,
record.LastSeenAddress,
hwid,
record.LastSeenUserName,
hwid is { Type: HwidType.Legacy } ? hwid.Hwid : null,
hwid is { Type: HwidType.Modern } ? [hwid.Hwid] : []);
}
public async Task<LocatedPlayerData?> LookupIdByNameOrIdAsync(string playerName, CancellationToken cancel = default)

View File

@@ -173,11 +173,11 @@ public sealed class PlayerPanelEui : BaseEui
{
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
// This won't get associated ip or hwid bans but they were not placed on this account anyways
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count;
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
// The only way to distinguish whether a role ban is the same is to compare the ban time.
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count();
}
else
{

View File

@@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems
}
// Check if the user has been banned
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null);
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
if (ban != null)
{
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));

View File

@@ -28,7 +28,8 @@ namespace Content.Server.Announcements
}
else
{
var message = string.Join(' ', new ArraySegment<string>(args, 1, args.Length-1));
// Explicit IEnumerable<string> due to overload ambiguity on .NET 9
var message = string.Join(' ', (IEnumerable<string>)new ArraySegment<string>(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");

View File

@@ -16,6 +16,7 @@ public sealed class GasProducerAnomalySystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Initialize()
{
@@ -56,8 +57,11 @@ public sealed class GasProducerAnomalySystem : EntitySystem
return;
var localpos = xform.Coordinates.Position;
var tilerefs = grid.GetLocalTilesIntersecting(
new Box2(localpos + new Vector2(-radius, -radius), localpos + new Vector2(radius, radius))).ToArray();
var tilerefs = _map.GetLocalTilesIntersecting(
xform.GridUid.Value,
grid,
new Box2(localpos + new Vector2(-radius, -radius), localpos + new Vector2(radius, radius)))
.ToArray();
if (tilerefs.Length == 0)
return;

View File

@@ -48,7 +48,9 @@ public sealed partial class AtmosMonitorComponent : Component
[DataField("gasThresholds")]
public Dictionary<Gas, AtmosAlarmThreshold>? GasThresholds;
// Stores a reference to the gas on the tile this is on.
/// <summary>
/// Stores a reference to the gas on the tile this entity is on (or the pipe network it monitors; see <see cref="MonitorsPipeNet"/>).
/// </summary>
[ViewVariables]
public GasMixture? TileGas;
@@ -65,4 +67,19 @@ public sealed partial class AtmosMonitorComponent : Component
/// </summary>
[DataField("registeredDevices")]
public HashSet<string> RegisteredDevices = new();
/// <summary>
/// Specifies whether this device monitors its own internal pipe network rather than the surrounding atmosphere.
/// </summary>
/// <remarks>
/// If 'true', the entity will require a NodeContainerComponent with one or more PipeNodes to function.
/// </remarks>
[DataField]
public bool MonitorsPipeNet = false;
/// <summary>
/// Specifies the name of the pipe node that this device is monitoring.
/// </summary>
[DataField]
public string NodeNameMonitoredPipe = "monitored";
}

View File

@@ -4,6 +4,9 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
@@ -25,6 +28,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainerSystem = default!;
// Commands
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
@@ -56,8 +60,15 @@ public sealed class AtmosMonitorSystem : EntitySystem
private void OnAtmosDeviceEnterAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceEnabledEvent args)
{
if (atmosMonitor.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, atmosMonitor.NodeNameMonitoredPipe, out var pipeNode))
{
atmosMonitor.TileGas = pipeNode.Air;
return;
}
atmosMonitor.TileGas = _atmosphereSystem.GetContainingMixture(uid, true);
}
private void OnMapInit(EntityUid uid, AtmosMonitorComponent component, MapInitEvent args)
{
if (component.TemperatureThresholdId != null)
@@ -206,7 +217,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Grid == null)
if (args.Grid == null)
return;
// if we're not monitoring atmos, don't bother
@@ -215,6 +226,10 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.GasThresholds == null)
return;
// If monitoring a pipe network, get its most recent gas mixture
if (component.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, component.NodeNameMonitoredPipe, out var pipeNode))
component.TileGas = pipeNode.Air;
UpdateState(uid, component.TileGas, component);
}

View File

@@ -1,6 +1,6 @@
using System.Globalization;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Server.Hands.Systems;
using Content.Server.Inventory;
@@ -14,6 +14,7 @@ using Content.Shared.Bed.Cryostorage;
using Content.Shared.Chat;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Mind.Components;
using Content.Shared.StationRecords;

View File

@@ -424,7 +424,7 @@ public record struct PriceCalculationEvent()
[ByRefEvent]
public record struct EstimatedPriceCalculationEvent()
{
public EntityPrototype Prototype;
public required EntityPrototype Prototype;
/// <summary>
/// The total price of the entity.

View File

@@ -118,11 +118,14 @@ namespace Content.Server.Connection
var serverId = (await _serverDbEntry.ServerEntity).Id;
var hwid = e.UserData.GetModernHwid();
var trust = e.UserData.Trust;
if (deny != null)
{
var (reason, msg, banHits) = deny.Value;
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, reason, serverId);
var id = await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, reason, serverId);
if (banHits is { Count: > 0 })
await _db.AddServerBanHitsAsync(id, banHits);
@@ -134,12 +137,12 @@ namespace Content.Server.Connection
}
else
{
await _db.AddConnectionLogAsync(userId, e.UserName, addr, e.UserData.HWId, null, serverId);
await _db.AddConnectionLogAsync(userId, e.UserName, addr, hwid, trust, null, serverId);
if (!ServerPreferencesManager.ShouldStorePrefs(e.AuthType))
return;
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, e.UserData.HWId);
await _db.UpdatePlayerRecordAsync(userId, e.UserName, addr, hwid);
}
}
@@ -197,7 +200,9 @@ namespace Content.Server.Connection
hwId = null;
}
var bans = await _db.GetServerBansAsync(addr, userId, hwId, includeUnbanned: false);
var modernHwid = e.UserData.ModernHWIds;
var bans = await _db.GetServerBansAsync(addr, userId, hwId, modernHwid, includeUnbanned: false);
if (bans.Count > 0)
{
var firstBan = bans[0];

View File

@@ -0,0 +1,24 @@
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Connection;
/// <summary>
/// Helper functions for working with <see cref="NetUserData"/>.
/// </summary>
public static class UserDataExt
{
/// <summary>
/// Get the preferred HWID that should be used for new records related to a player.
/// </summary>
/// <remarks>
/// Players can have zero or more HWIDs, but for logging things like connection logs we generally
/// only want a single one. This method returns a nullable method.
/// </remarks>
public static ImmutableTypedHwid? GetModernHwid(this NetUserData userData)
{
return userData.ModernHWIds.Length == 0
? null
: new ImmutableTypedHwid(userData.ModernHWIds[0], HwidType.Modern);
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Immutable;
using System.Net;
using Content.Server.IP;
using Content.Shared.Database;
using Robust.Shared.Network;
namespace Content.Server.Database;
@@ -52,9 +53,28 @@ public static class BanMatcher
return true;
}
return player.HWId is { Length: > 0 } hwIdVar
&& ban.HWId != null
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (player.HWId is { Length: > 0 } hwIdVar
&& hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
{
return true;
}
break;
case HwidType.Modern:
if (player.ModernHWIds is { Length: > 0 } modernHwIdVar)
{
foreach (var hwid in modernHwIdVar)
{
if (hwid.AsSpan().SequenceEqual(ban.HWId.Hwid.AsSpan()))
return true;
}
}
break;
}
return false;
}
/// <summary>
@@ -73,10 +93,15 @@ public static class BanMatcher
public IPAddress? Address;
/// <summary>
/// The hardware ID of the player.
/// The LEGACY hardware ID of the player. Corresponds with <see cref="NetUserData.HWId"/>.
/// </summary>
public ImmutableArray<byte>? HWId;
/// <summary>
/// The modern hardware IDs of the player. Corresponds with <see cref="NetUserData.ModernHWIds"/>.
/// </summary>
public ImmutableArray<ImmutableArray<byte>>? ModernHWIds;
/// <summary>
/// Exemption flags the player has been granted.
/// </summary>

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -121,7 +120,7 @@ public sealed record PlayerRecord(
string LastSeenUserName,
DateTimeOffset LastSeenTime,
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
ImmutableTypedHwid? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.CCVar;
using Content.Shared.Database;
@@ -13,7 +12,7 @@ namespace Content.Server.Database
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -28,7 +27,7 @@ namespace Content.Server.Database
public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
TypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -487,12 +487,14 @@ namespace Content.Server.Database
/// </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="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
public abstract Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -501,13 +503,15 @@ namespace Content.Server.Database
/// </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="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Include pardoned and expired bans.</param>
/// <returns>The user's ban history.</returns>
public abstract Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task AddServerBanAsync(ServerBanDef serverBan);
@@ -598,11 +602,13 @@ namespace Content.Server.Database
/// <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="modernHWIds">The modern HWIDs of the user.</param>
/// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
/// <returns>The user's role ban history.</returns>
public abstract Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned);
public abstract Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan);
@@ -692,7 +698,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
await using var db = await GetDb();
@@ -709,7 +715,7 @@ namespace Content.Server.Database
record.LastSeenTime = DateTime.UtcNow;
record.LastSeenAddress = address;
record.LastSeenUserName = userName;
record.LastSeenHWId = hwId.ToArray();
record.LastSeenHWId = hwId;
await db.DbContext.SaveChangesAsync();
}
@@ -755,7 +761,7 @@ namespace Content.Server.Database
player.LastSeenUserName,
new DateTimeOffset(NormalizeDatabaseTime(player.LastSeenTime)),
player.LastSeenAddress,
player.LastSeenHWId?.ToImmutableArray());
player.LastSeenHWId);
}
#endregion
@@ -764,11 +770,11 @@ namespace Content.Server.Database
/*
* CONNECTION LOG
*/
public abstract Task<int> AddConnectionLogAsync(
NetUserId userId,
public abstract Task<int> AddConnectionLogAsync(NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);

View File

@@ -82,12 +82,14 @@ namespace Content.Server.Database
/// </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>
/// <param name="hwId">The legacy HWID of the user.</param>
/// <param name="modernHWIds">The modern HWIDs 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);
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds);
/// <summary>
/// Looks up an user's ban history.
@@ -95,13 +97,15 @@ namespace Content.Server.Database
/// </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="hwId">The legacy HWId of the user.</param>
/// <param name="modernHWIds">The modern HWIDs 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,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true);
Task AddServerBanAsync(ServerBanDef serverBan);
@@ -150,12 +154,14 @@ namespace Content.Server.Database
/// <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="modernHWIds">The modern HWIDs 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,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true);
Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
@@ -193,7 +199,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId);
ImmutableTypedHwid? hwId);
Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
#endregion
@@ -204,7 +210,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId);
@@ -519,20 +526,22 @@ namespace Content.Server.Database
public Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId));
return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
}
public Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned=true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task AddServerBanAsync(ServerBanDef serverBan)
@@ -576,10 +585,11 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned = true)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, includeUnbanned));
return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
}
public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
@@ -621,7 +631,7 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId)
ImmutableTypedHwid? hwId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
@@ -643,12 +653,13 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, denied, serverId));
return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
}
public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -73,7 +74,8 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address == null && userId == null && hwId == null)
{
@@ -84,7 +86,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -94,7 +96,9 @@ namespace Content.Server.Database
public override async Task<List<ServerBanDef>> GetServerBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId, bool includeUnbanned)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
{
@@ -105,7 +109,7 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var query = MakeBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@@ -127,6 +131,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags,
@@ -134,16 +139,11 @@ namespace Content.Server.Database
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
IQueryable<ServerBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerBan, ServerUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.Ban);
if (address != null && !exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP))
{
@@ -156,15 +156,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
DebugTools.Assert(
query != null,
"At least one filter item (IP/UserID/HWID) must have been given to make query not null.");
@@ -186,6 +177,49 @@ namespace Content.Server.Database
return query.Distinct();
}
private static IQueryable<TBan>? MakeBanLookupQualityShared<TBan, TUnban>(
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbSet<TBan> set)
where TBan : class, IBanCommon<TUnban>
where TUnban : class, IUnbanCommon
{
IQueryable<TBan>? query = null;
if (userId is { } uid)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Legacy && b.HWId!.Hwid.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (modernHWIds != null)
{
foreach (var modernHwid in modernHWIds)
{
var newQ = set
.Include(p => p.Unban)
.Where(b => b.HWId!.Type == HwidType.Modern && b.HWId!.Hwid.SequenceEqual(modernHwid.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
}
return query;
}
private static ServerBanDef? ConvertBan(ServerBan? ban)
{
if (ban == null)
@@ -211,7 +245,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -249,7 +283,7 @@ namespace Content.Server.Database
db.PgDbContext.Ban.Add(new ServerBan
{
Address = serverBan.Address.ToNpgsqlInet(),
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
@@ -297,6 +331,7 @@ namespace Content.Server.Database
public override async Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
if (address == null && userId == null && hwId == null)
@@ -306,7 +341,7 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var query = MakeRoleBanLookupQuery(address, userId, hwId, db, includeUnbanned)
var query = MakeRoleBanLookupQuery(address, userId, hwId, modernHWIds, db, includeUnbanned)
.OrderByDescending(b => b.BanTime);
return await QueryRoleBans(query);
@@ -334,19 +369,15 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
DbGuardImpl db,
bool includeUnbanned)
{
IQueryable<ServerRoleBan>? query = null;
if (userId is { } uid)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.PlayerUserId == uid.UserId);
query = query == null ? newQ : query.Union(newQ);
}
var query = MakeBanLookupQualityShared<ServerRoleBan, ServerRoleUnban>(
userId,
hwId,
modernHWIds,
db.PgDbContext.RoleBan);
if (address != null)
{
@@ -357,15 +388,6 @@ namespace Content.Server.Database
query = query == null ? newQ : query.Union(newQ);
}
if (hwId != null && hwId.Value.Length > 0)
{
var newQ = db.PgDbContext.RoleBan
.Include(p => p.Unban)
.Where(b => b.HWId!.SequenceEqual(hwId.Value.ToArray()));
query = query == null ? newQ : query.Union(newQ);
}
if (!includeUnbanned)
{
query = query?.Where(p =>
@@ -402,7 +424,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
ban.BanTime,
ban.ExpirationTime,
ban.RoundId,
@@ -440,7 +462,7 @@ namespace Content.Server.Database
var ban = new ServerRoleBan
{
Address = serverRoleBan.Address.ToNpgsqlInet(),
HWId = serverRoleBan.HWId?.ToArray(),
HWId = serverRoleBan.HWId,
Reason = serverRoleBan.Reason,
Severity = serverRoleBan.Severity,
BanningAdmin = serverRoleBan.BanningAdmin?.UserId,
@@ -476,7 +498,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -488,9 +511,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.PgDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -9,6 +9,7 @@ using Content.Server.Administration.Logs;
using Content.Server.IP;
using Content.Server.Preferences.Managers;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
@@ -80,22 +81,24 @@ namespace Content.Server.Database
public override async Task<ServerBanDef?> GetServerBanAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned: false)).FirstOrDefault();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned: false)).FirstOrDefault();
}
public override async Task<List<ServerBanDef>> GetServerBansAsync(
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
return (await GetServerBanQueryAsync(db, address, userId, hwId, includeUnbanned)).ToList();
return (await GetServerBanQueryAsync(db, address, userId, hwId, modernHWIds, includeUnbanned)).ToList();
}
private async Task<IEnumerable<ServerBanDef>> GetServerBanQueryAsync(
@@ -103,6 +106,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
var exempt = await GetBanExemptionCore(db, userId);
@@ -119,6 +123,7 @@ namespace Content.Server.Database
UserId = userId,
ExemptFlags = exempt ?? default,
HWId = hwId,
ModernHWIds = modernHWIds,
IsNewPlayer = newPlayer,
};
@@ -161,7 +166,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -205,6 +210,7 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds,
bool includeUnbanned)
{
await using var db = await GetDbImpl();
@@ -214,7 +220,7 @@ namespace Content.Server.Database
var queryBans = await GetAllRoleBans(db.SqliteDbContext, includeUnbanned);
return queryBans
.Where(b => RoleBanMatches(b, address, userId, hwId))
.Where(b => RoleBanMatches(b, address, userId, hwId, modernHWIds))
.Select(ConvertRoleBan)
.ToList()!;
}
@@ -237,7 +243,8 @@ namespace Content.Server.Database
ServerRoleBan ban,
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId)
ImmutableArray<byte>? hwId,
ImmutableArray<ImmutableArray<byte>>? modernHWIds)
{
if (address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
{
@@ -249,7 +256,27 @@ namespace Content.Server.Database
return true;
}
return hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId);
switch (ban.HWId?.Type)
{
case HwidType.Legacy:
if (hwId is { Length: > 0 } hwIdVar && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
break;
case HwidType.Modern:
if (modernHWIds != null)
{
foreach (var modernHWId in modernHWIds)
{
if (modernHWId.AsSpan().SequenceEqual(ban.HWId.Hwid))
return true;
}
}
break;
}
return false;
}
public override async Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan)
@@ -262,7 +289,7 @@ namespace Content.Server.Database
Reason = serverBan.Reason,
Severity = serverBan.Severity,
BanningAdmin = serverBan.BanningAdmin?.UserId,
HWId = serverBan.HWId?.ToArray(),
HWId = serverBan.HWId,
BanTime = serverBan.BanTime.UtcDateTime,
ExpirationTime = serverBan.ExpirationTime?.UtcDateTime,
RoundId = serverBan.RoundId,
@@ -316,7 +343,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -376,7 +403,7 @@ namespace Content.Server.Database
ban.Id,
uid,
ban.Address.ToTuple(),
ban.HWId == null ? null : ImmutableArray.Create(ban.HWId),
ban.HWId,
// SQLite apparently always reads DateTime as unspecified, but we always write as UTC.
DateTime.SpecifyKind(ban.BanTime, DateTimeKind.Utc),
ban.ExpirationTime == null ? null : DateTime.SpecifyKind(ban.ExpirationTime.Value, DateTimeKind.Utc),
@@ -412,7 +439,8 @@ namespace Content.Server.Database
NetUserId userId,
string userName,
IPAddress address,
ImmutableArray<byte> hwId,
ImmutableTypedHwid? hwId,
float trust,
ConnectionDenyReason? denied,
int serverId)
{
@@ -424,9 +452,10 @@ namespace Content.Server.Database
Time = DateTime.UtcNow,
UserId = userId.UserId,
UserName = userName,
HWId = hwId.ToArray(),
HWId = hwId,
Denied = denied,
ServerId = serverId
ServerId = serverId,
Trust = trust,
};
db.SqliteDbContext.ConnectionLog.Add(connectionLog);

View File

@@ -1,4 +1,3 @@
using System.Collections.Immutable;
using System.Net;
using Content.Shared.Database;
using Robust.Shared.Network;
@@ -10,7 +9,7 @@ public sealed class ServerRoleBanDef
public int? Id { get; }
public NetUserId? UserId { get; }
public (IPAddress address, int cidrMask)? Address { get; }
public ImmutableArray<byte>? HWId { get; }
public ImmutableTypedHwid? HWId { get; }
public DateTimeOffset BanTime { get; }
public DateTimeOffset? ExpirationTime { get; }
@@ -26,7 +25,7 @@ public sealed class ServerRoleBanDef
int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray<byte>? hwId,
ImmutableTypedHwid? hwId,
DateTimeOffset banTime,
DateTimeOffset? expirationTime,
int? roundId,

View File

@@ -28,6 +28,7 @@ public sealed partial class DragonSystem : EntitySystem
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
private EntityQuery<CarpRiftsConditionComponent> _objQuery;
@@ -156,7 +157,7 @@ public sealed partial class DragonSystem : EntitySystem
}
// cant put a rift on solars
foreach (var tile in grid.GetTilesIntersecting(new Circle(_transform.GetWorldPosition(xform), RiftTileRadius), false))
foreach (var tile in _map.GetTilesIntersecting(xform.GridUid.Value, grid, new Circle(_transform.GetWorldPosition(xform), RiftTileRadius), false))
{
if (!tile.IsSpace(_tileDef))
continue;

View File

@@ -121,7 +121,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
activated.TimeLeft -= frameTime;
if (activated.TimeLeft <= 0 || !IsPowered(uid, electrified, transform))
{
_appearance.SetData(uid, ElectrifiedVisuals.IsPowered, false);
_appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, false);
RemComp<ActivatedElectrifiedComponent>(uid);
}
}
@@ -217,7 +217,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
return false;
EnsureComp<ActivatedElectrifiedComponent>(uid);
_appearance.SetData(uid, ElectrifiedVisuals.IsPowered, true);
_appearance.SetData(uid, ElectrifiedVisuals.ShowSparks, true);
siemens *= electrified.SiemensCoefficient;
if (!DoCommonElectrocutionAttempt(targetUid, uid, ref siemens) || siemens <= 0)
@@ -488,15 +488,4 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
}
_audio.PlayPvs(electrified.ShockNoises, targetUid, AudioParams.Default.WithVolume(electrified.ShockVolume));
}
public void SetElectrifiedWireCut(Entity<ElectrifiedComponent> ent, bool value)
{
if (ent.Comp.IsWireCut == value)
{
return;
}
ent.Comp.IsWireCut = value;
Dirty(ent);
}
}

View File

@@ -134,13 +134,6 @@ public sealed class DrainSystem : SharedDrainSystem
}
drain.Accumulator -= drain.DrainFrequency;
// Disable ambient sound from emptying manually
if (!drain.AutoDrain)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;
}
if (!managerQuery.TryGetComponent(uid, out var manager))
continue;
@@ -160,41 +153,44 @@ public sealed class DrainSystem : SharedDrainSystem
// This will ensure that UnitsPerSecond is per second...
var amount = drain.UnitsPerSecond * drain.DrainFrequency;
_puddles.Clear();
_lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles);
if (_puddles.Count == 0)
if (drain.AutoDrain)
{
_ambientSoundSystem.SetAmbience(uid, false);
continue;
}
_puddles.Clear();
_lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles);
_ambientSoundSystem.SetAmbience(uid, true);
amount /= _puddles.Count;
foreach (var puddle in _puddles)
{
// Queue the solution deletion if it's empty. EvaporationSystem might also do this
// but queuedelete should be pretty safe.
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution))
if (_puddles.Count == 0)
{
EntityManager.QueueDeleteEntity(puddle);
_ambientSoundSystem.SetAmbience(uid, false);
continue;
}
// Removes the lowest of:
// the drain component's units per second adjusted for # of puddles
// the puddle's remaining volume (making it cleanly zero)
// the drain's remaining volume in its buffer.
var transferSolution = _solutionContainerSystem.SplitSolution(puddle.Comp.Solution.Value,
FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume));
_ambientSoundSystem.SetAmbience(uid, true);
drainSolution.AddSolution(transferSolution, _prototypeManager);
amount /= _puddles.Count;
if (puddleSolution.Volume <= 0)
foreach (var puddle in _puddles)
{
QueueDel(puddle);
// Queue the solution deletion if it's empty. EvaporationSystem might also do this
// but queuedelete should be pretty safe.
if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution))
{
EntityManager.QueueDeleteEntity(puddle);
continue;
}
// Removes the lowest of:
// the drain component's units per second adjusted for # of puddles
// the puddle's remaining volume (making it cleanly zero)
// the drain's remaining volume in its buffer.
var transferSolution = _solutionContainerSystem.SplitSolution(puddle.Comp.Solution.Value,
FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume));
drainSolution.AddSolution(transferSolution, _prototypeManager);
if (puddleSolution.Volume <= 0)
{
QueueDel(puddle);
}
}
}

View File

@@ -10,12 +10,12 @@ using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -464,71 +464,4 @@ namespace Content.Server.GameTicking
#endregion
}
/// <summary>
/// Event raised broadcast before a player is spawned by the GameTicker.
/// You can use this event to spawn a player off-station on late-join but also at round start.
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
/// </summary>
[PublicAPI]
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
{
public ICommonSession Player { get; }
public HumanoidCharacterProfile Profile { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public EntityUid Station { get; }
public PlayerBeforeSpawnEvent(ICommonSession player,
HumanoidCharacterProfile profile,
string? jobId,
bool lateJoin,
EntityUid station)
{
Player = player;
Profile = profile;
JobId = jobId;
LateJoin = lateJoin;
Station = station;
}
}
/// <summary>
/// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
/// Can be used to give random players a role, modify their equipment, etc.
/// </summary>
[PublicAPI]
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
{
public EntityUid Mob { get; }
public ICommonSession Player { get; }
public string? JobId { get; }
public bool LateJoin { get; }
public bool Silent { get; }
public EntityUid Station { get; }
public HumanoidCharacterProfile Profile { get; }
// Ex. If this is the 27th person to join, this will be 27.
public int JoinOrder { get; }
public PlayerSpawnCompleteEvent(EntityUid mob,
ICommonSession player,
string? jobId,
bool lateJoin,
bool silent,
int joinOrder,
EntityUid station,
HumanoidCharacterProfile profile)
{
Mob = mob;
Player = player;
JobId = jobId;
LateJoin = lateJoin;
Silent = silent;
Station = station;
Profile = profile;
JoinOrder = joinOrder;
}
}
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Mind;
using Content.Server.Points;
using Content.Server.RoundEnd;
using Content.Server.Station.Systems;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Points;
using Content.Shared.Storage;

View File

@@ -34,10 +34,11 @@ public sealed class GatewayGeneratorSystem : EntitySystem
[Dependency] private readonly GatewaySystem _gateway = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly TileSystem _tile = default!;
[ValidatePrototypeId<DatasetPrototype>]
private const string PlanetNames = "names_borer";
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string PlanetNames = "NamesBorer";
// TODO:
// Fix shader some more
@@ -102,7 +103,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var mapId = _mapManager.CreateMap();
var mapUid = _mapManager.GetMapEntityId(mapId);
var gatewayName = SharedSalvageSystem.GetFTLName(_protoManager.Index<DatasetPrototype>(PlanetNames), seed);
var gatewayName = _salvage.GetFTLName(_protoManager.Index<LocalizedDatasetPrototype>(PlanetNames), seed);
_metadata.SetEntityName(mapUid, gatewayName);
var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset));

View File

@@ -1,7 +1,9 @@
using Content.Server.Popups;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Robust.Shared.Console;
using Content.Server.GameTicking;
namespace Content.Server.Ghost
{
@@ -23,6 +25,14 @@ namespace Content.Server.Ghost
return;
}
var gameTicker = _entities.System<GameTicker>();
if (!gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
playerStatus is not PlayerGameStatus.JoinedGame)
{
shell.WriteLine("ghost-command-error-lobby");
return;
}
if (player.AttachedEntity is { Valid: true } frozen &&
_entities.HasComponent<AdminFrozenComponent>(frozen))
{

View File

@@ -46,6 +46,8 @@ namespace Content.Server.Ghost
public sealed class GhostSystem : SharedGhostSystem
{
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly FollowerSystem _followerSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -408,6 +410,8 @@ namespace Content.Server.Ghost
private void WarpTo(EntityUid uid, EntityUid target)
{
_adminLog.Add(LogType.GhostWarp, $"{ToPrettyString(uid)} ghost warped to {ToPrettyString(target)}");
if ((TryComp(target, out WarpPointComponent? warp) && warp.Follow) || HasComp<MobStateComponent>(target))
{
_followerSystem.StartFollowingEntity(uid, target);

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Content.Server.Administration;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Raffles;
@@ -77,13 +77,13 @@ namespace Content.Server.Ghost.Roles
if (isProto)
{
if (!_protoManager.TryIndex<GhostRoleRaffleSettingsPrototype>(args[4], out var proto))
if (!_protoManager.TryIndex<GhostRoleRaffleSettingsPrototype>(args[3], out var proto))
{
var validProtos = string.Join(", ",
_protoManager.EnumeratePrototypes<GhostRoleRaffleSettingsPrototype>().Select(p => p.ID)
);
shell.WriteLine($"{args[4]} is not a valid raffle settings prototype. Valid options: {validProtos}");
shell.WriteLine($"{args[3]} is not a valid raffle settings prototype. Valid options: {validProtos}");
return;
}

View File

@@ -60,7 +60,6 @@ public sealed class RandomGiftSystem : EntitySystem
var coords = Transform(args.User).Coordinates;
var handsEnt = Spawn(component.SelectedEntity, coords);
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User)} used {ToPrettyString(uid)} which spawned {ToPrettyString(handsEnt)}");
EnsureComp<ItemComponent>(handsEnt); // For insane mode.
if (component.Wrapper is not null)
Spawn(component.Wrapper, coords);

View File

@@ -9,7 +9,6 @@ using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Rounding;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -46,7 +45,6 @@ namespace Content.Server.Light.EntitySystems
SubscribeLocalEvent<HandheldLightComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldLightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerb);
SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(OnActivate);
@@ -179,25 +177,7 @@ namespace Content.Server.Light.EntitySystems
}
}
private void AddToggleLightVerb(Entity<HandheldLightComponent> ent, ref GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !ent.Comp.ToggleOnInteract)
return;
var @event = args;
ActivationVerb verb = new()
{
Text = Loc.GetString("verb-common-toggle-light"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/light.svg.192dpi.png")),
Act = ent.Comp.Activated
? () => TurnOff(ent)
: () => TurnOn(@event.User, ent)
};
args.Verbs.Add(verb);
}
public bool TurnOff(Entity<HandheldLightComponent> ent, bool makeNoise = true)
public override bool TurnOff(Entity<HandheldLightComponent> ent, bool makeNoise = true)
{
if (!ent.Comp.Activated || !_lights.TryGetLight(ent, out var pointLightComponent))
{
@@ -211,7 +191,7 @@ namespace Content.Server.Light.EntitySystems
return true;
}
public bool TurnOn(EntityUid user, Entity<HandheldLightComponent> uid)
public override bool TurnOn(EntityUid user, Entity<HandheldLightComponent> uid)
{
var component = uid.Comp;
if (component.Activated || !_lights.TryGetLight(uid, out var pointLightComponent))

View File

@@ -4,7 +4,6 @@ using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Server.GameTicking;
using Content.Server.Medical.CrewMonitoring;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -14,8 +13,10 @@ using Content.Shared.Damage;
using Content.Shared.DeviceNetwork;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Interaction;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
@@ -383,7 +384,7 @@ public sealed class SuitSensorSystem : EntitySystem
// Get mob total damage crit threshold
int? totalDamageThreshold = null;
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, Shared.Mobs.MobState.Critical, out var critThreshold))
if (_mobThresholdSystem.TryGetThresholdForState(sensor.User.Value, MobState.Critical, out var critThreshold))
totalDamageThreshold = critThreshold.Value.Int();
// finally, form suit sensor status

View File

@@ -11,8 +11,8 @@ public sealed partial class PathfindingSystem
/// </summary>
public record struct BreadthPathArgs()
{
public Vector2i Start;
public List<Vector2i> Ends;
public required Vector2i Start;
public required List<Vector2i> Ends;
public bool Diagonals = false;

Some files were not shown because too many files have changed in this diff Show More