Upstream stable sync (#1558)

* Adjust uplink buy button to be under item icon (#38596)

* Adjusted uplink buy button to be under item icon

* Put the discount subtext under the icon

* Indent fixes, added margin

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Automatic changelog update

* More guidebook fixes (#38699)

* First commit
- Resolving a few different pages, fixing typos and updating spellings and phrasing to be more coherent.

* More slight changes

* Reverting slightly the salvage page change

* Fixing Science wording inconsistency

* Few other small guidebook fixes
- Updating NukeOps page to properly refer to "Corpsman" rather than "Agent".
- Typo and slight wording change in Traitors.xml.
- Updating small typos in Antagonists.xml and Zombies.xml pages.

* Automatic changelog update

* Fixes missing underwear FTLs (#38720)

* Validate `ShaderPrototype` IDs (#38728)

* Convert all shader prototype string literals to protoids in overlays

* Convert more shader prototype literal strings to protoids

* Convert ValidatePrototypeId to ProtoId

* Later

* Janitor Tool: Wire Brush (#38667)

* Wow! It's -brush-

* spacing.

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

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

* Update Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml

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

* fixed changes

---------

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

* Automatic changelog update

* [BUGFIX] Fixed revenant malfunction ability not working properly only MediBots and Stasis bed (#38664)

* fixed

* clean up

* orks fix smart

* review fix 1

* more requested changes

* less cursed

* more descriptive description

* better wording

* Automatic changelog update

* Tiny Tiny Cleanup of the EyeClosingSystem. (#38734)

Update EyeClosingSystem.cs

* Typofixes for figurine dialogue (#38737)

* Typofixes for figurine dialogue

* Forgot two

* fix: Use PredictedQueueDel for gib spell (#38729)

* Fix human skin tone distribution (#38701)

* fix: don't default to uncharged sprite state for cells (#38730)

* Fix Hristov description - remove inaccurate technical specs (#38746)

- Removes inaccurate 'armor piercing 14.5mm shells' reference
- Replaces it with a more funny description, matching the style of the other snipers and guns
- Keeps ammunition type '.60 anti-materiel ammo' specification
- Fixes issue #38590

Co-authored-by: Arthur Fiorese de Andrade <aandrade@cmcxs.gov.br>

* Validate `ProtoId`s in tests (#38745)

* Convert string literals to protoids in Content.Tests

* Convert string literals to protoids or consts in Content.IntegrationTests

* Fix linter failures
Tricksy static using misled me

* Validate remaining `ProtoId` strings (#38747)

Validate remaining ProtoId strings

* feat: allow mopping evaporating puddles (#38743)

* Automatic changelog update

* Cleanup warning in StomachSystem (#38748)

you did not see this

* Vox scars (#38592)

* Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl

* Revert "Added vox scars n'stuff, renamed vox_tattoos.ftl to just vox.ftl"

This reverts commit c73da55ba3b39ddf93b493aecd85604c54dd8a15.

* locale key fix

* Changed top surgery scar names to be more generalized

* Adjusted face scars

* Formatting fixes

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Automatic changelog update

* UnlockNode command to LEC. (#38751)

* commit

* Update UnlockNodeCommand.cs

* commit

* move command locale to its own file.

* Update Content.Server/Xenoarchaeology/Artifact/UnlockNodeCommand.cs

---------

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

* fix: spellbooks can have infinite charges (#38376)

* fix: spellbooks can have infinite charges

* refactor: indicate infinite spellbook charges with null

Not sure if I like this much better...

* Automatic changelog update

* Minor escape menu UX improvements (#38650)

* Automatic changelog update

* Meta.json cleanup - pins.rsi (#38781)

cleanup

* Log an error when adding an admin log containing a null char (#38782)

* Log an error when adding an admin log containing a null char

* Also log LogType and LogImpact

* Remove them

* Fix incorrect conditions on black pepper's forced cough (#38783)

no one to blame but myself

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

* Update Credits (#38786)

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

* Fixing incorrect headset being given to QM visitor (#38785)

* Stasis bed cleanup and bugfixes. (#38762)

* Stasis bed sent to shed

* Code Review

* Code Review 2

* Automatic changelog update

* Cleanup warnings: CS0414 (#38793)

Cleanup

* Move moth movement modifiers to the Base (#38795)

* fix centcom pda (#38796)

* Catchable items, playable basketball (#37702)

* catching

* fix

* improve

* fix linter

* cleanup

* fix prediction

* do the same here

* fix comment

* Automatic changelog update

* Fix storage voice control bug after handssystem refactor (#38810)

* Automatic changelog update

* Reducing the amount of space in the gogo hat  (#38808)

Reducing the amount of space in the hat from 7*4 to 4*3 to make a bit more sense with the fact it is only a hat.

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>

* Automatic changelog update

* Golden pai (#38549)

* Golden pai

* Make golden PAI public and rare

* Golden PAI = legendary salvage

* Automatic changelog update

* Cleanup: Remove ``TelephoneComponent.RequiresPower`` (#38823)

* Cleanup

* Forgot about yml

* Branded lighters addition (#35514)

* newlighters

* port complete

* donk pocket box addition

* should fix test fail, proper credit

* captalisation issue

* fixed

* typo corrected

* removed accidental change to combat bakery

* fixed lock message, made NT flippo start fueled.

* Requested changes

* fixed discount dan

* moved matchbox

* requested changes, discount dan more broken than ever (somehow)

* unbungle

* uhhhhh

* maybe fixes?

* changed inheritance

* fixed enum?

* nevermind

* finally fixed

* breaking changes

* inheritance unbungled

* Automatic changelog update

* Cleanup: Remove ``TryInsert`` method from the ``DisposableSystem`` and use event subscriptions instead (#38819)

Cleanup

* JumpBoots Attempt №2 (#36862)

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: unknown <wainzor1337@gmail.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>

* Automatic changelog update

* Replace `ValidatePrototypeId` uses with `ProtoId` or `EntProtoId` (#38814)

* The easy ones

* For certain values of easy

* Easy test

* Hair

* Fix sandbox violations

* Sort usings

* Fix SSD sleep misprediction (#38831)

init

* New science unlock: the H.A.R.M.P.A.C.K (#38824)

* Automatic changelog update

* Fix: Add cluwne translation in ftl (#38835)

Fix

* Artifact glue reagent (#38670)

* a

* Revert "a"

This reverts commit 2b9ba4ea67a9395d30b7ab37c8065f627f1a961a.

* f

* dev it

* a?

* ad

* forgor

* Revert "f"

This reverts commit 39228c7cbe4d28ba43b73580b55e01c3979eb869.

* derandomisation!

* reviv

* flavor

* fixe

* dwwasdwasdwa

* dwasdwasdwas

* fuck you

* Automatic changelog update

* Contraband category fixes (#38698)

* Adding contraband to some items missing it
- This includes updating contraband to follow more consistently where items are expected to be used
- Fixing the spacing in arrays to be more consistent with other files

* Updating PPD to be Eng contra

* Automatic changelog update

* Added Vox Heterochromia (#38595)

* Added Vox Heterochromia

* Forgot to change moth to vox

* minor formatting fix

---------

Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>

* Automatic changelog update

* Make mopping predicted (and some other stuff) (#38749)

* refactor: move puddle evaporation + absorbents to shared

* refactor: move SolutionRegeneration to shared

* refactor: make AbsorbentSystem visuals clientside

* style: general formatting/cleanup on touched files

- Few logical simplifications
- Add field for hard-coded sparkle effect ent
- Switch stuff to Entity<T>

No actual prediction fixes in this commit (though in
retrospect I should've done this commit last).

* fix: use predicted variants for predicted code

* fix: average out evaporation rates in mixtures

* refactor: move SolutionPurge to shared

* style: Basic SolutionPurgeComponent field cleanup

* fix: general prediction + timing + networking fixes

- Moves client side visuals back to shared because other
  players exist
- Don't accumulate CurTime in Purge/RegenerationSystem
- Network the next update field in Purge/RegenerationSystem to
  deal with UI mispredictions???

* fix: add udder bug workaround

Not needed for SolutionPurgeSystem which doesn't resolve
solutions (probably fine that SolutionPurgeSystem doesn't
cache since it's much rarer, though it probably should), and
likely not needed for AbsorbentSystem since it only resolves
against puddles which, I don't think can be in containers.

* fix: don't divide by zero for evaporation speed = 0.

* refactor: revert evaporation changes

Will cherry-pick these out in another PR.

Also reverting the evaporation speed bugfix since it's easier
to revert all at once. :)

* fix: component cleanup; autopause fields, use ProtoID

* fix: remove unused AbsorbentComponentState

* fix: ProtoId is not string

* refactor: move PuddleSystem.UpdateAppearance to shared

* style: general PuddleSystem.UpdateAppearance tweaks

- Switch to Entity<T>
- Use ProtoIds
- Minor simplifications

* fix: add udderly silly PVS workaround

* cleanup

* fix

---------

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

* fix: Register OnRequestMovePulledObject under the right system (#38484)

* fix: Register OnRequestMovePulledObject under the right system

* nukeops test flake

* fix: correct ToggleableVisuals for toy sword and pirate helm (#38867)

* Fix lingering ghost roles (and expand tests to catch it) (#38788)

* Improve and expand `TakeRoleAndReturn` to fail on bug #38292

* fix #38292 and expanded test cases

* use validated EntProtoIds for tests

remove unusued using declarations

* use const strings that match the TestPrototypes

* Automatic changelog update

* Fix non-humanoid mobs being destroyed on devour (#38087)

* Allow non-preference living things to be added to a devourer's stomach

* Fix ordering of devour logic

* Minor refactor for whitelist on storage and food preference

* Fix linter issue

* Coerce workflow to run again; also fix bad indenting error

* Code review changes

* Automatic changelog update

* Prefer file-scoped namespaces in `.editorconfig` (#38879)

Prefer file-scoped namespaces in .editorconfig

* Parroting Parrots part 1: Help maints! SQUAWK! Maints! (#38243)

* parrots have ears. add poly

* high tech parrot functionality

* adjust times

* add accent to radio message

* don't spam everything all at once probably

* learn about the existence of prob(float)

* actually use Prob(float) correctly

* newline

* add pet spawner for poly

* move chance to talk on radio to component

* missing comment

* minor edits and doc additions

* the reviewerrrrrrr

* parrot can't learn when crit or dead

* increase default memory

* rename poly to polly

* crude way to ignore whispers. chatcode please

* This is Polly. It is set to broadcast over the engineering frequency

* add missing initialize

* add displacement map for parrot ears

* review comments - Errant

* minor things

* large rework

* fix attempting to talk when entity has no channels

* use list of active radios again to track channels

* fix bad return, some comments

* fix long learn cooldown

* minor adjustments

* use FromMinutes

* the voices told me to make these changes

* remove default reassignment

* Review changes

* remove polly's accent

* decouple radio stuff from parrotsystem

* minor stuff

* split vocalization and parroting

* minor review work

* re-add missing check

* add admin verb for clearing parrot messages

* minor action icon update

* oops

* increase icon number text size

* Admin erase parrot messages associated with players

* part 1 beck review

* add whitelist and blacklist for parrots

* Downgrade missing component error to warning

* Add comment

* add some missing comments

* Remove active radio entity tracking, use all inventory slots

* Minor changes

* small review stuff

* review radio stuff

* swap ears displacement to invisible death displacement

* remove syncsprite

* vscode why do yo have to hurt my feelings

* review changes

* use checkboth

* Automatic changelog update

* Predict RepairableSystem (#38886)

* commit

* Update SharedRepairableSystem.cs

* compo

* final touches.

* Update RepairableComponent.cs

* Update RepairableSystem.cs

* Update Content.Shared/Repairable/RepairableSystem.cs

---------

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

* Replace usages of customTypeSerializer PrototypeIdListSerializer with something that doesn't take 20 separate words to type out (#37959)

* Replace usages of customTypeSerializer PrototypeIdListSerializer with something that doesn't take 20 separate words to type out

* Missed one

* Missed another

* Fix data field ids

* Fix yaml linter and misc errors (#37444)

* Fix yaml linter

* Revert "fix cluwne pda pen slot (#35611)"

This reverts commit 66e926843f.

* More fixes

* Try again with the engine requirement removed

* Decrease number of brass sheets dropped by clockwork windoors

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>

* Add VV button to the solution editor (#38889)

add vv button to solution editor

* Automatic changelog update

* fix: wide swings with resistanceBypass now bypass resists (#38496)

* Allow pAIs to emote like a borg (#38425)

* Automatic changelog update

* reduced motion flash effect version 3 (#37824)

* V3

* Apply suggestions from code review

* Automatic changelog update

* feat: allow admins to interact under subfloors (#38813)

* feat: allow admins to interact under subfloors

* feat: use BypassInteractionChecks instead

* Automatic changelog update

* Operation Remove Gun Bloat (#38104)

* Remove long-unused magazines

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Not sure why that failed, going to rerun tests real quick

* Testing if this resolves the tests. Anyone know how to run tests locally, without waiting for github?

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Fully removed boxes of vector ammo and the vector ammo tag

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* This might fix the tests

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Nulled more =

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Nulled more migrations

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>

* Minigun inhands + HMG multihand and slow move speed (#35344)

* hmg move speed

* minigun inhands

* even better inhands

* touch-up

* Automatic changelog update

* Add Bolas to SecTech vendor (#38902)

* Automatic changelog update

* Kobold/monkey AI holograms (#38888)

* Added kobold/monkey AI holograms

* Updated meta.json

* Automatic changelog update

* Fix localization error when trying to change hair on someone with a hat (#38907)

* change Identity.Name to Identity.Entity for delivery popups (#38909)

* Replace `AdvertiseComponent` with `DatasetVocalizerComponent` (#38887)

* Replace AdvertiseComponent with DatasetVocalizerComponent

* No vocalizing while broken or without power

* Kill AdvertiseComponent/System

* This really shouldn't be here

* xmldoc for VocalizerRequiresPowerComponent

* TryIndex -> Index

* Bagel genpop (#38829)

* skelaton (skeleton)

* basic genpop

* genpop mostly done

* general layout

* all details

* maints firelocks, tweaks

* invalids bye, remove residual air and fire alarm

* uhh tweaked something dont remember

* remove actions

* armory: truncheon and riot crate

* remove more invalid devices

* armory holopad, graffiti tweak

* no windoor into sec substation, add flasher

* Revert "no windoor into sec substation, add flasher"

This reverts commit a7b919fb74433b82f48f92f98d5521ecb1f3eae4.

* no sub windoor, flasher

* fix camera blocking turret

* move warden and armory, restyle decals

* maints flashlights :D

* fix locker room dark tile placement

* Automatic changelog update

* fix clones not getting the thieving skill (#38914)

* Automatic changelog update

* Make more objects spray paintable (Reviving #31328) (#37341)

* PaintableAirlockComponent and AirlockGroupPrototype have been replaced

* Slightly redesigned SprayPainterSystem for greater versatility

* Added handling of changes to the appearance of doors and storages

* PaintableGroup prototypes have been created

* Generating tabs with styles in the UI

* Fix error with undiscovered layer

* Slight improvement

* Removed unnecessary property

* The category for `PaintableGroup` was allocated to a separate prototype so that the engine itself would check if the category existed

* Added canisters, but repainting doesn't work

* Added localization to styles

* Fix sprite changing

* Added the ability to paint canisters

* slight ui improvement

* Fix yamllinter errors

* Fix test

* The UI now remembers which tab was open

* Fix build (?)

* Rename

* Charges have been added to the spray painter

* Added a charge texture for the spray painter

* Now spray painter can paint decals

* Increased number of charges

* Spawning dummy objects has been replaced by PrototypeManager

* added a signature about the painting of the object

* fix

* Code commenting

* Fix upstream

* Update Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* review

* Now decals can only be painted if the corresponding tab in the menu is open.

* Fixed a bug with pipe and decal tabs not being remembered

* Update EntityStorageVisualizerSystem.cs

* record

* loc

* Cleanup

* Revert electrified visuals

* more cleanup, fix charges, del ammo4

* no empty file, remove meta component

* closet exceptions, storage visualizer fixes

* enable/disable decal through alt-verb

* Fix missed merge conflicts

* fix snap offset, button event handlers

* simpler order, fix snap loc string

* Remove PaintableViz.BaseRSI, no decal item, A-Z

* State-respecting UI, BUI updates, FTL fixes

* revert DecalPlacerWindow changes

* revert unwanted changes, cleanup function order

* Limit SprayPainterAmmo write access to AmmoSystem

* Remove PaintedSystem

* spray paint ammo lathe recipe, youtool listing

* category as a list, groups as subtabs

* Restore inhand copyright in meta.json

* empty spray painter, recipe produces an empty one

* allow alpha on spray painter decals

* add comments

* paintable wall lockers

* Restrict painting more objects

* Suggested event changes, event cleanup

* component comments, fix ammo inhands

* uncleanable decals, dirty styles on mapinit

* organize paintables, separate emergency/closet grp

* fix categories newline at EOF

* airlock group whitespace cleanup

* realphabetize

* Clean up EntityStorageViz merge conflict markers

* Apply requested changes

* Apply suggestions from sowelipililimute's review

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* betrayal most foul

* Remove members from EntityPaintedEvent

* No emerg. group, steelsec to secure, locker/closet

* Enable repainting the medical wall locker

* comments, no flags on PaintableVisuals

* Remove locked variants from closets/wall closets

* removable decals

* off value consistency

* can't paint away those bones

* fix precedence

* Remove AirlockDepartment, AirlockGroup protos

Both unused.

* whitelist consistency re: ammo component

* add standing emergency closet styles

* alphabetize the spray painter listings

---------

Co-authored-by: Ertanic <black.ikra.14@gmail.com>
Co-authored-by: Эдуард <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* Automatic changelog update

* Add Serializable, NetSerializable attributes to DecalPaintMode (#38921)

Serializable, Net" on DecalPaintMode

* Rotated turret wall panel sprites (#38464)

rotated sprites

* Stun and Stamina Visuals (#37196)

* Stun animation

* Commit 2

* Almost working commit

* Best commit

* Minor cleanup and value adjustments

* Fix animation data getting wasted and cleaned up some stuff

* Don't animate if dead

* AppearanceSystem is for chumps

* Cleanup

* More cleanup

* More cleanup

* Half working commit

* Documentation

* Works

* ComponentHandleState my beloved

* AppearanceComp compatibility

* Address review

* Borgar

* AND NOW THE END IS NEAR

* AppearanceSystem compliance (Real)

* Don't need to log missing there

* I actually hate mob prototypes so much you don't even know

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>

* Automatic changelog update

* Fix AddHandCommand not working on aghosts (#38866)

* Component for clothes to suppress emotes and scream action in general, and the muzzle to suppress vocal emotes in particular (#32588)

* Component for clothes to suppress scream noise

GaggedComponent + AddGaggedClothingComponent and relevant systems to make them work.

Currently only stifles the scream _action_, not all emotes

because if a mime can silently emote, so can gagged you!

* fix comments

* swap to inventory relay

and make it more general such that specific emotes or emotes of a given category can be blocked

* power gloves shouldn't block snapping

* easy fixes

* blockable emote event

* pr comments, switch to using emote event mostly

* pr comments

add beforeEmoteEvent

add emote blocker name to popup

maybe some other stuff, I forget

* get rid of emoteevent's source because I don't need it anymore

* smol clean

* formatting, style, and one minor thing where having a muzzle in your pocket would gag you

* Automatic changelog update

* Tweaks to ShowRulesCommand structure, localization, and autocomplete. (#38855)

commit

* Automatic changelog update

* Convert Locker/Closet fills to Entity Tables: Part 2 (#38254)

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

* Fixing a singular pixel on the frame of the AI (#38936)

* Singular pixel ops

* Third commit - upholding CC-BY-SA by indicating changes made

* Bottle Drink Inhands (#38937)

* there is now bottles of booze in your hands

* Messed up the vermouth inhand fills slightly.

* oh whoops that's redundant

* Automatic changelog update

* refactor: rework the new status effect system to use containers (#38915)

* refactor: rework the new status effect system to use containers (#38915) (#38943)

Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>

* fix: don't divide by zero in FragmentIntoProjectiles (#38946)

* Accents Event to Shared (#38948)

* 27 file diff

* 27 file diff 2

* Apply suggestions from code review

---------

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

* Update Credits (#38955)

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

* SharedGunSystem spread bugfix (#38960)

two bytes

* Fixed error thrown when examining indestructible plastitanium windows (#38950)

* Fixed error thrown when examining indestructible plastitanium windows

* Fixed indentation to match yaml standards

* Fix extra dollar sign in admin log for machine toggle (#38961)

* Don't compile EF Core designer files on release builds (#38927)

* Hats (and glasses) for pets - Part 1 - Ian and McGriff (#38634)

* new branch new me

* cursed displacement

* 2 pixels of death

* forgot a line

* fixed some edge cases on corgis with the displacement

* fix conflict

* fix indentations

* a bit more displacement fixes

* Automatic changelog update

* [BUGFIX] Stops scurrets from suffocating in crates (#38951)

Prevent scurrets from dying!

* Automatic changelog update

* Cleanup warnings: CS0649 (#38797)

* make ocarina small (#38971)

Update instruments_wind.yml

* Automatic changelog update

* Add contraband parent to war declarator (#38972)

* Automatic changelog update

* Remove prototype caching from `ZombieComponent` (#38979)

Remove prototype caching from ZombieComponent

* Vox customization additions (+eyeshadows) (#38906)

vox customization stuff

* Automatic changelog update

* Refactor ExaminableDamage (#38978)

* Remove prototype caching in ExaminableDamageComponent

* Replace ExaminableDamagePrototype with LocalizedDatasetPrototype

* Allow null

* Voltage enum to shared (#38964)

* fix: ghosts shouldn't see whisper obfuscation (#38202)

* Fix devices in terminal mispredicting power state (#38647)

* The simple solution

* The better solution

* Revert "The better solution"

This reverts commit 611e56e031636734abab1ad3e77bf88b69a4de13.

* make biogenerator not accept low-nutrient plants (#38427)

* anti-noob

* dsaswa

* Minor grammar change

---------

Co-authored-by: beck-thompson <beck314159@hotmail.com>

* Make RunVerbAs take and return EntityUids (#38155)

fix: make RunVerbAs take and return EntityUids

* Automatic changelog update

* Buff parrot learn rates and radio chatter (#38984)

* buff parrots

* ok but not that smart

* oop

* Remove prototype caching from `TransformableContainerComponent` (#38988)

Remove prototype caching from TransformableContainer

* New holy books (#38986)

* Adds a few new holy books, implements one existing, and makes them pickable from loadout

* Almost forgot the yaml

* Forgot to change fork-specific paths

* Modified copyright to be less specific as requested by maintainer

* Newlines

* Replaced scroll torah with a book-bound tanakh, as recommended by dvir001

* Changed all mentions to use tanakh instead of torah

* Made the Mosin bayonet usable. (#38295)

* Made mosin an alt-melee weapon, added reagent fill for the weapon.

* fixes

---------

Co-authored-by: RedBookcase <Usualmoves@gmail.com>
Co-authored-by: beck-thompson <beck314159@hotmail.com>

* Automatic changelog update

* Adjust throwables hitboxes to match sprites (#38985)

Initial commit

* Automatic changelog update

* Moproaches (#38700)

* MOPROACHES

* rerun test - random test fail of doom

* added to random pest event, with same prob as cancer mouse

* rerun test - random test fail of doom 2 electric boogaloo

* changed moproach obtain methods

* no crafting of moproaches allowed :(

* Reverted content of file.

* requested changes

* requested changes

* requested changes

* Increase moproach health by 5 - requested by Emo on discord

* spaaaaaaaaaaaaaaaaaaaaaaaaace

* microwave recipe added

* no surgery this week

* spaaaaaaaaaaaaaaaaaaace 2 more space for your space

* space

* new mopshoes sprite

* in-hands

* inhands v2

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>

* Automatic changelog update

* Remove the Qur'an (#39000)

* Mostly fix reaction sound effect stacking :( (#38999)

* fix: band-aid the reaction sound effect stacking

It's so funny I'm so sad I'm writing this commit :(

* fix: remove unused hashset

Drive by fix. 'Tis never read from.

* fix: switch to just making it server only

* fix: uncomment the if lol

Commented it out for recording video oopsie

* Convert some voice samples to mono (#39002)

* Convert Xenoborg voice sounds to mono

* Convert female human cough sounds to mono

* PressureEvent removed (dead code) (#39004)

yep

* Predict DevourSystem. (#38970)

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

* Whitelist extension for tool belt (#35212)

* sd

* xd

* im so dumb

* revert to just remote signaller

* forgot

* add code comment

---------

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

* Automatic changelog update

* Fix firelocks failing to drop fast enough (#38918)

* Automatic changelog update

* Fix Mjollnir throw while on delay (#39018)

* init

* fuck dirty

* yippee

* Automatic changelog update

* Give admin bags explosion resistance (#38384)

Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>

* Automatic changelog update

* Inconsistent Produce Inhands Fix (#38860)

* V1 Sprites

* Adding heldPrefix

* Updated Sprites

* 4 New Inhands & Sprite Tweak

* WaterMelon/HolyMelon Front Change

* Meta Guh

* Sugarcane Updated Inhands

* Re-id 'Medical Doctor' guide entry to 'MedicalDoctor' (#39029)

chore: re-id 'Medical Doctor' guide entry

* Make diagonal windows prevent electrocution (#39032)

Add the window tag

* Automatic changelog update

* Carps Can No Longer Suicide (#39033)

Update carp.yml

* Wearable banana peels (#38868)

* Add wearable helmet sprites for banana and mimana peels

Made it so banana peels and their variants can be worn as head gear. The sprites were done by myself, so feel free to change or update them! Includes hamster compatibility.

* fixed a minor state bug

* Review fixes

- Removed blank space
- Added "FlipBrooke" to all modified meta.jsons, with the added note of which sprites I got the colors from/referenced.

* I forgot a comma

adds a comma and some periods

* NPC spiders sometimes spin webs 🕷️🕸️ (#38319)

* NPC spiders now spin webs

* oops

* move logic to always update next spawn, to prevent rare web spam

* WebSpawnCooldown is timespan

* remove vv

* add web spawn method, no sus action event method call

* dont spin web immediately at spawn

* move NextWebSpawn value init to update

* oop

* remove unused game timing

* web spawn cooldown to 45

* Automatic changelog update

* Fix Respirator Asserts (#38911)

* Fix errors

* Cleanup CanMetabolizeInhaledAir

* Wait no don't do that

* Revert changes for real

* Fix

* Code cleanup and some safety rails

* Better tests and also comments

* Better comments

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>

* Add supercritical sounds for ALL anomalies (#36425)

* add supercritical sounds for flesh anomaly and shadow anomaly

* fix attribution yml

* use sound collections, add TWO -- count em TWO tech anomaly supercrit sounds.

* add sound for present anomaly

* wrong soundcollection for flesh

* actually add the present anomaly sound...

* add fire anom sound

* add gravity anomaly supercritical sound

* add electric anomaly supercritical sound

* add bluespace anomaly supercritical and explosion sound

* add ice anomaly supercritical sound

* add fluid anomaly supercritical sound

* fix busted link

* typo

* add floral supercritical sound, fix typos

* Add supercritical sounds to injected anomalies

* add rock anomaly supercritical sound

* supercritical sound follows the entity, important for injected anomalies

* make the rock crit rumble a little less subtle at the start

* use CC0 freesound alternatives where available

* New pyro anomaly sound by GonTar

* New sounds by GonTar. Change AnomalyComponent so that you can change the length of supercritical properly in the prototype.

* oopsie, forgot the traps

* use TimeSpan for duration

* Automatic changelog update

* Metashield Rules Update (Removal) (#38953)

* Revert "Don't compile EF Core designer files on release builds" (#39057)

* Revert "Kobold/monkey AI holograms (#38888)"

This reverts commit 97fcebb92b.

Please check the maintainer meeting and the recording at time 41:58 for further information

https://docs.spacestation14.com/en/maintainer-meetings/maintainer-meeting-2025-07-19.html

* Replace SharedStatusEffectsSystem with StatusEffectsSystem

Updated references from SharedStatusEffectsSystem to StatusEffectsSystem in spell and magic weakness systems to use the correct status effects system implementation.

* Update examine_messages.yml

* fix

* Update round_end.yml

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
Co-authored-by: Mora <46364955+TrixxedHeart@users.noreply.github.com>
Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: Andrew Malcolm O'Neill <105134723+maland1@users.noreply.github.com>
Co-authored-by: Minerva <218184747+mnva0@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Velken <8467292+Velken@users.noreply.github.com>
Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Arthur Fiorese de Andrade <170277843+ADM2PLAY@users.noreply.github.com>
Co-authored-by: Arthur Fiorese de Andrade <aandrade@cmcxs.gov.br>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: āda <ss.adasts@gmail.com>
Co-authored-by: iaada <iaada@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: AsnDen <75905158+AsnDen@users.noreply.github.com>
Co-authored-by: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com>
Co-authored-by: Smugman <85798843+Smugman@users.noreply.github.com>
Co-authored-by: Голубь <124601871+Golubgik@users.noreply.github.com>
Co-authored-by: unknown <wainzor1337@gmail.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: Sparlight <twiksparlight@gmail.com>
Co-authored-by: Crude Oil <124208219+CroilBird@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: Nox <nebulousnox38@gmail.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Hitlinemoss <209321380+Hitlinemoss@users.noreply.github.com>
Co-authored-by: qwerltaz <69696513+qwerltaz@users.noreply.github.com>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Ertanic <black.ikra.14@gmail.com>
Co-authored-by: Эдуард <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: Centronias <me@centronias.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: Mono <182929384+Monotheonist@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: KOTOB <59124164+kotobdev@users.noreply.github.com>
Co-authored-by: devinschubert14 <59582670+devinschubert14@users.noreply.github.com>
Co-authored-by: SpaceManiac <tad@platymuus.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: ToastEnjoyer <masondoesgamingyes@gmail.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: Trosling <111116669+RHSvenson@users.noreply.github.com>
Co-authored-by: RedBookcase <crazykid1590@gmail.com>
Co-authored-by: RedBookcase <Usualmoves@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: Łukasz Lindert <lukasz.lindert@protonmail.com>
Co-authored-by: nabegator <stunprod228@outlook.com>
Co-authored-by: shampunj <140359015+shampunj@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: FlipBrooke <87075384+FlipBrooke@users.noreply.github.com>
Co-authored-by: Admiral-Obvious-001 <89495925+Admiral-Obvious-001@users.noreply.github.com>
This commit is contained in:
Red
2025-07-30 16:02:21 +03:00
committed by GitHub
1247 changed files with 21555 additions and 15543 deletions

View File

@@ -302,6 +302,13 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
return;
}
// PostgreSQL does not support storing null chars in text values.
if (message.Contains('\0'))
{
_sawmill.Error($"Null character detected in admin log message '{message}'! LogType: {type}, LogImpact: {impact}");
message = message.Replace("\0", "");
}
var log = new AdminLog
{
Id = NextLogId,

View File

@@ -383,8 +383,13 @@ public sealed class AdminSystem : EntitySystem
{
_chat.DeleteMessagesBy(uid);
var eraseEvent = new EraseEvent(uid);
if (!_minds.TryGetMind(uid, out var mindId, out var mind) || mind.OwnedEntity == null || TerminatingOrDeleted(mind.OwnedEntity.Value))
{
RaiseLocalEvent(ref eraseEvent);
return;
}
var entity = mind.OwnedEntity.Value;
@@ -444,6 +449,8 @@ public sealed class AdminSystem : EntitySystem
if (_playerManager.TryGetSessionById(uid, out var session))
_gameTicker.SpawnObserver(session);
RaiseLocalEvent(ref eraseEvent);
}
private void OnSessionPlayTimeUpdated(ICommonSession session)
@@ -451,3 +458,10 @@ public sealed class AdminSystem : EntitySystem
UpdatePlayerList(session);
}
}
/// <summary>
/// Event fired after a player is erased by an admin
/// </summary>
/// <param name="PlayerNetUserId">NetUserId of the player that was the target of the Erase</param>
[ByRefEvent]
public record struct EraseEvent(NetUserId PlayerNetUserId);

View File

@@ -24,25 +24,14 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly OutfitSystem _outfit = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
private static readonly EntProtoId DefaultTraitorRule = "Traitor";
private static readonly EntProtoId DefaultInitialInfectedRule = "Zombie";
private static readonly EntProtoId DefaultNukeOpRule = "LoneOpsSpawn";
private static readonly EntProtoId DefaultRevsRule = "Revolutionary";
private static readonly EntProtoId DefaultThiefRule = "Thief";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultInitialInfectedRule = "Zombie";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultNukeOpRule = "LoneOpsSpawn";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultRevsRule = "Revolutionary";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultThiefRule = "Thief";
[ValidatePrototypeId<StartingGearPrototype>]
private const string PirateGearId = "PirateGear";
private readonly EntProtoId _paradoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
@@ -175,7 +164,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "ParadoxClone"),
Act = () =>
{
var ruleEnt = _gameTicker.AddGameRule(_paradoxCloneRuleId);
var ruleEnt = _gameTicker.AddGameRule(ParadoxCloneRuleId);
if (!TryComp<ParadoxCloneRuleComponent>(ruleEnt, out var paradoxCloneRuleComp))
return;

View File

@@ -1,47 +0,0 @@
using Content.Server.Advertise.EntitySystems;
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Advertise.Components;
/// <summary>
/// Makes this entity periodically advertise by speaking a randomly selected
/// message from a specified dataset into local chat.
/// </summary>
[RegisterComponent, Access(typeof(AdvertiseSystem))]
public sealed partial class AdvertiseComponent : Component
{
/// <summary>
/// Minimum time in seconds to wait before saying a new ad, in seconds. Has to be larger than or equal to 1.
/// </summary>
[DataField]
public int MinimumWait { get; private set; } = 8 * 60;
/// <summary>
/// Maximum time in seconds to wait before saying a new ad, in seconds. Has to be larger than or equal
/// to <see cref="MinimumWait"/>
/// </summary>
[DataField]
public int MaximumWait { get; private set; } = 10 * 60;
/// <summary>
/// If true, the delay before the first advertisement (at MapInit) will ignore <see cref="MinimumWait"/>
/// and instead be rolled between 0 and <see cref="MaximumWait"/>. This only applies to the initial delay;
/// <see cref="MinimumWait"/> will be respected after that.
/// </summary>
[DataField]
public bool Prewarm = true;
/// <summary>
/// The identifier for the advertisements dataset prototype.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
/// <summary>
/// The next time an advertisement will be said.
/// </summary>
[DataField]
public TimeSpan NextAdvertisementTime { get; set; } = TimeSpan.Zero;
}

View File

@@ -1,106 +0,0 @@
using Content.Server.Advertise.Components;
using Content.Server.Chat.Systems;
using Content.Server.Power.Components;
using Content.Shared.VendingMachines;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Advertise.EntitySystems;
public sealed class AdvertiseSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ChatSystem _chat = default!;
/// <summary>
/// The maximum amount of time between checking if advertisements should be displayed
/// </summary>
private readonly TimeSpan _maximumNextCheckDuration = TimeSpan.FromSeconds(15);
/// <summary>
/// The next time the game will check if advertisements should be displayed
/// </summary>
private TimeSpan _nextCheckTime = TimeSpan.MinValue;
public override void Initialize()
{
SubscribeLocalEvent<AdvertiseComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ApcPowerReceiverComponent, AttemptAdvertiseEvent>(OnPowerReceiverAttemptAdvertiseEvent);
SubscribeLocalEvent<VendingMachineComponent, AttemptAdvertiseEvent>(OnVendingAttemptAdvertiseEvent);
_nextCheckTime = TimeSpan.MinValue;
}
private void OnMapInit(EntityUid uid, AdvertiseComponent advert, MapInitEvent args)
{
var prewarm = advert.Prewarm;
RandomizeNextAdvertTime(advert, prewarm);
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
}
private void RandomizeNextAdvertTime(AdvertiseComponent advert, bool prewarm = false)
{
var minDuration = prewarm ? 0 : Math.Max(1, advert.MinimumWait);
var maxDuration = Math.Max(minDuration, advert.MaximumWait);
var waitDuration = TimeSpan.FromSeconds(_random.Next(minDuration, maxDuration));
advert.NextAdvertisementTime = _gameTiming.CurTime + waitDuration;
}
public void SayAdvertisement(EntityUid uid, AdvertiseComponent? advert = null)
{
if (!Resolve(uid, ref advert))
return;
var attemptEvent = new AttemptAdvertiseEvent(uid);
RaiseLocalEvent(uid, ref attemptEvent);
if (attemptEvent.Cancelled)
return;
if (_prototypeManager.TryIndex(advert.Pack, out var advertisements))
_chat.TrySendInGameICMessage(uid, Loc.GetString(_random.Pick(advertisements.Values)), InGameICChatType.Speak, hideChat: true);
}
public override void Update(float frameTime)
{
var currentGameTime = _gameTiming.CurTime;
if (_nextCheckTime > currentGameTime)
return;
// _nextCheckTime starts at TimeSpan.MinValue, so this has to SET the value, not just increment it.
_nextCheckTime = currentGameTime + _maximumNextCheckDuration;
var query = EntityQueryEnumerator<AdvertiseComponent>();
while (query.MoveNext(out var uid, out var advert))
{
if (currentGameTime > advert.NextAdvertisementTime)
{
SayAdvertisement(uid, advert);
// The timer is always refreshed when it expires, to prevent mass advertising (ex: all the vending machines have no power, and get it back at the same time).
RandomizeNextAdvertTime(advert);
}
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
}
}
private static void OnPowerReceiverAttemptAdvertiseEvent(EntityUid uid, ApcPowerReceiverComponent powerReceiver, ref AttemptAdvertiseEvent args)
{
args.Cancelled |= !powerReceiver.Powered;
}
private static void OnVendingAttemptAdvertiseEvent(EntityUid uid, VendingMachineComponent machine, ref AttemptAdvertiseEvent args)
{
args.Cancelled |= machine.Broken;
}
}
[ByRefEvent]
public record struct AttemptAdvertiseEvent(EntityUid? Advertiser)
{
public bool Cancelled = false;
}

View File

@@ -0,0 +1,32 @@
using Content.Shared.Whitelist;
namespace Content.Server.Animals.Components;
/// <summary>
/// Makes an entity able to listen to messages from IC chat and attempt to commit them to memory
/// </summary>
[RegisterComponent]
public sealed partial class ParrotListenerComponent : Component
{
/// <summary>
/// Whitelist for purposes of limiting which entities a parrot will listen to
///
/// This is here because parrots can learn via local chat or radio from other parrots. this can quickly devolve from
/// SQUAWK! Polly wants a cracker! BRAAWK
/// to
/// BRAAWK! SQUAWK! RAWWK! Polly wants a cracker! AAWK! AWWK! Cracker! SQUAWK! BRAWWK! SQUAWK!
/// This is limited somewhat by the message length limit on ParrotMemoryComponent, but can be prevented entirely here
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for purposes of ignoring entities
/// As above, this is here to force parrots to ignore certain entities.
/// For example, polly will be consistently mapped around EngiDrobes, which will consistently say stuff like
/// "Guaranteed to protect your feet from industrial accidents!"
/// If polly ends up constantly advertising engineering drip, this can be used to prevent it.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -0,0 +1,57 @@
using Robust.Shared.Network;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components;
/// <summary>
/// Makes an entity able to memorize chat/radio messages
/// </summary>
[RegisterComponent]
[AutoGenerateComponentPause]
public sealed partial class ParrotMemoryComponent : Component
{
/// <summary>
/// List of SpeechMemory records this entity has learned
/// </summary>
[DataField]
public List<SpeechMemory> SpeechMemories = [];
/// <summary>
/// The % chance an entity with this component learns a phrase when learning is off cooldown
/// </summary>
[DataField]
public float LearnChance = 0.6f;
/// <summary>
/// Time after which another attempt can be made at learning a phrase
/// </summary>
[DataField]
public TimeSpan LearnCooldown = TimeSpan.FromMinutes(1);
/// <summary>
/// Next time at which the parrot can attempt to learn something
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextLearnInterval = TimeSpan.Zero;
/// <summary>
/// The number of speech entries that are remembered
/// </summary>
[DataField]
public int MaxSpeechMemory = 50;
/// <summary>
/// Minimum length of a speech entry
/// </summary>
[DataField]
public int MinEntryLength = 4;
/// <summary>
/// Maximum length of a speech entry
/// </summary>
[DataField]
public int MaxEntryLength = 50;
}
public record struct SpeechMemory(NetUserId? NetUserId, string Message);

View File

@@ -0,0 +1,247 @@
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.Animals.Components;
using Content.Server.Mind;
using Content.Server.Popups;
using Content.Server.Radio;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Server.Vocalization.Systems;
using Content.Shared.Database;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Animals.Systems;
/// <summary>
/// The ParrotMemorySystem handles remembering messages received through local chat (activelistener) or a radio
/// (radiovocalizer) and stores them in a list. When an entity with a VocalizerComponent attempts to vocalize, this will
/// try to set the message from memory.
/// </summary>
public sealed partial class ParrotMemorySystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EraseEvent>(OnErase);
SubscribeLocalEvent<ParrotMemoryComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<ParrotListenerComponent, MapInitEvent>(ListenerOnMapInit);
SubscribeLocalEvent<ParrotListenerComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<ParrotListenerComponent, HeadsetRadioReceiveRelayEvent>(OnHeadsetReceive);
SubscribeLocalEvent<ParrotMemoryComponent, TryVocalizeEvent>(OnTryVocalize);
}
private void OnErase(ref EraseEvent args)
{
DeletePlayerMessages(args.PlayerNetUserId);
}
private void OnGetVerbs(Entity<ParrotMemoryComponent> entity, ref GetVerbsEvent<Verb> args)
{
var user = args.User;
// limit this to admins
if (!_admin.IsAdmin(user))
return;
// simple verb that just clears the memory list
var clearMemoryVerb = new Verb()
{
Text = Loc.GetString("parrot-verb-clear-memory"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/clear-parrot.png")),
Act = () =>
{
entity.Comp.SpeechMemories.Clear();
_popup.PopupEntity(Loc.GetString("parrot-popup-memory-cleared"), entity, user, PopupType.Medium);
},
};
args.Verbs.Add(clearMemoryVerb);
}
private void ListenerOnMapInit(Entity<ParrotListenerComponent> entity, ref MapInitEvent args)
{
// If an entity has a ParrotListenerComponent it really ought to have an ActiveListenerComponent
if (!HasComp<ActiveListenerComponent>(entity))
Log.Warning($"Entity {ToPrettyString(entity)} has a ParrotListenerComponent but was not given an ActiveListenerComponent");
}
private void OnListen(Entity<ParrotListenerComponent> entity, ref ListenEvent args)
{
TryLearn(entity.Owner, args.Message, args.Source);
}
private void OnHeadsetReceive(Entity<ParrotListenerComponent> entity, ref HeadsetRadioReceiveRelayEvent args)
{
var message = args.RelayedEvent.Message;
var source = args.RelayedEvent.MessageSource;
TryLearn(entity.Owner, message, source);
}
/// <summary>
/// Called when an entity with a ParrotMemoryComponent tries to vocalize.
/// This function picks a message from memory and sets the event to handled
/// </summary>
private void OnTryVocalize(Entity<ParrotMemoryComponent> entity, ref TryVocalizeEvent args)
{
// return if this was already handled
if (args.Handled)
return;
// if there are no memories, return
if (entity.Comp.SpeechMemories.Count == 0)
return;
// get a random memory from the memory list
var memory = _random.Pick(entity.Comp.SpeechMemories);
args.Message = memory.Message;
args.Handled = true;
}
/// <summary>
/// Try to learn a new message, returning early if this entity cannot learn a new message,
/// the message doesn't pass certain checks, or the chance for learning a new message fails
/// </summary>
/// <param name="entity">Entity learning a new word</param>
/// <param name="incomingMessage">Message to learn</param>
/// <param name="source">Source EntityUid of the message</param>
public void TryLearn(Entity<ParrotMemoryComponent?, ParrotListenerComponent?> entity, string incomingMessage, EntityUid source)
{
if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2))
return;
if (!_whitelist.CheckBoth(source, entity.Comp2.Blacklist, entity.Comp2.Whitelist))
return;
if (source.Equals(entity) || _mobState.IsIncapacitated(entity))
return;
// can't learn too soon after having already learnt something else
if (_gameTiming.CurTime < entity.Comp1.NextLearnInterval)
return;
// remove whitespace around message, if any
var message = incomingMessage.Trim();
// ignore messages containing tildes. This is a crude way to ignore whispers that are too far away
// TODO: this isn't great. This should be replaced with a const or we should have a better way to check faraway messages
if (message.Contains('~'))
return;
// ignore empty messages. These probably aren't sent anyway but just in case
if (string.IsNullOrWhiteSpace(message))
return;
// ignore messages that are too short or too long
if (message.Length < entity.Comp1.MinEntryLength || message.Length > entity.Comp1.MaxEntryLength)
return;
// only from this point this message has a chance of being learned
// set new time for learn interval, regardless of whether the learning succeeds
entity.Comp1.NextLearnInterval = _gameTiming.CurTime + entity.Comp1.LearnCooldown;
// decide if this message passes the learning chance
if (!_random.Prob(entity.Comp1.LearnChance))
return;
// actually commit this message to memory
Learn((entity, entity.Comp1), message, source);
}
/// <summary>
/// Actually learn a message and commit it to memory
/// </summary>
/// <param name="entity">Entity learning a new word</param>
/// <param name="message">Message to learn</param>
/// <param name="source">Source EntityUid of the message</param>
private void Learn(Entity<ParrotMemoryComponent> entity, string message, EntityUid source)
{
// log a low-priority chat type log to the admin logger
// specifies what message was learnt by what entity, and who taught the message to that entity
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Parroting entity {ToPrettyString(entity):entity} learned the phrase \"{message}\" from {ToPrettyString(source):speaker}");
NetUserId? sourceNetUserId = null;
if (_mind.TryGetMind(source, out _, out var mind))
{
sourceNetUserId = mind.UserId;
}
var newMemory = new SpeechMemory(sourceNetUserId, message);
// add a new message if there is space in the memory
if (entity.Comp.SpeechMemories.Count < entity.Comp.MaxSpeechMemory)
{
entity.Comp.SpeechMemories.Add(newMemory);
return;
}
// if there's no space in memory, replace something at random
var replaceIdx = _random.Next(entity.Comp.SpeechMemories.Count);
entity.Comp.SpeechMemories[replaceIdx] = newMemory;
}
/// <summary>
/// Delete all messages from a specified player on all ParrotMemoryComponents
/// </summary>
/// <param name="playerNetUserId">The player of whom to delete messages</param>
private void DeletePlayerMessages(NetUserId playerNetUserId)
{
// query to enumerate all entities with a memorycomponent
var query = EntityQueryEnumerator<ParrotMemoryComponent>();
while (query.MoveNext(out _, out var memory))
{
DeletePlayerMessages(memory, playerNetUserId);
}
}
/// <summary>
/// Delete all messages from a specified player on a given ParrotMemoryComponent
/// </summary>
/// <param name="memoryComponent">The ParrotMemoryComponent on which to delete messages</param>
/// <param name="playerNetUserId">The player of whom to delete messages</param>
private void DeletePlayerMessages(ParrotMemoryComponent memoryComponent, NetUserId playerNetUserId)
{
// this is a sort of expensive operation that is hopefully rare and performed on just a few parrots
// with limited memory
for (var i = 0; i < memoryComponent.SpeechMemories.Count; i++)
{
var memory = memoryComponent.SpeechMemories[i];
// netuserid may be null if the message was learnt from a non-player entity
if (memory.NetUserId is null)
continue;
// skip if this memory was not learnt from the target user
if (!memory.NetUserId.Equals(playerNetUserId))
continue;
// order isn't important in this list so we can use the faster means of removing
memoryComponent.SpeechMemories.RemoveSwap(i);
}
}
}

View File

@@ -44,8 +44,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
public const float MinParticleVariation = 0.8f;
public const float MaxParticleVariation = 1.2f;
[ValidatePrototypeId<WeightedRandomPrototype>]
const string WeightListProto = "AnomalyBehaviorList";
private static readonly ProtoId<WeightedRandomPrototype> WeightListProto = "AnomalyBehaviorList";
/// <inheritdoc/>
public override void Initialize()
@@ -189,7 +188,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
#region Behavior
private string GetRandomBehavior()
{
var weightList = _prototype.Index<WeightedRandomPrototype>(WeightListProto);
var weightList = _prototype.Index(WeightListProto);
return weightList.Pick(_random);
}

View File

@@ -1,7 +1,6 @@
using Content.Shared.Arcade;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Arcade.SpaceVillain;
@@ -90,8 +89,8 @@ public sealed partial class SpaceVillainArcadeComponent : SharedSpaceVillainArca
/// The prototypes that can be dispensed as a reward for winning the game.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("possibleRewards", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> PossibleRewards = new();
[DataField]
public List<EntProtoId> PossibleRewards = new();
/// <summary>
/// The minimum number of prizes the arcade machine can have.

View File

@@ -596,8 +596,17 @@ namespace Content.Server.Atmos.EntitySystems
if (!reconsiderAdjacent)
return;
// Before updating the adjacent tile flags that determine whether air is allowed to flow
// or not, we explicitly update airtight data on these tiles right now.
// This ensures that UpdateAdjacentTiles has updated data before updating flags.
// This allows monstermos' floodfill check that determines if firelocks have dropped
// to work correctly.
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
InvalidateVisuals(ent, tile);
InvalidateVisuals(ent, other);
}

View File

@@ -1,44 +0,0 @@
namespace Content.Server.Atmos
{
public abstract class PressureEvent : EntityEventArgs
{
/// <summary>
/// The environment pressure.
/// </summary>
public float Pressure { get; }
/// <summary>
/// The modifier for the apparent pressure.
/// This number will be added to the environment pressure for calculation purposes.
/// It can be negative to reduce the felt pressure, or positive to increase it.
/// </summary>
/// <remarks>
/// Do not set this directly. Add to it, or subtract from it to modify it.
/// </remarks>
public float Modifier { get; set; } = 0f;
/// <summary>
/// The multiplier for the apparent pressure.
/// The environment pressure will be multiplied by this for calculation purposes.
/// </summary>
/// <remarks>
/// Do not set, add to or subtract from this directly. Multiply this by your multiplier only.
/// </remarks>
public float Multiplier { get; set; } = 1f;
protected PressureEvent(float pressure)
{
Pressure = pressure;
}
}
public sealed class LowPressureEvent : PressureEvent
{
public LowPressureEvent(float pressure) : base(pressure) { }
}
public sealed class HighPressureEvent : PressureEvent
{
public HighPressureEvent(float pressure) : base(pressure) { }
}
}

View File

@@ -1,23 +1,15 @@
using Content.Server.Bed.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Emag.Systems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
namespace Content.Server.Bed
{
public sealed class BedSystem : SharedBedSystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
private EntityQuery<SleepingComponent> _sleepingQuery;
@@ -27,11 +19,6 @@ namespace Content.Server.Bed
base.Initialize();
_sleepingQuery = GetEntityQuery<SleepingComponent>();
SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
}
public override void Update(float frameTime)
@@ -63,61 +50,5 @@ namespace Content.Server.Bed
}
}
}
private void UpdateAppearance(EntityUid uid, bool isOn)
{
_appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
}
private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
{
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
}
private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
{
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
}
private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)
{
UpdateAppearance(uid, args.Powered);
UpdateMetabolisms(uid, component, args.Powered);
}
private void OnEmagged(EntityUid uid, StasisBedComponent component, ref GotEmaggedEvent args)
{
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;
UpdateMetabolisms(uid, component, true);
args.Handled = true;
}
private void UpdateMetabolisms(EntityUid uid, StasisBedComponent component, bool shouldApply)
{
if (!TryComp<StrapComponent>(uid, out var strap) || strap.BuckledEntities.Count == 0)
return;
foreach (var buckledEntity in strap.BuckledEntities)
{
var metabolicEvent = new ApplyMetabolicMultiplierEvent(buckledEntity, component.Multiplier, shouldApply);
RaiseLocalEvent(buckledEntity, ref metabolicEvent);
}
}
}
}

View File

@@ -1,13 +0,0 @@
namespace Content.Server.Bed.Components
{
[RegisterComponent]
public sealed partial class StasisBedComponent : Component
{
/// <summary>
/// What the metabolic update rate will be multiplied by (higher = slower metabolism)
/// </summary>
[ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
[DataField]
public float Multiplier = 10f;
}
}

View File

@@ -1,12 +1,13 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
using Content.Shared.Administration;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Hands.Components;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Body.Commands
{
@@ -15,10 +16,9 @@ namespace Content.Server.Body.Commands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string DefaultHandPrototype = "LeftHandHuman";
private static readonly EntProtoId DefaultHandPrototype = "LeftHandHuman";
private static int _handIdAccumulator;
public string Command => "addhand";
public string Description => "Adds a hand to your entity.";
@@ -115,9 +115,17 @@ namespace Content.Server.Body.Commands
if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null)
{
var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}";
var location = _entManager.GetComponentOrNull<BodyPartComponent>(hand)?.Symmetry switch
{
BodyPartSymmetry.None => HandLocation.Middle,
BodyPartSymmetry.Left => HandLocation.Left,
BodyPartSymmetry.Right => HandLocation.Right,
_ => HandLocation.Right
};
_entManager.DeleteEntity(hand);
shell.WriteLine(text);
// You have no body and you must scream.
_entManager.System<HandsSystem>().AddHand(entity, $"{hand}-cmd-{_handIdAccumulator++}", location);
return;
}

View File

@@ -26,6 +26,18 @@ namespace Content.Server.Body.Components
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
/// <summary>
/// Multiplier applied to <see cref="UpdateInterval"/> for adjusting based on metabolic rate multiplier.
/// </summary>
[DataField]
public float UpdateIntervalMultiplier = 1f;
/// <summary>
/// Adjusted update interval based off of the multiplier value.
/// </summary>
[ViewVariables]
public TimeSpan AdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier;
/// <summary>
/// From which solution will this metabolizer attempt to metabolize chemicals
/// </summary>
@@ -46,14 +58,14 @@ namespace Content.Server.Body.Components
/// </summary>
[DataField]
[Access(typeof(MetabolizerSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public HashSet<ProtoId<MetabolizerTypePrototype>>? MetabolizerTypes = null;
public HashSet<ProtoId<MetabolizerTypePrototype>>? MetabolizerTypes;
/// <summary>
/// Should this metabolizer remove chemicals that have no metabolisms defined?
/// As a stop-gap, basically.
/// </summary>
[DataField]
public bool RemoveEmpty = false;
public bool RemoveEmpty;
/// <summary>
/// How many reagents can this metabolizer process at once?
@@ -67,7 +79,7 @@ namespace Content.Server.Body.Components
/// A list of metabolism groups that this metabolizer will act on, in order of precedence.
/// </summary>
[DataField("groups")]
public List<MetabolismGroupEntry>? MetabolismGroups = default!;
public List<MetabolismGroupEntry>? MetabolismGroups;
}
/// <summary>
@@ -78,7 +90,7 @@ namespace Content.Server.Body.Components
public sealed partial class MetabolismGroupEntry
{
[DataField(required: true)]
public ProtoId<MetabolismGroupPrototype> Id = default!;
public ProtoId<MetabolismGroupPrototype> Id;
[DataField("rateModifier")]
public FixedPoint2 MetabolismRateModifier = 1.0;

View File

@@ -1,5 +1,4 @@
using Content.Server.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage;
@@ -8,19 +7,9 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Body.Components
{
[RegisterComponent, Access(typeof(RespiratorSystem))]
[RegisterComponent, Access(typeof(RespiratorSystem)), AutoGenerateComponentPause]
public sealed partial class RespiratorComponent : Component
{
/// <summary>
/// Gas container for this entity
/// </summary>
[DataField]
public GasMixture Air = new()
{
Volume = 6, // 6 liters, the average lung capacity for a human according to Google
Temperature = Atmospherics.NormalBodyTemperature
};
/// <summary>
/// Volume of our breath in liters
/// </summary>
@@ -36,7 +25,7 @@ namespace Content.Server.Body.Components
/// <summary>
/// The next time that this body will inhale or exhale.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextUpdate;
/// <summary>
@@ -46,6 +35,18 @@ namespace Content.Server.Body.Components
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
/// <summary>
/// Multiplier applied to <see cref="UpdateInterval"/> for adjusting based on metabolic rate multiplier.
/// </summary>
[DataField]
public float UpdateIntervalMultiplier = 1f;
/// <summary>
/// Adjusted update interval based off of the multiplier value.
/// </summary>
[ViewVariables]
public TimeSpan AdjustedUpdateInterval => UpdateInterval * UpdateIntervalMultiplier;
/// <summary>
/// Saturation level. Reduced by UpdateInterval each tick.
/// Can be thought of as 'how many seconds you have until you start suffocating' in this configuration.

View File

@@ -48,7 +48,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
return;
// Could the entity metabolise the air in the linked gas tank?
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
if (!_respirator.CanMetabolizeInhaledAir(uid, tank.Value.Comp.Air))
return;
ToggleInternals(uid, uid, force: false, component, ToggleMode.On);

View File

@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -18,7 +19,8 @@ using Robust.Shared.Timing;
namespace Content.Server.Body.Systems
{
public sealed class MetabolizerSystem : EntitySystem
/// <inheritdoc/>
public sealed class MetabolizerSystem : SharedMetabolizerSystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -45,7 +47,7 @@ namespace Content.Server.Body.Systems
private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
}
private void OnUnpaused(Entity<MetabolizerComponent> ent, ref EntityUnpausedEvent args)
@@ -65,20 +67,9 @@ namespace Content.Server.Body.Systems
}
}
private void OnApplyMetabolicMultiplier(
Entity<MetabolizerComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(Entity<MetabolizerComponent> ent, ref ApplyMetabolicMultiplierEvent args)
{
// TODO REFACTOR THIS
// This will slowly drift over time due to floating point errors.
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;
return;
}
ent.Comp.UpdateInterval /= args.Multiplier;
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
}
public override void Update(float frameTime)
@@ -99,7 +90,7 @@ namespace Content.Server.Body.Systems
if (_gameTiming.CurTime < metab.NextUpdate)
continue;
metab.NextUpdate += metab.UpdateInterval;
metab.NextUpdate += metab.AdjustedUpdateInterval;
TryMetabolize((uid, metab));
}
}

View File

@@ -48,33 +48,32 @@ public sealed class RespiratorSystem : EntitySystem
// We want to process lung reagents before we inhale new reagents.
UpdatesAfter.Add(typeof(MetabolizerSystem));
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
// BodyComp stuff
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
SubscribeLocalEvent<BodyComponent, CanMetabolizeGasEvent>(CanBodyMetabolizeGas);
SubscribeLocalEvent<BodyComponent, SuffocationEvent>(OnSuffocation);
SubscribeLocalEvent<BodyComponent, StopSuffocatingEvent>(OnStopSuffocating);
}
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<RespiratorComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<RespiratorComponent, BodyComponent>();
while (query.MoveNext(out var uid, out var respirator, out var body))
var query = EntityQueryEnumerator<RespiratorComponent>();
while (query.MoveNext(out var uid, out var respirator))
{
if (_gameTiming.CurTime < respirator.NextUpdate)
continue;
respirator.NextUpdate += respirator.UpdateInterval;
respirator.NextUpdate += respirator.AdjustedUpdateInterval;
if (_mobState.IsDead(uid))
continue;
@@ -86,11 +85,11 @@ public sealed class RespiratorSystem : EntitySystem
switch (respirator.Status)
{
case RespiratorStatus.Inhaling:
Inhale(uid);
Inhale((uid, respirator));
respirator.Status = RespiratorStatus.Exhaling;
break;
case RespiratorStatus.Exhaling:
Exhale(uid);
Exhale((uid, respirator));
respirator.Status = RespiratorStatus.Inhaling;
break;
}
@@ -117,10 +116,10 @@ public sealed class RespiratorSystem : EntitySystem
}
}
public bool Inhale(Entity<RespiratorComponent?> entity)
public void Inhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return false;
return;
// Inhale gas
var ev = new InhaleLocationEvent
@@ -132,23 +131,22 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
if (ev.Gas is null)
{
return false;
}
return;
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
var inhaleEv = new InhaledGasEvent(gas);
RaiseLocalEvent(entity, ref inhaleEv);
return inhaleEv.Handled && inhaleEv.Succeeded;
}
public void Exhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
if (inhaleEv.Handled && inhaleEv.Succeeded)
return;
// If nothing could inhale the gas give it back.
_atmosSys.Merge(ev.Gas, gas);
}
public void Exhale(Entity<RespiratorComponent> entity)
{
// exhale gas
var ev = new ExhaleLocationEvent();
@@ -163,8 +161,16 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= GasMixture.SpaceGas;
}
var exhaleEv = new ExhaledGasEvent(ev.Gas);
RaiseLocalEvent(entity, ref exhaleEv);
Exhale(entity!, ev.Gas);
}
public void Exhale(Entity<RespiratorComponent?> entity, GasMixture gas)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return;
var ev = new ExhaledGasEvent(gas);
RaiseLocalEvent(entity, ref ev);
}
/// <summary>
@@ -182,52 +188,76 @@ public sealed class RespiratorSystem : EntitySystem
}
/// <summary>
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
/// gasses).
/// Checks if it's safe for a given entity to breathe the air from the environment it is currently situated in.
/// </summary>
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <returns>Returns true only if the air is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
if (!Inhale(ent))
return false;
// Get the gas at our location but don't actually remove it from the gas mixture.
var ev = new InhaleLocationEvent
{
Respirator = ent.Comp,
};
RaiseLocalEvent(ent, ref ev);
// If we don't have a body we can't be poisoned by gas, yet...
var success = TryMetabolizeGas((ent, ent.Comp));
ev.Gas ??= _atmosSys.GetContainingMixture(ent.Owner, excite: true);
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
Exhale(ent);
return success;
// If there's no air to breathe or we can't metabolize it then internals should be on.
return ev.Gas is not null && CanMetabolizeInhaledAir(ent, ev.Gas);
}
/// <summary>
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
/// (i.e., no toxic gasses).
/// Checks if a given entity can safely metabolize a given gas mixture.
/// </summary>
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <param name="gas">The gas mixture we are trying to metabolize.</param>
/// <returns>Returns true only if the gas mixture is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent, GasMixture gas)
{
if (!Resolve(ent, ref ent.Comp))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
var ev = new CanMetabolizeGasEvent(gas);
RaiseLocalEvent(ent, ref ev);
if (!ev.Handled || ev.Toxic)
return false;
gas = new GasMixture(gas);
var lungRatio = 1.0f / organs.Count;
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
var solution = _lungSystem.GasToReagent(gas);
return ev.Saturation > ent.Comp.UpdateInterval.TotalSeconds;
}
float saturation = 0;
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private void CanBodyMetabolizeGas(Entity<BodyComponent> ent, ref CanMetabolizeGasEvent args)
{
if (args.Handled)
return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return;
var solution = _lungSystem.GasToReagent(args.Gas);
var saturation = 0f;
foreach (var organ in organs)
{
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
if (!toxic)
continue;
args.Handled = true;
args.Toxic = true;
return;
}
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
args.Handled = true;
args.Saturation = saturation;
}
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
@@ -271,30 +301,6 @@ public sealed class RespiratorSystem : EntitySystem
_atmosSys.Merge(gas, outGas);
}
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private bool TryMetabolizeGas(Entity<RespiratorComponent, BodyComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return false;
float saturation = 0;
foreach (var organ in organs)
{
var solution = _lungSystem.GasToReagent(organ.Comp1.Air);
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
}
return saturation > ent.Comp1.UpdateInterval.TotalSeconds;
}
/// <summary>
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
/// </summary>
@@ -360,15 +366,11 @@ public sealed class RespiratorSystem : EntitySystem
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
return;
var ev = new SuffocationEvent();
RaiseLocalEvent(ent, ref ev);
}
private void StopSuffocation(Entity<RespiratorComponent> ent)
@@ -378,6 +380,22 @@ public sealed class RespiratorSystem : EntitySystem
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
var ev = new StopSuffocatingEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnSuffocation(Entity<BodyComponent> ent, ref SuffocationEvent args)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
private void OnStopSuffocating(Entity<BodyComponent> ent, ref StopSuffocatingEvent args)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
@@ -396,31 +414,16 @@ public sealed class RespiratorSystem : EntitySystem
Math.Clamp(respirator.Saturation, respirator.MinSaturation, respirator.MaxSaturation);
}
private void OnApplyMetabolicMultiplier(
Entity<RespiratorComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
private void OnApplyMetabolicMultiplier(Entity<RespiratorComponent> ent, ref ApplyMetabolicMultiplierEvent args)
{
// TODO REFACTOR THIS
// This will slowly drift over time due to floating point errors.
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;
ent.Comp.Saturation *= args.Multiplier;
ent.Comp.MaxSaturation *= args.Multiplier;
ent.Comp.MinSaturation *= args.Multiplier;
return;
}
// This way we don't have to worry about it breaking if the stasis bed component is destroyed
ent.Comp.UpdateInterval /= args.Multiplier;
ent.Comp.Saturation /= args.Multiplier;
ent.Comp.MaxSaturation /= args.Multiplier;
ent.Comp.MinSaturation /= args.Multiplier;
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
}
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
@@ -428,20 +431,65 @@ public sealed class RespiratorSystem : EntitySystem
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
RemoveGasFromBody(entity, args.Gas);
}
}
/// <summary>
/// Event raised when an entity first tries to inhale that returns a GasMixture from a given location.
/// </summary>
/// <param name="Gas">The gas that gets returned, null if there is none.</param>
/// <param name="Respirator">The Respirator component of the entity attempting to inhale</param>
[ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
/// <summary>
/// Event raised when an entity first tries to exhale a gas, determines where the gas they're exhaling will be sent.
/// </summary>
/// <param name="Gas">The gas mixture that the exhaled gas will be merged into.</param>
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);
/// <summary>
/// Event raised when an entity successfully inhales a gas, attempts to find a place to put the gas.
/// </summary>
/// <param name="Gas">The gas we're inhaling.</param>
/// <param name="Handled">Whether a system has responded appropriately.</param>
/// <param name="Succeeded">Whether we successfully managed to inhale the gas</param>
[ByRefEvent]
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
/// <summary>
/// Event raised when an entity is exhaling
/// </summary>
/// <param name="Gas">The gas mixture we're exhaling into.</param>
/// <param name="Handled">Whether we have successfully exhaled or not.</param>
[ByRefEvent]
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);
/// <summary>
/// Raised when an entity starts suffocating and when suffocation progresses.
/// </summary>
[ByRefEvent]
public record struct SuffocationEvent;
/// <summary>
/// Raised when an entity that was suffocating stops suffocating.
/// </summary>
[ByRefEvent]
public record struct StopSuffocatingEvent;
/// <summary>
/// An event raised to inhalation handlers that asks them nicely to simulate what it would be like to metabolize
/// a given volume of gas, without actually metabolizing it.
/// </summary>
/// <param name="Gas">The gas mixture we are testing.</param>
/// <param name="Toxic">Whether the gas returns as toxic to any respirator.</param>
/// <param name="Saturation">The amount of saturation we got from the gas.</param>
[ByRefEvent]
public record struct CanMetabolizeGasEvent(GasMixture Gas, bool Toxic = false, float Saturation = 0f, bool Handled = false);

View File

@@ -1,17 +1,14 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Server.EntityEffects;
using Content.Shared.Atmos;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Utility;
using Content.Server.EntityEffects;
namespace Content.Server.Botany;
[Prototype]
@@ -133,8 +130,8 @@ public partial class SeedData
/// <summary>
/// The entity prototype this seed spawns when it gets harvested.
/// </summary>
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> ProductPrototypes = new();
[DataField]
public List<EntProtoId> ProductPrototypes = new();
[DataField] public Dictionary<string, SeedChemQuantity> Chemicals = new();
@@ -243,8 +240,8 @@ public partial class SeedData
/// <summary>
/// The seed prototypes this seed may mutate into when prompted to.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
public List<string> MutationPrototypes = new();
[DataField]
public List<ProtoId<SeedPrototype>> MutationPrototypes = new();
/// <summary>
/// Log impact for when the seed is planted.
@@ -270,8 +267,8 @@ public partial class SeedData
Mysterious = Mysterious,
PacketPrototype = PacketPrototype,
ProductPrototypes = new List<string>(ProductPrototypes),
MutationPrototypes = new List<string>(MutationPrototypes),
ProductPrototypes = new List<EntProtoId>(ProductPrototypes),
MutationPrototypes = new List<ProtoId<SeedPrototype>>(MutationPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
ExudeGasses = new Dictionary<Gas, float>(ExudeGasses),
@@ -330,8 +327,8 @@ public partial class SeedData
Mysterious = other.Mysterious,
PacketPrototype = other.PacketPrototype,
ProductPrototypes = new List<string>(other.ProductPrototypes),
MutationPrototypes = new List<string>(other.MutationPrototypes),
ProductPrototypes = new List<EntProtoId>(other.ProductPrototypes),
MutationPrototypes = new List<ProtoId<SeedPrototype>>(other.MutationPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),

View File

@@ -9,13 +9,15 @@ namespace Content.Server.Botany;
public sealed class MutationSystem : EntitySystem
{
private static ProtoId<RandomPlantMutationListPrototype> RandomPlantMutations = "RandomPlantMutations";
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize()
{
_randomMutations = _prototypeManager.Index<RandomPlantMutationListPrototype>("RandomPlantMutations");
_randomMutations = _prototypeManager.Index(RandomPlantMutations);
}
/// <summary>

View File

@@ -29,8 +29,7 @@ public sealed partial class CargoSystem
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
private static readonly ProtoId<NameIdentifierGroupPrototype> BountyNameIdentifierGroup = "Bounty";
private EntityQuery<StackComponent> _stackQuery;
private EntityQuery<ContainerManagerComponent> _containerQuery;

View File

@@ -16,8 +16,7 @@ public sealed class CrewManifestCartridgeSystem : EntitySystem
[Dependency] private readonly CrewManifestSystem _crewManifest = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string CartridgePrototypeName = "CrewManifestCartridge";
private static readonly EntProtoId CartridgePrototypeName = "CrewManifestCartridge";
/// <summary>
/// Flag that shows that if crew manifest is allowed to be viewed from 'unsecure' entities,

View File

@@ -1,5 +1,7 @@
using System.Collections.Frozen;
using Content.Server.Popups;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Emoting;
using Content.Shared.Speech;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -10,6 +12,8 @@ namespace Content.Server.Chat.Systems;
// emotes using emote prototype
public partial class ChatSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
@@ -51,7 +55,8 @@ public partial class ChatSystem
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat(
EntityUid source,
string emoteId,
ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -62,8 +67,8 @@ public partial class ChatSystem
)
{
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
return;
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
return false;
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
}
/// <summary>
@@ -76,7 +81,8 @@ public partial class ChatSystem
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat(
EntityUid source,
EmotePrototype emote,
ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -84,43 +90,46 @@ public partial class ChatSystem
string? nameOverride = null,
bool ignoreActionBlocker = false,
bool forceEmote = false
)
)
{
if (!forceEmote && !AllowedToUseEmote(source, emote))
return;
return false;
var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
// check if proto has valid message for chat
if (emote.ChatMessages.Count != 0)
if (didEmote && emote.ChatMessages.Count != 0)
{
// not all emotes are loc'd, but for the ones that are we pass in entity
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
}
// do the rest of emote event logic here
TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
return didEmote;
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
{
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
return;
return false;
TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
{
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
return;
return false;
InvokeEmoteEvent(uid, proto);
return TryInvokeEmoteEvent(uid, proto);
}
/// <summary>
@@ -160,17 +169,17 @@ public partial class ChatSystem
/// </summary>
/// <param name="uid"></param>
/// <param name="textInput"></param>
private void TryEmoteChatInput(EntityUid uid, string textInput)
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
private bool TryEmoteChatInput(EntityUid uid, string textInput)
{
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
return;
return true;
if (!AllowedToUseEmote(uid, emote))
return;
return true;
InvokeEmoteEvent(uid, emote);
return;
return TryInvokeEmoteEvent(uid, emote);
static string TrimPunctuation(string textInput)
{
@@ -220,11 +229,50 @@ public partial class ChatSystem
return true;
}
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
/// <summary>
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
/// explaining why the emote didn't happen.
/// </summary>
/// <param name="uid">The entity which is emoting</param>
/// <param name="proto">The emote which is being performed</param>
/// <returns>True if the emote was performed, false otherwise.</returns>
private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
{
var beforeEv = new BeforeEmoteEvent(uid, proto);
RaiseLocalEvent(uid, ref beforeEv);
if (beforeEv.Cancelled)
{
if (beforeEv.Blocker != null)
{
_popupSystem.PopupEntity(
Loc.GetString(
"chat-system-emote-cancelled-blocked",
("emote", Loc.GetString(proto.Name).ToLower()),
("blocker", beforeEv.Blocker.Value)
),
uid,
uid
);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString("chat-system-emote-cancelled-generic",
("emote", Loc.GetString(proto.Name).ToLower())),
uid,
uid
);
}
return false;
}
var ev = new EmoteEvent(proto);
RaiseLocalEvent(uid, ref ev);
return true;
}
}
@@ -233,9 +281,8 @@ public partial class ChatSystem
/// Use it to play sound, change sprite or something else.
/// </summary>
[ByRefEvent]
public struct EmoteEvent
public sealed class EmoteEvent : HandledEntityEventArgs
{
public bool Handled;
public readonly EmotePrototype Emote;
public EmoteEvent(EmotePrototype emote)

View File

@@ -8,6 +8,7 @@ using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Speech.Prototypes;
using Content.Server.Speech.EntitySystems;
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
@@ -543,7 +544,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
if (data.Range <= WhisperClearRange)
if (data.Range <= WhisperClearRange || data.Observer)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
@@ -600,8 +601,10 @@ public sealed partial class ChatSystem : SharedChatSystem
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote)
TryEmoteChatInput(source, action);
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))
@@ -816,8 +819,7 @@ public sealed partial class ChatSystem : SharedChatSystem
return message;
}
[ValidatePrototypeId<ReplacementAccentPrototype>]
public const string ChatSanitize_Accent = "chatsanitize";
public static readonly ProtoId<ReplacementAccentPrototype> ChatSanitize_Accent = "chatsanitize";
public string SanitizeMessageReplaceWords(string message)
{

View File

@@ -1,47 +0,0 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Passively decreases a solution's quantity of reagent(s).
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(SolutionPurgeSystem))]
public sealed partial class SolutionPurgeComponent : Component
{
/// <summary>
/// The name of the solution to detract from.
/// </summary>
[DataField("solution", required: true), ViewVariables(VVAccess.ReadWrite)]
public string Solution = string.Empty;
/// <summary>
/// The reagent(s) to be ignored when purging the solution
/// </summary>
[DataField("preserve", customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public List<string> Preserve = new();
/// <summary>
/// Amount of reagent(s) that are purged
/// </summary>
[DataField("quantity", required: true), ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 Quantity = default!;
/// <summary>
/// How long it takes to purge once.
/// </summary>
[DataField("duration"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Duration = TimeSpan.FromSeconds(1);
/// <summary>
/// The time when the next purge will occur.
/// </summary>
[DataField("nextPurgeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
[AutoPausedField]
public TimeSpan NextPurgeTime = TimeSpan.FromSeconds(0);
}

View File

@@ -1,44 +0,0 @@
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Chemistry.Components;
/// <summary>
/// Passively increases a solution's quantity of a reagent.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(SolutionRegenerationSystem))]
public sealed partial class SolutionRegenerationComponent : Component
{
/// <summary>
/// The name of the solution to add to.
/// </summary>
[DataField("solution", required: true)]
public string SolutionName = string.Empty;
/// <summary>
/// The solution to add reagents to.
/// </summary>
[ViewVariables]
public Entity<SolutionComponent>? SolutionRef = null;
/// <summary>
/// The reagent(s) to be regenerated in the solution.
/// </summary>
[DataField(required: true)]
public Solution Generated = default!;
/// <summary>
/// How long it takes to regenerate once.
/// </summary>
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(1);
/// <summary>
/// The time when the next regeneration will occur.
/// </summary>
[DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextRegenTime = TimeSpan.FromSeconds(0);
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Animals.Systems;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.Components;
@@ -19,19 +19,19 @@ public sealed partial class TransformableContainerComponent : Component
/// It will revert to this when emptied.
/// /// It defaults to the description of the parent entity unless overwritten.
/// </summary>
[DataField("initialDescription")]
[DataField]
public string? InitialDescription;
/// <summary>
/// This stores whatever primary reagent is currently in the container.
/// It is used to help determine if a transformation is needed on solution update.
/// </summary>
[DataField("currentReagent")]
public ReagentPrototype? CurrentReagent;
[DataField]
public ProtoId<ReagentPrototype>? CurrentReagent;
/// <summary>
/// This returns whether this container in a transformed or initial state.
/// </summary>
///
[DataField("transformed")]
[DataField]
public bool Transformed;
}

View File

@@ -39,8 +39,7 @@ namespace Content.Server.Chemistry.EntitySystems
[Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string PillPrototypeId = "Pill";
private static readonly EntProtoId PillPrototypeId = "Pill";
public override void Initialize()
{

View File

@@ -1,29 +0,0 @@
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components.SolutionManager;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.EntitySystems;
public sealed class SolutionPurgeSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SolutionPurgeComponent, SolutionContainerManagerComponent>();
while (query.MoveNext(out var uid, out var purge, out var manager))
{
if (_timing.CurTime < purge.NextPurgeTime)
continue;
// timer ignores if it's empty, it's just a fixed cycle
purge.NextPurgeTime += purge.Duration;
if (_solutionContainer.TryGetSolution((uid, manager), purge.Solution, out var solution))
_solutionContainer.SplitSolutionWithout(solution.Value, purge.Quantity, purge.Preserve.ToArray());
}
}
}

View File

@@ -1,48 +0,0 @@
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.FixedPoint;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.EntitySystems;
public sealed class SolutionRegenerationSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SolutionRegenerationComponent, SolutionContainerManagerComponent>();
while (query.MoveNext(out var uid, out var regen, out var manager))
{
if (_timing.CurTime < regen.NextRegenTime)
continue;
// timer ignores if its full, it's just a fixed cycle
regen.NextRegenTime = _timing.CurTime + regen.Duration;
if (_solutionContainer.ResolveSolution((uid, manager), regen.SolutionName, ref regen.SolutionRef, out var solution))
{
var amount = FixedPoint2.Min(solution.AvailableVolume, regen.Generated.Volume);
if (amount <= FixedPoint2.Zero)
continue;
// dont bother cloning and splitting if adding the whole thing
Solution generated;
if (amount == regen.Generated.Volume)
{
generated = regen.Generated;
}
else
{
generated = regen.Generated.Clone().SplitSolution(amount);
}
_solutionContainer.TryAddSolution(regen.SolutionRef.Value, generated);
}
}
}
}

View File

@@ -45,8 +45,8 @@ public sealed class TransformableContainerSystem : EntitySystem
//the biggest reagent in the solution decides the appearance
var reagentId = solution.GetPrimaryReagentId();
//If biggest reagent didn't changed - don't change anything at all
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent.ID == reagentId?.Prototype)
//If biggest reagent didn't change - don't change anything at all
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent == reagentId?.Prototype)
{
return;
}
@@ -66,7 +66,7 @@ public sealed class TransformableContainerSystem : EntitySystem
private void OnRefreshNameModifiers(Entity<TransformableContainerComponent> entity, ref RefreshNameModifiersEvent args)
{
if (entity.Comp.CurrentReagent is { } currentReagent)
if (_prototypeManager.TryIndex(entity.Comp.CurrentReagent, out var currentReagent))
{
args.AddModifier("transformable-container-component-glass", priority: -1, ("reagent", currentReagent.LocalizedName));
}

View File

@@ -21,6 +21,8 @@ namespace Content.Server.Cluwne;
public sealed class CluwneSystem : EntitySystem
{
private static readonly ProtoId<DamageGroupPrototype> GeneticDamageGroup = "Genetic";
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
@@ -53,7 +55,7 @@ public sealed class CluwneSystem : EntitySystem
RemComp<CluwneComponent>(uid);
RemComp<ClumsyComponent>(uid);
RemComp<AutoEmoteComponent>(uid);
var damageSpec = new DamageSpecifier(_prototypeManager.Index<DamageGroupPrototype>("Genetic"), 300);
var damageSpec = new DamageSpecifier(_prototypeManager.Index(GeneticDamageGroup), 300);
_damageableSystem.TryChangeDamage(uid, damageSpec);
}
}

View File

@@ -6,6 +6,7 @@ using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Construction.Commands;
@@ -20,12 +21,9 @@ public sealed class TileWallsCommand : IConsoleCommand
public string Description => "Puts an underplating tile below every wall on a grid.";
public string Help => $"Usage: {Command} <gridId> | {Command}";
[ValidatePrototypeId<ContentTileDefinition>]
public const string TilePrototypeId = "Plating";
[ValidatePrototypeId<TagPrototype>]
public const string WallTag = "Wall";
public const string DiagonalTag = "Diagonal";
public static readonly ProtoId<ContentTileDefinition> TilePrototypeId = "Plating";
public static readonly ProtoId<TagPrototype> WallTag = "Wall";
public static readonly ProtoId<TagPrototype> DiagonalTag = "Diagonal";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{

View File

@@ -1,16 +1,18 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Damage.Components;
/// <summary>
/// This component shows entity damage severity when it is examined by player.
/// This component shows entity damage severity when it is examined by player.
/// </summary>
[RegisterComponent]
public sealed partial class ExaminableDamageComponent : Component
{
[DataField("messages", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<ExaminableDamagePrototype>))]
public string? MessagesProtoId;
public ExaminableDamagePrototype? MessagesProto;
/// <summary>
/// ID of the <see cref="LocalizedDatasetPrototype"/> containing messages to display a different damage levels.
/// The first message will be used at 0 damage with the others equally distributed across the range from undamaged to fully damaged.
/// </summary>
[DataField]
public ProtoId<LocalizedDatasetPrototype>? Messages;
}

View File

@@ -1,9 +1,6 @@
using System.Linq;
using Content.Server.Damage.Components;
using Content.Server.Damage.Components;
using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Examine;
using Content.Shared.Rounding;
using Robust.Shared.Prototypes;
@@ -12,59 +9,42 @@ namespace Content.Server.Damage.Systems;
public sealed class ExaminableDamageSystem : EntitySystem
{
[Dependency] private readonly DestructibleSystem _destructible = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExaminableDamageComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ExaminableDamageComponent, ExaminedEvent>(OnExamine);
}
private void OnInit(EntityUid uid, ExaminableDamageComponent component, ComponentInit args)
private void OnExamine(Entity<ExaminableDamageComponent> ent, ref ExaminedEvent args)
{
if (component.MessagesProtoId == null)
if (!_prototype.TryIndex(ent.Comp.Messages, out var proto) || proto.Values.Count == 0)
return;
component.MessagesProto = _prototype.Index<ExaminableDamagePrototype>(component.MessagesProtoId);
var percent = GetDamagePercent(ent);
var level = ContentHelpers.RoundToNearestLevels(percent, 1, proto.Values.Count - 1);
var msg = Loc.GetString(proto.Values[level]);
args.PushMarkup(msg, -99);
}
private void OnExamine(EntityUid uid, ExaminableDamageComponent component, ExaminedEvent args)
/// <summary>
/// Returns a value between 0 and 1 representing how damaged the entity is,
/// where 0 is undamaged and 1 is fully damaged.
/// </summary>
/// <returns>How damaged the entity is from 0 to 1</returns>
private float GetDamagePercent(Entity<ExaminableDamageComponent> ent)
{
if (component.MessagesProto == null)
return;
var messages = component.MessagesProto.Messages;
if (messages.Length == 0)
return;
var level = GetDamageLevel(uid, component);
var msg = Loc.GetString(messages[level]);
args.PushMarkup(msg,-99);
}
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
DamageableComponent? damageable = null, DestructibleComponent? destructible = null)
{
if (!Resolve(uid, ref component, ref damageable, ref destructible))
return 0;
if (component.MessagesProto == null)
return 0;
var maxLevels = component.MessagesProto.Messages.Length - 1;
if (maxLevels <= 0)
return 0;
var trigger = (DamageTrigger?) destructible.Thresholds
.LastOrDefault(threshold => threshold.Trigger is DamageTrigger)?.Trigger;
if (trigger == null)
if (!TryComp<DamageableComponent>(ent, out var damageable))
return 0;
var damage = damageable.TotalDamage;
var damageThreshold = trigger.Damage;
var fraction = damageThreshold == 0 ? 0f : (float) damage / damageThreshold;
var damageThreshold = _destructible.DestroyedAt(ent);
var level = ContentHelpers.RoundToNearestLevels(fraction, 1, maxLevels);
return level;
if (damageThreshold == 0)
return 0;
return (damage / damageThreshold).Float();
}
}

View File

@@ -19,6 +19,8 @@ namespace Content.Server.Destructible.Thresholds.Behaviors;
[DataDefinition]
public sealed partial class WeightedSpawnEntityBehavior : IThresholdBehavior
{
private static readonly EntProtoId TempEntityProtoId = "TemporaryEntityForTimedDespawnSpawners";
/// <summary>
/// A table of entities with assigned weights to randomly pick from
/// </summary>
@@ -64,7 +66,7 @@ public sealed partial class WeightedSpawnEntityBehavior : IThresholdBehavior
if (SpawnAfter != 0)
{
// if it fails to get the spawner, this won't ever work so just return
if (!system.PrototypeManager.TryIndex("TemporaryEntityForTimedDespawnSpawners", out var tempSpawnerProto))
if (!system.PrototypeManager.TryIndex(TempEntityProtoId, out var tempSpawnerProto))
return;
// spawn the spawner, assign it a lifetime, and assign the entity that it will spawn when despawned

View File

@@ -1,59 +0,0 @@
using Content.Server.Body.Systems;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Devour;
using Content.Shared.Devour.Components;
using Content.Shared.Humanoid;
namespace Content.Server.Devour;
public sealed class DevourSystem : SharedDevourSystem
{
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DevourerComponent, DevourDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<DevourerComponent, BeingGibbedEvent>(OnGibContents);
}
private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
var ichorInjection = new Solution(component.Chemical, component.HealRate);
if (component.FoodPreference == FoodPreference.All ||
(component.FoodPreference == FoodPreference.Humanoid && HasComp<HumanoidAppearanceComponent>(args.Args.Target)))
{
if (component.ShouldStoreDevoured && args.Args.Target is not null)
{
ContainerSystem.Insert(args.Args.Target.Value, component.Stomach);
}
_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection);
}
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
//If it's not human, it must be a structure
else if (args.Args.Target != null)
{
QueueDel(args.Args.Target.Value);
}
_audioSystem.PlayPvs(component.SoundDevour, uid);
}
private void OnGibContents(EntityUid uid, DevourerComponent component, ref BeingGibbedEvent args)
{
if (!component.ShouldStoreDevoured)
return;
// For some reason we have two different systems that should handle gibbing,
// and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process
ContainerSystem.EmptyContainer(component.Stomach);
}
}

View File

@@ -428,7 +428,7 @@ namespace Content.Server.Disposal.Tube
foreach (var entity in from.Container.ContainedEntities.ToArray())
{
_disposableSystem.TryInsert(holder, entity, holderComponent);
_containerSystem.Insert(entity, holderComponent.Container);
}
_atmosSystem.Merge(holderComponent.Air, from.Air);

View File

@@ -43,6 +43,8 @@ namespace Content.Server.Disposal.Unit
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<DisposalHolderComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<DisposalHolderComponent, ContainerIsInsertingAttemptEvent>(CanInsert);
SubscribeLocalEvent<DisposalHolderComponent, EntInsertedIntoContainerMessage>(OnInsert);
}
private void OnComponentStartup(EntityUid uid, DisposalHolderComponent holder, ComponentStartup args)
@@ -50,34 +52,16 @@ namespace Content.Server.Disposal.Unit
holder.Container = _containerSystem.EnsureContainer<Container>(uid, nameof(DisposalHolderComponent));
}
public bool TryInsert(EntityUid uid, EntityUid toInsert, DisposalHolderComponent? holder = null)
private void CanInsert(Entity<DisposalHolderComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (!Resolve(uid, ref holder))
return false;
if (!CanInsert(uid, toInsert, holder))
return false;
if (!_containerSystem.Insert(toInsert, holder.Container))
return false;
if (_physicsQuery.TryGetComponent(toInsert, out var physBody))
_physicsSystem.SetCanCollide(toInsert, false, body: physBody);
return true;
if (!HasComp<ItemComponent>(args.EntityUid) && !HasComp<BodyComponent>(args.EntityUid))
args.Cancel();
}
private bool CanInsert(EntityUid uid, EntityUid toInsert, DisposalHolderComponent? holder = null)
private void OnInsert(Entity<DisposalHolderComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (!Resolve(uid, ref holder))
return false;
if (!_containerSystem.CanInsert(toInsert, holder.Container))
{
return false;
}
return HasComp<ItemComponent>(toInsert) ||
HasComp<BodyComponent>(toInsert);
if (_physicsQuery.TryGetComponent(args.Entity, out var physBody))
_physicsSystem.SetCanCollide(args.Entity, false, body: physBody);
}
public void ExitDisposals(EntityUid uid, DisposalHolderComponent? holder = null, TransformComponent? holderTransform = null)

View File

@@ -1,4 +1,3 @@
using Content.Server.StatusEffectNew;
using Content.Shared.Bed.Sleep;
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffectNew;

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.StatusEffect;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Electrocution;
@@ -13,8 +14,7 @@ public sealed class ElectrocuteCommand : LocalizedEntityCommands
public override string Command => "electrocute";
[ValidatePrototypeId<StatusEffectPrototype>]
private const string ElectrocutionStatusEffect = "Electrocution";
private static readonly ProtoId<StatusEffectPrototype> ElectrocutionStatusEffect = "Electrocution";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{

View File

@@ -40,7 +40,6 @@ namespace Content.Server.Electrocution;
public sealed class ElectrocutionSystem : SharedElectrocutionSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
@@ -59,12 +58,8 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[ValidatePrototypeId<StatusEffectPrototype>]
private const string StatusEffectKey = "Electrocution";
[ValidatePrototypeId<DamageTypePrototype>]
private const string DamageType = "Shock";
private static readonly ProtoId<StatusEffectPrototype> StatusEffectKey = "Electrocution";
private static readonly ProtoId<DamageTypePrototype> DamageType = "Shock";
private static readonly ProtoId<TagPrototype> WindowTag = "Window";
// Multiply and shift the log scale for shock damage.
@@ -409,7 +404,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
if (shockDamage is { } dmg)
{
var actual = _damageable.TryChangeDamage(uid,
new DamageSpecifier(_prototypeManager.Index<DamageTypePrototype>(DamageType), dmg), origin: sourceUid);
new DamageSpecifier(_prototypeManager.Index(DamageType), dmg), origin: sourceUid);
if (actual != null)
{

View File

@@ -48,6 +48,8 @@ namespace Content.Server.EntityEffects;
public sealed class EntityEffectSystem : EntitySystem
{
private static readonly ProtoId<WeightedRandomFillSolutionPrototype> RandomPickBotanyReagent = "RandomPickBotanyReagent";
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly ChatSystem _chat = default!;
@@ -854,7 +856,7 @@ public sealed class EntityEffectSystem : EntitySystem
return;
var chemicals = plantholder.Seed.Chemicals;
var randomChems = _protoManager.Index<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent").Fills;
var randomChems = _protoManager.Index(RandomPickBotanyReagent).Fills;
// Add a random amount of a random chemical to this set of chemicals
if (randomChems != null)

View File

@@ -43,7 +43,7 @@ namespace Content.Server.EntityList
var i = 0;
foreach (var entity in prototype.Entities(_prototypeManager))
foreach (var entity in prototype.GetEntities(_prototypeManager))
{
EntityManager.SpawnEntity(entity.ID, EntityManager.GetComponent<TransformComponent>(attached).Coordinates);
i++;

View File

@@ -70,14 +70,13 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
/// find errors. However some components, like rogue arrows, or some commands like the admin-smite need to have
/// a "default" option specified outside of yaml data-fields. Hence this const string.
/// </remarks>
[ValidatePrototypeId<ExplosionPrototype>]
public const string DefaultExplosionPrototypeId = "Default";
public static readonly ProtoId<ExplosionPrototype> DefaultExplosionPrototypeId = "Default";
public override void Initialize()
{
base.Initialize();
DebugTools.Assert(_prototypeManager.HasIndex<ExplosionPrototype>(DefaultExplosionPrototypeId));
DebugTools.Assert(_prototypeManager.HasIndex(DefaultExplosionPrototypeId));
// handled in ExplosionSystem.GridMap.cs
SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);

View File

@@ -58,6 +58,11 @@ public sealed class ProjectileGrenadeSystem : EntitySystem
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
var shootCount = 0;
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
// Just in case
if (totalCount == 0)
return;
var segmentAngle = 360 / totalCount;
while (TrySpawnContents(grenadeCoord, component, out var contentUid))

View File

@@ -1,346 +1,6 @@
using System.Numerics;
using Content.Server.Popups;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
using Content.Shared.Fluids.Components;
using Content.Shared.Interaction;
using Content.Shared.Timing;
using Content.Shared.Weapons.Melee;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Fluids.EntitySystems;
/// <inheritdoc/>
public sealed class AbsorbentSystem : SharedAbsorbentSystem
{
private static readonly EntProtoId Sparkles = "PuddleSparkle";
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly PopupSystem _popups = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AbsorbentComponent, ComponentInit>(OnAbsorbentInit);
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<AbsorbentComponent, UserActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<AbsorbentComponent, SolutionContainerChangedEvent>(OnAbsorbentSolutionChange);
}
private void OnAbsorbentInit(EntityUid uid, AbsorbentComponent component, ComponentInit args)
{
// TODO: I know dirty on init but no prediction moment.
UpdateAbsorbent(uid, component);
}
private void OnAbsorbentSolutionChange(EntityUid uid, AbsorbentComponent component, ref SolutionContainerChangedEvent args)
{
UpdateAbsorbent(uid, component);
}
private void UpdateAbsorbent(EntityUid uid, AbsorbentComponent component)
{
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out _, out var solution))
return;
var oldProgress = component.Progress.ShallowClone();
component.Progress.Clear();
var mopReagent = solution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(solution));
if (mopReagent > FixedPoint2.Zero)
{
component.Progress[solution.GetColorWithOnly(_prototype, _puddleSystem.GetAbsorbentReagents(solution))] = mopReagent.Float();
}
var otherColor = solution.GetColorWithout(_prototype, _puddleSystem.GetAbsorbentReagents(solution));
var other = (solution.Volume - mopReagent).Float();
if (other > 0f)
{
component.Progress[otherColor] = other;
}
var remainder = solution.AvailableVolume;
if (remainder > FixedPoint2.Zero)
{
component.Progress[Color.DarkGray] = remainder.Float();
}
if (component.Progress.Equals(oldProgress))
return;
Dirty(uid, component);
}
private void OnActivateInWorld(EntityUid uid, AbsorbentComponent component, UserActivateInWorldEvent args)
{
if (args.Handled)
return;
Mop(uid, args.Target, uid, component);
args.Handled = true;
}
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Handled || args.Target == null)
return;
Mop(args.User, args.Target.Value, args.Used, component);
args.Handled = true;
}
public void Mop(EntityUid user, EntityUid target, EntityUid used, AbsorbentComponent component)
{
if (!_solutionContainerSystem.TryGetSolution(used, component.SolutionName, out var absorberSoln))
return;
if (TryComp<UseDelayComponent>(used, out var useDelay)
&& _useDelay.IsDelayed((used, useDelay)))
return;
// If it's a puddle try to grab from
if (!TryPuddleInteract(user, used, target, component, useDelay, absorberSoln.Value) && component.UseAbsorberSolution)
{
// If it's refillable try to transfer
if (!TryRefillableInteract(user, used, target, component, useDelay, absorberSoln.Value))
return;
}
}
/// <summary>
/// Logic for an absorbing entity interacting with a refillable.
/// </summary>
private bool TryRefillableInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, UseDelayComponent? useDelay, Entity<SolutionComponent> absorbentSoln)
{
if (!TryComp(target, out RefillableSolutionComponent? refillable))
return false;
if (!_solutionContainerSystem.TryGetRefillableSolution((target, refillable, null), out var refillableSoln, out var refillableSolution))
return false;
if (refillableSolution.Volume <= 0)
{
// Target empty - only transfer absorbent contents into refillable
if (!TryTransferFromAbsorbentToRefillable(user, used, target, component, absorbentSoln, refillableSoln.Value))
return false;
}
else
{
// Target non-empty - do a two-way transfer
if (!TryTwoWayAbsorbentRefillableTransfer(user, used, target, component, absorbentSoln, refillableSoln.Value))
return false;
}
_audio.PlayPvs(component.TransferSound, target);
if (useDelay != null)
_useDelay.TryResetDelay((used, useDelay));
return true;
}
/// <summary>
/// Logic for an transferring solution from absorber to an empty refillable.
/// </summary>
private bool TryTransferFromAbsorbentToRefillable(
EntityUid user,
EntityUid used,
EntityUid target,
AbsorbentComponent component,
Entity<SolutionComponent> absorbentSoln,
Entity<SolutionComponent> refillableSoln)
{
var absorbentSolution = absorbentSoln.Comp.Solution;
if (absorbentSolution.Volume <= 0)
{
_popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty", ("target", target)), user, user);
return false;
}
var refillableSolution = refillableSoln.Comp.Solution;
var transferAmount = component.PickupAmount < refillableSolution.AvailableVolume ?
component.PickupAmount :
refillableSolution.AvailableVolume;
if (transferAmount <= 0)
{
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", used)), used, user);
return false;
}
// Prioritize transferring non-evaporatives if absorbent has any
var contaminants = _solutionContainerSystem.SplitSolutionWithout(absorbentSoln, transferAmount, _puddleSystem.GetAbsorbentReagents(absorbentSoln.Comp.Solution));
if (contaminants.Volume > 0)
{
_solutionContainerSystem.TryAddSolution(refillableSoln, contaminants);
}
else
{
var evaporatives = _solutionContainerSystem.SplitSolution(absorbentSoln, transferAmount);
_solutionContainerSystem.TryAddSolution(refillableSoln, evaporatives);
}
return true;
}
/// <summary>
/// Logic for an transferring contaminants to a non-empty refillable & reabsorbing water if any available.
/// </summary>
private bool TryTwoWayAbsorbentRefillableTransfer(
EntityUid user,
EntityUid used,
EntityUid target,
AbsorbentComponent component,
Entity<SolutionComponent> absorbentSoln,
Entity<SolutionComponent> refillableSoln)
{
var contaminantsFromAbsorbent = _solutionContainerSystem.SplitSolutionWithout(absorbentSoln, component.PickupAmount, _puddleSystem.GetAbsorbentReagents(absorbentSoln.Comp.Solution));
var absorbentSolution = absorbentSoln.Comp.Solution;
if (contaminantsFromAbsorbent.Volume == FixedPoint2.Zero && absorbentSolution.AvailableVolume == FixedPoint2.Zero)
{
// Nothing to transfer to refillable and no room to absorb anything extra
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-space", ("used", used)), user, user);
// We can return cleanly because nothing was split from absorbent solution
return false;
}
var waterPulled = component.PickupAmount < absorbentSolution.AvailableVolume ?
component.PickupAmount :
absorbentSolution.AvailableVolume;
var refillableSolution = refillableSoln.Comp.Solution;
var waterFromRefillable = refillableSolution.SplitSolutionWithOnly(waterPulled, _puddleSystem.GetAbsorbentReagents(refillableSoln.Comp.Solution));
_solutionContainerSystem.UpdateChemicals(refillableSoln);
if (waterFromRefillable.Volume == FixedPoint2.Zero && contaminantsFromAbsorbent.Volume == FixedPoint2.Zero)
{
// Nothing to transfer in either direction
_popups.PopupEntity(Loc.GetString("mopping-system-target-container-empty-water", ("target", target)), user, user);
// We can return cleanly because nothing was split from refillable solution
return false;
}
var anyTransferOccurred = false;
if (waterFromRefillable.Volume > FixedPoint2.Zero)
{
// transfer water to absorbent
_solutionContainerSystem.TryAddSolution(absorbentSoln, waterFromRefillable);
anyTransferOccurred = true;
}
if (contaminantsFromAbsorbent.Volume > 0)
{
if (refillableSolution.AvailableVolume <= 0)
{
_popups.PopupEntity(Loc.GetString("mopping-system-full", ("used", target)), user, user);
}
else
{
// transfer as much contaminants to refillable as will fit
var contaminantsForRefillable = contaminantsFromAbsorbent.SplitSolution(refillableSolution.AvailableVolume);
_solutionContainerSystem.TryAddSolution(refillableSoln, contaminantsForRefillable);
anyTransferOccurred = true;
}
// absorb everything that did not fit in the refillable back by the absorbent
_solutionContainerSystem.TryAddSolution(absorbentSoln, contaminantsFromAbsorbent);
}
return anyTransferOccurred;
}
/// <summary>
/// Logic for an absorbing entity interacting with a puddle.
/// </summary>
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, UseDelayComponent? useDelay, Entity<SolutionComponent> absorberSoln)
{
if (!TryComp(target, out PuddleComponent? puddle))
return false;
if (!_solutionContainerSystem.ResolveSolution(target, puddle.SolutionName, ref puddle.Solution, out var puddleSolution) || puddleSolution.Volume <= 0)
return false;
// Check if the puddle has any non-evaporative reagents
if (_puddleSystem.CanFullyEvaporate(puddleSolution))
{
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-evaporate", ("target", target)), user, user);
return true;
}
Solution puddleSplit;
var isRemoved = false;
if (absorber.UseAbsorberSolution)
{
// Check if we have any evaporative reagents on our absorber to transfer
var absorberSolution = absorberSoln.Comp.Solution;
var available = absorberSolution.GetTotalPrototypeQuantity(_puddleSystem.GetAbsorbentReagents(absorberSolution));
// No material
if (available == FixedPoint2.Zero)
{
_popups.PopupEntity(Loc.GetString("mopping-system-no-water", ("used", used)), user, user);
return true;
}
var transferMax = absorber.PickupAmount;
var transferAmount = available > transferMax ? transferMax : available;
puddleSplit = puddleSolution.SplitSolutionWithout(transferAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution));
var absorberSplit = absorberSolution.SplitSolutionWithOnly(puddleSplit.Volume, _puddleSystem.GetAbsorbentReagents(absorberSolution));
// Do tile reactions first
var transform = Transform(target);
var gridUid = transform.GridUid;
if (TryComp(gridUid, out MapGridComponent? mapGrid))
{
var tileRef = _mapSystem.GetTileRef(gridUid.Value, mapGrid, transform.Coordinates);
_puddleSystem.DoTileReactions(tileRef, absorberSplit);
}
_solutionContainerSystem.AddSolution(puddle.Solution.Value, absorberSplit);
}
else
{
puddleSplit = puddleSolution.SplitSolutionWithout(absorber.PickupAmount, _puddleSystem.GetAbsorbentReagents(puddleSolution));
// Despawn if we're done
if (puddleSolution.Volume == FixedPoint2.Zero)
{
// Spawn a *sparkle*
Spawn(Sparkles, GetEntityQuery<TransformComponent>().GetComponent(target).Coordinates);
QueueDel(target);
isRemoved = true;
}
}
_solutionContainerSystem.AddSolution(absorberSoln, puddleSplit);
_audio.PlayPvs(absorber.PickupSound, isRemoved ? used : target);
if (useDelay != null)
_useDelay.TryResetDelay((used, useDelay));
var userXform = Transform(user);
var targetPos = _transform.GetWorldPosition(target);
var localPos = Vector2.Transform(targetPos, _transform.GetInvWorldMatrix(userXform));
localPos = userXform.LocalRotation.RotateVec(localPos);
_melee.DoLunge(user, used, Angle.Zero, localPos, null, false);
return true;
}
}
public sealed class AbsorbentSystem : SharedAbsorbentSystem;

View File

@@ -61,6 +61,7 @@ public sealed partial class PuddleSystem
if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var puddleSolution))
continue;
// Yes, this means that 50u water + 50u holy water evaporates twice as fast as 100u water.
foreach ((string evaporatingReagent, FixedPoint2 evaporatingSpeed) in GetEvaporationSpeeds(puddleSolution))
{
var reagentTick = evaporation.EvaporationAmount * EvaporationCooldown.TotalSeconds * evaporatingSpeed;

View File

@@ -51,7 +51,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
@@ -61,17 +60,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[ValidatePrototypeId<ReagentPrototype>]
private const string Blood = "Blood";
[ValidatePrototypeId<ReagentPrototype>]
private const string Slime = "Slime";
[ValidatePrototypeId<ReagentPrototype>]
private const string CopperBlood = "CopperBlood";
private static string[] _standoutReagents = [Blood, Slime, CopperBlood];
// Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
// loses & then gains reagents in a single tick.
private HashSet<EntityUid> _deletionQueue = [];
@@ -92,7 +80,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Shouldn't need re-anchoring.
SubscribeLocalEvent<PuddleComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<PuddleComponent, SolutionContainerChangedEvent>(OnSolutionUpdate);
SubscribeLocalEvent<PuddleComponent, SpreadNeighborsEvent>(OnPuddleSpread);
SubscribeLocalEvent<PuddleComponent, SlipEvent>(OnPuddleSlip);
@@ -325,11 +312,13 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
TickEvaporation();
}
private void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
protected override void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
{
if (args.SolutionId != entity.Comp.SolutionName)
return;
base.OnSolutionUpdate(entity, ref args);
if (args.Solution.Volume <= 0)
{
_deletionQueue.Add(entity);
@@ -340,46 +329,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
UpdateSlip((entity, entity.Comp), args.Solution);
UpdateSlow(entity, args.Solution);
//UpdateEvaporation(entity, args.Solution); //CP14 Force evaporation under sky via YML
UpdateAppearance(entity, entity.Comp);
}
private void UpdateAppearance(EntityUid uid, PuddleComponent? puddleComponent = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref puddleComponent, ref appearance, false))
{
return;
}
var volume = FixedPoint2.Zero;
Color color = Color.White;
if (_solutionContainerSystem.ResolveSolution(uid, puddleComponent.SolutionName, ref puddleComponent.Solution,
out var solution))
{
volume = solution.Volume / puddleComponent.OverflowVolume;
// Make blood stand out more
// Kinda EH
// Could potentially do alpha per-solution but future problem.
color = solution.GetColorWithout(_prototypeManager, _standoutReagents);
color = color.WithAlpha(0.7f);
foreach (var standout in _standoutReagents)
{
var quantity = solution.GetTotalPrototypeQuantity(standout);
if (quantity <= FixedPoint2.Zero)
continue;
var interpolateValue = quantity.Float() / solution.Volume.Float();
color = Color.InterpolateBetween(color,
_prototypeManager.Index<ReagentPrototype>(standout).SubstanceColor, interpolateValue);
}
}
_appearance.SetData(uid, PuddleVisuals.CurrentVolume, volume.Float(), appearance);
_appearance.SetData(uid, PuddleVisuals.SolutionColor, color, appearance);
}
private void UpdateSlip(Entity<PuddleComponent> entity, Solution solution)
@@ -456,15 +405,15 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
if (slipperyUnits > 0)
{
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
slipComp.SlipData.ParalyzeTime = (stunTimer/(float)slipperyUnits);
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
}
// Only make it super slippery if there is enough super slippery units for its own puddle
slipComp.SlipData.SuperSlippery = superSlipperyUnits >= smallPuddleThreshold;
// Lower tile friction based on how slippery it is, lets items slide across a puddle of lube
slipComp.SlipData.SlipFriction = (float)(puddleFriction/solution.Volume);
slipComp.SlipData.SlipFriction = (float)(puddleFriction / solution.Volume);
_tile.SetModifier(entity, slipComp.SlipData.SlipFriction);
Dirty(entity, slipComp);
@@ -754,20 +703,6 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
#endregion
public void DoTileReactions(TileRef tileRef, Solution solution)
{
for (var i = solution.Contents.Count - 1; i >= 0; i--)
{
var (reagent, quantity) = solution.Contents[i];
var proto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
var removed = proto.ReactionTile(tileRef, quantity, EntityManager, reagent.Data);
if (removed <= FixedPoint2.Zero)
continue;
solution.RemoveReagent(reagent, removed);
}
}
/// <summary>
/// Tries to get the relevant puddle entity for a tile.
/// </summary>

View File

@@ -36,11 +36,8 @@ namespace Content.Server.GameTicking
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly AdminSystem _admin = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string ObserverPrototypeName = "MobObserver";
[ValidatePrototypeId<EntityPrototype>]
public const string AdminObserverPrototypeName = "AdminObserver";
public static readonly EntProtoId ObserverPrototypeName = "MobObserver";
public static readonly EntProtoId AdminObserverPrototypeName = "AdminObserver";
/// <summary>
/// How many players have joined the round through normal methods.

View File

@@ -93,7 +93,7 @@ namespace Content.Server.GameTicking
InitializePlayer();
InitializeLobbyBackground();
InitializeGamePreset();
DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == FallbackOverflowJobName,
DebugTools.Assert(_prototypeManager.Index(FallbackOverflowJob).Name == FallbackOverflowJobName,
"Overflow role does not have the correct name!");
InitializeGameRules();
InitializeReplays();

View File

@@ -1,8 +1,6 @@
using Content.Server.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.GameTicking.Presets
{
@@ -33,8 +31,8 @@ namespace Content.Server.GameTicking.Presets
[DataField("maxPlayers")]
public int? MaxPlayers;
[DataField("rules", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public IReadOnlyList<string> Rules { get; private set; } = Array.Empty<string>();
[DataField]
public IReadOnlyList<EntProtoId> Rules { get; private set; } = Array.Empty<EntProtoId>();
/// <summary>
/// If specified, the gamemode will only be run with these maps.

View File

@@ -25,6 +25,7 @@ using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules;
@@ -38,11 +39,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
[Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly TagSystem _tag = default!;
[ValidatePrototypeId<CurrencyPrototype>]
private const string TelecrystalCurrencyPrototype = "Telecrystal";
[ValidatePrototypeId<TagPrototype>]
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
private static readonly ProtoId<CurrencyPrototype> TelecrystalCurrencyPrototype = "Telecrystal";
private static readonly ProtoId<TagPrototype> NukeOpsUplinkTagPrototype = "NukeOpsUplink";
public override void Initialize()
@@ -485,7 +483,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
SetWinType(ent, WinType.CrewMajor, false);
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing) // It's still worth checking if operatives have all died, even if the round-end behaviour is nothing.
if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing) // It's still worth checking if operatives have all died, even if the round-end behaviour is nothing.
return; // Shouldn't actually try to end the round in the case of nothing though.
_roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,

View File

@@ -36,8 +36,9 @@ public sealed class GatewayGeneratorSystem : EntitySystem
[Dependency] private readonly SharedSalvageSystem _salvage = default!;
[Dependency] private readonly TileSystem _tile = default!;
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string PlanetNames = "NamesBorer";
private static readonly ProtoId<LocalizedDatasetPrototype> PlanetNames = "NamesBorer";
private static readonly ProtoId<BiomeTemplatePrototype> BiomeTemplate = "Continental";
private static readonly ProtoId<DungeonConfigPrototype> DungeonConfig = "Experiment";
// TODO:
// Fix shader some more
@@ -101,7 +102,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var random = new Random(seed);
var mapUid = _maps.CreateMap();
var gatewayName = _salvage.GetFTLName(_protoManager.Index<LocalizedDatasetPrototype>(PlanetNames), seed);
var gatewayName = _salvage.GetFTLName(_protoManager.Index(PlanetNames), seed);
_metadata.SetEntityName(mapUid, gatewayName);
var origin = new Vector2i(random.Next(-MaxOffset, MaxOffset), random.Next(-MaxOffset, MaxOffset));
@@ -111,7 +112,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
};
AddComp(mapUid, restricted);
_biome.EnsurePlanet(mapUid, _protoManager.Index<BiomeTemplatePrototype>("Continental"), seed);
_biome.EnsurePlanet(mapUid, _protoManager.Index(BiomeTemplate), seed);
var grid = Comp<MapGridComponent>(mapUid);
@@ -183,7 +184,7 @@ public sealed class GatewayGeneratorSystem : EntitySystem
var dungeonRotation = _dungeon.GetDungeonRotation(seed);
var dungeonPosition = (origin + dungeonRotation.RotateVec(new Vector2i(0, dungeonDistance))).Floored();
_dungeon.GenerateDungeon(_protoManager.Index<DungeonConfigPrototype>("Experiment"), args.MapUid, grid, dungeonPosition, seed);
_dungeon.GenerateDungeon(_protoManager.Index(DungeonConfig), args.MapUid, grid, dungeonPosition, seed);
// TODO: Dungeon mobs + loot.

View File

@@ -74,6 +74,7 @@ namespace Content.Server.Ghost
private EntityQuery<PhysicsComponent> _physicsQuery;
private static readonly ProtoId<TagPrototype> AllowGhostShownByEventTag = "AllowGhostShownByEvent";
private static readonly ProtoId<DamageTypePrototype> AsphyxiationDamageType = "Asphyxiation";
public override void Initialize()
{
@@ -588,7 +589,7 @@ namespace Content.Server.Ghost
dealtDamage = playerDeadThreshold - damageable.TotalDamage;
}
DamageSpecifier damage = new(_prototypeManager.Index<DamageTypePrototype>("Asphyxiation"), dealtDamage);
DamageSpecifier damage = new(_prototypeManager.Index(AsphyxiationDamageType), dealtDamage);
_damageable.TryChangeDamage(playerEntity, damage, true);
}

View File

@@ -510,6 +510,11 @@ public sealed class GhostRoleSystem : EntitySystem
DebugTools.AssertNotNull(player.ContentData());
// After taking a ghost role, the player cannot return to the original body, so wipe the player's current mind
// unless it is a visiting mind
if(_mindSystem.TryGetMind(player.UserId, out _, out var mind) && !mind.IsVisitingEntity)
_mindSystem.WipeMind(player);
var newMind = _mindSystem.CreateMind(player.UserId,
Comp<MetaDataComponent>(mob).EntityName);

View File

@@ -1,5 +1,4 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Implants.Components;
@@ -12,6 +11,6 @@ public sealed partial class AutoImplantComponent : Component
/// <summary>
/// List of implants to inject.
/// </summary>
[DataField("implants", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> Implants = new();
[DataField(required: true)]
public List<EntProtoId> Implants = new();
}

View File

@@ -10,55 +10,46 @@ using Robust.Shared.Network;
namespace Content.Server.Info;
[AdminCommand(AdminFlags.Admin)]
public sealed class ShowRulesCommand : IConsoleCommand
public sealed class ShowRulesCommand : LocalizedCommands
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public string Command => "showrules";
public string Description => "Opens the rules popup for the specified player.";
public string Help => "showrules <username> [seconds]";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "showrules";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
string target;
float seconds;
switch (args.Length)
if (args.Length is < 1 or > 2)
{
case 1:
{
target = args[0];
seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
break;
}
case 2:
{
if (!float.TryParse(args[1], out seconds))
{
shell.WriteError($"{args[1]} is not a valid amount of seconds.\n{Help}");
return;
}
target = args[0];
break;
}
default:
{
shell.WriteLine(Help);
return;
}
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
var seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
if (!_player.TryGetSessionByUsername(target, out var player))
if (args.Length == 2 && !float.TryParse(args[1], out seconds))
{
shell.WriteError("Unable to find a player with that name.");
return;
shell.WriteError(Loc.GetString("cmd-showrules-invalid-seconds", ("seconds", args[1])));
return;
}
if (!_player.TryGetSessionByUsername(args[0], out var player))
{
shell.WriteError(Loc.GetString("shell-target-player-does-not-exist"));
return;
}
var coreRules = _configuration.GetCVar(CCVars.RulesFile);
var message = new SendRulesInformationMessage { PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true};
var message = new SendRulesInformationMessage
{ PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true };
_net.ServerSendMessage(message, player.Channel);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length == 1
? CompletionResult.FromOptions(CompletionHelper.SessionNames(players: _player))
: CompletionResult.Empty;
}
}

View File

@@ -2,7 +2,6 @@
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Jobs;
@@ -12,8 +11,8 @@ namespace Content.Server.Jobs;
[UsedImplicitly]
public sealed partial class AddImplantSpecial : JobSpecial
{
[DataField("implants", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<EntityPrototype>))]
public HashSet<String> Implants { get; private set; } = new();
[DataField]
public HashSet<EntProtoId> Implants { get; private set; } = new();
public override void AfterEquip(EntityUid mob)
{

View File

@@ -70,8 +70,7 @@ namespace Content.Server.Kitchen.EntitySystems
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string MalfunctionSpark = "Spark";
private static readonly EntProtoId MalfunctionSpark = "Spark";
private static readonly ProtoId<TagPrototype> MetalTag = "Metal";
private static readonly ProtoId<TagPrototype> PlasticTag = "Plastic";

View File

@@ -137,13 +137,13 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
if (component.Target is not { } target)
return;
// Check if the target getting their hair altered has any clothes that hides their hair
// Check if the target getting their hair altered has any clothes that hides their hair
if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
{
_popup.PopupEntity(
component.Target == message.Actor
? Loc.GetString("magic-mirror-blocked-by-hat-self")
: Loc.GetString("magic-mirror-blocked-by-hat-self-target"),
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
message.Actor,
message.Actor,
PopupType.Medium);
@@ -222,7 +222,7 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
_popup.PopupEntity(
component.Target == message.Actor
? Loc.GetString("magic-mirror-blocked-by-hat-self")
: Loc.GetString("magic-mirror-blocked-by-hat-self-target"),
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
message.Actor,
message.Actor,
PopupType.Medium);
@@ -301,7 +301,7 @@ public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
_popup.PopupEntity(
component.Target == message.Actor
? Loc.GetString("magic-mirror-blocked-by-hat-self")
: Loc.GetString("magic-mirror-blocked-by-hat-self-target"),
: Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
message.Actor,
message.Actor,
PopupType.Medium);

View File

@@ -146,7 +146,7 @@ namespace Content.Server.Mapping
// map successfully created. run misc helpful mapping commands
if (player.AttachedEntity is { Valid: true } playerEntity &&
EntityManager.GetComponent<MetaDataComponent>(playerEntity).EntityPrototype?.ID != GameTicker.AdminObserverPrototypeName)
(EntityManager.GetComponent<MetaDataComponent>(playerEntity).EntityPrototype is not { } proto || proto != GameTicker.AdminObserverPrototypeName))
{
shell.ExecuteCommand("aghost");
}

View File

@@ -2,7 +2,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Ghost;
using Content.Server.Popups;
using Content.Server.Repairable;
using Content.Server.Stack;
using Content.Server.Wires;
using Content.Shared.Body.Systems;
@@ -19,6 +18,7 @@ using Content.Shared.Materials;
using Content.Shared.Mind;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Repairable;
using Content.Shared.Stacks;
using Robust.Server.GameObjects;
using Robust.Shared.Player;

View File

@@ -4,6 +4,7 @@ using Content.Server.Materials.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Server.Audio;
namespace Content.Server.Materials;
@@ -13,6 +14,7 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -39,7 +41,16 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
var matAmount = solution.Value.Comp.Solution.Contents
.Where(r => ent.Comp.ExtractionReagents.Contains(r.Reagent.Prototype))
.Sum(r => r.Quantity.Float());
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, (int) matAmount);
var changed = (int)matAmount;
if (changed == 0)
{
_popup.PopupEntity(Loc.GetString("material-extractor-comp-wrongreagent", ("used", args.Used)), args.User, args.User);
return;
}
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, changed);
_audio.PlayPvs(ent.Comp.ExtractSound, ent);
QueueDel(args.Used);

View File

@@ -17,9 +17,9 @@ using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Jittering;
using Content.Shared.Materials;
using Content.Shared.Medical;
using Content.Shared.Mind;
using Content.Shared.Materials;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
@@ -30,6 +30,7 @@ using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Medical.BiomassReclaimer
@@ -53,8 +54,7 @@ namespace Content.Server.Medical.BiomassReclaimer
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[ValidatePrototypeId<MaterialPrototype>]
public const string BiomassPrototype = "Biomass";
public static readonly ProtoId<MaterialPrototype> BiomassPrototype = "Biomass";
public override void Update(float frameTime)
{

View File

@@ -32,8 +32,7 @@ namespace Content.Server.Medical
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[ValidatePrototypeId<SoundCollectionPrototype>]
private const string VomitCollection = "Vomit";
private static readonly ProtoId<SoundCollectionPrototype> VomitCollection = "Vomit";
private readonly SoundSpecifier _vomitSound = new SoundCollectionSpecifier(VomitCollection,
AudioParams.Default.WithVariation(0.2f).WithVolume(-4f));

View File

@@ -85,7 +85,7 @@ public sealed class PullController : VirtualController
{
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
.Register<PullingSystem>();
.Register<PullController>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_pullableQuery = GetEntityQuery<PullableComponent>();

View File

@@ -4,6 +4,7 @@ using Content.Server.NodeContainer;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using Content.Shared.Power;
namespace Content.Server.Power.Components
{
@@ -94,11 +95,4 @@ namespace Content.Server.Power.Components
TryFindAndSetNet();
}
}
public enum Voltage
{
High = NodeGroupID.HVPower,
Medium = NodeGroupID.MVPower,
Apc = NodeGroupID.Apc,
}
}

View File

@@ -85,8 +85,8 @@ public sealed class PowerChargeSystem : EntitySystem
if (!Resolve(uid, ref powerReceiver))
return;
if (user is { } )
_adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}");
if (user is { })
_adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set {ToPrettyString(uid):target} to {(on ? "on" : "off")}");
component.SwitchedOn = on;
UpdatePowerState(component, powerReceiver);

View File

@@ -45,6 +45,7 @@ namespace Content.Server.Power.EntitySystems
UpdatesAfter.Add(typeof(NodeGroupSystem));
_solver = new(_cfg.GetCVar(CCVars.DebugPow3rDisableParallel));
SubscribeLocalEvent<ApcPowerReceiverComponent, MapInitEvent>(ApcPowerReceiverMapInit);
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentInit>(ApcPowerReceiverInit);
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentShutdown>(ApcPowerReceiverShutdown);
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentRemove>(ApcPowerReceiverRemove);
@@ -74,6 +75,11 @@ namespace Content.Server.Power.EntitySystems
_solver = new(val);
}
private void ApcPowerReceiverMapInit(Entity<ApcPowerReceiverComponent> ent, ref MapInitEvent args)
{
_appearance.SetData(ent, PowerDeviceVisuals.Powered, ent.Comp.Powered);
}
private void ApcPowerReceiverInit(EntityUid uid, ApcPowerReceiverComponent component, ComponentInit args)
{
AllocLoad(component.NetworkLoad);

View File

@@ -4,6 +4,7 @@ using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
using Content.Shared.NodeContainer;
using Content.Shared.Power;
using Content.Shared.Power.Generator;
using Content.Shared.Timing;
using Content.Shared.Verbs;

View File

@@ -54,8 +54,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
private readonly JobQueue _dungeonJobQueue = new(DungeonJobTime);
private readonly Dictionary<DungeonJob.DungeonJob, CancellationTokenSource> _dungeonJobs = new();
[ValidatePrototypeId<ContentTileDefinition>]
public const string FallbackTileId = "FloorSteel";
public static readonly ProtoId<ContentTileDefinition> FallbackTileId = "FloorSteel";
public override void Initialize()
{

View File

@@ -99,7 +99,19 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args)
{
if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor))
// TODO: change this when a code refactor is done
// this is currently done this way because receiving radio messages on an entity otherwise requires that entity
// to have an ActiveRadioComponent
var parent = Transform(uid).ParentUid;
if (parent.IsValid())
{
var relayEvent = new HeadsetRadioReceiveRelayEvent(args);
RaiseLocalEvent(parent, ref relayEvent);
}
if (TryComp(parent, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
}

View File

@@ -6,6 +6,12 @@ namespace Content.Server.Radio;
[ByRefEvent]
public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource, MsgChatMessage ChatMsg);
/// <summary>
/// Event raised on the parent entity of a headset radio when a radio message is received
/// </summary>
[ByRefEvent]
public readonly record struct HeadsetRadioReceiveRelayEvent(RadioReceiveEvent RelayedEvent);
/// <summary>
/// Use this event to cancel sending message per receiver
/// </summary>

View File

@@ -1,42 +0,0 @@
using Content.Shared.Damage;
using Content.Shared.Tools;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Repairable
{
[RegisterComponent]
public sealed partial class RepairableComponent : Component
{
/// <summary>
/// All the damage to change information is stored in this <see cref="DamageSpecifier"/>.
/// </summary>
/// <remarks>
/// If this data-field is specified, it will change damage by this amount instead of setting all damage to 0.
/// in order to heal/repair the damage values have to be negative.
/// </remarks>
[DataField]
public DamageSpecifier? Damage;
[DataField]
public int FuelCost = 5;
[DataField]
public ProtoId<ToolQualityPrototype> QualityNeeded = "Welding";
[DataField]
public int DoAfterDelay = 1;
/// <summary>
/// A multiplier that will be applied to the above if an entity is repairing themselves.
/// </summary>
[DataField]
public float SelfRepairPenalty = 3f;
/// <summary>
/// Whether or not an entity is allowed to repair itself.
/// </summary>
[DataField]
public bool AllowSelfRepair = true;
}
}

View File

@@ -1,87 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Repairable;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
namespace Content.Server.Repairable
{
public sealed class RepairableSystem : SharedRepairableSystem
{
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
public override void Initialize()
{
SubscribeLocalEvent<RepairableComponent, InteractUsingEvent>(Repair);
SubscribeLocalEvent<RepairableComponent, RepairFinishedEvent>(OnRepairFinished);
}
private void OnRepairFinished(EntityUid uid, RepairableComponent component, RepairFinishedEvent args)
{
if (args.Cancelled)
return;
if (!TryComp(uid, out DamageableComponent? damageable) || damageable.TotalDamage == 0)
return;
if (component.Damage != null)
{
var damageChanged = _damageableSystem.TryChangeDamage(uid, component.Damage, true, false, origin: args.User);
_adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} repaired {ToPrettyString(uid):target} by {damageChanged?.GetTotal()}");
}
else
{
// Repair all damage
_damageableSystem.SetAllDamage(uid, damageable, 0);
_adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} repaired {ToPrettyString(uid):target} back to full health");
}
var str = Loc.GetString("comp-repairable-repair",
("target", uid),
("tool", args.Used!));
_popup.PopupEntity(str, uid, args.User);
var ev = new RepairedEvent((uid, component), args.User);
RaiseLocalEvent(uid, ref ev);
}
public async void Repair(EntityUid uid, RepairableComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
// Only try repair the target if it is damaged
if (!TryComp<DamageableComponent>(uid, out var damageable) || damageable.TotalDamage == 0)
return;
float delay = component.DoAfterDelay;
// Add a penalty to how long it takes if the user is repairing itself
if (args.User == args.Target)
{
if (!component.AllowSelfRepair)
return;
delay *= component.SelfRepairPenalty;
}
// Run the repairing doafter
args.Handled = _toolSystem.UseTool(args.Used, args.User, uid, delay, component.QualityNeeded, new RepairFinishedEvent(), component.FuelCost);
}
}
/// <summary>
/// Event raised on an entity when its successfully repaired.
/// </summary>
/// <param name="Ent"></param>
/// <param name="User"></param>
[ByRefEvent]
public readonly record struct RepairedEvent(Entity<RepairableComponent> Ent, EntityUid User);
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Database;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Research.Systems;
@@ -165,9 +166,9 @@ public sealed partial class ResearchSystem
return;
component.MainDiscipline = null;
component.CurrentTechnologyCards = new List<string>();
component.SupportedDisciplines = new List<string>();
component.UnlockedTechnologies = new List<string>();
component.UnlockedRecipes = new List<string>();
component.SupportedDisciplines = new List<ProtoId<TechDisciplinePrototype>>();
component.UnlockedTechnologies = new List<ProtoId<TechnologyPrototype>>();
component.UnlockedRecipes = new List<ProtoId<LatheRecipePrototype>>();
Dirty(uid, component);
}
}

View File

@@ -35,6 +35,7 @@ namespace Content.Server.Revenant.EntitySystems;
public sealed partial class RevenantSystem
{
[Dependency] private readonly EmagSystem _emagSystem = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -345,8 +346,7 @@ public sealed partial class RevenantSystem
_whitelistSystem.IsBlacklistPass(component.MalfunctionBlacklist, ent))
continue;
var ev = new GotEmaggedEvent(uid, EmagType.Interaction | EmagType.Access);
RaiseLocalEvent(ent, ref ev);
_emagSystem.TryEmagEffect(uid, uid, ent);
}
}
}

View File

@@ -47,8 +47,7 @@ public sealed partial class RevenantSystem : EntitySystem
[Dependency] private readonly VisibilitySystem _visibility = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string RevenantShopId = "ActionRevenantShop";
private static readonly EntProtoId RevenantShopId = "ActionRevenantShop";
public override void Initialize()
{

View File

@@ -8,8 +8,8 @@ namespace Content.Server.Salvage;
public sealed partial class SalvageSystem
{
[ValidatePrototypeId<EntityPrototype>]
public const string CoordinatesDisk = "CoordinatesDisk";
public static readonly EntProtoId CoordinatesDisk = "CoordinatesDisk";
public static readonly ProtoId<LocalizedDatasetPrototype> PlanetNames = "NamesBorer";
private void OnSalvageClaimMessage(EntityUid uid, SalvageExpeditionConsoleComponent component, ClaimSalvageMessage args)
{
@@ -28,7 +28,7 @@ public sealed partial class SalvageSystem
var mission = GetMission(_prototypeManager.Index<SalvageDifficultyPrototype>(missionparams.Difficulty), missionparams.Seed);
data.NextOffer = _timing.CurTime + mission.Duration + TimeSpan.FromSeconds(1);
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), missionparams.Seed));
_labelSystem.Label(cdUid, GetFTLName(_prototypeManager.Index(PlanetNames), missionparams.Seed));
_audio.PlayPvs(component.PrintSound, uid);
UpdateConsoles((station.Value, data));

View File

@@ -8,6 +8,7 @@ using Content.Shared.Radio;
using Content.Shared.Salvage.Magnet;
using Robust.Shared.Exceptions;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Salvage;
@@ -15,8 +16,7 @@ public sealed partial class SalvageSystem
{
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
[ValidatePrototypeId<RadioChannelPrototype>]
private const string MagnetChannel = "Supply";
private static readonly ProtoId<RadioChannelPrototype> MagnetChannel = "Supply";
private EntityQuery<SalvageMobRestrictionsComponent> _salvMobQuery;
private EntityQuery<MobStateComponent> _mobStateQuery;

View File

@@ -100,7 +100,7 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
destComp.Enabled = true;
_metaData.SetEntityName(
mapUid,
_entManager.System<SharedSalvageSystem>().GetFTLName(_prototypeManager.Index<LocalizedDatasetPrototype>("NamesBorer"), _missionParams.Seed));
_entManager.System<SharedSalvageSystem>().GetFTLName(_prototypeManager.Index(SalvageSystem.PlanetNames), _missionParams.Seed));
_entManager.AddComponent<FTLBeaconComponent>(mapUid);
// Saving the mission mapUid to a CD is made optional, in case one is somehow made in a process without a CD entity
@@ -255,7 +255,7 @@ public sealed class SpawnSalvageMissionJob : Job<bool>
}
}
var allLoot = _prototypeManager.Index<SalvageLootPrototype>(SharedSalvageSystem.ExpeditionsLootProto);
var allLoot = _prototypeManager.Index(SharedSalvageSystem.ExpeditionsLootProto);
var lootBudget = difficultyProto.LootBudget;
foreach (var rule in allLoot.LootRules)

View File

@@ -25,11 +25,8 @@ public sealed class FTLDiskCommand : LocalizedCommands
public override string Command => "ftldisk";
[ValidatePrototypeId<EntityPrototype>]
public const string CoordinatesDisk = "CoordinatesDisk";
[ValidatePrototypeId<EntityPrototype>]
public const string DiskCase = "DiskCase";
public static readonly EntProtoId CoordinatesDisk = "CoordinatesDisk";
public static readonly EntProtoId DiskCase = "DiskCase";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)

View File

@@ -6,6 +6,7 @@ using Content.Shared.Access;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Popups;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Events;
@@ -13,7 +14,7 @@ using Content.Shared.Shuttles.Systems;
using Content.Shared.UserInterface;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Content.Shared.DeviceNetwork.Components;
using Robust.Shared.Prototypes;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Server.Shuttles.Systems;
@@ -65,8 +66,7 @@ public sealed partial class EmergencyShuttleSystem
private CancellationTokenSource? _roundEndCancelToken;
[ValidatePrototypeId<AccessLevelPrototype>]
private const string EmergencyRepealAllAccess = "EmergencyShuttleRepealAll";
private static readonly ProtoId<AccessLevelPrototype> EmergencyRepealAllAccess = "EmergencyShuttleRepealAll";
private static readonly Color DangerColor = Color.Red;
/// <summary>

View File

@@ -21,6 +21,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.GameTicking;
using Content.Shared.Localizations;
using Content.Shared.Shuttles.Components;
@@ -33,10 +34,10 @@ using Robust.Shared.Configuration;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.DeviceNetwork.Components;
namespace Content.Server.Shuttles.Systems;
@@ -73,8 +74,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
private bool _emergencyShuttleEnabled;
[ValidatePrototypeId<TagPrototype>]
private const string DockTag = "DockEmergency";
private static readonly ProtoId<TagPrototype> DockTag = "DockEmergency";
public override void Initialize()
{

View File

@@ -199,7 +199,7 @@ public sealed partial class BorgSystem
else
{
item = component.ProvidedContainer.ContainedEntities
.FirstOrDefault(ent => Prototype(ent)?.ID == itemProto);
.FirstOrDefault(ent => Prototype(ent)?.ID == itemProto.Id);
if (!item.IsValid())
{
Log.Debug($"no items found: {component.ProvidedContainer.ContainedEntities.Count}");

View File

@@ -1,12 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Actions;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Body.Events;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.Alert;
using Content.Shared.Body.Events;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
@@ -29,10 +31,9 @@ using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Silicons.Borgs;
@@ -61,8 +62,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[ValidatePrototypeId<JobPrototype>]
public const string BorgJobId = "Borg";
public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
/// <inheritdoc/>
public override void Initialize()

View File

@@ -22,42 +22,24 @@ public sealed class IonStormSystem : EntitySystem
[Dependency] private readonly IRobustRandom _robustRandom = default!;
// funny
[ValidatePrototypeId<DatasetPrototype>]
private const string Threats = "IonStormThreats";
[ValidatePrototypeId<DatasetPrototype>]
private const string Objects = "IonStormObjects";
[ValidatePrototypeId<DatasetPrototype>]
private const string Crew = "IonStormCrew";
[ValidatePrototypeId<DatasetPrototype>]
private const string Adjectives = "IonStormAdjectives";
[ValidatePrototypeId<DatasetPrototype>]
private const string Verbs = "IonStormVerbs";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberBase = "IonStormNumberBase";
[ValidatePrototypeId<DatasetPrototype>]
private const string NumberMod = "IonStormNumberMod";
[ValidatePrototypeId<DatasetPrototype>]
private const string Areas = "IonStormAreas";
[ValidatePrototypeId<DatasetPrototype>]
private const string Feelings = "IonStormFeelings";
[ValidatePrototypeId<DatasetPrototype>]
private const string FeelingsPlural = "IonStormFeelingsPlural";
[ValidatePrototypeId<DatasetPrototype>]
private const string Musts = "IonStormMusts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Requires = "IonStormRequires";
[ValidatePrototypeId<DatasetPrototype>]
private const string Actions = "IonStormActions";
[ValidatePrototypeId<DatasetPrototype>]
private const string Allergies = "IonStormAllergies";
[ValidatePrototypeId<DatasetPrototype>]
private const string AllergySeverities = "IonStormAllergySeverities";
[ValidatePrototypeId<DatasetPrototype>]
private const string Concepts = "IonStormConcepts";
[ValidatePrototypeId<DatasetPrototype>]
private const string Drinks = "IonStormDrinks";
[ValidatePrototypeId<DatasetPrototype>]
private const string Foods = "IonStormFoods";
private static readonly ProtoId<DatasetPrototype> Threats = "IonStormThreats";
private static readonly ProtoId<DatasetPrototype> Objects = "IonStormObjects";
private static readonly ProtoId<DatasetPrototype> Crew = "IonStormCrew";
private static readonly ProtoId<DatasetPrototype> Adjectives = "IonStormAdjectives";
private static readonly ProtoId<DatasetPrototype> Verbs = "IonStormVerbs";
private static readonly ProtoId<DatasetPrototype> NumberBase = "IonStormNumberBase";
private static readonly ProtoId<DatasetPrototype> NumberMod = "IonStormNumberMod";
private static readonly ProtoId<DatasetPrototype> Areas = "IonStormAreas";
private static readonly ProtoId<DatasetPrototype> Feelings = "IonStormFeelings";
private static readonly ProtoId<DatasetPrototype> FeelingsPlural = "IonStormFeelingsPlural";
private static readonly ProtoId<DatasetPrototype> Musts = "IonStormMusts";
private static readonly ProtoId<DatasetPrototype> Requires = "IonStormRequires";
private static readonly ProtoId<DatasetPrototype> Actions = "IonStormActions";
private static readonly ProtoId<DatasetPrototype> Allergies = "IonStormAllergies";
private static readonly ProtoId<DatasetPrototype> AllergySeverities = "IonStormAllergySeverities";
private static readonly ProtoId<DatasetPrototype> Concepts = "IonStormConcepts";
private static readonly ProtoId<DatasetPrototype> Drinks = "IonStormDrinks";
private static readonly ProtoId<DatasetPrototype> Foods = "IonStormFoods";
/// <summary>
/// Randomly alters the laws of an individual silicon.

View File

@@ -1,44 +1,23 @@
using System.Text.RegularExpressions;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Speech;
namespace Content.Server.Speech
namespace Content.Server.Speech;
public sealed class AccentSystem : EntitySystem
{
public sealed class AccentSystem : EntitySystem
public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?‽])(?![\.!\?‽])", RegexOptions.Compiled);
public override void Initialize()
{
public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?‽])(?![\.!\?‽])", RegexOptions.Compiled);
public override void Initialize()
{
SubscribeLocalEvent<TransformSpeechEvent>(AccentHandler);
}
private void AccentHandler(TransformSpeechEvent args)
{
var accentEvent = new AccentGetEvent(args.Sender, args.Message);
RaiseLocalEvent(args.Sender, accentEvent, true);
args.Message = accentEvent.Message;
}
SubscribeLocalEvent<TransformSpeechEvent>(AccentHandler);
}
public sealed class AccentGetEvent : EntityEventArgs
private void AccentHandler(TransformSpeechEvent args)
{
/// <summary>
/// The entity to apply the accent to.
/// </summary>
public EntityUid Entity { get; }
var accentEvent = new AccentGetEvent(args.Sender, args.Message);
/// <summary>
/// The message to apply the accent transformation to.
/// Modify this to apply the accent.
/// </summary>
public string Message { get; set; }
public AccentGetEvent(EntityUid entity, string message)
{
Entity = entity;
Message = message;
}
RaiseLocalEvent(args.Sender, accentEvent, true);
args.Message = accentEvent.Message;
}
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Server.Speech.Components;
/// <summary>
/// Suppresses emotes with the given categories or ID.
/// Additionally, if the Scream Emote would be blocked, also blocks the Scream Action.
/// </summary>
[RegisterComponent]
public sealed partial class EmoteBlockerComponent : Component
{
/// <summary>
/// Which categories of emotes are blocked by this component.
/// </summary>
[DataField]
public HashSet<EmoteCategory> BlocksCategories = [];
/// <summary>
/// IDs of which specific emotes are blocked by this component.
/// </summary>
[DataField]
public HashSet<ProtoId<EmotePrototype>> BlocksEmotes = [];
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Speech.Components;
using Content.Shared.Speech;
namespace Content.Server.Speech.EntitySystems
{

View File

@@ -1,4 +1,5 @@
using Content.Server.Speech.Components;
using Content.Shared.Speech;
using Robust.Shared.Random;
namespace Content.Server.Speech.EntitySystems

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
using Content.Shared.Speech;
namespace Content.Server.Speech.EntitySystems;

View File

@@ -4,6 +4,7 @@ using Content.Server.PowerCell;
using Content.Shared.Speech.Components;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Speech;
using Robust.Shared.Random;
namespace Content.Server.Speech.EntitySystems;

View File

@@ -0,0 +1,41 @@
using Content.Server.Speech.Components;
using Content.Shared.Emoting;
using Content.Shared.Inventory;
namespace Content.Server.Speech.EntitySystems;
public sealed class EmoteBlockerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmoteBlockerComponent, BeforeEmoteEvent>(OnEmoteEvent);
SubscribeLocalEvent<EmoteBlockerComponent, InventoryRelayedEvent<BeforeEmoteEvent>>(OnRelayedEmoteEvent);
}
private static void OnRelayedEmoteEvent(Entity<EmoteBlockerComponent> entity, ref InventoryRelayedEvent<BeforeEmoteEvent> args)
{
OnEmoteEvent(entity, ref args.Args);
}
private static void OnEmoteEvent(Entity<EmoteBlockerComponent> entity, ref BeforeEmoteEvent args)
{
if (entity.Comp.BlocksEmotes.Contains(args.Emote))
{
args.Cancel();
args.Blocker = entity;
return;
}
foreach (var blockedCat in entity.Comp.BlocksCategories)
{
if (blockedCat == args.Emote.Category)
{
args.Cancel();
args.Blocker = entity;
return;
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Speech.Components;
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
using Content.Shared.Speech;
namespace Content.Server.Speech.EntitySystems;
@@ -32,7 +33,7 @@ public sealed class FrenchAccentSystem : EntitySystem
// spaces out ! ? : and ;.
msg = RegexSpacePunctuation.Replace(msg, " $&");
// replaces th with 'z or 's depending on the case
foreach (Match match in RegexTh.Matches(msg))
{

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
using Content.Shared.Speech;
namespace Content.Server.Speech.EntitySystems;

View File

@@ -1,7 +1,8 @@
using System.Text;
using Content.Server.Speech.Components;
using Robust.Shared.Random;
using System.Text.RegularExpressions;
using Content.Server.Speech.Components;
using Content.Shared.Speech;
using Robust.Shared.Random;
namespace Content.Server.Speech.EntitySystems;

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