Upstream sync (#846)

* Additional Ionstorm Law Updates (#34197)

* Added a couple entires, removed the references to IRL countries, and fixed a grammar mistake on "Telecomunications Equipment"

* Fixed another awkward grammar situation

* Commented out a bunch of law elements I felt weren't good for discussion, added some new ones to help fill in the missing areas

* Reverted pylons, and added more entries to help fill out the lists more

* reverted all deletions

* implemented feedback, also realized the IonStormActions section was rather short, so I added a bunch there

* realized I didn't remove the extra resources line, also added more things, and fixed one grammatical error

* Removed Portugal again, added more Musts since that list was the new shortest one.

* Automatic changelog update

* Add option to disable bwoink sound. (#33782)

* Add option to disable bwoink sound.

* Now it's working only with active admin status.

* No bwoink, only "notification sound"

* Moar changes

* Another one

* Automatic changelog update

* Pride Scarves  (#34448)

* More scarfs (#216)

* Sprites + Colored Scarf Implimentation

Doesn't include into quickthreads or other clothing vendors

* File Paths are important

* Metas, items exist, winterdrobe

I'm iffy on them being in the winterdrobe but the other option is quickthreads so idk

* File Path Fix

* Typo fix

* Resolved merge conflict

* Moved scarves around

* Moved scarf textures out of the _CD directory

* Removed final CD folder

* Removed extra scarfs

* Removed extra scarfs, again

* Replaced ClothingNeckBase with ClothingScarfBase

---------

Co-authored-by: PursuitInAshes <91865152+PursuitInAshes@users.noreply.github.com>
Co-authored-by: TakoDragon <69509841+BackeTako@users.noreply.github.com>

* Automatic changelog update

* Replace the djstation intercoms with freelance intercoms (#34478)

* Add Freelance intercom prototype, and replace the djstation ruin with them instead of three master keys

* removed a new line

* Update nix flake for .NET 9 (#34480)

* Automatic changelog update

* Holopad networking rework (#34112)

* Initial commit

* Finalizing main changes

* Addressed reviews

* Fixed a few issues

* Switched to using global overrides

* Removed unnecessary references

* Make GasMixture enumerable

I noticed that enumerating gases is frequently done in an annoying way with Enum.GetValues. So I made it better. Now GasMixture is IEnumerable<(Gas gas, float moles)> and it just works.

* Improve canister admin logs.

1. Now clearly says "opened"/"closed" when changing the release valve.
2. Clearly says whether the valve was opened while a canister was inserted or not.
3. When a tank is ejected, logs if the valve is open and the ejection started spilling into the environment.

Fixes #34488

* Optimize & clean up RadiationSystem (#34459)

* Optimize & clean up RadiationSystem

* comments

* Update Content.Server/Radiation/Systems/RadiationSystem.GridCast.cs

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>

---------

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>

* Update engine to v240.0.1 (#34497)

* Various Locale Typo Fixes (and spaces) (#34483)

Random spelling error and FTL linting (+PowersinkSystem because there was an misspelt locale for that)

* Space lizard plushie can now be worn on your head (#33809)

* space weh can now be work on top of head

* space weh can now be worn on top of head

* trim out unnecessary comps

* Automatic changelog update

* Adds bullet collision to wall mounted cameras (#34500)

* Automatic changelog update

* Change MaskComponent to accommodate sprites namings (#33451)

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

* Automatic changelog update

* Add a 10u vial of plasma to the chemical locker (#33871)

Add 10u plasma to the chemistry locker so the chemists stop stealing tables.

* Automatic changelog update

* Rarer Highcaps (#34469)

* Removed highcap from seclite

* Changed cyborg starting battery to highcap, reduced seclite wattage to make it last as long as it used to.

* Gave cyborgs back their medcap

* Rounded seclite wattage down to 0.5

* Automatic changelog update

* New dry fire sound (#34447)

* new empty.ogg

* source to tg commit

* Automatic changelog update

* CentComm Map Updates (#34475)

* Bandage fix denied animations playing on devices without them

* CentComm blast door prototype

* CentComm button

* CentComm window shutters

* CC Updates

* Save as grid

* Remove an extra detective figurine

I like them better in the interrogation room

* Remove paramed locker, let pumps shut off

* Fix wrong HOP locker prototype

* Automatic changelog update

* Cog power setup fix (#34188)

* many changes

* contentingregrationtests

* serialized invalid removed

* blank

* "Changes and fixes as suggested"

* blank

* blank

* added desk bells

* engi rework rework rework

* added gate to content integration

* tweaks

* aaa

* bbb

* added holopads

* ccc

* Update default.yml

* hotfix

* aaa

* bbb

* many many tweaks and fixes

* aaa

* decals and maints

* aaa

* bbb

* ccc

* cog power setup was bad

* made it artsy

---------

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

* Add Airlocks with Bar and Kitchen access (#33821)

* Add kitchen access to Bar-Cafeteria airlocks on Cog

* Fix merge conflict

* Remove mapping changes

* Add Glass and Maints versions

* Fixed minor spelling mistake in Noir Trenchcoat description. (#34519)

Update coats.yml

* Update Credits (#34507)

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

* Make storage implant drop items on gibbing (#33493)

* Make storage implant drop items on gib/removal

* Better way

* Fix error

* Forgotten trash

* Cleanup

* Unused var

* Update Content.Server/Implants/ImplantedSystem.cs

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

---------

Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.co>
Co-authored-by: 0x6273 <0x40@keemail.me>

* Automatic changelog update

* Fix `emergency_elkridge` being saved as a map (#34496)

Save `emergency_elkridge` as a grid

* Update engine to v240.1.0 (#34524)

* Fixes some mobs not being able to honk/weh (#33777)

* fixes some mobs not being able to honk/weh

* addresss review

* Automatic changelog update

* Welding gas mask toggleable with action (#32691)

welding mask removable

* Automatic changelog update

* Update engine to v240.1.1 (#34527)

* Automatic changelog update

* Plasma Dirt Fix (#34534)

did the dirt thing

* Plasma station population tweak (#34549)

* Plasma Station initial commit

* Map fixes 1

Expanded science's SMES array
Added advanced SMES
Redone stamped documents with custom stamps
Expanded atmospherics with more storage tanks
Added status displays
Add missing beacons to solars
Replaced the passive gates in science with valves
Removed protolathe in engineering
Added guitar to CE office
Replaced throngler plushie with weh cloak
Add a lattice tile outside the atmos burn chamber and storange tanks
Added atmos network monitor in bridge

* Add cargo and emergency shuttle

* Updated maps

* Add plasma to map testing list

* Map fixes 2

Reworked pipenets to not go under walls
Redid salvage and disposals
Reworked the bar to include a new bar extension facing the pool
Replaced arrivals cryo with an arcade
Replaced the toilets in the service plaza with cryo
Removed the cryo in dorms
Added more details to hallways
Redid tools room to include a front desk for the janitor closet
Reconnected sci to power roundstart
Removed some unideal spawns
Expanded the TEG airlock to be 2x3 instead of 1x3
Reduced the size of the SMES bank from 10 to 6
Disabled the plasma miners (downstreams or admins can re-enable them)
Replaced illegal maint items

* Fixes a 6 pack destroying the universe

Ok maybe cracking a cold one with the boys wasn't a great idea.

* Map fixes 3

* Quick research assistant fix

* Map fixes 4

* Map fixes 5

* webedit go brrrt

* Map fixes 6

* Map fixes 7

* Map fixes 8

* Fixes non-existent object

It's amazing this game runs at all

* Map fixes 9

* update pools

* Map fixes 10

* forgot to clear my multitool

I love mapping I love mapping I love mapping I love mapping I love mapping

* Tweaked player counts

* Update population caps

Removed population cap of 60 players to make plasma into a highpop map - it's that easy!

---------

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

* Automatic changelog update

* Job contraband rework (#33385)

* contraband system rework to allow restriction by job, not just department

* Fixing detective trenchcoat inheritance

* removing unnecessary using declarations

* trying to fix testing error by re-adding diagnostics using declaration

* removing unecessary dependency, making allowedJobs nullable

* Adding all of slarti's requested changes except for the hacky job icon method fix

* removing accidental whitespace

* choosing to use the non-localized version because we're comparing the string against the AllowedJobs field, and the contraband classes that fill that field are written in english

* removing unneeded using dec, fixing nesting logic problem

* didn't remove the old nesting, doing that now

* using localized job title and localizing the allowed jobs string, removing usages of JobTitle field. Also networked the _jobTitle field instead.

* rewrite some stuff

* fixes

* fix energy pen

---------

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

* Automatic changelog update

* Electrified doors/windoors now spark, new tips to deal with doors without access or when electrified (#34502)

* new tips to open doors (throwing PDA/ID, dragging body)

* electrified door sprite for players

* tooltip to reset AI electrified doors

* windoor electrified sprite

* highsec electrified visual

* increase tip dataset to 138

* corrected square bracket convention in this commit

* removed door corpse tip from prior commit

* Automatic changelog update

* Blueprint double emergency tank (#34232)

* blueprint

* NitrogenTank

* description

* Automatic changelog update

* Add system to kick people if they connect to multiple servers at once. (#34563)

* Automatic changelog update

* bagel update (#34572)

* bagel update

* Decal fault tolerance

* Also fix writes

* fix mv cable, decal issues, yar

* fix a floor tile and some other shit

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>

* remove tropico from devmap (#34585)

* Update wizden config to disallow multiple connections to multiple wizden servers (#34584)

* Give the chef access to cloth boxes (#34403)

* Give the chef access to fiber bags

* Update Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml

---------

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

* Automatic changelog update

* lecter visual update (#34589)

* Automatic changelog update

* Box Station - Update (#34605)

* Various changes

* Implemented a bunch of changes Emisse wanted, finished up all my atmos changes I wanted to make as well

* Added reporter to prototype, and fixed dirt

* Resolved outstanding issues

* Fixed a floating camera

* pluralize the job name in the contra description (#34559)

* pluralize the job name in the contra description

* pluralization specific to contraband descriptions

* Automatic changelog update

* Feature/make radial menu great again (#32653)

* it works! kinda

* so it works now

* minor cleanup

* central button now is useful too

* more cleanup

* minor cleanup

* more cleanup

* refactor: migrated code from toolbox (as it was rejected as too specific)

* feat: moved border drawing for radial menu into RadialMenuTextureButton. Radial menu position setting into was moved to OverrideArrange to not being called on every frame

* refactor: major reworks!

* renamed DrawBagleSector to DrawAnnulusSector

* Remove strange indexing

* Regularize math

* refactor: re-orienting segment elements to be Y-mirrored

* refactor: extracted radial menu radius multiplier property, changed color pallet for radial menu button

* refactor: removed icon backgrounds on textures used in current radial menu buttons with sectors, RadialContainer Radius renamed and now actually changed control radius.

* refactor: in RadialMenuTextureButtonWithSector all sector colors are converted to and from sRGB in property getter-setters

* refactor: renamed srgb to include Srgb suffix so devs gonna see that its srgb clearly

* fix: enabled any functional keys pressed when pushing radial menu buttons

* fix: radial menu sector now scales with UIScale

* fix: accept only one event when clicking on radial menu ContextualButton

* fix: now radial menu buttons accepts only click/alt-click, now clicks outside menu closes menu always

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>

* Automatic changelog update

* Return Drozd full-auto and semi-auto firing modes (#34604)

return drozd full auto and firerate, arrange available modes in alphabetical order

* Automatic changelog update

* added missing allowed department to the restricted severity (#34558)

* added missing allow job to the base restricted severity

* no need to make a list

* no more linq in ContrabandTest

* less nesting in ContrabandTest

* Automatic changelog update

* C4 Helmet (#34076)

* worn bomb

* Update meta.json

* Update meta.json

* Automatic changelog update

* Make radioactive material radioactive (#34436)

* Make radioactive material radioactive

* Increase the slopes of item uranium

No free power for engineering

* Glowing uranium

* Revert "Increase the slopes of item uranium"

This reverts commit 2acbda26

* Nerf Wall Rocks

* Automatic changelog update

* Small fixes for Meta station (#34613)

* Fixed a few minor things, removed salvage's shuttle parts (sorry), added more equipment to armory.

* Added back salvage shuttle equipment, now in a crate

* Fix

* replace all instances of "department-{id}" with department.name (#34607)

replace all instances of "department-{id}" with department.name"

* Renaming sexy mime and clown mask (#34258)

* Renamed sexy clown and mime mask and removed lusty xenomorph poster.

* removing odd file changes. not sure why these were edited at all.

* gaaaahhh

* removed some of the old templates

* Renaming sexy mime and clown instances.

* removed unwanted gitignore edit

* Renaming sexy clown and sexy mime mask and
Removing lusty exomorph poster.

* Remove lusty xenomorph poster.

* reverted changes to maps files, actually. Other contrib is learning, appologies for excessive commits

* requested changes

* lusty migration removal

* dirty hanging whitespace

* correct date format comment

---------

Co-authored-by: NathanielJ14 <nathanielalbert1202@gmail.com>

* Automatic changelog update

* Moved Cyborg Recharging Circuit Board from Lathe to Circuit Imprinter (#34612)

Moved Cyborg Recharging Cirbuit Board from Lathe to Circuit Imprinter

* Automatic changelog update

* Replace starter borg brain with Positronic (#34614)

THE FLESH IS WEAK

* Automatic changelog update

* Elkridge Depot fixes and changes (#34539)

* fixed grav gen room, gave atmos room to branch off, replaced AME controller crate with AME controller

* added rust to maints, expanded atmos, expanded bar, expanded cap bedroom, fixed some tiles under doors, removed some gamer loot

* removed interrogator lamp and some more gamer loot

* fixed some more tiles under doors, replaced intercomall with intercomcommand in bridge and vault

* added clothing drobes to arrivals, added bin to HoP, more stuff i forgot

* i forgot to move the file from /bin to /resources...

* delete radstorm locale (#34630)

* display the current version in the changelog window (#34556)

* just works first try

* introduce more magic numbers into the codebase

* idiot doesnt know the difference between AND/OR

* Update Credits (#34644)

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

* New solar sprites, new solar panel upgrades, and some solar panel fixes. (#29224)

* New solar sprites, new solar panel upgrades, and some solar panel fixes.

This adds and changes a few things for solar panels!

* New sprites for all solar panels and all related states.
* Move from xform.WorldRotation to _xformSystem.SetWorldRotation within
  the solar code.
* Few random fixes that Rider suggested as warnings.
* Solar Tracker Electronics was using what looks like to be the airlock
  controller electronics, so that's now updated to something a bit more
  realistic. It also uses the engineering circuit sprite instead of the
  generic
* New Solar Panels! Adds Plasma and Uranium Glass solar panels. These
  can be constructed by adding the respective glass to the panel. Plasma
  is a slight increase of power and health, and uranium is double the
  power and health of glass. Thus giving engineers something to update
  if they want to use solar panels and possibly giving small outposts
  a way to make a bit more power without a large and complex power
  setup.

* Add in solar sprites that were not in the meta file.

* Updated sprites based on feedback.

* Fix the rotation of the solar panel sprites.

* Automatic changelog update

* Added Unused HoS's Flask to HoS locker (#34658)

added hos flask to locker spawn

* Automatic changelog update

* Fland Fix (#34670)

Update fland.yml

* Reroute Meta station power, engineering cosmetic changes, minor fixes  (#34669)

* reroute meta power plus cosmetic fixes

* fix a lot of other things

* make changes from conflict

* Amber Station Changes (#34656)

* various changes

* Increased player limit

* slight modification

* Followed proper yaml formatting, increased lawyers

* Box Station Changes (#34655)

* Various changes

* Implemented a bunch of changes Emisse wanted, finished up all my atmos changes I wanted to make as well

* Added reporter to prototype, and fixed dirt

* Resolved outstanding issues

* Fixed a floating camera

* Added cameras to TEG, and some minor changes

* Fix mv cable crate typo (#34673)

fix mv cable crate typo

* Plasma station patch 1 (#34602)

* Plasma Station initial commit

* Map fixes 1

Expanded science's SMES array
Added advanced SMES
Redone stamped documents with custom stamps
Expanded atmospherics with more storage tanks
Added status displays
Add missing beacons to solars
Replaced the passive gates in science with valves
Removed protolathe in engineering
Added guitar to CE office
Replaced throngler plushie with weh cloak
Add a lattice tile outside the atmos burn chamber and storange tanks
Added atmos network monitor in bridge

* Add cargo and emergency shuttle

* Updated maps

* Add plasma to map testing list

* Map fixes 2

Reworked pipenets to not go under walls
Redid salvage and disposals
Reworked the bar to include a new bar extension facing the pool
Replaced arrivals cryo with an arcade
Replaced the toilets in the service plaza with cryo
Removed the cryo in dorms
Added more details to hallways
Redid tools room to include a front desk for the janitor closet
Reconnected sci to power roundstart
Removed some unideal spawns
Expanded the TEG airlock to be 2x3 instead of 1x3
Reduced the size of the SMES bank from 10 to 6
Disabled the plasma miners (downstreams or admins can re-enable them)
Replaced illegal maint items

* Fixes a 6 pack destroying the universe

Ok maybe cracking a cold one with the boys wasn't a great idea.

* Map fixes 3

* Quick research assistant fix

* Map fixes 4

* Map fixes 5

* webedit go brrrt

* Map fixes 6

* Map fixes 7

* Map fixes 8

* Fixes non-existent object

It's amazing this game runs at all

* Map fixes 9

* update pools

* Map fixes 10

* forgot to clear my multitool

I love mapping I love mapping I love mapping I love mapping I love mapping

* Tweaked player counts

* Update population caps

Removed population cap of 60 players to make plasma into a highpop map - it's that easy!

* Map fixes 11

* Map fixes 12

* Map fixes 13

* Map fixes 14

Now it's personal

* Map fixes 15

---------

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

* Automatic changelog update

* add a chem dispenser to the nukie planet (#34674)

* add a chem dispenser to the nukie planet

* begone test fail

* Adds better description to pneumatic valve and build menu description (#32655)

* adds description to pneumatic valve and build menu description

* Change one atmosphere to kPa

* Automatic changelog update

* fixrotations - Modified Targetted Entities (#34638)

* Fix match box (#34632)

FixMatchBox

Now you can't put anything but matches in a box of matches.

Co-authored-by: Helm4142 <Helm4142@users.noreply.github.com>

* Automatic changelog update

* Hi-viz vest now actually hi-viz (#34087)

* 27-12-2024-light-hi-viz

* 28-12-2024-meta-fix

* Storage UI V2 (#33045)

* Automatic changelog update

* Added Pain Numbness Trait (#34538)

* added pain-numbness component and system

* added numb as a trait that pulls the pain numbness component

* removed new event as mob threshold event as already being fired

* checked for MobThresholdsComponent first before running VerifyThresholds

* refacted force say to using LocalizedDatasetPrototype and added numb messages

* added severity check alert

* added comment for BeforeForceSayEvent

* removed space formatting

* changed Cancelled to CancelUpdate, fixed spacing and added two more damage-force-say-numb

* changed prefix damage-force-say-numb to 5 (whoops)

* Automatic changelog update

* Update submodule to 241.0.0 (#34678)

* Add puddle drawdepth (#32369)

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

* Automatic changelog update

* Revert "Fix match box" (#34681)

Revert "Fix match box (#34632)"

This reverts commit 88456a4a8c.

* update .editorconfig (#34677)

* Make crew monitor update blips at consistent rates (#32555)

* Added the ability for pAIs and station maps to be stored in engineering belts (#33048)

Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>

* Automatic changelog update

* Engineering guidebook megaupdate v2 (#33062)

Significantly updates the Engineering guidebook (more explicitly the Atmos section) to have a lot more relevant and useful information.

Right now engineering has been getting update after update with no real change to the relevant guidebook entry. This has lead to a lot of out of date information and bad practices being prevalent in the guidebook, something that pains me to read.

* Automatic changelog update

* Omega: fix cryo pipe (#34663)

fixed the freezer pipe in cryo room

* Add more escape pods on Packed (#34628)

* add escape pods north and south of station

* move south escape pod a bit to the east

* Storage sidebar fix (#34680)

* Add conditional camera offset based on cursor - Hristov Rework, Part 1 (#31626)

* Automatic changelog update

* Amber Station Stuff (#34686)

* Removed salv upper docking airlock, gave south west solars some uranium glass.

* Routed HV wire through maints more, gave disposal blast doors a lever, added beacons and signs to escape pods

* Fake mindshield componentry and Implanter (#34079)

* Fake Mindshield (With some bad sprites)

- Add FakeMindshield System and Component

- Add FakeMindsheildImplantSystem and Component

- modify ShowMindShieldIconsSystem to check for FakeMindshields

- add all supporting yaml for the Implants, action and uplink

- add loc file stuff

- add unfinished sprites

* Cleanup, add to thief toolbox, remove metagame

- Move Implant sameness check to AFTER the implant DoAfter
to prevent instant identification of Deception Implants

- cleanup the systems and components

- add the fake mindshield to the Thief toolbox

* part 1 of fixing the folder problem

* Make the fakemindshield sprite folder lowercase

* CR - Move ImplantCheck into shared, cleanup

- Moved ImplantCheck and eventsubscription into Shared

- Remove Client/Server extensions of FakeMindshieldImplantSystem and
FakeMindShieldSystem and make shared Sealed

- make OnToggleMindshield Private, use the event!

* CR - Cleanup extra lines, fix some Prototype

- cleaned up extra liens in ImplanterSystem and
SharedFakeMindshieldSystem from when i was developing

- Uplink catalog no longer lists the implant in 2 spots,
only implants now, also uses the On state action icon

- added a comment about why it's reraising the action event
rather than directly interacting with the FakeMindshield Component

* Fake Mindshield CR:

- Added a comment about IsEnabled

- moved OnFakeMindShieldToggle to Entity<> from Uid, Comp

- fixed some formatting in uplink_catalog

* CR - Add a bit more comment

* Automatic changelog update

* reworking the chunked telecomms salv ruin (#34529)

* aaa

* bbb

* ccc

* ddd

* switched to grid

* pain

---------

Co-authored-by: TytosB <duanlaintytos@yahoo.com>

* loop update (#34688)

loop loot gone

* bagel update (#34690)

* bagel update

* fixe

* move tesla gen

* Fix airsensors not having a nitrogen threshold (#34689)

* Buff frezon to acceptable values, pending a frezon rework (#34049)

buff frezon

* Automatic changelog update

* Stun baton precise attack thrust animation (#34693)

use weaponarcthrust instead of wide swing on single attack

* Automatic changelog update

* Add disposal units to marathon chapelroid (#34709)

add disposal units to marathon chapelroid

* Oasis: add some emergency O2/N2 lockers (#34715)

Added O2, N2, and Fire lockers to Oasis station as per discussions in the issue thread

Co-authored-by: Orange-Winds <sebastian.pelka@hotmail.com>

* Removes radioactive suppermatter from Plasma Station (#34726)

Removes radioactive suppermatter

Literally 1984 can't have shit on SS14 e.t.c. e.t.c.

Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com>

* New salvage ruin: the ruined prison ship (#34651)

* aaa

* little tweaks

* changed to grid and added to protos

* ccc

* we dont talk about it

* ddd

---------

Co-authored-by: TytosB <dunalintytos@yahoo.com>

* Buff savlage vault-medium-1's loot (#34732)

* aaa

* little tweaks

* changed to grid and added to protos

* ccc

* we dont talk about it

* ddd

* aaa

---------

Co-authored-by: TytosB <dunalintytos@yahoo.com>

* drozd visual update (#34574)

* drozd visual update

* fix wielded sprotes

* update sprotes

* Automatic changelog update

* Nuke Timer MinimumTime (#34734)

* Implemented the thing

* Made requested changes

* Automatic changelog update

* Fix thrust animation rotation (#34713)

* Fix thrust animation rotation

* directly update sprite instead

* Automatic changelog update

* Fix vulture spawning additional salvage debris (#34593)

fix vulture spawning additional salvage debris

* Nukie briefing fix (#34737)

payloadpayloadpayloadpayloadpayloadpayload

* Sentient medibot now can inject (#32110)

medibot player injection

Co-authored-by: YourUsername <you@example.com>

* Criminal Records Computer Better UX + Filtering (#32352)

* First pass at new Criminal Records Computer

need buttons to highlight.

* Filter status tabs/buttons now activate correctly via UpdateState

* Removed unneeded Directives

* Fix typo + undo VSCode changes

* Implement Emo Feedback

Loc NA and use inject deps
Cannot use inject deps on sprite system.

* try to undo vscode launch.json change

* Added requests + Filter dropdown list + jobs

Fixed maintainer fix requests,
Added Job to announcement channel output
Removed toggle buttons in-place of a dropdown list

* Fixed missed merge conflict

+ fixed an bug with filterstatus not showing on re-open ui

* Update criminal-records.ftl

Fixed lint error. whoops.

* Update Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs

typo

Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>

* impliment chromiumboy feedback

hopefully this will do it....

---------

Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>

* Added some more borg names (#32502)

added a handful of new names

* Automatic changelog update

* Update ScalingViewport for Engine PR (#28786)

* Update submodule to 242.0.0 (#34739)

* fix stop bleeding popup (#34729)

* fix stop bleeding popup

* add identity

* Fix RoleTimeRequirement localization (#34735)

* fix job restriction localization

* Update Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs

* Update Content.Shared/Roles/JobRequirement/RoleTimeRequirement.cs

---------

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

* reworking chem and adding psychology to loop (#34749)

* aaa

* little tweaks

* changed to grid and added to protos

* ccc

* we dont talk about it

* ddd

* aaa

* psychology real

---------

Co-authored-by: TytosB <dunalintytos@yahoo.com>

* Fix shuttle console angular velocity (#34748)

* Syndicate and CentComm Radio Implanters (#33533)

* Add Syndicate Radio Implant

* Fix description grammar

* remove unused var

* Update - Small fixes

* Un mess with imports

* Remove unused tag

* Correction

* Clear lists instead of remove

* Update 0 check

* Add Centcomm implant

* Correct centcom channel

* Correct params

* No more crying

* Update Content.Shared/Implants/Components/RadioImplantComponent.cs

* Update Content.Server/Implants/RadioImplantSystem.cs

* Update Content.Server/Implants/RadioImplantSystem.cs

---------

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

* Automatic changelog update

* New Highpop map: Convex recreational complex (#33346)

* start ig

* command work +AI

* finished bridge, added signs to departments

* engi partly done

* finish engi + atmos except for cams

* added med yippie

* carGODO

* nerd department finished

* se(x)c finish

* courtroom + service power

* man im tweaking out

* service propa done

* hallways

* added cams everywhere and named all the doors :pain:

* arrivals maints + exterior catwalk

* more maints

* voxob

* draft done

* holopadpilled + gaspipesensor based

* added convex name everywhere it needed to be

* nvm i guess we dont do oxford comma

* redid ALL THE HGOLOPADS FAAAAAAAAAAAAAAAAAAAAA

* Fix conflicts?

* ok let me try something

* this works right?

* atmos updated + jani lights

* uniform hallway tiles +roundstart TEG

* fixed some hidden atmos stuff

* Update map_attributions.txt

* :finnadie: no map review ...

* the fog is coming

* maints theater redone

* light dirt and the murder of most trims

* 💔

* decal based and tileoverlay pilled

* updated arrivals/library

* bungus

* yvgh

* 6 hours later...

* finished for real?

* Automatic changelog update

* Update cigarette.yml (#34756)

simple change to one of Dan's Smokes that fixes a typo.

* Seperate EMAG into EMAG and Authentication Disruptor (#34337)

* Automatic changelog update

* Astro Asteroid Sand (#34463)

* Added astro asteroid sand

* fixed the stack name

* adjusted tile sprites for asteroid and ice

* Adjusted meta.json

* Realized the other tile sprites all have solid lines on their perimeters

* Update Resources/Prototypes/Stacks/floor_tile_stacks.yml

that works

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>

* Update tiles.yml

* Update Resources/Locale/en-US/tiles/tiles.ftl

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>

* adjusted the snow sprite

* Update meta.json

---------

Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>

* Automatic changelog update

* The Goliath Hardsuit (#34721)

* Entities for hardsuit and helmet, graph and crafting recipe; modifies goliath hide drop rate

* Added hardsuit sprites, corrected some crafting YAML

* fixed crafting recipe, adjusted resource requirements

* Added durathread material req

---------

Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>

* Automatic changelog update

* make RefreshOverlay default to the player session (#32354)

* Loop station minor tweaks (#34758)

* small fixes

* eee

* Juice that makes you go boom (#34730)

* Juice that makes you go boom

* moved explosive juice to fun yml - fixed ExplosionReactionEffect.cs not having TileBreakScale parameter - made Drazil plushie major contraband (they are evil!!!!!)

* removed JASON!!!! JASOOON!!! JASON!!!

* don't do commits at 1am

* Update fun.yml

fix ident

* no more bullying the server (only 1 explosion)

* Automatic changelog update

* Adjust inventory size of ginormous scrap (#33454)

Adjusts the size of ginormous scrap to fit in bags of holding

* Automatic changelog update

* After getting banned, you now have to re-read the rules! (#33270)

* first commit

* opps

* Reset cooldown instead

* Added ccvar

* Not replicated!

* More robust Particle Accelerator menu (retry) (#34037)

Particle Acceleratir fixed

Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com>

* Serializable emag flags (#34766)

* serializable idk

* guh

* Fix Double Muzzle Flash (#33981)

* Pass user to effects to properly fix double muzzle flash.

* Use gun when user is null.

* Update MuzzleFlashEvent.cs

* Update MuzzleFlashEvent.cs

* Update GunSystem.cs

* Update SharedGunSystem.cs

* Update MuzzleFlashEvent.cs

* Update SharedGunSystem.cs

* Update SharedGunSystem.cs

* Update Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs

Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>

---------

Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>

* Automatic changelog update

* fix BankClientComponent never updating (#33123)

* fix

* oh right

* Add history tab to bounty console (#33932)

* Add struct for holding historical data on cargo bounties

* Add localisation strings for bounty history

* Add new XAML entry for display bounty history

* Expand cargo bounty menu to include tabs

* Ensure station databases hold historical bounty data

* Add to the bounty history when removing one from active

* Feed bounty history into cargo's bounty system

* Move tab title setting to constructor

* Remove redundant access specifications

* Remove un-needed override

* Fixup BountyHistoryEntry backing code

* Fix formatting in CargoBountyMenu

* Reformat BountyHistoryData

* Rework TryRemoveBounty to use new Entity type

* Add Enum for showing bounty results

* Rework look and feel of History tab

* Add visible text when no bounties have been completed yet

* Remove control

* Swap default to null

* Reverse ordering of bounties so last entry comes first

* Remove redundant Visible

* Move enum docs into the enum

* Automatic changelog update

* Minor shotgun changes and comments for future changes (#33512)

* kammerer ammo, firerate and comment

* bulldog description

* enforcer description and comment

* DB and sawn-off firerate parity, sawn-off comment

* description changes

* reduce kammerer firerate again

* Automatic changelog update

* Fixed: Ore now correctly drops the right amount of ore (#34557)

Fixed bug

* Automatic changelog update

* Implement mrp rule changes pursuant to community votes (#34772)

* #34771 Fix door system assuming all door layer states are in single RSIs (#34775)

* #34771 Fix door system assuming all door layer states are in single RSIs

* Delta confirmed monarch of sloggery

* Do a dirty, rotten web edit

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

---------

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

* HOTFIX update staging submodule to 242.0.1 (#34833)

* HOTFIX revert lecter visual update #34589 (#34826)

* Allowed windows to be properly clicked on (#34751)

* Fix hitting through directional windows (and more!) (#34793)

* Revert "Add taped logo back for 10th anniversary" (#34831)

* Hristov & .60 changes - Hristov Rework, Part 2 (#31662)

* Initial commit

* Updated values to reflect new resistances

* Review fixes

* Review fixes

* LINQ BEGONETH

* revert engine

* Revert "Storage sidebar fix (#34680)"

This reverts commit 3e091c4dfa.

* Revert "Storage UI V2 (#33045)"

This reverts commit fd25dac720.

* Revert "Update ScalingViewport for Engine PR (#28786)"

This reverts commit 3ad83378bb.

* Update physical.yml

* Update CP14SharpeningSystem.cs

---------

Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: c4llv07e <igor@c4llv07e.xyz>
Co-authored-by: PursuitInAshes <91865152+PursuitInAshes@users.noreply.github.com>
Co-authored-by: TakoDragon <69509841+BackeTako@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: Mono <182929384+Monotheonist@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
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: Nox <nebulousnox38@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: War Pigeon <54217755+minus1over12@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>
Co-authored-by: Alfred Baumann <93665570+CheesePlated@users.noreply.github.com>
Co-authored-by: Tezzaide <ewankayne@hotmail.co.uk>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: 0x6273 <0x40@keemail.me>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: compilatron <40789662+Compilatron144@users.noreply.github.com>
Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com>
Co-authored-by: John <35928781+sporkyz@users.noreply.github.com>
Co-authored-by: Nim <128169402+Nimfar11@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: Ignaz "Ian" Kraft <ignaz.k@live.de>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: Eoin Mcloughlin <helloworld@eoinrul.es>
Co-authored-by: JustinWinningham <justinmwinningham@gmail.com>
Co-authored-by: NathanielJ14 <nathanielalbert1202@gmail.com>
Co-authored-by: Velken <8467292+Velken@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: CaasGit <87243814+CaasGit@users.noreply.github.com>
Co-authored-by: Coolsurf6 <coolsurf24@yahoo.com.au>
Co-authored-by: SlimSlam <73899110+SlimmSlamm@users.noreply.github.com>
Co-authored-by: Dinner <180707738+DinnerCalzone@users.noreply.github.com>
Co-authored-by: Helm4142 <158806576+Helm4142@users.noreply.github.com>
Co-authored-by: Helm4142 <Helm4142@users.noreply.github.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: August Sun <45527070+august-sun@users.noreply.github.com>
Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>
Co-authored-by: NakataRin <45946146+NakataRin@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Zachary Higgs <compgeek223@gmail.com>
Co-authored-by: TytosB <duanlaintytos@yahoo.com>
Co-authored-by: Orange-Winds <orange.wind@outlook.com>
Co-authored-by: Orange-Winds <sebastian.pelka@hotmail.com>
Co-authored-by: TytosB <dunalintytos@yahoo.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: godisdeadLOL <169250097+godisdeadLOL@users.noreply.github.com>
Co-authored-by: YourUsername <you@example.com>
Co-authored-by: James Simonson <jamessimo89@gmail.com>
Co-authored-by: darkdan <145926356+Starbuckss14@users.noreply.github.com>
Co-authored-by: Preston Smith <92108534+thetolbean@users.noreply.github.com>
Co-authored-by: DR-DOCTOR-EVIL-EVIL <hudsonirwin11@icloud.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: SpaceRox1244 <138547931+SpaceRox1244@users.noreply.github.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com>
Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Co-authored-by: BarryNorfolk <barrynorfolkman@protonmail.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: pubbi <63283968+impubbi@users.noreply.github.com>
This commit is contained in:
Ed
2025-02-06 13:57:14 +03:00
committed by GitHub
parent 55a8fc4049
commit a0a827ea5a
628 changed files with 312888 additions and 51155 deletions

View File

@@ -344,6 +344,9 @@ resharper_keep_existing_attribute_arrangement = true
resharper_wrap_chained_binary_patterns = chop_if_long
resharper_wrap_chained_method_calls = chop_if_long
resharper_csharp_trailing_comma_in_multiline_lists = true
resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
indent_size = 2

5
.envrc
View File

@@ -1,4 +1,5 @@
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
set -e
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
fi
use flake

View File

@@ -46,7 +46,7 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog" };
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;

View File

@@ -39,6 +39,6 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
if (message is not CargoBountyConsoleState state)
return;
_menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
_menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip);
}
}

View File

@@ -0,0 +1,22 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="10 10 10 0"
HorizontalExpand="True">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<RichTextLabel Name="RewardLabel"/>
<RichTextLabel Name="ManifestLabel"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" MinWidth="120" Margin="0 0 10 0">
<RichTextLabel Name="TimestampLabel" HorizontalAlignment="Right" />
<RichTextLabel Name="IdLabel" HorizontalAlignment="Right" />
</BoxContainer>
</BoxContainer>
<customControls:HSeparator Margin="5 10 5 10"/>
<RichTextLabel Name="NoticeLabel" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -0,0 +1,49 @@
using Content.Client.Message;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class BountyHistoryEntry : BoxContainer
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public BountyHistoryEntry(CargoBountyHistoryData bounty)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
return;
var items = new List<string>();
foreach (var entry in bountyPrototype.Entries)
{
items.Add(Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name))));
}
ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id)));
TimestampLabel.SetMarkup(bounty.Timestamp.ToString(@"hh\:mm\:ss"));
if (bounty.Result == CargoBountyHistoryData.BountyResult.Completed)
{
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-completed-label"));
}
else
{
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-skipped-label",
("id", bounty.ActorName ?? "")));
}
}
}

View File

@@ -11,15 +11,28 @@
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="BountyEntriesContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
</BoxContainer>
</ScrollContainer>
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True">
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="BountyEntriesContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<Label Name="NoHistoryLabel"
Text="{Loc 'bounty-console-history-empty-label'}"
Visible="False"
Align="Center" />
<BoxContainer Name="BountyHistoryContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
</TabContainer>
</PanelContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">

View File

@@ -15,9 +15,12 @@ public sealed partial class CargoBountyMenu : FancyWindow
public CargoBountyMenu()
{
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle(0, Loc.GetString("bounty-console-tab-available-label"));
MasterTabContainer.SetTabTitle(1, Loc.GetString("bounty-console-tab-history-label"));
}
public void UpdateEntries(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
public void UpdateEntries(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
{
BountyEntriesContainer.Children.Clear();
foreach (var b in bounties)
@@ -32,5 +35,21 @@ public sealed partial class CargoBountyMenu : FancyWindow
{
MinHeight = 10
});
BountyHistoryContainer.Children.Clear();
if (history.Count == 0)
{
NoHistoryLabel.Visible = true;
}
else
{
NoHistoryLabel.Visible = false;
// Show the history in reverse, so last entry is first in the list
for (var i = history.Count - 1; i >= 0; i--)
{
BountyHistoryContainer.AddChild(new BountyHistoryEntry(history[i]));
}
}
}
}

View File

@@ -8,6 +8,8 @@ using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Client.Changelog
@@ -15,8 +17,9 @@ namespace Content.Client.Changelog
[GenerateTypedNameReferences]
public sealed partial class ChangelogWindow : FancyWindow
{
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly ChangelogManager _changelog = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public ChangelogWindow()
{
@@ -67,8 +70,22 @@ namespace Content.Client.Changelog
Tabs.SetTabTitle(i++, Loc.GetString($"changelog-tab-title-{changelog.Name}"));
}
var version = typeof(ChangelogWindow).Assembly.GetName().Version ?? new Version(1, 0);
VersionLabel.Text = Loc.GetString("changelog-version-tag", ("version", version.ToString()));
// Try to get the current version from the build.json file
var version = _cfg.GetCVar(CVars.BuildVersion);
var forkId = _cfg.GetCVar(CVars.BuildForkId);
var versionText = Loc.GetString("changelog-version-unknown");
// Make sure these aren't empty, like in a dev env
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(forkId))
{
versionText = Loc.GetString("changelog-version-tag",
("fork", forkId),
("version", version[..7])); // Only show the first 7 characters
}
// if else statements are ugly, shut up
VersionLabel.Text = versionText;
TabsUpdated();
}

View File

@@ -1,4 +1,4 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
@@ -7,25 +7,25 @@
MinSize="450 450">
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
</ui:RadialMenuTextureButton>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
<!-- General -->
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Vocal -->
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Hands -->
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
</ui:RadialMenu>

View File

@@ -50,7 +50,6 @@ public sealed partial class EmotesMenu : RadialMenu
var button = new EmoteMenuButton
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = Loc.GetString(emote.Name),
ProtoId = emote.ID,
@@ -106,7 +105,7 @@ public sealed partial class EmotesMenu : RadialMenu
}
public sealed class EmoteMenuButton : RadialMenuTextureButton
public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<EmotePrototype> ProtoId { get; set; }
}

View File

@@ -20,7 +20,7 @@ namespace Content.Client.Clickable
"/Textures/Logo",
};
private const float Threshold = 0.25f;
private const float Threshold = 0.1f;
private const int ClickRadius = 2;
[Dependency] private readonly IResourceCache _resourceCache = default!;

View File

@@ -22,7 +22,7 @@ public sealed class CrewManifestSection : BoxContainer
AddChild(new Label()
{
StyleClasses = { "LabelBig" },
Text = Loc.GetString($"department-{section.ID}")
Text = Loc.GetString(section.Name)
});
var gridContainer = new GridContainer()

View File

@@ -39,6 +39,8 @@ public sealed class CriminalRecordsConsoleBoundUserInterface : BoundUserInterfac
SendMessage(new CriminalRecordChangeStatus(status, null));
_window.OnDialogConfirmed += (status, reason) =>
SendMessage(new CriminalRecordChangeStatus(status, reason));
_window.OnStatusFilterPressed += (statusFilter) =>
SendMessage(new CriminalRecordSetStatusFilter(statusFilter));
_window.OnHistoryUpdated += UpdateHistory;
_window.OnHistoryClosed += () => _historyWindow?.Close();
_window.OnClose += Close;

View File

@@ -1,36 +1,142 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'criminal-records-console-window-title'}"
MinSize="660 400">
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'criminal-records-console-window-title'}"
MinSize="695 440">
<BoxContainer Orientation="Vertical">
<!-- Record search bar
TODO: make this into a control shared with general records -->
<BoxContainer Margin="5 5 5 10" HorizontalExpand="true" VerticalAlignment="Center">
<OptionButton Name="FilterType" MinWidth="200" Margin="0 0 10 0"/> <!-- Populated in constructor -->
<LineEdit Name="FilterText" PlaceHolder="{Loc 'criminal-records-filter-placeholder'}" HorizontalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<!-- Record listing -->
<BoxContainer Orientation="Vertical" Margin="5" MinWidth="250" MaxWidth="250">
<Label Name="RecordListingTitle" Text="{Loc 'criminal-records-console-records-list-title'}" HorizontalExpand="True" Align="Center"/>
<Label Name="NoRecords" Text="{Loc 'criminal-records-console-no-records'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
<ScrollContainer VerticalExpand="True">
<ItemList Name="RecordListing"/> <!-- Populated when loading state -->
</ScrollContainer>
<BoxContainer Name="AllList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="8">
<!-- Record search bar -->
<BoxContainer Margin="5 5 5 10"
HorizontalExpand="true"
VerticalAlignment="Center">
<OptionButton Name="FilterType"
MinWidth="250"
Margin="0 0 10 0" />
<!-- Populated in constructor -->
<LineEdit Name="FilterText"
PlaceHolder="{Loc 'criminal-records-filter-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<Label Name="RecordUnselected" Text="{Loc 'criminal-records-console-select-record-info'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
<!-- Selected record info -->
<BoxContainer Name="PersonContainer" Orientation="Vertical" Margin="5" Visible="False">
<Label Name="PersonName" StyleClasses="LabelBig"/>
<Label Name="PersonPrints"/>
<Label Name="PersonDna"/>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
<Label Name="StatusLabel" Text="{Loc 'criminal-records-console-status'}" FontColorOverride="DarkGray"/>
<OptionButton Name="StatusOptionButton"/> <!-- Populated in constructor -->
<BoxContainer Orientation="Horizontal"
VerticalExpand="True">
<!-- Record listing -->
<BoxContainer Orientation="Vertical"
Margin="10 10"
MinWidth="250"
MaxWidth="250">
<Label Name="RecordListingTitle"
Text="{Loc 'criminal-records-console-records-list-title'}"
HorizontalExpand="True"
Align="Center" />
<Label Name="NoRecords"
Text="{Loc 'criminal-records-console-no-records'}"
HorizontalExpand="True"
Align="Center"
FontColorOverride="DarkGray" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="RecordListing" />
<!-- Populated when loading state -->
</ScrollContainer>
</BoxContainer>
<RichTextLabel Name="WantedReason" Visible="False"/>
<Button Name="HistoryButton" Text="{Loc 'criminal-records-console-crime-history'}"/>
<Label Name="RecordUnselected"
Text="{Loc 'criminal-records-console-select-record-info'}"
HorizontalExpand="True"
Align="Center"
FontColorOverride="DarkGray" />
<!-- Selected record info -->
<BoxContainer Name="PersonContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="5"
Visible="False">
<Label Name="PersonName"
Margin="0 0 0 5"
StyleClasses="LabelBig" />
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'crew-monitoring-user-interface-job'}:"
FontColorOverride="DarkGray" />
<Label Text=":"
FontColorOverride="DarkGray" />
<TextureRect Name="PersonJobIcon"
TextureScale="2 2"
Margin="6 0"
VerticalAlignment="Center" />
<Label Name="PersonJob" />
</BoxContainer>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'general-station-record-prints-filter'}"
FontColorOverride="DarkGray" />
<Label Text=":"
Margin="0 0 6 0"
FontColorOverride="DarkGray" />
<Label Name="PersonPrints" />
</BoxContainer>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'general-station-record-dna-filter'}"
FontColorOverride="DarkGray" />
<Label Text=":"
Margin="0 0 6 0"
FontColorOverride="DarkGray" />
<Label Name="PersonDna" />
</BoxContainer>
<PanelContainer StyleClasses="LowDivider"
Margin="0 5 0 5" />
<BoxContainer Orientation="Horizontal"
Margin="0 5 0 5">
<Label Name="StatusLabel"
Text="{Loc 'criminal-records-console-status'}"
FontColorOverride="DarkGray" />
<Label Text=":"
FontColorOverride="DarkGray" />
<Label Name="PersonStatus"
FontColorOverride="DarkGray" />
<AnimatedTextureRect Name="PersonStatusTX"
Margin="8 0" />
<OptionButton Name="StatusOptionButton"
MinWidth="130" />
<!-- Populated in constructor -->
</BoxContainer>
<RichTextLabel Name="WantedReason"
Visible="False"
MaxWidth="425" />
<Button Name="HistoryButton"
Text="{Loc 'criminal-records-console-crime-history'}"
Margin="0 5" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<OptionButton
Name="CrewListFilter"
MinWidth="250"
Margin="10 0 10 0" />
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal"
Margin="10 2 5 0"
VerticalAlignment="Bottom">
<Label Text="{Loc 'criminal-records-console-flavor-left'}"
StyleClasses="WindowFooterText" />
<Label Text="{Loc 'criminal-records-console-flavor-right'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19" />
</BoxContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -13,6 +13,9 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
namespace Content.Client.CriminalRecords;
@@ -24,6 +27,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
private readonly IPrototypeManager _proto;
private readonly IRobustRandom _random;
private readonly AccessReaderSystem _accessReader;
[Dependency] private readonly IEntityManager _entManager = default!;
private readonly SpriteSystem _spriteSystem;
public readonly EntityUid Console;
@@ -33,10 +38,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public Action<uint?>? OnKeySelected;
public Action<StationRecordFilterType, string>? OnFiltersChanged;
public Action<SecurityStatus>? OnStatusSelected;
public Action<uint>? OnCheckStatus;
public Action<CriminalRecord, bool, bool>? OnHistoryUpdated;
public Action? OnHistoryClosed;
public Action<SecurityStatus, string>? OnDialogConfirmed;
public Action<SecurityStatus>? OnStatusFilterPressed;
private uint _maxLength;
private bool _access;
private uint? _selectedKey;
@@ -46,6 +53,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
private StationRecordFilterType _currentFilterType;
private SecurityStatus _currentCrewListFilter;
public CriminalRecordsConsoleWindow(EntityUid console, uint maxLength, IPlayerManager playerManager, IPrototypeManager prototypeManager, IRobustRandom robustRandom, AccessReaderSystem accessReader)
{
RobustXamlLoader.Load(this);
@@ -55,10 +64,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
_proto = prototypeManager;
_random = robustRandom;
_accessReader = accessReader;
IoCManager.InjectDependencies(this);
_spriteSystem = _entManager.System<SpriteSystem>();
_maxLength = maxLength;
_currentFilterType = StationRecordFilterType.Name;
_currentCrewListFilter = SecurityStatus.None;
OpenCentered();
foreach (var item in Enum.GetValues<StationRecordFilterType>())
@@ -71,6 +84,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
AddStatusSelect(status);
}
//Populate status to filter crew list
foreach (var item in Enum.GetValues<SecurityStatus>())
{
CrewListFilter.AddItem(GetCrewListFilterLocals(item), (int)item);
}
OnClose += () => _reasonDialog?.Close();
RecordListing.OnItemSelected += args =>
@@ -97,6 +116,20 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
}
};
//Select Status to filter crew
CrewListFilter.OnItemSelected += eventArgs =>
{
var type = (SecurityStatus)eventArgs.Id;
if (_currentCrewListFilter != type)
{
_currentCrewListFilter = type;
StatusFilterPressed(type);
}
};
FilterText.OnTextEntered += args =>
{
FilterListingOfRecords(args.Text);
@@ -104,16 +137,21 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
StatusOptionButton.OnItemSelected += args =>
{
SetStatus((SecurityStatus) args.Id);
SetStatus((SecurityStatus)args.Id);
};
HistoryButton.OnPressed += _ =>
{
if (_selectedRecord is {} record)
if (_selectedRecord is { } record)
OnHistoryUpdated?.Invoke(record, _access, true);
};
}
public void StatusFilterPressed(SecurityStatus statusSelected)
{
OnStatusFilterPressed?.Invoke(statusSelected);
}
public void UpdateState(CriminalRecordsConsoleState state)
{
if (state.Filter != null)
@@ -129,10 +167,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
}
}
if (state.FilterStatus != _currentCrewListFilter)
{
_currentCrewListFilter = state.FilterStatus;
}
_selectedKey = state.SelectedKey;
FilterType.SelectId((int)_currentFilterType);
CrewListFilter.SelectId((int)_currentCrewListFilter);
NoRecords.Visible = state.RecordListing == null || state.RecordListing.Count == 0;
PopulateRecordListing(state.RecordListing);
@@ -179,7 +221,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
// in parallel to synchronize the items in RecordListing with `entries`.
int i = RecordListing.Count - 1;
int j = entries.Count - 1;
while(i >= 0 && j >= 0)
while (i >= 0 && j >= 0)
{
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
if (strcmp == 0)
@@ -212,23 +254,44 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
while (j >= 0)
{
RecordListing.Insert(0, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
j--;
}
}
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
{
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");
var na = Loc.GetString("generic-not-available-shorthand");
PersonName.Text = stationRecord.Name;
PersonPrints.Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", stationRecord.Fingerprint ?? na));
PersonDna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", stationRecord.DNA ?? na));
PersonJob.Text = stationRecord.JobTitle ?? na;
StatusOptionButton.SelectId((int) criminalRecord.Status);
if (criminalRecord.Reason is {} reason)
// Job icon
if (_proto.TryIndex<JobIconPrototype>(stationRecord.JobIcon, out var proto))
{
PersonJobIcon.Texture = _spriteSystem.Frame0(proto.Icon);
}
PersonPrints.Text = stationRecord.Fingerprint ?? Loc.GetString("generic-not-available-shorthand");
PersonDna.Text = stationRecord.DNA ?? Loc.GetString("generic-not-available-shorthand");
if (criminalRecord.Status != SecurityStatus.None)
{
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/security_icons.rsi"), GetStatusIcon(criminalRecord.Status));
}
PersonStatusTX.SetFromSpriteSpecifier(specifier);
PersonStatusTX.DisplayRect.TextureScale = new Vector2(3f, 3f);
StatusOptionButton.SelectId((int)criminalRecord.Status);
if (criminalRecord.Reason is { } reason)
{
var message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-wanted-reason"));
if (criminalRecord.Status == SecurityStatus.Suspected)
{
message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-suspected-reason"));
}
message.AddText($": {reason}");
WantedReason.SetMessage(message);
WantedReason.Visible = true;
}
@@ -288,9 +351,37 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
_reasonDialog.OnClose += () => { _reasonDialog = null; };
}
private string GetStatusIcon(SecurityStatus status)
{
return status switch
{
SecurityStatus.Paroled => "hud_paroled",
SecurityStatus.Wanted => "hud_wanted",
SecurityStatus.Detained => "hud_incarcerated",
SecurityStatus.Discharged => "hud_discharged",
SecurityStatus.Suspected => "hud_suspected",
_ => "SecurityIconNone"
};
}
private string GetTypeFilterLocals(StationRecordFilterType type)
{
return Loc.GetString($"criminal-records-{type.ToString().ToLower()}-filter");
}
private string GetCrewListFilterLocals(SecurityStatus type)
{
string result;
// If "NONE" override to "show all"
if (type == SecurityStatus.None)
{
result = Loc.GetString("criminal-records-console-show-all");
}
else
{
result = Loc.GetString($"criminal-records-status-{type.ToString().ToLower()}");
}
return result;
}
}

View File

@@ -21,112 +21,131 @@ public sealed class DoorSystem : SharedDoorSystem
protected override void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
{
var comp = ent.Comp;
comp.OpenSpriteStates = new(2);
comp.ClosedSpriteStates = new(2);
comp.OpenSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.ClosedSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.OpenSpriteStates.Add((DoorVisualLayers.Base, comp.OpenSpriteState));
comp.ClosedSpriteStates.Add((DoorVisualLayers.Base, comp.ClosedSpriteState));
comp.OpeningAnimation = new Animation()
comp.OpeningAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.Base,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f),
},
},
},
};
comp.ClosingAnimation = new Animation()
comp.ClosingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.Base,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f),
},
},
},
};
comp.EmaggingAnimation = new Animation ()
comp.EmaggingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick()
new AnimationTrackSpriteFlick
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f) }
}
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f),
},
},
},
};
}
private void OnAppearanceChange(EntityUid uid, DoorComponent comp, ref AppearanceChangeEvent args)
private void OnAppearanceChange(Entity<DoorComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if(!AppearanceSystem.TryGetData<DoorState>(uid, DoorVisuals.State, out var state, args.Component))
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
if (AppearanceSystem.TryGetData<string>(uid, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
}
foreach (var layer in args.Sprite.AllLayers)
{
layer.Rsi = res?.RSI;
}
}
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
UpdateSpriteLayers(args.Sprite, baseRsi);
TryComp<AnimationPlayerComponent>(uid, out var animPlayer);
if (_animationSystem.HasRunningAnimation(uid, animPlayer, DoorComponent.AnimationKey))
_animationSystem.Stop(uid, animPlayer, DoorComponent.AnimationKey); // Halt all running anomations.
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
args.Sprite.DrawDepth = comp.ClosedDrawDepth;
switch(state)
UpdateAppearanceForDoorState(entity, args.Sprite, state);
}
private void UpdateAppearanceForDoorState(Entity<DoorComponent> entity, SpriteComponent sprite, DoorState state)
{
sprite.DrawDepth = state is DoorState.Open ? entity.Comp.OpenDrawDepth : entity.Comp.ClosedDrawDepth;
switch (state)
{
case DoorState.Open:
args.Sprite.DrawDepth = comp.OpenDrawDepth;
foreach(var (layer, layerState) in comp.OpenSpriteStates)
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
{
args.Sprite.LayerSetState(layer, layerState);
sprite.LayerSetState(layer, layerState);
}
break;
return;
case DoorState.Closed:
foreach(var (layer, layerState) in comp.ClosedSpriteStates)
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
{
args.Sprite.LayerSetState(layer, layerState);
sprite.LayerSetState(layer, layerState);
}
break;
return;
case DoorState.Opening:
if (animPlayer != null && comp.OpeningAnimationTime != 0.0)
_animationSystem.Play((uid, animPlayer), (Animation)comp.OpeningAnimation, DoorComponent.AnimationKey);
break;
if (entity.Comp.OpeningAnimationTime == 0.0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Closing:
if (animPlayer != null && comp.ClosingAnimationTime != 0.0 && comp.CurrentlyCrushing.Count == 0)
_animationSystem.Play((uid, animPlayer), (Animation)comp.ClosingAnimation, DoorComponent.AnimationKey);
break;
if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Denying:
if (animPlayer != null)
_animationSystem.Play((uid, animPlayer), (Animation)comp.DenyingAnimation, DoorComponent.AnimationKey);
break;
case DoorState.Welded:
break;
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Emagging:
if (animPlayer != null)
_animationSystem.Play((uid, animPlayer), (Animation)comp.EmaggingAnimation, DoorComponent.AnimationKey);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid door visual state {state}");
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.AnimationKey);
return;
}
}
private void UpdateSpriteLayers(SpriteComponent sprite, string baseRsi)
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
return;
}
sprite.BaseRSI = res.RSI;
}
}

View File

@@ -51,7 +51,6 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
var button = new GhostRoleRadioMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64, 64),
ToolTip = Loc.GetString(ghostRoleProto.Name),
ProtoId = ghostRoleProto.ID,
@@ -100,7 +99,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
}
}
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButton
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<GhostRolePrototype> ProtoId { get; set; }
}

View File

@@ -2,45 +2,38 @@ using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Holopad;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Holopad;
public sealed class HolopadSystem : SharedHolopadSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<HolopadHologramComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
}
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
private void OnComponentStartup(Entity<HolopadHologramComponent> entity, ref ComponentStartup ev)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
UpdateHologramSprite(uid);
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
}
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
private void OnShaderRender(Entity<HolopadHologramComponent> entity, ref BeforePostShaderRenderEvent ev)
{
if (ev.Sprite.PostShader == null)
return;
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
}
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
@@ -57,100 +50,66 @@ public sealed class HolopadSystem : SharedHolopadSystem
RaiseNetworkEvent(netEv);
}
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
private void UpdateHologramSprite(EntityUid hologram, EntityUid? target)
{
var targetPlayer = GetEntity(ev.TargetPlayer);
var player = _playerManager.LocalSession?.AttachedEntity;
// Ignore the request if received by a player who isn't the target
if (targetPlayer != player)
return;
if (!TryComp<SpriteComponent>(player, out var playerSprite))
return;
var spriteLayerData = new List<PrototypeLayerData>();
if (playerSprite.Visible)
{
// Record the RSI paths, state names and shader paramaters of all visible layers
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
{
if (!playerSprite.TryGetLayer(i, out var layer))
continue;
if (!layer.Visible ||
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
string.IsNullOrEmpty(layer.State.Name))
continue;
var layerDatum = new PrototypeLayerData();
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
layerDatum.State = layer.State.Name;
if (layer.CopyToShaderParameters != null)
{
var key = (string)layer.CopyToShaderParameters.LayerKey;
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
otherLayer.Visible)
{
layerDatum.MapKeys = new() { key };
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
{
LayerKey = key,
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
ParameterUV = layer.CopyToShaderParameters.ParameterUV
};
}
}
spriteLayerData.Add(layerDatum);
}
}
// Return the recorded data to the server
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
RaiseNetworkEvent(evResponse);
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
{
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
}
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
{
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
return;
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
// Get required components
if (!TryComp<SpriteComponent>(hologram, out var hologramSprite) ||
!TryComp<HolopadHologramComponent>(hologram, out var holopadhologram))
return;
// Remove all sprite layers
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
hologramSprite.RemoveLayer(i);
if (layerData == null || layerData.Length == 0)
if (TryComp<SpriteComponent>(target, out var targetSprite))
{
layerData = new PrototypeLayerData[1];
layerData[0] = new PrototypeLayerData()
// Use the target's holographic avatar (if available)
if (TryComp<HolographicAvatarComponent>(target, out var targetAvatar) &&
targetAvatar.LayerData != null)
{
RsiPath = holopadhologram.RsiPath,
State = holopadhologram.RsiState
};
for (int i = 0; i < targetAvatar.LayerData.Length; i++)
{
var layer = targetAvatar.LayerData[i];
hologramSprite.AddLayer(targetAvatar.LayerData[i], i);
}
}
// Otherwise copy the target's current physical appearance
else
{
hologramSprite.CopyFrom(targetSprite);
}
}
for (int i = 0; i < layerData.Length; i++)
// There is no target, display a default sprite instead (if available)
else
{
var layer = layerData[i];
layer.Shader = "unshaded";
if (string.IsNullOrEmpty(holopadhologram.RsiPath) || string.IsNullOrEmpty(holopadhologram.RsiState))
return;
hologramSprite.AddLayer(layerData[i], i);
var layer = new PrototypeLayerData();
layer.RsiPath = holopadhologram.RsiPath;
layer.State = holopadhologram.RsiState;
hologramSprite.AddLayer(layer);
}
UpdateHologramShader(uid, hologramSprite, holopadhologram);
// Override specific values
hologramSprite.Color = Color.White;
hologramSprite.Offset = holopadhologram.Offset;
hologramSprite.DrawDepth = (int)DrawDepth.Mobs;
hologramSprite.NoRotation = true;
hologramSprite.DirectionOverride = Direction.South;
hologramSprite.EnableDirectionOverride = true;
// Remove shading from all layers (except displacement maps)
for (int i = 0; i < hologramSprite.AllLayers.Count(); i++)
{
if (hologramSprite.TryGetLayer(i, out var layer) && layer.ShaderPrototype != "DisplacedStencilDraw")
hologramSprite.LayerSetShader(i, "unshaded");
}
UpdateHologramShader(hologram, hologramSprite, holopadhologram);
}
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)

View File

@@ -170,7 +170,7 @@ namespace Content.Client.LateJoin
foreach (var department in departments)
{
var departmentName = Loc.GetString($"department-{department.ID}");
var departmentName = Loc.GetString(department.Name);
_jobCategories[id] = new Dictionary<string, BoxContainer>();
var stationAvailable = _gameTicker.JobsAvailable[id];
var jobsAvailable = new List<JobPrototype>();

View File

@@ -840,7 +840,7 @@ namespace Content.Client.Lobby.UI
foreach (var department in departments)
{
var departmentName = Loc.GetString($"department-{department.ID}");
var departmentName = Loc.GetString(department.Name);
if (!_jobCategories.TryGetValue(department.ID, out var category))
{

View File

@@ -25,6 +25,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SharedTransformSystem _transformSystem;
private readonly SpriteSystem _spriteSystem;
private NetEntity? _trackedEntity;
@@ -36,10 +37,10 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_transformSystem = _entManager.System<SharedTransformSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
}
public void Set(string stationName, EntityUid? mapUid)
@@ -290,7 +291,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
{
NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid,
new NavMapBlip
(coordinates.Value,
(CoordinatesToLocal(coordinates.Value),
_blipTexture,
(_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
sensor.SuitSensorUid == _trackedEntity));
@@ -356,7 +357,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data))
{
data = new NavMapBlip
(data.Coordinates,
(CoordinatesToLocal(data.Coordinates),
data.Texture,
(currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
castSensor.SuitSensorUid == currTrackedEntity);
@@ -421,6 +422,26 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
return false;
}
/// <summary>
/// Converts the input coordinates to an EntityCoordinates which are in
/// reference to the grid that the map is displaying. This is a stylistic
/// choice; this window deliberately limits the rate that blips update,
/// but if the blip is attached to another grid which is moving, that
/// blip will move smoothly, unlike the others. By converting the
/// coordinates, we are back in control of the blip movement.
/// </summary>
private EntityCoordinates CoordinatesToLocal(EntityCoordinates refCoords)
{
if (NavMap.MapUid != null)
{
return _transformSystem.WithEntityId(refCoords, (EntityUid)NavMap.MapUid);
}
else
{
return refCoords;
}
}
private void ClearOutDatedData()
{
SensorsTable.RemoveAllChildren();

View File

@@ -0,0 +1,19 @@
using System.Numerics;
using Content.Client.Movement.Systems;
using Content.Shared.Movement.Components;
namespace Content.Client.Movement.Components;
[RegisterComponent]
public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
{
/// <summary>
/// The location the offset will attempt to pan towards; based on the cursor's position in the game window.
/// </summary>
public Vector2 TargetPosition = Vector2.Zero;
/// <summary>
/// The current positional offset being applied. Used to enable gradual panning.
/// </summary>
public Vector2 CurrentPosition = Vector2.Zero;
}

View File

@@ -1,6 +1,7 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
@@ -52,4 +53,14 @@ public sealed class ContentEyeSystem : SharedContentEyeSystem
{
RaisePredictiveEvent(new RequestEyeEvent(drawFov, drawLight));
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var eyeEntities = AllEntityQuery<ContentEyeComponent, EyeComponent>();
while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
{
UpdateEyeOffset((entity, eyeComponent));
}
}
}

View File

@@ -0,0 +1,91 @@
using System.Numerics;
using Content.Client.Movement.Components;
using Content.Shared.Camera;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Map;
using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
public partial class EyeCursorOffsetSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContentEyeSystem _contentEye = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
// This value is here to make sure the user doesn't have to move their mouse
// all the way out to the edge of the screen to get the full offset.
static private float _edgeOffset = 0.9f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EyeCursorOffsetComponent, GetEyeOffsetEvent>(OnGetEyeOffsetEvent);
}
private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent component, ref GetEyeOffsetEvent args)
{
var offset = OffsetAfterMouse(uid, component);
if (offset == null)
return;
args.Offset += offset.Value;
}
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
{
var localPlayer = _player.LocalPlayer?.ControlledEntity;
var mousePos = _inputManager.MouseScreenPosition;
var screenSize = _clyde.MainWindow.Size;
var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
if (localPlayer == null)
return null;
var playerPos = _transform.GetWorldPosition(localPlayer.Value);
if (component == null)
{
component = EnsureComp<EyeCursorOffsetComponent>(uid);
}
// Doesn't move the offset if the mouse has left the game window!
if (mousePos.Window != WindowId.Invalid)
{
// The offset must account for the in-world rotation.
var eyeRotation = _eyeManager.CurrentEye.Rotation;
var mouseActualRelativePos = Vector2.Transform(mouseNormalizedPos, System.Numerics.Quaternion.CreateFromAxisAngle(-System.Numerics.Vector3.UnitZ, (float)(eyeRotation.Opposite().Theta))); // I don't know, it just works.
// Caps the offset into a circle around the player.
mouseActualRelativePos *= component.MaxOffset;
if (mouseActualRelativePos.Length() > component.MaxOffset)
{
mouseActualRelativePos = mouseActualRelativePos.Normalized() * component.MaxOffset;
}
component.TargetPosition = mouseActualRelativePos;
//Makes the view not jump immediately when moving the cursor fast.
if (component.CurrentPosition != component.TargetPosition)
{
Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
if (vectorOffset.Length() > component.OffsetSpeed)
{
vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
}
component.CurrentPosition += vectorOffset;
}
}
return component.CurrentPosition;
}
}

View File

@@ -19,6 +19,7 @@
<CheckBox Name="RestartSoundsCheckBox" Text="{Loc 'ui-options-restart-sounds'}" />
<CheckBox Name="EventMusicCheckBox" Text="{Loc 'ui-options-event-music'}" />
<CheckBox Name="AdminSoundsCheckBox" Text="{Loc 'ui-options-admin-sounds'}" />
<CheckBox Name="BwoinkSoundCheckBox" Text="{Loc 'ui-options-bwoink-sound'}" />
</BoxContainer>
</BoxContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -1,3 +1,4 @@
using Content.Client.Administration.Managers;
using Content.Client.Audio;
using Content.Shared.CCVar;
using Robust.Client.Audio;
@@ -12,8 +13,9 @@ namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class AudioTab : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IAudioManager _audio = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public AudioTab()
{
@@ -61,10 +63,30 @@ public sealed partial class AudioTab : Control
Control.AddOptionCheckBox(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.BwoinkSoundEnabled, BwoinkSoundCheckBox);
Control.Initialize();
}
protected override void EnteredTree()
{
base.EnteredTree();
_admin.AdminStatusUpdated += UpdateAdminButtonsVisibility;
UpdateAdminButtonsVisibility();
}
protected override void ExitedTree()
{
base.ExitedTree();
_admin.AdminStatusUpdated -= UpdateAdminButtonsVisibility;
}
private void UpdateAdminButtonsVisibility()
{
BwoinkSoundCheckBox.Visible = _admin.IsActive();
}
private void OnMasterVolumeSliderChanged(float value)
{
// TODO: I was thinking of giving OptionsTabControlRow a flag to "set CVar immediately", but I'm deferring that

View File

@@ -56,35 +56,35 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
protected virtual void DeactivateInternal() { }
private void OnStartup(EntityUid uid, T component, ComponentStartup args)
private void OnStartup(Entity<T> ent, ref ComponentStartup args)
{
RefreshOverlay(uid);
RefreshOverlay();
}
private void OnRemove(EntityUid uid, T component, ComponentRemove args)
private void OnRemove(Entity<T> ent, ref ComponentRemove args)
{
RefreshOverlay(uid);
RefreshOverlay();
}
private void OnPlayerAttached(LocalPlayerAttachedEvent args)
{
RefreshOverlay(args.Entity);
RefreshOverlay();
}
private void OnPlayerDetached(LocalPlayerDetachedEvent args)
{
if (_player.LocalSession?.AttachedEntity == null)
if (_player.LocalSession?.AttachedEntity is null)
Deactivate();
}
private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args)
private void OnCompEquip(Entity<T> ent, ref GotEquippedEvent args)
{
RefreshOverlay(args.Equipee);
RefreshOverlay();
}
private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args)
private void OnCompUnequip(Entity<T> ent, ref GotUnequippedEvent args)
{
RefreshOverlay(args.Equipee);
RefreshOverlay();
}
private void OnRoundRestart(RoundRestartCleanupEvent args)
@@ -92,24 +92,24 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
Deactivate();
}
protected virtual void OnRefreshEquipmentHud(EntityUid uid, T component, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
protected virtual void OnRefreshEquipmentHud(Entity<T> ent, ref InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
{
OnRefreshComponentHud(uid, component, args.Args);
OnRefreshComponentHud(ent, ref args.Args);
}
protected virtual void OnRefreshComponentHud(EntityUid uid, T component, RefreshEquipmentHudEvent<T> args)
protected virtual void OnRefreshComponentHud(Entity<T> ent, ref RefreshEquipmentHudEvent<T> args)
{
args.Active = true;
args.Components.Add(component);
args.Components.Add(ent.Comp);
}
protected void RefreshOverlay(EntityUid uid)
protected void RefreshOverlay()
{
if (uid != _player.LocalSession?.AttachedEntity)
if (_player.LocalSession?.AttachedEntity is not { } entity)
return;
var ev = new RefreshEquipmentHudEvent<T>(TargetSlots);
RaiseLocalEvent(uid, ev);
RaiseLocalEvent(entity, ref ev);
if (ev.Active)
Update(ev);

View File

@@ -28,7 +28,7 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
private void OnHandleState(Entity<ShowHealthBarsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component)

View File

@@ -47,7 +47,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
private void OnHandleState(Entity<ShowHealthIconsComponent> ent, ref AfterAutoHandleStateEvent args)
{
RefreshOverlay(ent);
RefreshOverlay();
}
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)

View File

@@ -15,6 +15,16 @@ public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShiel
base.Initialize();
SubscribeLocalEvent<MindShieldComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
SubscribeLocalEvent<FakeMindShieldComponent, GetStatusIconsEvent>(OnGetStatusIconsEventFake);
}
// TODO: Probably need to get this OFF of client since this can be read by bad actors rather easily
// ...imagine cheating in a game about silly paper dolls
private void OnGetStatusIconsEventFake(EntityUid uid, FakeMindShieldComponent component, ref GetStatusIconsEvent ev)
{
if(!IsActive)
return;
if (component.IsEnabled && _prototype.TryIndex(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
ev.StatusIcons.Add(fakeStatusIconPrototype);
}
private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)

View File

@@ -1,50 +1,129 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.ParticleAccelerator.UI"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc 'particle-accelerator-control-menu-device-version-label'}"
MinSize="420 320"
SetSize="420 320">
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 5">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StatusLabel" HorizontalExpand="True"/>
<RichTextLabel Name="StatusStateLabel"/>
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.ParticleAccelerator.UI"
Title="{Loc 'particle-accelerator-control-menu-device-version-label'}"
MinSize="320 120">
<!-- Main Container -->
<BoxContainer Orientation="Vertical"
VerticalExpand="True">
<!-- Sub-Main container -->
<BoxContainer Orientation="Horizontal"
VerticalExpand="True"
HorizontalExpand="True">
<!-- Info part -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
Margin="8">
<!-- Info -->
<BoxContainer Orientation="Vertical"
SeparationOverride="4">
<!-- Status -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StatusLabel" HorizontalExpand="True"/>
<Control MinWidth="8"/>
<RichTextLabel Name="StatusStateLabel"/>
</BoxContainer>
<!-- Power -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="PowerLabel"
HorizontalExpand="True"
VerticalAlignment="Center"/>
<Control MinWidth="8"/>
<Button Name="OffButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-off-button'}"
StyleClasses="OpenRight"/>
<Button Name="OnButton"
ToggleMode="False"
Text="{Loc 'particle-accelerator-control-menu-on-button'}"
StyleClasses="OpenLeft"/>
</BoxContainer>
<!-- Strenght -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StrengthLabel"
HorizontalExpand="True"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
<Control MinWidth="8"/>
<SpinBox Name="StateSpinBox" Value="0"/>
</BoxContainer>
<!-- Power -->
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="DrawLabel" HorizontalExpand="True"/>
<Control MinWidth="8"/>
<RichTextLabel Name="DrawValueLabel"/>
</BoxContainer>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="PowerLabel" Margin="0 0 20 0" HorizontalExpand="True" VerticalAlignment="Center"/>
<Button Name="OffButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-off-button'}" StyleClasses="OpenRight"/>
<Button Name="OnButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-on-button'}" StyleClasses="OpenLeft"/>
<Control MinHeight="8" VerticalExpand="True"/> <!-- Filler -->
<!-- Alarm -->
<BoxContainer Name="AlarmControl"
Orientation="Vertical"
VerticalAlignment="Center"
Visible="False">
<controls:StripeBack Margin="-8 0">
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="BigAlarmLabel"
HorizontalAlignment="Center"/>
<RichTextLabel Name="BigAlarmLabelTwo"
HorizontalAlignment="Center"/>
</BoxContainer>
</controls:StripeBack>
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}"
HorizontalAlignment="Center"
StyleClasses="LabelSubText"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="StrengthLabel" Margin="0 0 20 0" HorizontalExpand="True" HorizontalAlignment="Left" VerticalAlignment="Center"/>
<SpinBox Name="StateSpinBox" Value="0"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="DrawLabel" HorizontalExpand="True"/>
<RichTextLabel Name="DrawValueLabel"/>
</BoxContainer>
<Control MinHeight="10" VerticalExpand="True"/>
<BoxContainer Name="AlarmControl" Orientation="Vertical" VerticalAlignment="Center" Visible="False">
<RichTextLabel Name="BigAlarmLabel" HorizontalAlignment="Center"/>
<RichTextLabel Name="BigAlarmLabelTwo" HorizontalAlignment="Center"/>
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
</BoxContainer>
<Control MinHeight="10" VerticalExpand="True"/>
</BoxContainer>
<customControls:VSeparator Margin="0 0 0 10"/>
<BoxContainer Orientation="Vertical" Margin="20 0 20 0" VerticalAlignment="Center">
<PanelContainer Name="BackPanel" HorizontalAlignment="Center">
<PanelContainer StyleClasses="LowDivider" Margin="0 -8" HorizontalAlignment="Right"/>
<!-- PA Visual part -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Center"
Margin="8">
<PanelContainer Name="BackPanel"
HorizontalAlignment="Center">
<PanelContainer.PanelOverride>
<gfx:StyleBoxTexture Modulate="#202023" PatchMarginBottom="10" PatchMarginLeft="10" PatchMarginRight="10" PatchMarginTop="10"/>
<gfx:StyleBoxTexture Modulate="#202023"
PatchMarginBottom="8"
PatchMarginLeft="8"
PatchMarginRight="8"
PatchMarginTop="8"/>
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" HorizontalAlignment="Center" VerticalExpand="True">
<GridContainer Columns="3" VSeparationOverride="0" HSeparationOverride="0" HorizontalAlignment="Center">
<BoxContainer Orientation="Vertical"
SeparationOverride="6"
VerticalExpand="True"
VerticalAlignment="Stretch"
HorizontalExpand="True"
HorizontalAlignment="Center">
<!-- PA Visualisation -->
<GridContainer Columns="3"
VSeparationOverride="0"
HSeparationOverride="0"
HorizontalAlignment="Center">
<Control/>
<ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/>
<Control/>
@@ -58,17 +137,47 @@
<ui:PASegmentControl Name="EmitterForeTexture" BaseState="emitter_fore"/>
<ui:PASegmentControl Name="EmitterPortTexture" BaseState="emitter_port"/>
</GridContainer>
<Control MinHeight="5"/>
<Button Name="ScanButton" Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}" HorizontalAlignment="Center"/>
<Button Name="ScanButton"
Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}"
HorizontalAlignment="Center"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
<controls:StripeBack>
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 4 0 4"/>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal" Margin="12 0 0 0">
<Label Text="{Loc 'particle-accelerator-control-menu-foo-bar-baz'}" StyleClasses="LabelSubText"/>
<!-- Footer -->
<BoxContainer Orientation="Vertical"
VerticalAlignment="Bottom">
<controls:StripeBack>
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}"
HorizontalAlignment="Center"
StyleClasses="LabelSubText"
Margin="0 4"/>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal"
Margin="12 0 6 2"
VerticalAlignment="Bottom">
<!-- Footer title -->
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-left'}"
StyleClasses="WindowFooterText" />
<!-- Version -->
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-right'}"
StyleClasses="WindowFooterText"
HorizontalAlignment="Right"
HorizontalExpand="True"
Margin="0 0 4 0" />
<TextureRect StyleClasses="NTLogoDark"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
HorizontalAlignment="Right"
SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -11,37 +11,37 @@
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
<!-- Entry layer (shows main categories) -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
</ui:RadialMenuTextureButton>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
<!-- Walls and flooring -->
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Windows and grilles -->
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Airlocks -->
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Computer and machine frames -->
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Lighting -->
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
</ui:RadialMenu>

View File

@@ -74,7 +74,6 @@ public sealed partial class RCDMenu : RadialMenu
var button = new RCDMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = tooltip,
ProtoId = protoId,
@@ -99,9 +98,7 @@ public sealed partial class RCDMenu : RadialMenu
// is visible in the main radial container (as these all start with Visible = false)
foreach (var child in main.Children)
{
var castChild = child as RadialMenuTextureButton;
if (castChild is not RadialMenuTextureButton)
if (child is not RadialMenuTextureButton castChild)
continue;
if (castChild.TargetLayer == proto.Category)
@@ -169,12 +166,7 @@ public sealed partial class RCDMenu : RadialMenu
}
}
public sealed class RCDMenuButton : RadialMenuTextureButton
public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<RCDPrototype> ProtoId { get; set; }
public RCDMenuButton()
{
}
}

View File

@@ -9,7 +9,7 @@ public sealed class RadiationSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
public List<RadiationRay>? Rays;
public List<DebugRadiationRay>? Rays;
public Dictionary<NetEntity, Dictionary<Vector2i, float>>? ResistanceGrids;
public override void Initialize()

View File

@@ -81,13 +81,19 @@ public sealed partial class NavScreen : BoxContainer
// Get the positive reduced angle.
var displayRot = -worldRot.Reduced();
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
GridPosition.Text = Loc.GetString("shuttle-console-position-value",
("X", $"{worldPos.X:0.0}"),
("Y", $"{worldPos.Y:0.0}"));
GridOrientation.Text = Loc.GetString("shuttle-console-orientation-value",
("angle", $"{displayRot.Degrees:0.0}"));
var gridVelocity = gridBody.LinearVelocity;
gridVelocity = displayRot.RotateVec(gridVelocity);
// Get linear velocity relative to the console entity
GridLinearVelocity.Text = $"{gridVelocity.X + 10f * float.Epsilon:0.0}, {gridVelocity.Y + 10f * float.Epsilon:0.0}";
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity + 10f * float.Epsilon:0.0}";
GridLinearVelocity.Text = Loc.GetString("shuttle-console-linear-velocity-value",
("X", $"{gridVelocity.X + 10f * float.Epsilon:0.0}"),
("Y", $"{gridVelocity.Y + 10f * float.Epsilon:0.0}"));
GridAngularVelocity.Text = Loc.GetString("shuttle-console-angular-velocity-value",
("angularVelocity", $"{-MathHelper.RadiansToDegrees(gridBody.AngularVelocity) + 10f * float.Epsilon:0.0}"));
}
}

View File

@@ -1,4 +1,4 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
@@ -7,7 +7,7 @@
MinSize="450 450">
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -54,7 +54,6 @@ public sealed partial class StationAiMenu : RadialMenu
// TODO: This radial boilerplate is quite annoying
var button = new StationAiMenuButton(action.Event)
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
};
@@ -121,7 +120,7 @@ public sealed partial class StationAiMenu : RadialMenu
}
}
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector
{
public BaseStationAiAction Action = action;
}

View File

@@ -1,4 +1,3 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using System.Linq;
using System.Numerics;
@@ -8,6 +7,11 @@ namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialContainer : LayoutContainer
{
/// <summary>
/// Increment of radius per child element to be rendered.
/// </summary>
private const float RadiusIncrement = 5f;
/// <summary>
/// Specifies the anglular range, in radians, in which child elements will be placed.
/// The first value denotes the angle at which the first element is to be placed, and
@@ -49,10 +53,30 @@ public class RadialContainer : LayoutContainer
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
/// <summary>
/// Determines how far from the radial container's center that its child elements will be placed
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
/// To correctly display dynamic amount of elements control actually resizes depending on amount of child buttons,
/// but uses this property as base value for final radius calculation.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float Radius { get; set; } = 100f;
public float InitialRadius { get; set; } = 100f;
/// <summary>
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
/// This is dynamically calculated (based on child button count) radius, result of <see cref="InitialRadius"/> and
/// <see cref="RadiusIncrement"/> multiplied by currently visible child button count.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public float CalculatedRadius { get; private set; }
/// <summary>
/// Determines radial menu button sectors inner radius, is a multiplier of <see cref="InitialRadius"/>.
/// </summary>
public float InnerRadiusMultiplier { get; set; } = 0.5f;
/// <summary>
/// Determines radial menu button sectors outer radius, is a multiplier of <see cref="InitialRadius"/>.
/// </summary>
public float OuterRadiusMultiplier { get; set; } = 1.5f;
/// <summary>
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
@@ -67,37 +91,74 @@ public class RadialContainer : LayoutContainer
{
}
protected override void Draw(DrawingHandleScreen handle)
/// <inheritdoc />
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
const float baseRadius = 100f;
const float radiusIncrement = 5f;
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
var children = ReserveSpaceForHiddenChildren
? Children
: Children.Where(x => x.Visible);
var childCount = children.Count();
// Add padding from the center at higher child counts so they don't overlap.
Radius = baseRadius + (childCount * radiusIncrement);
// Add padding from the center at higher child counts so they don't overlap.
CalculatedRadius = InitialRadius + (childCount * RadiusIncrement);
var isAntiClockwise = RadialAlignment == RAlignment.AntiClockwise;
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
var arc = AngularRange.Y - AngularRange.X;
arc = (arc < 0) ? MathF.Tau + arc : arc;
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
arc = arc < 0
? MathF.Tau + arc
: arc;
arc = isAntiClockwise
? MathF.Tau - arc
: arc;
// Account for both circular arrangements and arc-based arrangements
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f)
? 0
: 1;
// Determine the separation between child elements
var sepAngle = arc / (childCount - childMod);
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
sepAngle *= isAntiClockwise
? -1f
: 1f;
var controlCenter = finalSize * 0.5f;
// Adjust the positions of all the child elements
foreach (var (i, child) in children.Select((x, i) => (i, x)))
var query = children.Select((x, index) => (index, x));
foreach (var (childIndex, child) in query)
{
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
const float angleOffset = MathF.PI * 0.5f;
var targetAngleOfChild = AngularRange.X + sepAngle * (childIndex + 0.5f) + angleOffset;
// flooring values for snapping float values to physical grid -
// it prevents gaps and overlapping between different button segments
var position = new Vector2(
MathF.Floor(CalculatedRadius * MathF.Cos(targetAngleOfChild)),
MathF.Floor(-CalculatedRadius * MathF.Sin(targetAngleOfChild))
) + controlCenter - child.DesiredSize * 0.5f + Position;
SetPosition(child, position);
// radial menu buttons with sector need to also know in which sector and around which point
// they should be rendered, how much space sector should should take etc.
if (child is IRadialMenuItemWithSector tb)
{
tb.AngleSectorFrom = sepAngle * childIndex;
tb.AngleSectorTo = sepAngle * (childIndex + 1);
tb.AngleOffset = angleOffset;
tb.InnerRadius = CalculatedRadius * InnerRadiusMultiplier;
tb.OuterRadius = CalculatedRadius * OuterRadiusMultiplier;
tb.ParentCenter = controlCenter;
}
}
return base.ArrangeOverride(finalSize);
}
/// <summary>
@@ -109,4 +170,5 @@ public class RadialContainer : LayoutContainer
Clockwise,
AntiClockwise,
}
}

View File

@@ -3,6 +3,9 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using System.Linq;
using System.Numerics;
using Content.Shared.Input;
using Robust.Client.Graphics;
using Robust.Shared.Input;
namespace Content.Client.UserInterface.Controls;
@@ -12,11 +15,16 @@ public class RadialMenu : BaseWindow
/// <summary>
/// Contextual button used to traverse through previous layers of the radial menu
/// </summary>
public TextureButton? ContextualButton { get; set; }
public RadialMenuContextualCentralTextureButton ContextualButton { get; }
/// <summary>
/// Button that represents outer area of menu (closes menu on outside clicks).
/// </summary>
public RadialMenuOuterAreaButton MenuOuterAreaButton { get; }
/// <summary>
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
/// </summary>
/// </summary>
public string? BackButtonStyleClass
{
get
@@ -52,7 +60,7 @@ public class RadialMenu : BaseWindow
}
}
private List<Control> _path = new();
private readonly List<Control> _path = new();
private string? _backButtonStyleClass;
private string? _closeButtonStyleClass;
@@ -60,8 +68,8 @@ public class RadialMenu : BaseWindow
/// A free floating menu which enables the quick display of one or more radial containers
/// </summary>
/// <remarks>
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
/// the menu), along with a contextual button at the menu center, which will either return the user
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
/// the menu), along with a contextual button at the menu center, which will either return the user
/// to the previous layer or close the menu if there are no previous layers left to traverse.
/// To create a functional radial menu, simply parent one or more named radial containers to it,
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
@@ -78,23 +86,56 @@ public class RadialMenu : BaseWindow
}
// Auto generate a contextual button for moving back through visited layers
ContextualButton = new TextureButton()
ContextualButton = new RadialMenuContextualCentralTextureButton
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(64f, 64f),
};
MenuOuterAreaButton = new RadialMenuOuterAreaButton();
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
MenuOuterAreaButton.OnButtonUp += _ => Close();
AddChild(ContextualButton);
AddChild(MenuOuterAreaButton);
// Hide any further add children, unless its promoted to the active layer
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
OnChildAdded += child =>
{
child.Visible = GetCurrentActiveLayer() == child;
SetupContextualButtonData(child);
};
}
private void SetupContextualButtonData(Control child)
{
if (child is RadialContainer { Visible: true } container)
{
var parentCenter = MinSize * 0.5f;
ContextualButton.ParentCenter = parentCenter;
MenuOuterAreaButton.ParentCenter = parentCenter;
ContextualButton.InnerRadius = container.CalculatedRadius * container.InnerRadiusMultiplier;
MenuOuterAreaButton.OuterRadius = container.CalculatedRadius * container.OuterRadiusMultiplier;
}
}
/// <inheritdoc />
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var result = base.ArrangeOverride(finalSize);
var currentLayer = GetCurrentActiveLayer();
if (currentLayer != null)
{
SetupContextualButtonData(currentLayer);
}
return result;
}
private Control? GetCurrentActiveLayer()
{
var children = Children.Where(x => x != ContextualButton);
var children = Children.Where(x => x != ContextualButton && x != MenuOuterAreaButton);
if (!children.Any())
return null;
@@ -116,7 +157,7 @@ public class RadialMenu : BaseWindow
foreach (var child in Children)
{
if (child == ContextualButton)
if (child == ContextualButton || child == MenuOuterAreaButton)
continue;
// Hide layers which are not of interest
@@ -129,6 +170,7 @@ public class RadialMenu : BaseWindow
else
{
child.Visible = true;
SetupContextualButtonData(child);
result = true;
}
}
@@ -158,7 +200,7 @@ public class RadialMenu : BaseWindow
// Hide all children except the contextual button
foreach (var child in Children)
{
if (child != ContextualButton)
if (child != ContextualButton && child != MenuOuterAreaButton)
child.Visible = false;
}
@@ -172,49 +214,86 @@ public class RadialMenu : BaseWindow
}
}
/// <summary>
/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks
/// from interactions.
/// </summary>
[Virtual]
public class RadialMenuButton : Button
public class RadialMenuTextureButtonBase : TextureButton
{
/// <summary>
/// Upon clicking this button the radial menu will transition to the named layer
/// </summary>
public string? TargetLayer { get; set; }
/// <summary>
/// A simple button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuButton()
/// <inheritdoc />
protected RadialMenuTextureButtonBase()
{
OnButtonUp += OnClicked;
EnableAllKeybinds = true;
}
private void OnClicked(ButtonEventArgs args)
/// <inheritdoc />
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
if (TargetLayer == null || TargetLayer == string.Empty)
return;
var parent = FindParentMultiLayerContainer(this);
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
base.KeyBindUp(args);
}
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
/// <summary>
/// Special button for closing radial menu or going back between radial menu levels.
/// Is looking like just <see cref="TextureButton "/> but considers whole space around
/// itself (til radial menu buttons) as itself in case of clicking. But this 'effect'
/// works only if control have parent, and ActiveContainer property is set.
/// Also considers all space outside of radial menu buttons as itself for clicking.
/// </summary>
public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
{
public float InnerRadius { get; set; }
public Vector2? ParentCenter { get; set; }
/// <inheritdoc />
protected override bool HasPoint(Vector2 point)
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
if (ParentCenter == null)
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
return base.HasPoint(point);
}
return null;
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
var innerRadiusSquared = InnerRadius * InnerRadius;
// comparing to squared values is faster then making sqrt
return distSquared < innerRadiusSquared;
}
}
/// <summary>
/// Menu button for outer area of radial menu (covers everything 'outside').
/// </summary>
public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
{
public float OuterRadius { get; set; }
public Vector2? ParentCenter { get; set; }
/// <inheritdoc />
protected override bool HasPoint(Vector2 point)
{
if (ParentCenter == null)
{
return base.HasPoint(point);
}
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
var outerRadiusSquared = OuterRadius * OuterRadius;
// comparing to squared values is faster, then making sqrt
return distSquared > outerRadiusSquared;
}
}
[Virtual]
public class RadialMenuTextureButton : TextureButton
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
{
/// <summary>
/// Upon clicking this button the radial menu will be moved to the named layer
@@ -226,6 +305,7 @@ public class RadialMenuTextureButton : TextureButton
/// </summary>
public RadialMenuTextureButton()
{
EnableAllKeybinds = true;
OnButtonUp += OnClicked;
}
@@ -246,10 +326,329 @@ public class RadialMenuTextureButton : TextureButton
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
if (ancestor is RadialMenu menu)
return menu;
}
return null;
}
}
public interface IRadialMenuItemWithSector
{
/// <summary>
/// Angle in radian where button sector should start.
/// </summary>
public float AngleSectorFrom { set; }
/// <summary>
/// Angle in radian where button sector should end.
/// </summary>
public float AngleSectorTo { set; }
/// <summary>
/// Outer radius for drawing segment and pointer detection.
/// </summary>
public float OuterRadius { set; }
/// <summary>
/// Outer radius for drawing segment and pointer detection.
/// </summary>
public float InnerRadius { set; }
/// <summary>
/// Offset in radian by which menu button should be rotated.
/// </summary>
public float AngleOffset { set; }
/// <summary>
/// Coordinates of center in parent component - button container.
/// </summary>
public Vector2 ParentCenter { set; }
}
[Virtual]
public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
{
private Vector2[]? _sectorPointsForDrawing;
private float _angleSectorFrom;
private float _angleSectorTo;
private float _outerRadius;
private float _innerRadius;
private float _angleOffset;
private bool _isWholeCircle;
private Vector2? _parentCenter;
private Color _backgroundColorSrgb = Color.ToSrgb(new Color(70, 73, 102, 128));
private Color _hoverBackgroundColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
private Color _borderColorSrgb = Color.ToSrgb(new Color(173, 216, 230, 70));
private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
/// <summary>
/// Marker, that control should render border of segment. Is false by default.
/// </summary>
/// <remarks>
/// By default color of border is same as color of background. Use <see cref="BorderColor"/>
/// and <see cref="HoverBorderColor"/> to change it.
/// </remarks>
public bool DrawBorder { get; set; } = false;
/// <summary>
/// Marker, that control should render background of all sector. Is true by default.
/// </summary>
public bool DrawBackground { get; set; } = true;
/// <summary>
/// Marker, that control should render separator lines.
/// Separator lines are used to visually separate sector of radial menu items.
/// Is true by default
/// </summary>
public bool DrawSeparators { get; set; } = true;
/// <summary>
/// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
/// </summary>
public Color BackgroundColor
{
get => Color.FromSrgb(_backgroundColorSrgb);
set => _backgroundColorSrgb = Color.ToSrgb(value);
}
/// <summary>
/// Color of background in hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
/// </summary>
public Color HoverBackgroundColor
{
get => Color.FromSrgb(_hoverBackgroundColorSrgb);
set => _hoverBackgroundColorSrgb = Color.ToSrgb(value);
}
/// <summary>
/// Color of button border. Accepts RGB color, works with sRGB for DrawPrimitive internally.
/// </summary>
public Color BorderColor
{
get => Color.FromSrgb(_borderColorSrgb);
set => _borderColorSrgb = Color.ToSrgb(value);
}
/// <summary>
/// Color of button border when button is hovered. Accepts RGB color, works with sRGB for DrawPrimitive internally.
/// </summary>
public Color HoverBorderColor
{
get => Color.FromSrgb(_hoverBorderColorSrgb);
set => _hoverBorderColorSrgb = Color.ToSrgb(value);
}
/// <summary>
/// Color of separator lines.
/// Separator lines are used to visually separate sector of radial menu items.
/// </summary>
public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128);
/// <inheritdoc />
float IRadialMenuItemWithSector.AngleSectorFrom
{
set
{
_angleSectorFrom = value;
_isWholeCircle = IsWholeCircle(value, _angleSectorTo);
}
}
/// <inheritdoc />
float IRadialMenuItemWithSector.AngleSectorTo
{
set
{
_angleSectorTo = value;
_isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
}
}
/// <inheritdoc />
float IRadialMenuItemWithSector.OuterRadius { set => _outerRadius = value; }
/// <inheritdoc />
float IRadialMenuItemWithSector.InnerRadius { set => _innerRadius = value; }
/// <inheritdoc />
public float AngleOffset { set => _angleOffset = value; }
/// <inheritdoc />
Vector2 IRadialMenuItemWithSector.ParentCenter { set => _parentCenter = value; }
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuTextureButtonWithSector()
{
}
/// <inheritdoc />
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_parentCenter == null)
{
return;
}
// draw sector where space that button occupies actually is
var containerCenter = (_parentCenter.Value - Position) * UIScale;
var angleFrom = _angleSectorFrom + _angleOffset;
var angleTo = _angleSectorTo + _angleOffset;
if (DrawBackground)
{
var segmentColor = DrawMode == DrawModeEnum.Hover
? _hoverBackgroundColorSrgb
: _backgroundColorSrgb;
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, segmentColor);
}
if (DrawBorder)
{
var borderColor = DrawMode == DrawModeEnum.Hover
? _hoverBorderColorSrgb
: _borderColorSrgb;
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false);
}
if (!_isWholeCircle && DrawSeparators)
{
DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
}
}
/// <inheritdoc />
protected override bool HasPoint(Vector2 point)
{
if (_parentCenter == null)
{
return base.HasPoint(point);
}
var outerRadiusSquared = _outerRadius * _outerRadius;
var innerRadiusSquared = _innerRadius * _innerRadius;
var distSquared = (point + Position - _parentCenter.Value).LengthSquared();
var isInRadius = distSquared < outerRadiusSquared && distSquared > innerRadiusSquared;
if (!isInRadius)
{
return false;
}
// difference from the center of the parent to the `point`
var pointFromParent = point + Position - _parentCenter.Value;
// Flip Y to get from ui coordinates to natural coordinates
var angle = MathF.Atan2(-pointFromParent.Y, pointFromParent.X) - _angleOffset;
if (angle < 0)
{
// atan2 range is -pi->pi, while angle sectors are
// 0->2pi, so remap the result into that range
angle = MathF.PI * 2 + angle;
}
var isInAngle = angle >= _angleSectorFrom && angle < _angleSectorTo;
return isInAngle;
}
/// <summary>
/// Draw segment between two concentrated circles from and to certain angles.
/// </summary>
/// <param name="drawingHandleScreen">Drawing handle, to which rendering should be delegated.</param>
/// <param name="center">Point where circle center should be.</param>
/// <param name="radiusInner">Radius of internal circle.</param>
/// <param name="radiusOuter">Radius of external circle.</param>
/// <param name="angleSectorFrom">Angle in radian, from which sector should start.</param>
/// <param name="angleSectorTo">Angle in radian, from which sector should start.</param>
/// <param name="color">Color for drawing.</param>
/// <param name="filled">Should figure be filled, or have only border.</param>
private void DrawAnnulusSector(
DrawingHandleScreen drawingHandleScreen,
Vector2 center,
float radiusInner,
float radiusOuter,
float angleSectorFrom,
float angleSectorTo,
Color color,
bool filled = true
)
{
const float minimalSegmentSize = MathF.Tau / 128f;
var requestedSegmentSize = angleSectorTo - angleSectorFrom;
var segmentCount = (int)(requestedSegmentSize / minimalSegmentSize) + 1;
var anglePerSegment = requestedSegmentSize / (segmentCount - 1);
var bufferSize = segmentCount * 2;
if (_sectorPointsForDrawing == null || _sectorPointsForDrawing.Length != bufferSize)
{
_sectorPointsForDrawing ??= new Vector2[bufferSize];
}
for (var i = 0; i < segmentCount; i++)
{
var angle = angleSectorFrom + anglePerSegment * i;
// Flip Y to get from ui coordinates to natural coordinates
var unitPos = new Vector2(MathF.Cos(angle), -MathF.Sin(angle));
var outerPoint = center + unitPos * radiusOuter;
var innerPoint = center + unitPos * radiusInner;
if (filled)
{
// to make filled sector we need to create strip from triangles
_sectorPointsForDrawing[i * 2] = outerPoint;
_sectorPointsForDrawing[i * 2 + 1] = innerPoint;
}
else
{
// to make border of sector we need points ordered as sequences on radius
_sectorPointsForDrawing[i] = outerPoint;
_sectorPointsForDrawing[bufferSize - 1 - i] = innerPoint;
}
}
var type = filled
? DrawPrimitiveTopology.TriangleStrip
: DrawPrimitiveTopology.LineStrip;
drawingHandleScreen.DrawPrimitives(type, _sectorPointsForDrawing, color);
}
private static void DrawSeparatorLines(
DrawingHandleScreen drawingHandleScreen,
Vector2 center,
float radiusInner,
float radiusOuter,
float angleSectorFrom,
float angleSectorTo,
Color color
)
{
var fromPoint = new Angle(-angleSectorFrom).RotateVec(Vector2.UnitX);
drawingHandleScreen.DrawLine(
center + fromPoint * radiusOuter,
center + fromPoint * radiusInner,
color
);
var toPoint = new Angle(-angleSectorTo).RotateVec(Vector2.UnitX);
drawingHandleScreen.DrawLine(
center + toPoint * radiusOuter,
center + toPoint * radiusInner,
color
);
}
private static bool IsWholeCircle(float angleSectorFrom, float angleSectorTo)
{
return new Angle(angleSectorFrom).EqualsApprox(new Angle(angleSectorTo));
}
}

View File

@@ -45,6 +45,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
public IAHelpUIHandler? UIHelper;
private bool _discordRelayActive;
private bool _hasUnreadAHelp;
private bool _bwoinkSoundEnabled;
private string? _aHelpSound;
public override void Initialize()
@@ -56,6 +57,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
_config.OnValueChanged(CCVars.AHelpSound, v => _aHelpSound = v, true);
_config.OnValueChanged(CCVars.BwoinkSoundEnabled, v => _bwoinkSoundEnabled = v, true);
}
public void UnloadButton()
@@ -135,7 +137,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
}
if (message.PlaySound && localPlayer.UserId != message.TrueSender)
{
if (_aHelpSound != null)
if (_aHelpSound != null && (_bwoinkSoundEnabled || !_adminManager.IsActive()))
_audio.PlayGlobal(_aHelpSound, Filter.Local(), false);
_clyde.RequestWindowAttention();
}

View File

@@ -3,6 +3,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Traits.Assorted;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -94,7 +95,11 @@ public sealed class DamageOverlayUiController : UIController
{
case MobState.Alive:
{
if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
if (EntityManager.HasComponent<PainNumbnessComponent>(entity))
{
_overlay.BruteLevel = 0;
}
else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
{
_overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
}

View File

@@ -160,6 +160,7 @@ public sealed partial class MeleeWeaponSystem
const float length = 0.15f;
var startOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
var endOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance));
sprite.Rotation += spriteRotation;
return new Animation()
{

View File

@@ -170,7 +170,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
var targetCoordinates = xform.Coordinates;
var targetLocalAngle = xform.LocalRotation;
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range);
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
}
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)

View File

@@ -0,0 +1,49 @@
using System.Numerics;
using Content.Client.Movement.Components;
using Content.Client.Movement.Systems;
using Content.Shared.Camera;
using Content.Shared.Hands;
using Content.Shared.Movement.Components;
using Content.Shared.Wieldable;
using Content.Shared.Wieldable.Components;
using Robust.Client.Timing;
namespace Content.Client.Wieldable;
public sealed class WieldableSystem : SharedWieldableSystem
{
[Dependency] private readonly EyeCursorOffsetSystem _eyeOffset = default!;
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, ItemUnwieldedEvent>(OnEyeOffsetUnwielded);
SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, HeldRelayedEvent<GetEyeOffsetRelayedEvent>>(OnGetEyeOffset);
}
public void OnEyeOffsetUnwielded(Entity<CursorOffsetRequiresWieldComponent> entity, ref ItemUnwieldedEvent args)
{
if (!TryComp(entity.Owner, out EyeCursorOffsetComponent? cursorOffsetComp))
return;
if (_gameTiming.IsFirstTimePredicted)
cursorOffsetComp.CurrentPosition = Vector2.Zero;
}
public void OnGetEyeOffset(Entity<CursorOffsetRequiresWieldComponent> entity, ref HeldRelayedEvent<GetEyeOffsetRelayedEvent> args)
{
if (!TryComp(entity.Owner, out WieldableComponent? wieldableComp))
return;
if (!wieldableComp.Wielded)
return;
var offset = _eyeOffset.OffsetAfterMouse(entity.Owner, null);
if (offset == null)
return;
args.Args.Offset += offset.Value;
}
}

View File

@@ -0,0 +1,41 @@
using Content.Shared.Contraband;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
[TestFixture]
public sealed class ContrabandTest
{
[Test]
public async Task EntityShowDepartmentsAndJobs()
{
await using var pair = await PoolManager.GetServerClient();
var client = pair.Client;
var protoMan = client.ResolveDependency<IPrototypeManager>();
var componentFactory = client.ResolveDependency<IComponentFactory>();
await client.WaitAssertion(() =>
{
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
continue;
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
if (!severity.ShowDepartmentsAndJobs)
continue;
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Wires;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Emag.Components;
using Content.Shared.Wires;
namespace Content.Server.Access;
@@ -31,11 +30,9 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
if (!EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
{
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
}
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
return true;
}
@@ -58,7 +55,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
{
if (!wire.IsCut)
{
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access) && !EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
{
access.Enabled = true;
EntityManager.Dirty(wire.Owner, access);

View File

@@ -245,6 +245,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
Dirty(accessReaderEnt.Value);
}

View File

@@ -1,6 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
namespace Content.Server.Administration.Managers;
@@ -30,36 +28,15 @@ public sealed partial class BanManager
private TimeSpan _banNotificationRateLimitStart;
private int _banNotificationRateLimitCount;
private void OnDatabaseNotification(DatabaseNotification notification)
private bool OnDatabaseNotificationEarlyFilter()
{
if (notification.Channel != BanNotificationChannel)
return;
if (notification.Payload == null)
{
_sawmill.Error("Got ban notification with null payload!");
return;
}
BanNotificationData data;
try
{
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
return;
}
if (!CheckBanRateLimit())
{
_sawmill.Verbose("Not processing ban notification due to rate limit");
return;
return false;
}
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
return true;
}
private async void ProcessBanNotification(BanNotificationData data)

View File

@@ -53,7 +53,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
{
_netManager.RegisterNetMessage<MsgRoleBans>();
_db.SubscribeToNotifications(OnDatabaseNotification);
_db.SubscribeToJsonNotification<BanNotificationData>(
_taskManager,
_sawmill,
BanNotificationChannel,
ProcessBanNotification,
OnDatabaseNotificationEarlyFilter);
_userDbData.AddOnLoadPlayer(CachePlayerData);
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
@@ -160,6 +165,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
null);
await _db.AddServerBanAsync(banDef);
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher!
var adminName = banningAdmin == null
? Loc.GetString("system-user")
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");

View File

@@ -0,0 +1,114 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Content.Server.Database;
using Content.Shared.CCVar;
using Robust.Server.Player;
using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.Server.Administration.Managers;
/// <summary>
/// Handles kicking people that connect to multiple servers on the same DB at once.
/// </summary>
/// <seealso cref="CCVars.AdminAllowMultiServerPlay"/>
public sealed class MultiServerKickManager
{
public const string NotificationChannel = "multi_server_kick";
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IServerDbManager _dbManager = null!;
[Dependency] private readonly ILogManager _logManager = null!;
[Dependency] private readonly IConfigurationManager _cfg = null!;
[Dependency] private readonly IAdminManager _adminManager = null!;
[Dependency] private readonly ITaskManager _taskManager = null!;
[Dependency] private readonly IServerNetManager _netManager = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = null!;
private ISawmill _sawmill = null!;
private bool _allowed;
public void Initialize()
{
_sawmill = _logManager.GetSawmill("multi_server_kick");
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_cfg.OnValueChanged(CCVars.AdminAllowMultiServerPlay, b => _allowed = b, true);
_dbManager.SubscribeToJsonNotification<NotificationData>(
_taskManager,
_sawmill,
NotificationChannel,
OnNotification,
OnNotificationEarlyFilter
);
}
// ReSharper disable once AsyncVoidMethod
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (_allowed)
return;
if (e.NewStatus != SessionStatus.InGame)
return;
// Send notification to other servers so they can kick this player that just connected.
try
{
await _dbManager.SendNotification(new DatabaseNotification
{
Channel = NotificationChannel,
Payload = JsonSerializer.Serialize(new NotificationData
{
PlayerId = e.Session.UserId,
ServerId = (await _serverDbEntry.ServerEntity).Id,
}),
});
}
catch (Exception ex)
{
_sawmill.Error($"Failed to send notification for multi server kick: {ex}");
}
}
private bool OnNotificationEarlyFilter()
{
if (_allowed)
{
_sawmill.Verbose("Received notification for player join, but multi server play is allowed on this server. Ignoring");
return false;
}
return true;
}
// ReSharper disable once AsyncVoidMethod
private async void OnNotification(NotificationData notification)
{
if (!_playerManager.TryGetSessionById(new NetUserId(notification.PlayerId), out var player))
return;
if (notification.ServerId == (await _serverDbEntry.ServerEntity).Id)
return;
if (_adminManager.IsAdmin(player, includeDeAdmin: true))
return;
_sawmill.Info($"Kicking {player} for connecting to another server. Multi-server play is not allowed.");
_netManager.DisconnectChannel(player.Channel, _loc.GetString("multi-server-kick-reason"));
}
private sealed class NotificationData
{
[JsonPropertyName("player_id")]
public Guid PlayerId { get; set; }
[JsonPropertyName("server_id")]
public int ServerId { get; set; }
}
}

View File

@@ -116,8 +116,11 @@ public sealed class TechAnomalySystem : EntitySystem
if (_random.Prob(tech.Comp.EmagSupercritProbability))
{
_emag.DoEmagEffect(tech, source);
_emag.DoEmagEffect(tech, sink);
var sourceEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
RaiseLocalEvent(source, ref sourceEv);
var sinkEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
RaiseLocalEvent(sink, ref sinkEv);
}
CreateNewLink(tech, source, sink);

View File

@@ -21,6 +21,7 @@ public sealed class FireAlarmSystem : EntitySystem
{
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
@@ -77,11 +78,18 @@ public sealed class FireAlarmSystem : EntitySystem
private void OnEmagged(EntityUid uid, FireAlarmComponent component, ref GotEmaggedEvent args)
{
if (TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
{
// Remove the atmos alarmable component permanently from this device.
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
RemCompDeferred<AtmosAlarmableComponent>(uid);
}
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (!TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
return;
// Remove the atmos alarmable component permanently from this device.
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
RemCompDeferred<AtmosAlarmableComponent>(uid);
args.Handled = true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Binary.Components
{
@@ -38,6 +39,7 @@ namespace Content.Server.Atmos.Piping.Binary.Components
public float LowerThreshold { get; set; } = 0.01f;
[DataField("higherThreshold")]
[GuidebookData]
public float HigherThreshold { get; set; } = DefaultHigherThreshold;
public static readonly float DefaultHigherThreshold = 2 * Atmospherics.MaxOutputPressure;

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Trinary.Components
{
@@ -27,6 +28,7 @@ namespace Content.Server.Atmos.Piping.Trinary.Components
[ViewVariables(VVAccess.ReadWrite)]
[DataField("threshold")]
[GuidebookData]
public float Threshold { get; set; } = Atmospherics.OneAtmosphere;
[DataField("maxTransferRate")]

View File

@@ -1,5 +1,6 @@
using Content.Shared.Atmos;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Guidebook;
using Robust.Shared.Audio;
namespace Content.Server.Atmos.Piping.Unary.Components
@@ -61,5 +62,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField("accessDeniedSound")]
public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
#region GuidebookData
[GuidebookData]
public float Volume => Air.Volume;
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Unary.Components
{
@@ -29,6 +30,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
public float MaxTransferRate = Atmospherics.MaxTransferRate;
[DataField("maxPressure")]
[GuidebookData]
public float MaxPressure { get; set; } = GasVolumePumpComponent.DefaultHigherThreshold;
[DataField("inlet")]

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Piping.Unary.Components
{
@@ -13,6 +14,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// thermomachine to heat or cool air.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float HeatCapacity = 5000;
[DataField, ViewVariables(VVAccess.ReadWrite)]
@@ -21,6 +23,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// <summary>
/// Tolerance for temperature setpoint hysteresis.
/// </summary>
[GuidebookData]
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float TemperatureTolerance = 2f;
@@ -44,6 +47,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Ignored if heater.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MinTemperature = 73.15f;
/// <summary>
@@ -51,6 +55,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Ignored if freezer.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxTemperature = 593.15f;
/// <summary>
@@ -63,6 +68,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// An percentage of the energy change that is leaked into the surrounding environment rather than the inlet pipe.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float EnergyLeakPercentage;
/// <summary>

View File

@@ -1,6 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.Guidebook;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Piping.Unary.Components
@@ -35,6 +36,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// In releasing mode, do not pump when environment pressure is below this limit.
/// </summary>
[DataField]
[GuidebookData]
public float UnderPressureLockoutThreshold = 80; // this must be tuned in conjunction with atmos.mmos_spacing_speed
/// <summary>
@@ -58,7 +60,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[DataField]
public bool IsPressureLockoutManuallyDisabled = false;
/// <summary>
/// The time when the manual pressure lockout will be reenabled.
/// The time when the manual pressure lockout will be reenabled.
/// </summary>
[DataField]
[AutoPausedField]
@@ -101,6 +103,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// Max pressure of the target gas (NOT relative to source).
/// </summary>
[DataField]
[GuidebookData]
public float MaxPressure = Atmospherics.MaxOutputPressure;
/// <summary>
@@ -172,5 +175,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
InternalPressureBound = data.InternalPressureBound;
PressureLockoutOverride = data.PressureLockoutOverride;
}
#region GuidebookData
[GuidebookData]
public float DefaultExternalBound => Atmospherics.OneAtmosphere;
#endregion
}
}

View File

@@ -109,7 +109,15 @@ public sealed class GasCanisterSystem : EntitySystem
var item = canister.GasTankSlot.Item;
_slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
if (canister.ReleaseValve)
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
}
else
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
}
}
private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
@@ -124,24 +132,24 @@ public sealed class GasCanisterSystem : EntitySystem
private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
{
var impact = LogImpact.High;
// filling a jetpack with plasma is less important than filling a room with it
impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High;
var hasItem = canister.GasTankSlot.HasItem;
var impact = hasItem ? LogImpact.Medium : LogImpact.High;
var containedGasDict = new Dictionary<Gas, float>();
var containedGasArray = Enum.GetValues(typeof(Gas));
for (int i = 0; i < containedGasArray.Length; i++)
{
containedGasDict.Add((Gas)i, canister.Air[i]);
}
_adminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");
_adminLogger.Add(
LogType.CanisterValve,
impact,
$"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
canister.ReleaseValve = args.Valve;
DirtyUI(uid, canister);
}
private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
{
return string.Join(", ", canister.Comp.Air);
}
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
{
_atmos.React(canister.Air, canister);

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Portable
{
@@ -45,5 +46,12 @@ namespace Content.Server.Atmos.Portable
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float TransferRate = 800;
#region GuidebookData
[GuidebookData]
public float Volume => Air.Volume;
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Portable.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Portable;
@@ -23,12 +24,14 @@ public sealed partial class SpaceHeaterComponent : Component
/// Maximum target temperature the device can be set to
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxTemperature = Atmospherics.T20C + 20;
/// <summary>
/// Minimal target temperature the device can be set to
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MinTemperature = Atmospherics.T0C - 10;
/// <summary>

View File

@@ -20,6 +20,7 @@ namespace Content.Server.Bed
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SleepingSystem _sleepingSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
@@ -114,7 +115,12 @@ namespace Content.Server.Bed
private void OnEmagged(EntityUid uid, StasisBedComponent component, ref GotEmaggedEvent args)
{
args.Repeatable = true;
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
// Reset any metabolisms first so they receive the multiplier correctly
UpdateMetabolisms(uid, component, false);
component.Multiplier = 1 / component.Multiplier;

View File

@@ -12,15 +12,22 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
/// <summary>
/// Maximum amount of bounties a station can have.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int MaxBounties = 6;
/// <summary>
/// A list of all the bounties currently active for a station.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public List<CargoBountyData> Bounties = new();
/// <summary>
/// A list of all the bounties that have been completed or
/// skipped for a station.
/// </summary>
[DataField]
public List<CargoBountyHistoryData> History = new();
/// <summary>
/// Used to determine unique order IDs
/// </summary>

View File

@@ -8,6 +8,7 @@ using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.NameIdentifier;
using Content.Shared.Paper;
using Content.Shared.Stacks;
@@ -16,6 +17,7 @@ using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems;
@@ -25,6 +27,7 @@ public sealed partial class CargoSystem
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
@@ -54,7 +57,7 @@ public sealed partial class CargoSystem
return;
var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, untilNextSkip));
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip));
}
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
@@ -95,13 +98,13 @@ public sealed partial class CargoSystem
return;
}
if (!TryRemoveBounty(station, bounty.Value))
if (!TryRemoveBounty(station, bounty.Value, true, args.Actor))
return;
FillBountyDatabase(station);
db.NextSkipTime = _timing.CurTime + db.SkipDelay;
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
_audio.PlayPvs(component.SkipSound, uid);
}
@@ -179,7 +182,7 @@ public sealed partial class CargoSystem
continue;
}
TryRemoveBounty(station, bounty.Value);
TryRemoveBounty(station, bounty.Value, false);
FillBountyDatabase(station);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
}
@@ -434,24 +437,44 @@ public sealed partial class CargoSystem
}
[PublicAPI]
public bool TryRemoveBounty(EntityUid uid, string dataId, StationCargoBountyDatabaseComponent? component = null)
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
string dataId,
bool skipped,
EntityUid? actor = null)
{
if (!TryGetBountyFromId(uid, dataId, out var data, component))
if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp))
return false;
return TryRemoveBounty(uid, data.Value, component);
return TryRemoveBounty(ent, data.Value, skipped, actor);
}
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
CargoBountyData data,
bool skipped,
EntityUid? actor = null)
{
if (!Resolve(uid, ref component))
if (!Resolve(ent, ref ent.Comp))
return false;
for (var i = 0; i < component.Bounties.Count; i++)
for (var i = 0; i < ent.Comp.Bounties.Count; i++)
{
if (component.Bounties[i].Id == data.Id)
if (ent.Comp.Bounties[i].Id == data.Id)
{
component.Bounties.RemoveAt(i);
string? actorName = null;
if (actor != null)
{
var getIdentityEvent = new TryGetIdentityShortInfoEvent(ent.Owner, actor.Value);
RaiseLocalEvent(getIdentityEvent);
actorName = getIdentityEvent.Title;
}
ent.Comp.History.Add(new CargoBountyHistoryData(data,
skipped
? CargoBountyHistoryData.BountyResult.Skipped
: CargoBountyHistoryData.BountyResult.Completed,
_gameTiming.CurTime,
actorName));
ent.Comp.Bounties.RemoveAt(i);
return true;
}
}
@@ -492,7 +515,7 @@ public sealed partial class CargoSystem
}
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
}
}

View File

@@ -8,7 +8,7 @@ using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Paper;
@@ -21,6 +21,7 @@ namespace Content.Server.Cargo.Systems
public sealed partial class CargoSystem
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
@@ -41,6 +42,7 @@ namespace Content.Server.Cargo.Systems
SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated);
SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
Reset();
}
@@ -75,6 +77,17 @@ namespace Content.Server.Cargo.Systems
_timer = 0;
}
private void OnEmagged(Entity<CargoOrderConsoleComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent, EmagType.Interaction))
return;
args.Handled = true;
}
private void UpdateConsole(float frameTime)
{
_timer += frameTime;
@@ -85,9 +98,11 @@ namespace Content.Server.Cargo.Systems
{
_timer -= Delay;
foreach (var account in EntityQuery<StationBankAccountComponent>())
var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
while (stationQuery.MoveNext(out var uid, out var bank))
{
account.Balance += account.IncreasePerSecond * Delay;
var balanceToAdd = bank.IncreasePerSecond * Delay;
UpdateBankAccount(uid, bank, balanceToAdd);
}
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
@@ -192,7 +207,7 @@ namespace Content.Server.Cargo.Systems
order.Approved = true;
_audio.PlayPvs(component.ConfirmSound, uid);
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, player);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
@@ -213,7 +228,7 @@ namespace Content.Server.Cargo.Systems
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
orderDatabase.Orders.Remove(order);
DeductFunds(bank, cost);
UpdateBankAccount(station.Value, bank, -cost);
UpdateOrders(station.Value);
}
@@ -536,11 +551,6 @@ namespace Content.Server.Cargo.Systems
}
private void DeductFunds(StationBankAccountComponent component, int amount)
{
component.Balance = Math.Max(0, component.Balance - amount);
}
#region Station
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)

View File

@@ -60,6 +60,7 @@ namespace Content.Server.Cloning
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
@@ -276,10 +277,15 @@ namespace Content.Server.Cloning
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (!this.IsPowered(uid, EntityManager))
return;
_audio.PlayPvs(clonePod.SparkSound, uid);
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
args.Handled = true;
}
@@ -309,7 +315,7 @@ namespace Content.Server.Cloning
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
@@ -327,7 +333,7 @@ namespace Content.Server.Cloning
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!HasComp<EmaggedComponent>(uid))
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}

View File

@@ -16,7 +16,6 @@ using Content.Shared.Chat;
using Content.Shared.Communications;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Emag.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Robust.Server.GameObjects;
@@ -177,7 +176,7 @@ namespace Content.Server.Communications
private bool CanUse(EntityUid user, EntityUid console)
{
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent) && !HasComp<EmaggedComponent>(console))
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent))
{
return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent);
}

View File

@@ -89,6 +89,8 @@ public sealed class FixRotationsCommand : IConsoleCommand
valid |= tagSystem.HasTag(child, "ForceFixRotations");
// override
valid &= !tagSystem.HasTag(child, "ForceNoFixRotations");
// remove diagonal entities as well
valid &= !tagSystem.HasTag(child, "Diagonal");
if (!valid)
continue;

View File

@@ -13,6 +13,8 @@ using Robust.Server.GameObjects;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.IdentityManagement;
using Content.Shared.Security.Components;
using System.Linq;
using Content.Shared.Roles.Jobs;
namespace Content.Server.CriminalRecords.Systems;
@@ -42,6 +44,7 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
subs.Event<CriminalRecordAddHistory>(OnAddHistory);
subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
subs.Event<CriminalRecordSetStatusFilter>(OnStatusFilterPressed);
});
}
@@ -57,6 +60,11 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
ent.Comp.ActiveKey = msg.SelectedKey;
UpdateUserInterface(ent);
}
private void OnStatusFilterPressed(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordSetStatusFilter msg)
{
ent.Comp.FilterStatus = msg.FilterStatus;
UpdateUserInterface(ent);
}
private void OnFiltersChanged(Entity<CriminalRecordsConsoleComponent> ent, ref SetStationRecordFilter msg)
{
@@ -112,13 +120,26 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
}
// will probably never fail given the checks above
name = _records.RecordName(key.Value);
officer = Loc.GetString("criminal-records-console-unknown-officer");
var jobName = "Unknown";
_records.TryGetRecord<GeneralStationRecord>(key.Value, out var entry);
if (entry != null)
jobName = entry.JobTitle;
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
if (tryGetIdentityShortInfoEvent.Title != null)
officer = tryGetIdentityShortInfoEvent.Title;
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
(string, object)[] args;
if (reason != null)
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason) };
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason), ("job", jobName) };
else
args = new (string, object)[] { ("name", name), ("officer", officer) };
args = new (string, object)[] { ("name", name), ("officer", officer), ("job", jobName) };
// figure out which radio message to send depending on transition
var statusString = (oldStatus, msg.Status) switch
@@ -193,8 +214,18 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
return;
}
// get the listing of records to display
var listing = _records.BuildListing((owningStation.Value, stationRecords), console.Filter);
// filter the listing by the selected criminal record status
//if NONE, dont filter by status, just show all crew
if (console.FilterStatus != SecurityStatus.None)
{
listing = listing
.Where(x => _records.TryGetRecord<CriminalRecord>(new StationRecordKey(x.Key, owningStation.Value), out var record) && record.Status == console.FilterStatus)
.ToDictionary(x => x.Key, x => x.Value);
}
var state = new CriminalRecordsConsoleState(listing, console.Filter);
if (console.ActiveKey is { } id)
{
@@ -205,6 +236,9 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
state.SelectedKey = id;
}
// Set the Current Tab aka the filter status type for the records list
state.FilterStatus = console.FilterStatus;
_ui.SetUiState(uid, CriminalRecordsConsoleKey.Key, state);
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs;
@@ -47,7 +48,7 @@ public sealed class DamageForceSaySystem : EntitySystem
}
}
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null)
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
@@ -57,7 +58,13 @@ public sealed class DamageForceSaySystem : EntitySystem
_timing.CurTime < component.NextAllowedTime)
return;
var suffix = Loc.GetString(suffixOverride ?? component.ForceSayStringPrefix + _random.Next(1, component.ForceSayStringCount));
var ev = new BeforeForceSayEvent(component.ForceSayStringDataset);
RaiseLocalEvent(uid, ev);
if (!_prototype.TryIndex(ev.Prefix, out var prefixList))
return;
var suffix = Loc.GetString(_random.Pick(prefixList.Values));
// set cooldown & raise event
component.NextAllowedTime = _timing.CurTime + component.Cooldown;
@@ -80,7 +87,7 @@ public sealed class DamageForceSaySystem : EntitySystem
if (!args.FellAsleep)
return;
TryForceSay(uid, component, true, "damage-force-say-sleep");
TryForceSay(uid, component);
AllowNextSpeech(uid);
}

View File

@@ -1112,7 +1112,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
.SingleOrDefaultAsync());
}
public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
{
await using var db = await GetDb();
@@ -1122,7 +1122,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
return;
}
dbPlayer.LastReadRules = date.UtcDateTime;
dbPlayer.LastReadRules = date?.UtcDateTime;
await db.DbContext.SaveChangesAsync();
}
@@ -1801,6 +1801,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
#endregion
public abstract Task SendNotification(DatabaseNotification notification);
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
// Normalize DateTimes here so they're always Utc. Thanks.
protected abstract DateTime NormalizeDatabaseTime(DateTime time);

View File

@@ -282,7 +282,7 @@ namespace Content.Server.Database
#region Rules
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
Task SetLastReadRules(NetUserId player, DateTimeOffset? time);
#endregion
@@ -350,6 +350,15 @@ namespace Content.Server.Database
/// <param name="notification">The notification to trigger</param>
void InjectTestNotification(DatabaseNotification notification);
/// <summary>
/// Send a notification to all other servers connected to the same database.
/// </summary>
/// <remarks>
/// The local server will receive the sent notification itself again.
/// </remarks>
/// <param name="notification">The notification to send.</param>
Task SendNotification(DatabaseNotification notification);
#endregion
}
@@ -821,7 +830,7 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.GetLastReadRules(player));
}
public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
public Task SetLastReadRules(NetUserId player, DateTimeOffset? time)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SetLastReadRules(player, time));
@@ -1045,6 +1054,12 @@ namespace Content.Server.Database
HandleDatabaseNotification(notification);
}
public Task SendNotification(DatabaseNotification notification)
{
DbWriteOpsMetric.Inc();
return RunDbCommand(() => _db.SendNotification(notification));
}
private async void HandleDatabaseNotification(DatabaseNotification notification)
{
lock (_notificationHandlers)

View File

@@ -0,0 +1,76 @@
using System.Text.Json;
using Robust.Shared.Asynchronous;
namespace Content.Server.Database;
public static class ServerDbManagerExt
{
/// <summary>
/// Subscribe to a database notification on a specific channel, formatted as JSON.
/// </summary>
/// <param name="dbManager">The database manager to subscribe on.</param>
/// <param name="taskManager">The task manager used to run the main callback on the main thread.</param>
/// <param name="sawmill">Sawmill to log any errors to.</param>
/// <param name="channel">
/// The notification channel to listen on. Only notifications on this channel will be handled.
/// </param>
/// <param name="action">
/// The action to run on the notification data.
/// This runs on the main thread.
/// </param>
/// <param name="earlyFilter">
/// An early filter callback that runs before the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <param name="filter">
/// A filter callback that runs after the JSON message is deserialized.
/// Return false to not handle the notification.
/// This does not run on the main thread.
/// </param>
/// <typeparam name="TData">The type of JSON data to deserialize.</typeparam>
public static void SubscribeToJsonNotification<TData>(
this IServerDbManager dbManager,
ITaskManager taskManager,
ISawmill sawmill,
string channel,
Action<TData> action,
Func<bool>? earlyFilter = null,
Func<TData, bool>? filter = null)
{
dbManager.SubscribeToNotifications(notification =>
{
if (notification.Channel != channel)
return;
if (notification.Payload == null)
{
sawmill.Error($"Got {channel} notification with null payload!");
return;
}
if (earlyFilter != null && !earlyFilter())
return;
TData data;
try
{
data = JsonSerializer.Deserialize<TData>(notification.Payload)
?? throw new JsonException("Content is null");
}
catch (JsonException e)
{
sawmill.Error($"Got invalid JSON in {channel} notification: {e}");
return;
}
if (filter != null && !filter(data))
return;
taskManager.RunOnMainThread(() =>
{
action(data);
});
});
}
}

View File

@@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Content.Server.Database;
@@ -17,6 +18,7 @@ public sealed partial class ServerDbPostgres
private static readonly string[] NotificationChannels =
[
BanManager.BanNotificationChannel,
MultiServerKickManager.NotificationChannel,
];
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
@@ -111,6 +113,14 @@ public sealed partial class ServerDbPostgres
});
}
public override async Task SendNotification(DatabaseNotification notification)
{
await using var db = await GetDbImpl();
await db.PgDbContext.Database.ExecuteSqlAsync(
$"SELECT pg_notify({notification.Channel}, {notification.Payload})");
}
public override void Shutdown()
{
_notificationTokenSource.Cancel();

View File

@@ -537,6 +537,12 @@ namespace Content.Server.Database
return await base.AddAdminMessage(message);
}
public override Task SendNotification(DatabaseNotification notification)
{
// Notifications not implemented on SQLite.
return Task.CompletedTask;
}
protected override DateTime NormalizeDatabaseTime(DateTime time)
{
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);

View File

@@ -15,7 +15,6 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// The type of explosion. Determines damage types and tile break chance scaling.
/// </summary>
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
[JsonIgnore]
public string ExplosionType = default!;
/// <summary>
@@ -23,14 +22,12 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// chance.
/// </summary>
[DataField]
[JsonIgnore]
public float MaxIntensity = 5;
/// <summary>
/// How quickly intensity drops off as you move away from the epicenter
/// </summary>
[DataField]
[JsonIgnore]
public float IntensitySlope = 1;
/// <summary>
@@ -41,15 +38,20 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
/// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles.
/// </remarks>
[DataField]
[JsonIgnore]
public float MaxTotalIntensity = 100;
/// <summary>
/// The intensity of the explosion per unit reaction.
/// </summary>
[DataField]
[JsonIgnore]
public float IntensityPerUnit = 1;
/// <summary>
/// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger
/// explosives that don't space tiles, without having to create a new explosion-type prototype.
/// </summary>
[DataField]
public float TileBreakScale = 1f;
public override bool ShouldLog => true;
@@ -72,6 +74,7 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
ExplosionType,
intensity,
IntensitySlope,
MaxIntensity);
MaxIntensity,
TileBreakScale);
}
}

View File

@@ -157,6 +157,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
IoCManager.Resolve<IBanManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().PostInit();
IoCManager.Resolve<MultiServerKickManager>().Initialize();
}
}

View File

@@ -50,6 +50,7 @@ public sealed class FaxSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private const string PaperSlotId = "Paper";
@@ -227,7 +228,7 @@ public sealed class FaxSystem : EntitySystem
return;
}
if (component.KnownFaxes.ContainsValue(newName) && !HasComp<EmaggedComponent>(uid)) // Allow existing names if emagged for fun
if (component.KnownFaxes.ContainsValue(newName) && !_emag.CheckFlag(uid, EmagType.Interaction)) // Allow existing names if emagged for fun
{
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-exist"), uid);
return;
@@ -246,7 +247,12 @@ public sealed class FaxSystem : EntitySystem
private void OnEmagged(EntityUid uid, FaxMachineComponent component, ref GotEmaggedEvent args)
{
_audioSystem.PlayPvs(component.EmagSound, uid);
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}
@@ -260,7 +266,7 @@ public sealed class FaxSystem : EntitySystem
switch (command)
{
case FaxConstants.FaxPingCommand:
var isForSyndie = HasComp<EmaggedComponent>(uid) &&
var isForSyndie = _emag.CheckFlag(uid, EmagType.Interaction) &&
args.Data.ContainsKey(FaxConstants.FaxSyndicateData);
if (!isForSyndie && !component.ResponsePings)
return;
@@ -405,7 +411,7 @@ public sealed class FaxSystem : EntitySystem
{ DeviceNetworkConstants.Command, FaxConstants.FaxPingCommand }
};
if (HasComp<EmaggedComponent>(uid))
if (_emag.CheckFlag(uid, EmagType.Interaction))
payload.Add(FaxConstants.FaxSyndicateData, true);
_deviceNetworkSystem.QueuePacket(uid, null, payload);

View File

@@ -15,6 +15,7 @@ using Content.Shared.Telephone;
using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -35,22 +36,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
private float _updateTimer = 1.0f;
private const float UpdateTime = 1.0f;
private const float MinTimeBetweenSyncRequests = 0.5f;
private TimeSpan _minTimeSpanBetweenSyncRequests;
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
public override void Initialize()
{
base.Initialize();
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
// Holopad UI and bound user interface messages
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
@@ -68,7 +62,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Networked events
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
// Component start/shutdown events
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
@@ -268,16 +261,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (source.Comp.Hologram == null)
GenerateHologram(source);
// Receiver holopad holograms have to be generated now instead of waiting for their own event
// to fire because holographic avatars get synced immediately
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
GenerateHologram((args.Receiver, receivingHolopad));
if (source.Comp.User != null)
{
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User.Value);
}
// Re-link the user to refresh the sprite data
LinkHolopadToUser(source, source.Comp.User);
}
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
@@ -323,22 +311,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
{
var uid = args.SenderSession.AttachedEntity;
if (!Exists(uid))
return;
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
return;
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
return;
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
}
#endregion
#region: Component start/shutdown events
@@ -485,8 +457,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
}
}
}
_recentlyUpdatedHolograms.Clear();
}
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
@@ -555,10 +525,16 @@ public sealed class HolopadSystem : SharedHolopadSystem
QueueDel(hologram);
}
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid? user)
{
if (user == null)
{
UnlinkHolopadFromUser(entity, null);
return;
}
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
holopadUser = AddComp<HolopadUserComponent>(user);
holopadUser = AddComp<HolopadUserComponent>(user.Value);
if (user != entity.Comp.User?.Owner)
{
@@ -567,51 +543,44 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Assigns the new user in their place
holopadUser.LinkedHolopads.Add(entity);
entity.Comp.User = (user, holopadUser);
entity.Comp.User = (user.Value, holopadUser);
}
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
{
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
return;
}
// We have no apriori sprite data for the hologram, request
// the current appearance of the user from the client
RequestHolopadUserSpriteUpdate((user, holopadUser));
// Add the new user to PVS and sync their appearance with any
// holopads connected to the one they are using
_pvs.AddGlobalOverride(user.Value);
SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
}
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
if (user == null)
return;
entity.Comp.User = null;
SyncHolopadHologramAppearanceWithTarget(entity, null);
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram != null)
{
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
// Send message with no sprite data to the client
// This will set the holgram sprite to a generic icon
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
RaiseNetworkEvent(ev);
}
}
if (!HasComp<HolopadUserComponent>(user))
if (user == null)
return;
user.Value.Comp.LinkedHolopads.Remove(entity);
if (!user.Value.Comp.LinkedHolopads.Any())
if (!user.Value.Comp.LinkedHolopads.Any() &&
user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
{
_pendingRequestsForSpriteState.Remove(user.Value);
_pvs.RemoveGlobalOverride(user.Value);
RemComp<HolopadUserComponent>(user.Value);
}
}
private void SyncHolopadHologramAppearanceWithTarget(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
{
foreach (var linkedHolopad in GetLinkedHolopads(entity))
{
if (linkedHolopad.Comp.Hologram == null)
continue;
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
RemComp<HolopadUserComponent>(user.Value);
if (user == null)
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
Dirty(linkedHolopad.Comp.Hologram.Value);
}
}
@@ -646,31 +615,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
Dirty(entity);
}
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
{
if (!_pendingRequestsForSpriteState.Add(user))
return;
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
RaiseNetworkEvent(ev);
}
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
{
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
{
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
{
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
continue;
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
RaiseNetworkEvent(ev);
}
}
}
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
{
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))

View File

@@ -1,4 +1,5 @@
using Content.Shared.Implants.Components;
using Content.Server.Body.Components;
using Content.Shared.Implants.Components;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
@@ -9,6 +10,7 @@ public sealed partial class ImplanterSystem
{
SubscribeLocalEvent<ImplantedComponent, ComponentInit>(OnImplantedInit);
SubscribeLocalEvent<ImplantedComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ImplantedComponent, BeingGibbedEvent>(OnGibbed);
}
private void OnImplantedInit(EntityUid uid, ImplantedComponent component, ComponentInit args)
@@ -22,4 +24,10 @@ public sealed partial class ImplanterSystem
//If the entity is deleted, get rid of the implants
_container.CleanContainer(component.ImplantContainer);
}
private void OnGibbed(Entity<ImplantedComponent> ent, ref BeingGibbedEvent args)
{
//If the entity is gibbed, get rid of the implants
_container.CleanContainer(ent.Comp.ImplantContainer);
}
}

View File

@@ -58,17 +58,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
return;
}
// Check if we are trying to implant a implant which is already implanted
if (implant.HasValue && !component.AllowMultipleImplants && CheckSameImplant(target, implant.Value))
{
var name = Identity.Name(target, EntityManager, args.User);
var msg = Loc.GetString("implanter-component-implant-already", ("implant", implant), ("target", name));
_popup.PopupEntity(msg, target, args.User);
args.Handled = true;
return;
}
//Implant self instantly, otherwise try to inject the target.
if (args.User == target)
Implant(target, target, uid, component);
@@ -79,14 +68,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
args.Handled = true;
}
public bool CheckSameImplant(EntityUid target, EntityUid implant)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
return false;
var implantPrototype = Prototype(implant);
return implanted.ImplantContainer.ContainedEntities.Any(entity => Prototype(entity) == implantPrototype);
}
/// <summary>
/// Attempt to implant someone else.

View File

@@ -0,0 +1,76 @@
using Content.Server.Radio.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
public sealed class RadioImplantSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadioImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
SubscribeLocalEvent<RadioImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
}
/// <summary>
/// If implanted with a radio implant, installs the necessary intrinsic radio components
/// </summary>
private void OnImplantImplanted(Entity<RadioImplantComponent> ent, ref ImplantImplantedEvent args)
{
if (args.Implanted == null)
return;
var activeRadio = EnsureComp<ActiveRadioComponent>(args.Implanted.Value);
foreach (var channel in ent.Comp.RadioChannels)
{
if (activeRadio.Channels.Add(channel))
ent.Comp.ActiveAddedChannels.Add(channel);
}
EnsureComp<IntrinsicRadioReceiverComponent>(args.Implanted.Value);
var intrinsicRadioTransmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(args.Implanted.Value);
foreach (var channel in ent.Comp.RadioChannels)
{
if (intrinsicRadioTransmitter.Channels.Add(channel))
ent.Comp.TransmitterAddedChannels.Add(channel);
}
}
/// <summary>
/// Removes intrinsic radio components once the Radio Implant is removed
/// </summary>
private void OnRemove(Entity<RadioImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
if (TryComp<ActiveRadioComponent>(args.Container.Owner, out var activeRadioComponent))
{
foreach (var channel in ent.Comp.ActiveAddedChannels)
{
activeRadioComponent.Channels.Remove(channel);
}
ent.Comp.ActiveAddedChannels.Clear();
if (activeRadioComponent.Channels.Count == 0)
{
RemCompDeferred<ActiveRadioComponent>(args.Container.Owner);
}
}
if (!TryComp<IntrinsicRadioTransmitterComponent>(args.Container.Owner, out var radioTransmitterComponent))
return;
foreach (var channel in ent.Comp.TransmitterAddedChannels)
{
radioTransmitterComponent.Channels.Remove(channel);
}
ent.Comp.TransmitterAddedChannels.Clear();
if (radioTransmitterComponent.Channels.Count == 0 || activeRadioComponent?.Channels.Count == 0)
{
RemCompDeferred<IntrinsicRadioTransmitterComponent>(args.Container.Owner);
}
}
}

View File

@@ -34,7 +34,7 @@ public sealed class RulesManager
{
PopupTime = _cfg.GetCVar(CCVars.RulesWaitTime),
CoreRules = _cfg.GetCVar(CCVars.RulesFile),
ShouldShowRules = !isLocalhost && !hasCooldown
ShouldShowRules = !isLocalhost && !hasCooldown,
};
_netManager.ServerSendMessage(showRulesMessage, e.Channel);
}

View File

@@ -79,6 +79,7 @@ namespace Content.Server.IoC
IoCManager.Register<MappingManager>();
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
}
}
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.UserInterface;
using Content.Shared.Database;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.Lathe;
using Content.Shared.Materials;
@@ -42,6 +43,7 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly PopupSystem _popup = default!;
@@ -292,7 +294,7 @@ namespace Content.Server.Lathe
{
if (uid != args.Lathe || !TryComp<TechnologyDatabaseComponent>(uid, out var technologyDatabase))
return;
if (!args.getUnavailable && !HasComp<EmaggedComponent>(uid))
if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
return;
foreach (var recipe in component.EmagDynamicRecipes)
{

View File

@@ -72,7 +72,10 @@ public sealed class HealingSystem : EntitySystem
_bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
if (isBleeding != bloodstream.BleedAmount > 0)
{
_popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User);
var popup = (args.User == entity.Owner)
? Loc.GetString("medical-item-stop-bleeding-self")
: Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
_popupSystem.PopupEntity(popup, entity, args.User);
}
}

View File

@@ -22,6 +22,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -42,6 +43,7 @@ public sealed class SuitSensorSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
@@ -369,7 +371,7 @@ public sealed class SuitSensorSystem : EntitySystem
userJobIcon = card.Comp.JobIcon;
foreach (var department in card.Comp.JobDepartments)
userJobDepartments.Add(Loc.GetString($"department-{department}"));
userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
}
// get health mob state

View File

@@ -35,7 +35,7 @@ public sealed class MiningSystem : EntitySystem
return;
var coords = Transform(uid).Coordinates;
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield);
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield+1);
for (var i = 0; i < toSpawn; i++)
{
Spawn(proto.OreEntity, coords.Offset(_random.NextVector2(0.2f)));

View File

@@ -0,0 +1,12 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates;
namespace Content.Server.Movement.Components;
[RegisterComponent]
public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
{
}

View File

@@ -1,5 +1,5 @@
using Content.Server.Chat.Systems;
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Emag.Components;
@@ -55,34 +55,11 @@ public sealed partial class MedibotInjectOperator : HTNOperator
if (!_entMan.TryGetComponent<MedibotComponent>(owner, out var botComp))
return HTNOperatorStatus.Failed;
if (!_entMan.TryGetComponent<DamageableComponent>(target, out var damage))
if (!_medibot.CheckInjectable((owner, botComp), target) || !_medibot.TryInject((owner, botComp), target))
return HTNOperatorStatus.Failed;
if (!_solutionContainer.TryGetInjectableSolution(target, out var injectable, out _))
return HTNOperatorStatus.Failed;
if (!_interaction.InRangeUnobstructed(owner, target))
return HTNOperatorStatus.Failed;
var total = damage.TotalDamage;
// always inject healthy patients when emagged
if (total == 0 && !_entMan.HasComponent<EmaggedComponent>(owner))
return HTNOperatorStatus.Failed;
if (!_entMan.TryGetComponent<MobStateComponent>(target, out var mobState))
return HTNOperatorStatus.Failed;
var state = mobState.CurrentState;
if (!_medibot.TryGetTreatment(botComp, mobState.CurrentState, out var treatment) || !treatment.IsValid(total))
return HTNOperatorStatus.Failed;
_entMan.EnsureComponent<NPCRecentlyInjectedComponent>(target);
_solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _);
_popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target);
_audio.PlayPvs(botComp.InjectSound, target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: true, hideLog: true);
return HTNOperatorStatus.Finished;
}
}

View File

@@ -1,6 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Damage;

View File

@@ -1,4 +1,4 @@
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
namespace Content.Server.NPC.Systems;

View File

@@ -25,6 +25,13 @@ namespace Content.Server.Nuke
[ViewVariables(VVAccess.ReadWrite)]
public int Timer = 300;
/// <summary>
/// If the nuke is disarmed, this sets the minimum amount of time the timer can have.
/// The remaining time will reset to this value if it is below it.
/// </summary>
[DataField]
public int MinimumTime = 180;
/// <summary>
/// How long until the bomb can arm again after deactivation.
/// Used to prevent announcements spam.

View File

@@ -522,6 +522,9 @@ public sealed class NukeSystem : EntitySystem
_sound.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
// reset nuke remaining time to either itself or the minimum time, whichever is higher
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
// disable sound and reset it
component.PlayedAlertSound = false;
component.AlertAudioStream = _audio.Stop(component.AlertAudioStream);

View File

@@ -21,6 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems;
public sealed class FatExtractorSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly HungerSystem _hunger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@@ -36,8 +37,13 @@ public sealed class FatExtractorSystem : EntitySystem
private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
args.Repeatable = false;
}
private void OnClosed(EntityUid uid, FatExtractorComponent component, ref StorageAfterCloseEvent args)
@@ -103,7 +109,7 @@ public sealed class FatExtractorSystem : EntitySystem
if (_hunger.GetHunger(hunger) < component.NutritionPerSecond)
return false;
if (hunger.CurrentThreshold < component.MinHungerThreshold && !HasComp<EmaggedComponent>(uid))
if (hunger.CurrentThreshold < component.MinHungerThreshold && !_emag.CheckFlag(uid, EmagType.Interaction))
return false;
return true;

View File

@@ -24,6 +24,7 @@ namespace Content.Server.Nutrition.EntitySystems
{
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly FoodSystem _foodSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -63,7 +64,7 @@ namespace Content.Server.Nutrition.EntitySystems
forced = false;
}
if (entity.Comp.ExplodeOnUse || HasComp<EmaggedComponent>(entity.Owner))
if (entity.Comp.ExplodeOnUse || _emag.CheckFlag(entity, EmagType.Interaction))
{
_explosionSystem.QueueExplosion(entity.Owner, "Default", entity.Comp.ExplosionIntensity, 0.5f, 3, canCreateVacuum: false);
EntityManager.DeleteEntity(entity);
@@ -161,8 +162,15 @@ namespace Content.Server.Nutrition.EntitySystems
args.Args.Target.Value);
}
}
private void OnEmagged(Entity<VapeComponent> entity, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(entity, EmagType.Interaction))
return;
args.Handled = true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Power.EntitySystems;
using Content.Shared.Guidebook;
namespace Content.Server.Power.Components
{
@@ -16,6 +17,7 @@ namespace Content.Server.Power.Components
/// Maximum charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField]
[GuidebookData]
public float MaxCharge;
/// <summary>

View File

@@ -1,4 +1,5 @@
using Content.Server.Power.Pow3r;
using Content.Shared.Guidebook;
namespace Content.Server.Power.Components
{
@@ -24,6 +25,7 @@ namespace Content.Server.Power.Components
[DataField("maxSupply")]
[ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float MaxSupply
{
get => NetworkBattery.MaxSupply;

View File

@@ -1,5 +1,6 @@
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Guidebook;
namespace Content.Server.Power.Components
{
@@ -8,6 +9,7 @@ namespace Content.Server.Power.Components
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("supplyRate")]
[GuidebookData]
public float MaxSupply { get => NetworkSupply.MaxSupply; set => NetworkSupply.MaxSupply = value; }
[ViewVariables(VVAccess.ReadWrite)]

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