Stable upstream sync (#1266)

* Add some salvage ruins (#36814)

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

* Amber Station - Added Genpop (#36997)

* Automatic changelog update

* MapManager warning cleanup on tests (#36940)

lets see if this works.

* Clear MIDI masters properly to avoid replay freezes (#36809)

While trying to play a replay I noticed that the replay would freeze
when seeking in some cases. After some debugging, I discovered that two
MIDI renderers had each other as master, which caused an infinite loop
processing MIDI events.

I'm not entirely sure of the sequence of events that leads to this
during replay playback, but I did notice that MIDI render masters are
never set to null. This is in the best case just a memory leak, in the
worst case probably the source of the bug, so... I fixed that.

* make scrubber widenet in panic mode (#37013)

* Automatic changelog update

* Shoulder-length hairstyles resprite (#37000)

* sprite

* final

* final (final fr)

* Automatic changelog update

* Fix mail cutting thinking your arms are infinite length (#37019)

* init

* hand

* Automatic changelog update

* Xenoborgs part 2 (#36844)

* add lawsets for the xenoborgs and mothership core

* add xenoborg names

* add xenoborg radio

* add xenoborg device frequency

* add xenoborg access

* add xenoborg contraband

* Update Resources/Locale/en-US/station-laws/laws.ftl

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>

* add xenoborg access to the universal Id card and universal access config

* remove 6th law of xenoborg and mothership lawset (got jointed into the 5th law)

* added xenoborg and mothership law boards

* add more names

* add Xenoborg faction

* moved all lawboards into a separate yml file

* removed custom xenoborg contraband severity

* add Xenoborg and Mothership components

* add xenoborg laser guns

* add self recharging fire extinguisher

* add mothership pinpointer

* add material bag

* add infinite jetpack

* add a only blue energy dagger

* add xenoborg jammer

* add refueling welding tool

* add nocturine hypo

* add nuclear small power cell

* add cloaking device

* add xenoborg door remote

* add custom sprites for xenoborg modules

* add custom sprites for xenoborg module actions

* removed Xenoborg Comp until is actually needed

* add xenoborg module tags

* spelling

* add xenoborg module bases

* organazied xenoborg modules sprites better

* add generic xenoborg modules

* add heavy xenoborg modules

* add engi xenoborg modules

* small fix to meta file in actions_borg.rsi

* renamed mothership comp to XenoborgMothership

* fixed the base for the xenoborg engi modules

* add scout xenoborg modules

* add stealth xenoborg modules

* localization for names and descriptions of the xenoborg modules

* fixed issues related to the XenoborgMothership component

* revert localization (it wasn't working for some reason)

* fixes

* fixed issue with container slot in the cloaking device

* Update description of small capacity nuclear power cell

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

* Fix indentation in material bag

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

* Spelling

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

* fix parameter order in some prototypes

* rename proto id InfiniteJetpack to JetpackXenoborg

* localize pinpointer targets

* Revert "localize pinpointer targets"

doesn't work

* added lines in the end of files (and in the middle of one)

* reorder paramenter in some entities

* fixed some descriptions

* minor fixes

---------

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

* Disown atmos and botany (#37017)

* Disown atmos and botany

* Update .github/CODEOWNERS

---------

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

* Fix t-ray scanner exception spam (#37018)

* Automatic changelog update

* Remove Contact Slowdown when Weightless or in the Air (#33299)

* removed contact slowdowns from entities that are weightless or in the air

* fixed kudzu not applying contact slowdown to airbone entities

* revert kudzu fix

* reimplemented kudzu fix with bool datafield

* update variable serialization format

Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>

* empty commit

* cleaned up and added documentation

* cached airborne check

* rerun tests

* minor review

---------

Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>

* Automatic changelog update

* Theatre access to Service Request Computer (#37003)

* Automatic changelog update

* BatteryWeaponPowerCell tweaks (#33500)

* BatteryWeaponPowerCell tweaks

* add update ammo ev & shuttle guns tweaks

* MilonPL requested changes

* revert changes in OnPowerCellChanged

* Add events to get charge info & change current charge

---------

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

* Buffing slugs and replacing beanbags from the Bulldog bundle (#33517)

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

* Automatic changelog update

* Rebalance magnet debris, update worldgen (#37025)

* Automatic changelog update

* Display obvious plant mutations in examine text (#32650)

* Effect mutations now display on examine

* ChangeSpecies shouldn't stay on the list after running. Name cleanup

* EmoGarbage Review - convert description to LocId and add minor logic fix

* fix the dastardly yaml

---------

Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>

* Automatic changelog update

* SSD sleep take 2 (#34039)

* ssd sleep part 2

* forgot this

* apply review

* yeah

* add onmapinit

* cache cvar values

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>

* Automatic changelog update

* More Mail Sprites (#37023)

* Commit

* Sprite update

* Last push for real for real

* Final Commit for real for real for real

* oasis genpop (#37031)

* Automatic changelog update

* re-buffs proto kinetic accelerator (#37012)

* new ruins

* genpop

* nvm

* abc

* fuckin

* turnstyle

* ruins

* stamp

* abc

* bruh

* efg

* pka fixed

* range tweak

* Automatic changelog update

* Chem master more unit transfer buttons (#36995)

* Changes chem master unit transfers to be the same as the chem dispenser

* adds chem master transfer buttons for 15u, 20u, and 30u

* Automatic changelog update

* Add borders to the asteroid sand (#35397)

* tiles

* Fix

* borderless

* Astrosand

* Add tile

* Fix

* Push horn  (#36009)

* Empty commit

* epic super duper cool fr push horn draft

* whoops turns out theres a system that does that thingi already x.x

* bunch of like fixis and generalization

* general progress

* most stuffies done

* last thingi hopefully

* small fixies, mostly preventing bypassing the delay by spamming

* rename to fit better

* rename for real i forgor to add

* weird fixie but last commit didn workie

* oki shold be fine now

* lastish cleanup

* fixies

* missed a space

* removed unnecessary component check

* getting the typos out of the way first

* moved the component to shared

* rest of fixies

* Automatic changelog update

* Fix solution visualization after drawing with a whitelist (#36657)

* Fix clientsided alerts being overwritten by server (#37033)

Initial commit

* Automatic changelog update

* Added Space Carp Tooth Arrows and Sharkminnow Spears, buffs sharkminnow teeth. (#31257)

* carp arrow, sharkminnow tooth spear

* review

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>

* Automatic changelog update

* Add inhand sprites for mini jetpack (#37041)

* Inhand sprites for mini jetpack

* Attribution

* Show other speso colours, add larger denominations (Frontier#1496) (#37030)

* Automatic changelog update

* Added warning when attempting to run RUN_THIS on a zip repo download (Attempt 2) (#36922)

* Added warning when attempting to run RUN_THIS on a zip repo download

* Fix it actually

* Update git_helper.py

* Add toolbox sound effects (#37048)

* Automatic changelog update

* Mail visual update (#37049)

* Automatic changelog update

* Un-copypaste wallmount substation prototype to give them a UI (#37047)

*sigh*

* Automatic changelog update

* Centcomm carapace, moved armour from vests.yml to armor.yml (#35301)

* centcomm carapace, loadout for CC official, move armoured vests from vests.yml to armor.yml

* revert rename from previous commit; command carapace returned to captain's carapace

* meta.json 4 space

* restored deleted comments for some armours and original description for slim armour

* Add recently added elite web vest

---------

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

* Automatic changelog update

* Fix NPCs stalling when too many exist (#37056)

Fix NPC stalling when too many exist

* Automatic changelog update

* feex oasis portal (#37058)

* fix so that the meat patty uses the meat patty sprite on custom burgers (#37064)

* Fix examine prediction (#36999)

If our current ID is different to the received one we should just discard it even if it's for the same entity.

Note that this doesn't quite fix my prediction issue as client and server are using slightly different timings for the event.

* add StationTrackerComponent (#36803)

* maybe I am cooking

* logmissing

* copy paste oops

* add some stuff

* review

* fix

* rerun tests

---------

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

* Minor ReflectionSystem refactor (#37039)

* ReflectComponentLogicFix

Added bool InRightPlace and updated relevant system

* Using SlotFlags

* edits

* refactor

* add missing relay

---------

Co-authored-by: BIGZi0348 <svalker0348@gmail.com>

* Automatic changelog update

* Update submodule to 255.1.0 (#37071)

* new salv ruins (#36947)

* add new salv ruin: telesci (#36653)

* Automatic changelog update

* Fix turnstile collision with firelocks (#37074)

* Automatic changelog update

* Skeletons leave glove fiber evidence (#37077)

Fix skeleton forensics

* Automatic changelog update

* Fix action ent prediction (#37076)

Buttons don't get created in the predicted methods only on state handler.

* Automatic changelog update

* Make universal access config better (#37079)

* add a universal ID card to the universal access config

* make the admin access config have infinite range

* now checks for the BypassInteractionRange Tag instead

* Add suggested fix to the AccessOverrider system

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

* remove other stuff I added

* another suggested change to avoid weird behaviour where you can use it from a distance

---------

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

* Automatic changelog update

* Fix listcontainer constantly disposing children (#37089)

* Fixes battery weapons changing firemode on wield (#37085)

Credit to Happyrobot33 for the implementation

Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>

* Automatic changelog update

* cleanup snipers.yml (#37094)

* cleanup revolvers.yml (#37095)

* Localizable craftmenu (#32339)

* Now the name of the target craft items is taken directly from the prototypes

* Deleting unnecessary fields

* Deleting unnecessary fields

* Added suffix field

* Added override via localization keys

* My favorite ItemList and TextureRect have been replaced with ListContainer and EntityPrototypeView

* Fix suffix

* Fix construction ghosts... maybe

* Remove suffix from UI

* Suffixes have been removed from prototypes

* Added a description for the secret door

* Fix search..?

* The Icon field of ConstructionPrototype has been removed

* StackPrototypes used in the construction menu have been localized

* TagConstructionGraphStep used in the construction menu have been localized

* The search bar has been localized

* Fix localization and prototypes

* Recipes are now only loaded when the crafting window is opened.

* Fix crooked merge grid of the crafting menu.

* Localization update

* Fix cyborg graph

* Revert "Recipes are now only loaded when the crafting window is opened."

This reverts commit 97749483542c2d6272bda16edf49612c69a0761a.

* Fix loc

* fix merge

* Fix upstream

* Some of the logic has been moved to Shared

* fix

* Small adjustments

* Very small change

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>

* Vox now can eat trash other other inedible things (#35681)

* EAT TRASH EXHALE AMMONIA

Update to files

* Forgot to port this file

* 1D4 Guidance

* Summary (required)

Suggested changes and fixes

* Cries in conflicts

* Why do we exist? To suffer?

* 1 citation for being stupid

* THE ANTIDOTE, THE ANTIDOTE FOR THE POISON

* I was inverted sir

* vox organs cleanup

* vox reagents

* guidebook

* weh

---------

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

* Automatic changelog update

* Fix food slicing showing utensil popup (#37105)

Fix knife slicing showing utensil popup

* Select current target in mailing unit UI, prevent UI jumping (#37098)

* Atmos air (6500 kPa) marker (#37061)

* air GM atmosphere

* atmos fix air miner

* Automatic changelog update

* Updates the Pirate Captain Hardsuit Helmet light sprites. (#37027)

* Add noir glasses (#36923)

hardboiled

* Automatic changelog update

* Overhauled stamina slowdown behavior (#36336)

* Automatic changelog update

* Fix vending machine manager wire error (#37100)

Skip updating amounts for removed entries

* fix asteroid tiles (#37103)

aw

* Pointing arrow smite no longer lasts forever (#37102)

fix: pointing arrow smite no longer lasts forever

* Automatic changelog update

* Wizard Helmet in the Magic Vend (#37084)

Whats the limit for stuff you can put in a commit message lol

* Automatic changelog update

* fix logic gate draw depth (#37053)

fix logic gate layering

* Species are now picked at random in the developer environment! (#37057)

* Various changes to random species in dev

* This should be split

* Mob Movement Major Refactor (#36847)

* Conveyor optimisations

- Optimise movement for moving stuff. Better flags + less resolves + slapped parallelrobustjob on it.
- Sleeping for entities getting conveyed into walls.

* Blocker version

* Finish

* Final

* Fix conveyor power mispredict

* Bagel save

* Revert "Bagel save"

This reverts commit 1b93fda81fb852d89b89b0beae0b80f8a61165f2.

* Conveyor resave

* Init Commit

* windows yelling at me to update commit

* working commit, need prediciton and more dehardcoding

* Project 0 warnings

* Working Commit (Near Final)

* ryder got confused commit

* I love Merge Conflicts :)

* Working commit, no prediction

* Forgot the yaml changes

* Comments and typos

* Apparently while the reduced launch mult of lube was initialized it was never used so I revered back to default

* Fixed an incorrect divisor

* bit of cleanup

* Prediciton fixed, and puddles now affect all entities

* FORGOT TO RENAME A VERY IMPORTANT VARIABLE OOPS

* Really big I forgor moment

* Even bigger I forgor moment

* four more merge conflicts to fix four more oopsies

* fixed actual divide by zero moment and also im very dumb

* Even bigger I forgor moment

* four more merge conflicts to fix four more oopsies

* fixed actual divide by zero moment and also im very dumb

* Fix all test fails

* code cleanup

* Webedit whitespace

* Code cleaup

* whitespace webedit

* whitespace webedit

* whitespace webedit

* whitespace removal

* Comments and cleanup

* Re-Added 20 warnings as per Ork's request

* Cleanups

* Spacing fix

* bugfixes and cleanup

* Small bugfix

* Fix prediction

* Mob movement rewrite

* Bandaid

* Working version

* Tentatively working

* Friction to fix cornering

* More fixes

* Refactor mob movement

Trying to cleanup relay ordering / tryupdaterelative being cooked, purge ToParent, and fix all the eye rotation shenanigans.

* Building

* Re-implement jetpacks

* Reorganise weightless movement

* More work

* Fix camera

* reh

* Revert bagel

* Revert this

* Revert held move buttons

* Puddles work but are unpredicted and unoptimized

* Fixes

* Puddle code...

* Actually dirty the slipComp for real

* Sliding component done plus an extra suggestion from ArtisticRoomba

* Atomized Commit

* Added Friction field to Reagent Prototype per design discussion

* Cleaned up Working Commit

* a

* Delete stinkers

* Fix this code smell

* Reviewed

* Funky re-save

* Our conveyance

* Better conveyor sleeping

* Remove this

* Revert "Better conveyor sleeping"

This reverts commit f5281f64bbae95b7b9feb56295c5cf931f9fb2e1.

* Revert that

Way too janky

* Also this

* a

* Working Commit - Still a lot to do

* Acceleration refactor

* Minor jetpack cleanup

* frictionnomovement no longer nullable

* Shared Mover Feels 99% done

* OffGrid/Weightless/Throwing Friction saved

* Fix merge conflicts

* Fix a debug assert

* Final Commit for today

* Some fixes

* Actually use those CCVars Properly

* Need to fix throwing

* Second to last Commit for real

* Jetpack bug fixed

* Jetpack bug fixed

* Test fail patch

* Small patch

* Skates Component cleanup + Bring Accel back to 5 (oops)

* Fix test fail oops

* yaml cleanup make dragons not fat

---------

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

* Automatic changelog update

* Predicted internals (#33800)

* Predicted gas pumps

I wanted to try out atmos and first thing I found.

* a

* Atmos device prediction

- Canisters
- Tanks
- Internals

AirMixes aren't predicted so nothing on that front but all the UIs should be a lot closer.

* Remove details range

* Gas tank prediction

* Even more sweeping changes

* Alerts

* rehg

* Popup fix

* Fix merge conflicts

* Fix

* Review

* Automatic changelog update

* Add condition support to entity tables (#36819)

* Fix AI movement (#37114)

Don't relay blocking anymore.

* Automatic changelog update

* New Salvage Ruin - Atmos Interchange (#37115)

* Automatic changelog update

* Fix station beacon not updating (#37121)

* Automatic changelog update

* Fix for vox dropping all bodyparts when gibbed (#37111)

vox parts cleanup

* Automatic changelog update

* Movement Rewrite Hotfix (#37122)

* One line bugfix

* also divide friction by 5

* Undo that

* Clarify that PA crate order includes boards (#37109)

init

* Automatic changelog update

* noRot on 2-way lever (#37125)

* Traumoxadone (#37126)

* init

* salicylic acid

* Automatic changelog update

* fix clone appearance (#37130)

* New Weapon: Knuckle Dusters (#33470)

* New Weapon: Knuckle Dusters

* Tag YAML Error Fix

* Crafting Graph Node Error

(Thank you slarticodefast)

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

* Crafting Node Error Part 2 Electric Boogaloo

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

* Contraban & QM Dusters Nerf

* Stun Knuckledusters (Unfinished)

* Typo

* Fix test fails

* The dastardly maintainer balance webedit

* Fix contraband parenting

* Fix construction failure

---------

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

* Automatic changelog update

* Renders reagent grinders over lights (#31218)

* Adds a new layer to DrawDepth.cs for use with objects similar to the reagent grinder and properly summarises its uses

* applies new layer in DrawDepth.cs to reagent_grinder.yml

* Fix merge conflict

* oops

* fix atmos grid markers (#37142)

* fix atmos grid markers

* 1984 CL

* randomize names for mindshielded eventhumanoids (#37143)

* Automatic changelog update

* make node scanner don't show interface if scanned entity not a artefact (#37146)

* a

* a

* Revert "a"

This reverts commit 2b9ba4ea67a9395d30b7ab37c8065f627f1a961a.

* Fix some maintenance doors not using wires configuration + cleanup + minor changes (#36735)

* Kill useless maints doors + general fixes

* Migrate the common maints airlock

* Fix parenting

* Service Theatre

* migrate out the other removed airlock

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>

* Automatic changelog update

* Fix throwing prediction (#37086)

* Fix throwing prediction

- Disposals is still janky but I think that's disposals in general not being predicted and the disposals throw not being predicted and short-lived.
- Would need to check RMC.
- Couldn't repro the underlying issues however thrown items don't slip anymore so (and we also don't predict their land / stopping anymore so).

* primary constructor

---------

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

* Explosions warnings cleanup (#36167)

* Explosions warnings cleanup

* Revert unnecessary change

* Cleaner Entity instantiation

* Remove conflicting changes with #36098

* ClothingOuterEVASuitSyndicate (#36738)

* ClothingOuterEVASuitSyndicate

* Update migration.yml

* Resolve merge conflict in migration.yml

---------

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

* Sentry turrets - Part 5: Reuseable UI components (#35149)

* Initial commit

* Updated how monotone buttons are styled

* Removed unnecessary textures

* Updated attributions

* Addressing reviewer comments

* Adjusted monotone checkbox styling

* Revert "Resprited the Chief Engineer's mantle/manica" (#37060)

Revert "Resprited the Chief Engineer's mantle/manica (#36697)"

This reverts commit d0c2a64436.

* Fix skeletons spawning in folded body bags (#37151)

* Fix skeleton spawning

* Add comments

* Fix the comments

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

---------

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

* Automatic changelog update

* Replace uplink thieving gloves with chameleon thieving gloves (#36369)

* replace thieving gloves with chameleon thieving gloves

* increase TC cost by one

* add a TODO comment

* Automatic changelog update

* Omega Update (Genpop) (#37157)

* Automatic changelog update

* Add more ruins (#37161)

* Update Credits (#37163)

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

* Salvage Threat: Gibtonite (#37160)

* Automatic changelog update

* Revert "add material composition to some salv treasure" (#37149)

* Revert "add material composition to some salv treasure (#31970)"

This reverts commit 125258ea48.

* Remove ring materials as well

* Automatic changelog update

* Don't despawn off-grid salv mob corpses (#37169)

* Automatic changelog update

* Bagel Station - Removed Gamer Loot (#37171)

* Automatic changelog update

* Update GDPR erase script to latest DB schema (#37162)

* Remove update from DeviceLinkSystem (#37152)

The tick updates were purely used to decrease the invoke counter once per tick. Now instead we just calculate the effective counter value with some trivial math on the tick number. This completely removes the need for an update function.

The relative tick is not stored to map files. If we really need this, we can add a TickOffsetSerializer (similar to TimeOffsetSerializer), but I doubt it matters.

* Refactor magic speak system to be a component added to actions (#36328)

* Allow shelves to be placed rotated when built by hand (#37186)

fix: shelves can be placed rotated when built by hand

* Automatic changelog update

* Fix for Whoopie Cushions (Fixes #32028) (#36984)

* Fixes whoopie cushions so they no longer launch you into oblivion when stacked in a large pile.

* Update Resources/Prototypes/Entities/Objects/Fun/toys.yml

* streamlined the components for the prototype

---------

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

* Automatic changelog update

* Salv Ruins Again (#37193)

* Fix `ComponentTogglerSystem` deletion error (#37198)

Don't try to toggle if target is terminating or deleted

* Fland Update (Genpop) (#37207)

* Automatic changelog update

* Convex genpop update (#37216)

* Automatic changelog update

* Ghost friction fix (#37124)

* commit

* fix for real

* More filters for station records (#37213)

* Automatic changelog update

* Removing redundant trash tags on medipens (#37215)

removes redundant tag components

* StaminaSystem to SharedStaminaSystem (#37199)

* Init Commit

* Partial class

* Hands system slipped through

* AI Law Board File Clean Up (#37195)

* Allow Pacifists to Use Bola (#37188)

* Automatic changelog update

* Fix dough rolling (#37183)

* Automatic changelog update

* Fix effects of hosted anomaly when transform is parented (#37179)

* hosted anomaly effects now are at the correct location when the host is in a container or buckled

* oops, not keeping the uid and transform together

* use world positions to get the position of the anomaly -- my previous method was reinventing the wheel.

* Automatic changelog update

* Split out the CloneComponents into its own method (#37155)

* Split out the CloneComponents into its own method

* CR - Move some extra info in

- add TryComp for status effects

- Move some remcomps around

- Make Special event raising components to handle special
components that reference entities that have ownership

* CR - Extra recommendation on the prototype

thanks slarti

* Solve the yaml linter problem

* CR - Typos, grammar and some extra Status effect

* cleanup

---------

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

* Fix brains, borgs etc not counting as marooned (#37148)

* init

* comments

* comment

* no more debug

* Add collapse button to lobby right panel (#37140)

* Add collapse button to lobby right panel

* Half sized buttons

* Automatic changelog update

* Port fancy speech bubbles (#29349)

* Automatic changelog update

* Water bottle dispenser fix & Bottle Yaml Organizing (#37108)

* A New Parent/Category For The Soda & Tonic Water Bottle

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* New Parent For water bottle & Cleanup

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* More Cleanup

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Details

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Organizing For Additions

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Streamlining waterbottle to be compatible with DrinkBottleVisualsAll Parent

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Replacing/Renaming Parents

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* New Tonic/Soda Water Bottle Sprites

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Misc Fixes

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* New Parent for Small Glass Bottles & Organizing

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Organizing & uSize Parity.

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* File Parity

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Merged Categories & Misc Fixes

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Removed Silly

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Parent Name Parity & Cleanup

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Large Glass Bottles Category & Cleanup

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

---------

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>

* Automatic changelog update

* Ichor double-metabolize fix + Very minor cleanup (#37107)

Not a yaml vacation apparently

* Darken reptilian eye sockets to reduce the effect of mixels (#37082)

Initial commit

* Fixed holy water metabolism rate (#37106)

* Fixed holy water metabolizing at 1u instead of .5u a second

* Update Resources/Prototypes/Reagents/medicine.yml

---------

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

* Automatic changelog update

* Make departmental orders consoles print slips (#36944)

* Make departmental orders consoles print slips

* feed back cycle

* Automatic changelog update

* Fix borg soap (#36961)

* Create SoapBorg and replace SoapNT with SoapBorg in BorgModuleCustodial

* Reparent SoapBorg

No longer a container so the soap reagent cannot be extracted from the soap, no longer a food, no longer slippery since it can't be removed from the borg to be placed on the ground

* Move SoapBorg to end of soap.yml

* correct comp order

* fix failing tests

* Automatic changelog update

* Cryotube draw-depth (#37240)

* removes changing draw depth when occupied

* changes collision to square

* small texture changes
fixes slight perspective size when comparing side columns to the direct column facing the camera
removes the baked in pipe exit (the yml's sprite adds it anyway)

* Fire damage system fixes (#37241)

Fire fixes

* Automatic changelog update

* Make animals drop giblets into container or floor when they inserted into container (#37228)

* a

* Revert "a"

This reverts commit 2b9ba4ea67a9395d30b7ab37c8065f627f1a961a.

* auausasuasuausuuAUSTRALIA!!!!!!aausuasusdasda

* 77+33!=100

* Moth displacement maps (#37231)

moth displacement maps

* Automatic changelog update

* Four-way pipe junction, swapping junction construction fix (#37092)

* pipe x-junction assets, yml, construction

* remove duplicate asset

* attribution

* x junction instead of junctioncross for utilities.yml

* Automatic changelog update

* Raises max chest markings for all species (except Reptilian) to 2 (#37065)

* Update arachnid.yml

* Update diona.yml

* Update human.yml

* Update moth.yml

* Update slime.yml

* Update vox.yml

* Automatic changelog update

* make throw insert container code more clear (#36873)

make throw insert container more clear

* fix pka admin log (#37255)

* Genpop closet cargo orders (#37237)

* Genpop closet cargo orders

* change icon to locker base

* Automatic changelog update

* Make container draw disableble for mob-affecting Hyposprays (#30683)

* Seperate container draw from affects mobs

* Spaces

* More spaces

* Fix toggle

* Use better ands

* Reorder checks for Performance™️

---------

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

* Fix chairs deleting players (#37261)

Unbuckle entities when a strap is about to be deleted

* Add 3 new Exomorph posters (#37260)

Initial commit

* Automatic changelog update

* AI context menu fix (#37224)

* AI context menu fix

* Revert "AI context menu fix"

This reverts commit 86a0476fcb0aa952c0dcadb1bc4246532abd62b7.

* Better implementation

* Retry

* Automatic changelog update

* Cleanup warnings: CS0414 (#36950)

* Clean up

* Use #pragma

* Cleanup warnings: CS8321, CS0105, CS0168 (#36949)

* Clean up

* CS0168

* Cargo request and bounty console deny sound cooldown (#37234)

* Cargo bounty console deny sound cooldown

* ordering computer cooldown

* Update Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs

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

* Update Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs

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

* Update Content.Server/Cargo/Systems/CargoSystem.Bounty.cs

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

* AutoGenerateComponentPause

---------

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

* Move random species selection earlier in player spawning logic (#37258)

* Select random species earlier in spawning logic

* ternary operator

* Move it even earlier to fix more bugs

* For DamagedSiliconAccent use Destructible threshold for default "DamageAtMaxThreshold" (#37252)

* set DamageAtMaxCorruption as nullable with null default and use destructible trigger threshold for this if null.

* fix documentation

* these really don't need to be passed by reference

* Small InventorySystem.Equip Unequip Reason bugfix (#37265)

Fix small bug

* Amber Station - Security and AI Sat Overhaul (#37262)

* Automatic changelog update

* Fix debug asserts in WoolySystem and UdderSystem (#35314)

* Implement Rules amendment (#37200)

* Implement Rules amendment

* Update Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml

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

* Update RuleR9MassSabotage.xml

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>

* Fix borg chassis gibbing not dropping items (#37276)

BeingGibbedEvent and TryEjectPowerCell

* Automatic changelog update

* Revert "Traumoxadone (#37126)"

This reverts commit efc8d8600d.

* Add ratelimit retry to discord changelog bot and continue publish changelog error. (#37051)

* Add ratelimit retry to discord changelog bot and continue publish changelog error.

oops we missed some changelogs cause of this... this should prevent anything funny

* Update actions_changelogs_since_last_run.py

* fuck the cl

* Tweaks to the push horn so its less of a shitter tool (#37281)

* Empty commit

* tweaks to the push horn

* forgor to change the delay

* keeping the speed the same for now

* Automatic changelog update

* [HOTFIX] Ensure that mobs wake up when zombified (#37346)

zombie sleep fix

* Update CP14SharedMagicSystem.cs

* Update LockSystem.cs

* remove desc name icon from recipes

* Update ContentLocalizationManager.cs

* Update walls.yml

* part of fixes

* gf

* f

* Update asteroid.yml

* s

---------

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: KingFroozy <140668342+KingFroozy@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kirus59 <145689588+Kirus59@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Stomf <5dorkydorks@gmail.com>
Co-authored-by: drakewill-CRL <46307022+drakewill-CRL@users.noreply.github.com>
Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com>
Co-authored-by: abadaba695 <spacestation13thingy@gmail.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: BIGZi0348 <svalker0348@gmail.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: Matthew Herber <32679887+happyrobot33@users.noreply.github.com>
Co-authored-by: Ertanic <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: YoungThug <ramialanbagy@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: 3nderall <101940324+3nderall@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: J <billsmith116@gmail.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: youtissoum <51883137+youtissoum@users.noreply.github.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: Spanky <scott@wearejacob.com>
Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: brainfood1183 <113240905+brainfood1183@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: B_Kirill <153602297+B-Kirill@users.noreply.github.com>
Co-authored-by: archee1 <archee3@hotmail.co.uk>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Krunklehorn <42424291+Krunklehorn@users.noreply.github.com>
Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+Flareguy@users.noreply.github.com>
Co-authored-by: Zalycon <84675130+Zalycon@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
Red
2025-05-26 12:45:00 +03:00
committed by GitHub
1246 changed files with 155697 additions and 65895 deletions

View File

@@ -2,6 +2,7 @@
concurrency:
group: publish-testing
cancel-in-progress: true
on:
workflow_dispatch:

View File

@@ -2,6 +2,7 @@ name: Publish
concurrency:
group: publish
cancel-in-progress: true
on:
workflow_dispatch:
@@ -48,12 +49,14 @@ jobs:
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
- name: Publish changelog (Discord)
continue-on-error: true
run: Tools/actions_changelogs_since_last_run.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
- name: Publish changelog (RSS)
continue-on-error: true
run: Tools/actions_changelog_rss.py
env:
CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}

View File

@@ -5,6 +5,7 @@ import subprocess
import sys
import os
import shutil
import time
from pathlib import Path
from typing import List
@@ -104,7 +105,21 @@ def reset_solution():
with SOLUTION_PATH.open("w") as f:
f.write(content)
def check_for_zip_download():
# Check if .git exists,
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if not os.path.isdir(os.path.join(cur_dir, ".git")):
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"
"Please read and follow https://docs.spacestation14.com/en/general-development/setup/setting-up-a-development-environment.html \n"
"If you just want a Sandbox Server, you are following the wrong guide! You can download a premade server following the instructions here:"
"https://docs.spacestation14.com/en/general-development/setup/server-hosting-tutorial.html \n"
"Closing automatically in 30 seconds.")
time.sleep(30)
exit(1)
if __name__ == '__main__':
check_for_zip_download()
install_hooks()
update_submodules()

View File

@@ -202,6 +202,7 @@ namespace Content.Client.Actions
return;
OnActionAdded?.Invoke(actionId);
ActionsUpdated?.Invoke();
}
protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
@@ -210,6 +211,7 @@ namespace Content.Client.Actions
return;
OnActionRemoved?.Invoke(actionId);
ActionsUpdated?.Invoke();
}
public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Alert;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -15,6 +16,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
public event EventHandler? ClearAlerts;
public event EventHandler<IReadOnlyDictionary<AlertKey, AlertState>>? SyncAlerts;
@@ -27,6 +29,12 @@ public sealed class ClientAlertsSystem : AlertsSystem
SubscribeLocalEvent<AlertsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<AlertsComponent, ComponentHandleState>(OnHandleState);
}
protected override void HandledAlert()
{
_ui.ClickSound();
}
protected override void LoadPrototypes()
{
base.LoadPrototypes();
@@ -52,8 +60,24 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast)
return;
// Save all client-sided alerts to later put back in
var clientAlerts = new Dictionary<AlertKey, AlertState>();
foreach (var alert in alerts.Comp.Alerts)
{
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
{
if (alertProto.ClientHandled)
clientAlerts[alert.Key] = alert.Value;
}
}
alerts.Comp.Alerts = new(cast.Alerts);
foreach (var alert in clientAlerts)
{
alerts.Comp.Alerts[alert.Key] = alert.Value;
}
UpdateHud(alerts);
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Client.Atmos.EntitySystems;
public sealed class GasTankSystem : SharedGasTankSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
}
private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
{
bui.Update<GasTankBoundUserInterfaceState>();
}
}
}

View File

@@ -0,0 +1,32 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.NodeContainer;
namespace Content.Client.Atmos.Piping.Unary.Systems;
public sealed class GasCanisterSystem : SharedGasCanisterSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
}
private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? component = null, NodeContainerComponent? nodes = null)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(uid, GasCanisterUiKey.Key, out var bui))
{
bui.Update<GasCanisterBoundUserInterfaceState>();
}
}
}

View File

@@ -1,4 +1,7 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.IdentityManagement;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
@@ -32,22 +35,22 @@ namespace Content.Client.Atmos.UI
private void OnTankEjectPressed()
{
SendMessage(new GasCanisterHoldingTankEjectMessage());
SendPredictedMessage(new GasCanisterHoldingTankEjectMessage());
}
private void OnReleasePressureSet(float value)
{
SendMessage(new GasCanisterChangeReleasePressureMessage(value));
SendPredictedMessage(new GasCanisterChangeReleasePressureMessage(value));
}
private void OnReleaseValveOpenPressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(true));
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(true));
}
private void OnReleaseValveClosePressed()
{
SendMessage(new GasCanisterChangeReleaseValveMessage(false));
SendPredictedMessage(new GasCanisterChangeReleaseValveMessage(false));
}
/// <summary>
@@ -57,17 +60,21 @@ namespace Content.Client.Atmos.UI
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast)
if (_window == null || state is not GasCanisterBoundUserInterfaceState cast || !EntMan.TryGetComponent(Owner, out GasCanisterComponent? component))
return;
_window.SetCanisterLabel(cast.CanisterLabel);
var canisterLabel = Identity.Name(Owner, EntMan);
var tankLabel = component.GasTankSlot.Item != null ? Identity.Name(component.GasTankSlot.Item.Value, EntMan) : null;
_window.SetCanisterLabel(canisterLabel);
_window.SetCanisterPressure(cast.CanisterPressure);
_window.SetPortStatus(cast.PortStatus);
_window.SetTankLabel(cast.TankLabel);
_window.SetTankLabel(tankLabel);
_window.SetTankPressure(cast.TankPressure);
_window.SetReleasePressureRange(cast.ReleasePressureMin, cast.ReleasePressureMax);
_window.SetReleasePressure(cast.ReleasePressure);
_window.SetReleaseValve(cast.ReleaseValve);
_window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
_window.SetReleasePressure(component.ReleasePressure);
_window.SetReleaseValve(component.ReleaseValve);
}
protected override void Dispose(bool disposing)

View File

@@ -13,9 +13,6 @@ namespace Content.Client.Atmos.UI;
[UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private const float MaxPressure = Atmospherics.MaxOutputPressure;
[ViewVariables]
private GasPressurePumpWindow? _window;

View File

@@ -0,0 +1,24 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class InternalsSystem : SharedInternalsSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InternalsComponent, AfterAutoHandleStateEvent>(OnInternalsAfterState);
}
private void OnInternalsAfterState(Entity<InternalsComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.GasTankEntity != null && _ui.TryGetOpenUi(ent.Comp.GasTankEntity.Value, SharedGasTankUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -38,9 +38,10 @@
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<Control MinHeight="5" Name="OrdersSpacer"/>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="1">
SizeFlagsStretchRatio="1"
Name="Orders">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>

View File

@@ -208,6 +208,7 @@ namespace Content.Client.Cargo.UI
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name;
var account = _protoManager.Index(order.Account);
var row = new CargoOrderRow
{
@@ -219,7 +220,9 @@ namespace Content.Client.Cargo.UI
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
("productName", productName),
("orderAmount", order.OrderQuantity),
("orderRequester", order.Requester))
("orderRequester", order.Requester),
("accountColor", account.Color),
("account", Loc.GetString(account.Code)))
},
Description =
{
@@ -282,6 +285,9 @@ namespace Content.Client.Cargo.UI
AccountActionButton.Disabled = TransferSpinBox.Value <= 0 ||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
Orders.Visible = !orderConsole.SlipPrinter;
}
}
}

View File

@@ -11,11 +11,10 @@
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<Label Name="ProductName"
<RichTextLabel Name="ProductName"
Access="Public"
HorizontalExpand="True"
StyleClasses="LabelSubText"
ClipText="True" />
StyleClasses="LabelSubText" />
<Label Name="Description"
Access="Public"
HorizontalExpand="True"

View File

@@ -36,6 +36,7 @@ namespace Content.Client.Cargo.UI
{
var product = protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name;
var account = protoManager.Index(order.Account);
var row = new CargoOrderRow
{
@@ -47,7 +48,9 @@ namespace Content.Client.Cargo.UI
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
("productName", productName),
("orderAmount", order.OrderQuantity - order.NumDispatched),
("orderRequester", order.Requester))
("orderRequester", order.Requester),
("accountColor", account.Color),
("account", Loc.GetString(account.Code)))
},
Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))}

View File

@@ -16,6 +16,7 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
private readonly TimeSpan _typingTimeout = TimeSpan.FromSeconds(2);
private TimeSpan _lastTextChange;
private bool _isClientTyping;
private bool _isClientChatFocused;
public override void Initialize()
{
@@ -31,7 +32,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
return;
// client typed something - show typing indicator
ClientUpdateTyping(true);
_isClientTyping = true;
ClientUpdateTyping();
_lastTextChange = _time.CurTime;
}
@@ -42,7 +44,19 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
return;
// client submitted text - hide typing indicator
ClientUpdateTyping(false);
_isClientTyping = false;
ClientUpdateTyping();
}
public void ClientChangedChatFocus(bool isFocused)
{
// don't update it if player don't want to show typing
if (!_cfg.GetCVar(CCVars.ChatShowTypingIndicator))
return;
// client submitted text - hide typing indicator
_isClientChatFocused = isFocused;
ClientUpdateTyping();
}
public override void Update(float frameTime)
@@ -55,23 +69,25 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
var dif = _time.CurTime - _lastTextChange;
if (dif > _typingTimeout)
{
// client didn't typed anything for a long time - hide indicator
ClientUpdateTyping(false);
// client didn't typed anything for a long time - change indicator
_isClientTyping = false;
ClientUpdateTyping();
}
}
}
private void ClientUpdateTyping(bool isClientTyping)
private void ClientUpdateTyping()
{
if (_isClientTyping == isClientTyping)
return;
// check if player controls any entity.
// check if player controls any pawn
if (_playerManager.LocalEntity == null)
return;
_isClientTyping = isClientTyping;
RaisePredictiveEvent(new TypingChangedEvent(isClientTyping));
var state = TypingIndicatorState.None;
if (_isClientChatFocused)
state = _isClientTyping ? TypingIndicatorState.Typing : TypingIndicatorState.Idle;
// send a networked event to server
RaisePredictiveEvent(new TypingChangedEvent(state));
}
private void OnShowTypingChanged(bool showTyping)
@@ -79,7 +95,8 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
// hide typing indicator immediately if player don't want to show it anymore
if (!showTyping)
{
ClientUpdateTyping(false);
_isClientTyping = false;
ClientUpdateTyping();
}
}
}

View File

@@ -35,7 +35,6 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
return;
}
AppearanceSystem.TryGetData<bool>(uid, TypingIndicatorVisuals.IsTyping, out var isTyping, args.Component);
var layerExists = args.Sprite.LayerMapTryGet(TypingIndicatorLayers.Base, out var layer);
if (!layerExists)
layer = args.Sprite.LayerMapReserveBlank(TypingIndicatorLayers.Base);
@@ -44,6 +43,17 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
args.Sprite.LayerSetState(layer, proto.TypingState);
args.Sprite.LayerSetShader(layer, proto.Shader);
args.Sprite.LayerSetOffset(layer, proto.Offset);
args.Sprite.LayerSetVisible(layer, isTyping);
AppearanceSystem.TryGetData<TypingIndicatorState>(uid, TypingIndicatorVisuals.State, out var state);
args.Sprite.LayerSetVisible(layer, state != TypingIndicatorState.None);
switch (state)
{
case TypingIndicatorState.Idle:
args.Sprite.LayerSetState(layer, proto.IdleState);
break;
case TypingIndicatorState.Typing:
args.Sprite.LayerSetState(layer, proto.TypingState);
break;
}
}
}

View File

@@ -116,7 +116,10 @@ namespace Content.Client.Chemistry.UI
("1", ChemMasterReagentAmount.U1, StyleBase.ButtonOpenBoth),
("5", ChemMasterReagentAmount.U5, StyleBase.ButtonOpenBoth),
("10", ChemMasterReagentAmount.U10, StyleBase.ButtonOpenBoth),
("15", ChemMasterReagentAmount.U15, StyleBase.ButtonOpenBoth),
("20", ChemMasterReagentAmount.U20, StyleBase.ButtonOpenBoth),
("25", ChemMasterReagentAmount.U25, StyleBase.ButtonOpenBoth),
("30", ChemMasterReagentAmount.U30, StyleBase.ButtonOpenBoth),
("50", ChemMasterReagentAmount.U50, StyleBase.ButtonOpenBoth),
("100", ChemMasterReagentAmount.U100, StyleBase.ButtonOpenBoth),
(Loc.GetString("chem-master-window-buffer-all-amount"), ChemMasterReagentAmount.All, StyleBase.ButtonOpenLeft),

View File

@@ -44,7 +44,7 @@ public sealed class HyposprayStatusControl : Control
PrevMaxVolume = solution.MaxVolume;
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
var modeStringLocalized = Loc.GetString(_parent.Comp.OnlyAffectsMobs switch
var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
{
false => "hypospray-all-mode-text",
true => "hypospray-mobs-only-mode-text",

View File

@@ -1,4 +1,5 @@
using Content.Client.Guidebook.Components;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chemistry;
using Content.Shared.Containers.ItemSlots;
using JetBrains.Annotations;
@@ -31,8 +32,7 @@ namespace Content.Client.Chemistry.UI
// Setup window layout/elements
_window = this.CreateWindow<ReagentDispenserWindow>();
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
_window.HelpGuidebookIds = EntMan.GetComponent<GuideHelpComponent>(Owner).Guides;
_window.SetInfoFromEntity(EntMan, Owner);
// Setup static button actions.
_window.EjectButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(SharedReagentDispenser.OutputSlotName));

View File

@@ -1,8 +1,10 @@
using System.Linq;
using Content.Shared.Construction.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Placement;
using Robust.Client.Utility;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Construction
{
@@ -45,7 +47,14 @@ namespace Content.Client.Construction
public override void StartHijack(PlacementManager manager)
{
base.StartHijack(manager);
manager.CurrentTextures = _prototype?.Layers.Select(sprite => sprite.DirFrame0()).ToList();
if (_prototype is null || !_constructionSystem.TryGetRecipePrototype(_prototype.ID, out var targetProtoId))
return;
if (!IoCManager.Resolve<IPrototypeManager>().TryIndex(targetProtoId, out EntityPrototype? proto))
return;
manager.CurrentTextures = SpriteComponent.GetPrototypeTextures(proto, IoCManager.Resolve<IResourceCache>()).ToList();
}
}
}

View File

@@ -1,11 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Popups;
using Content.Shared.Construction;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Construction.Steps;
using Content.Shared.Examine;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Wall;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -15,6 +14,7 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Construction
{
@@ -25,7 +25,6 @@ namespace Content.Client.Construction
public sealed class ConstructionSystem : SharedConstructionSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -33,6 +32,8 @@ namespace Content.Client.Construction
private readonly Dictionary<int, EntityUid> _ghosts = new();
private readonly Dictionary<string, ConstructionGuide> _guideCache = new();
private readonly Dictionary<string, string> _recipesMetadataCache = [];
public bool CraftingEnabled { get; private set; }
/// <inheritdoc />
@@ -40,6 +41,8 @@ namespace Content.Client.Construction
{
base.Initialize();
WarmupRecipesCache();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<LocalPlayerAttachedEvent>(HandlePlayerAttached);
SubscribeNetworkEvent<AckStructureConstructionMessage>(HandleAckStructure);
@@ -63,6 +66,77 @@ namespace Content.Client.Construction
ClearGhost(component.GhostId);
}
public bool TryGetRecipePrototype(string constructionProtoId, [NotNullWhen(true)] out string? targetProtoId)
{
if (_recipesMetadataCache.TryGetValue(constructionProtoId, out targetProtoId))
return true;
targetProtoId = null;
return false;
}
private void WarmupRecipesCache()
{
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (!PrototypeManager.TryIndex(constructionProto.Graph, out var graphProto))
continue;
if (constructionProto.TargetNode is not { } targetNodeId)
continue;
if (!graphProto.Nodes.TryGetValue(targetNodeId, out var targetNode))
continue;
// Recursion is for wimps.
var stack = new Stack<ConstructionGraphNode>();
stack.Push(targetNode);
do
{
var node = stack.Pop();
// I never realized if this uid affects anything...
// EntityUid? userUid = args.SenderSession.State.ControlledEntity.HasValue
// ? GetEntity(args.SenderSession.State.ControlledEntity.Value)
// : null;
// We try to get the id of the target prototype, if it fails, we try going through the edges.
if (node.Entity.GetId(null, null, new(EntityManager)) is not { } entityId)
{
// If the stack is not empty, there is a high probability that the loop will go to infinity.
if (stack.Count == 0)
{
foreach (var edge in node.Edges)
{
if (graphProto.Nodes.TryGetValue(edge.Target, out var graphNode))
stack.Push(graphNode);
}
}
continue;
}
// If we got the id of the prototype, we exit the “recursion” by clearing the stack.
stack.Clear();
if (!PrototypeManager.TryIndex(constructionProto.ID, out ConstructionPrototype? recipe))
continue;
if (!PrototypeManager.TryIndex(entityId, out var proto))
continue;
var name = recipe.SetName.HasValue ? Loc.GetString(recipe.SetName) : proto.Name;
var desc = recipe.SetDescription.HasValue ? Loc.GetString(recipe.SetDescription) : proto.Description;
recipe.Name = name;
recipe.Description = desc;
_recipesMetadataCache.Add(constructionProto.ID, entityId);
} while (stack.Count > 0);
}
}
private void OnConstructionGuideReceived(ResponseConstructionGuide ev)
{
_guideCache[ev.ConstructionId] = ev.Guide;
@@ -88,7 +162,7 @@ namespace Content.Client.Construction
private void HandleConstructionGhostExamined(EntityUid uid, ConstructionGhostComponent component, ExaminedEvent args)
{
if (component.Prototype == null)
if (component.Prototype?.Name is null)
return;
using (args.PushGroup(nameof(ConstructionGhostComponent)))
@@ -97,7 +171,7 @@ namespace Content.Client.Construction
"construction-ghost-examine-message",
("name", component.Prototype.Name)));
if (!_prototypeManager.TryIndex(component.Prototype.Graph, out ConstructionGraphPrototype? graph))
if (!PrototypeManager.TryIndex(component.Prototype.Graph, out var graph))
return;
var startNode = graph.Nodes[component.Prototype.StartNode];
@@ -198,6 +272,9 @@ namespace Content.Client.Construction
return false;
}
if (!TryGetRecipePrototype(prototype.ID, out var targetProtoId) || !PrototypeManager.TryIndex(targetProtoId, out EntityPrototype? targetProto))
return false;
if (GhostPresent(loc))
return false;
@@ -214,16 +291,43 @@ namespace Content.Client.Construction
comp.GhostId = ghost.GetHashCode();
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
_ghosts.Add(comp.GhostId, ghost.Value);
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
sprite.Color = new Color(48, 255, 48, 128);
for (int i = 0; i < prototype.Layers.Count; i++)
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
{
sprite.AddBlankLayer(i); // There is no way to actually check if this already exists, so we blindly insert a new one
sprite.LayerSetSprite(i, prototype.Layers[i]);
sprite.LayerSetShader(i, "unshaded");
sprite.LayerSetVisible(i, true);
sprite.AddBlankLayer(0);
sprite.LayerSetSprite(0, icon.Icon);
sprite.LayerSetShader(0, "unshaded");
sprite.LayerSetVisible(0, true);
}
else if (targetProto.Components.TryGetValue("Sprite", out _))
{
var dummy = EntityManager.SpawnEntity(targetProtoId, MapCoordinates.Nullspace);
var targetSprite = EntityManager.EnsureComponent<SpriteComponent>(dummy);
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
{
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
continue;
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
state.StateId.Name is null)
continue;
sprite.AddBlankLayer(i);
sprite.LayerSetSprite(i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
sprite.LayerSetShader(i, "unshaded");
sprite.LayerSetVisible(i, true);
}
EntityManager.DeleteEntity(dummy);
}
else
return false;
if (prototype.CanBuildInImpassable)
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);

View File

@@ -1,11 +1,12 @@
<DefaultWindow xmlns="https://spacestation14.io">
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" MinWidth="243" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/>
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'construction-menu-search'}" HorizontalExpand="True"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/>
<controls:ListContainer Name="Recipes" Access="Public" Group="True" Toggle="True" VerticalExpand="True" />
<ScrollContainer Name="RecipesGridScrollContainer" VerticalExpand="True" Access="Public" Visible="False">
<GridContainer Name="RecipesGrid" Columns="5" Access="Public"/>
</ScrollContainer>
@@ -18,7 +19,7 @@
<Control>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" Align="Center">
<TextureRect Name="TargetTexture" HorizontalAlignment="Right" Stretch="Keep" Margin="0 0 10 0"/>
<EntityPrototypeView Name="TargetTexture" HorizontalAlignment="Right" Stretch="Fill" Margin="0 0 10 0"/>
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="TargetName"/>
<RichTextLabel Name="TargetDesc"/>

View File

@@ -1,14 +1,11 @@
using System;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Construction.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.Client.Construction.UI
{
@@ -28,7 +25,7 @@ namespace Content.Client.Construction.UI
bool GridViewButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
ItemList Recipes { get; }
ListContainer Recipes { get; }
ItemList RecipeStepList { get; }
@@ -36,14 +33,14 @@ namespace Content.Client.Construction.UI
GridContainer RecipesGrid { get; }
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler<ItemList.Item?> RecipeSelected;
event EventHandler<ConstructionMenu.ConstructionMenuListData?> RecipeSelected;
event EventHandler RecipeFavorited;
event EventHandler<bool> BuildButtonToggled;
event EventHandler<bool> EraseButtonToggled;
event EventHandler ClearAllGhosts;
void ClearRecipeInfo();
void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem, bool isFavorite);
void SetRecipeInfo(string name, string description, EntityPrototype? targetPrototype, bool isItem, bool isFavorite);
void ResetPlacement();
#region Window Control
@@ -94,8 +91,36 @@ namespace Content.Client.Construction.UI
Title = Loc.GetString("construction-menu-title");
BuildButton.Text = Loc.GetString("construction-menu-place-ghost");
Recipes.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]);
Recipes.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null);
Recipes.ItemPressed += (_, data) => RecipeSelected?.Invoke(this, data as ConstructionMenuListData);
Recipes.NoItemSelected += () => RecipeSelected?.Invoke(this, null);
Recipes.GenerateItem += (data, button) =>
{
if (data is not ConstructionMenuListData (var prototype, var targetPrototype))
return;
var entProtoView = new EntityPrototypeView()
{
SetSize = new(32f),
Stretch = SpriteView.StretchMode.Fill,
Scale = new(2),
Margin = new(0, 2),
};
entProtoView.SetPrototype(targetPrototype);
var label = new Label()
{
Text = prototype.Name,
Margin = new(5, 0),
};
var box = new BoxContainer();
box.AddChild(entProtoView);
box.AddChild(label);
button.AddChild(box);
button.ToolTip = prototype.Description;
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
};
SearchBar.OnTextChanged += _ =>
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
@@ -121,7 +146,7 @@ namespace Content.Client.Construction.UI
public event EventHandler? ClearAllGhosts;
public event EventHandler<(string search, string catagory)>? PopulateRecipes;
public event EventHandler<ItemList.Item?>? RecipeSelected;
public event EventHandler<ConstructionMenuListData?>? RecipeSelected;
public event EventHandler? RecipeFavorited;
public event EventHandler<bool>? BuildButtonToggled;
public event EventHandler<bool>? EraseButtonToggled;
@@ -133,13 +158,17 @@ namespace Content.Client.Construction.UI
}
public void SetRecipeInfo(
string name, string description, Texture iconTexture, bool isItem, bool isFavorite)
string name,
string description,
EntityPrototype? targetPrototype,
bool isItem,
bool isFavorite)
{
BuildButton.Disabled = false;
BuildButton.Text = Loc.GetString(isItem ? "construction-menu-place-ghost" : "construction-menu-craft");
TargetName.SetMessage(name);
TargetDesc.SetMessage(description);
TargetTexture.Texture = iconTexture;
TargetTexture.SetPrototype(targetPrototype?.ID);
FavoriteButton.Visible = true;
FavoriteButton.Text = Loc.GetString(
isFavorite ? "construction-add-favorite-button" : "construction-remove-from-favorite-button");
@@ -150,9 +179,11 @@ namespace Content.Client.Construction.UI
BuildButton.Disabled = true;
TargetName.SetMessage(string.Empty);
TargetDesc.SetMessage(string.Empty);
TargetTexture.Texture = null;
TargetTexture.SetPrototype(null);
FavoriteButton.Visible = false;
RecipeStepList.Clear();
}
public sealed record ConstructionMenuListData(ConstructionPrototype Prototype, EntityPrototype TargetPrototype) : ListData;
}
}

View File

@@ -10,10 +10,8 @@ using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Construction.UI
{
@@ -30,18 +28,20 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
private readonly SpriteSystem _spriteSystem;
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, TextureButton> _recipeButtons = new();
private Dictionary<string, ContainerButton> _recipeButtons = new();
private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all";
private const string FavoriteCatName = "construction-category-favorites";
private const string ForAllCategoryName = "construction-category-all";
private bool CraftingAvailable
{
get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible;
@@ -98,15 +98,18 @@ namespace Content.Client.Construction.UI
_placementManager.PlacementChanged += OnPlacementChanged;
_constructionView.OnClose += () => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
_constructionView.OnClose +=
() => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
_constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
_constructionView.PopulateRecipes += OnViewPopulateRecipes;
_constructionView.RecipeSelected += OnViewRecipeSelected;
_constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b);
_constructionView.EraseButtonToggled += (_, b) =>
{
if (_constructionSystem is null) return;
if (b) _placementManager.Clear();
if (_constructionSystem is null)
return;
if (b)
_placementManager.Clear();
_placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null));
_constructionView.EraseButtonPressed = b;
};
@@ -117,7 +120,7 @@ namespace Content.Client.Construction.UI
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
}
public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args)
public void OnHudCraftingButtonToggled(BaseButton.ButtonToggledEventArgs args)
{
WindowOpen = args.Pressed;
}
@@ -139,7 +142,7 @@ namespace Content.Client.Construction.UI
_constructionView.ResetPlacement();
}
private void OnViewRecipeSelected(object? sender, ItemList.Item? item)
private void OnViewRecipeSelected(object? sender, ConstructionMenu.ConstructionMenuListData? item)
{
if (item is null)
{
@@ -148,12 +151,15 @@ namespace Content.Client.Construction.UI
return;
}
_selected = (ConstructionPrototype) item.Metadata!;
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
_selected = item.Prototype;
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected);
}
private void OnGridViewRecipeSelected(object? sender, ConstructionPrototype? recipe)
private void OnGridViewRecipeSelected(object? _, ConstructionPrototype? recipe)
{
if (recipe is null)
{
@@ -163,65 +169,21 @@ namespace Content.Client.Construction.UI
}
_selected = recipe;
if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement();
if (_placementManager is { IsActive: true, Eraser: false })
UpdateGhostPlacement();
PopulateInfo(_selected);
}
private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args)
{
var (search, category) = args;
if (_constructionSystem is null)
return;
var recipes = new List<ConstructionPrototype>();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
if (isEmptyCategory)
_selectedCategory = string.Empty;
else
_selectedCategory = category;
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
continue;
if (!recipe.CrystallPunkAllowed) //CrystallEdge clearing recipes
continue;
if (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
continue;
if (!string.IsNullOrEmpty(search))
{
if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant()))
continue;
}
if (!isEmptyCategory)
{
if (category == _favoriteCatName)
{
if (!_favoritedRecipes.Contains(recipe))
{
continue;
}
}
else if (recipe.Category != category)
{
continue;
}
}
recipes.Add(recipe);
}
recipes.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.InvariantCulture));
var actualRecipes = GetAndSortRecipes(args);
var recipesList = _constructionView.Recipes;
recipesList.Clear();
var recipesGrid = _constructionView.RecipesGrid;
recipesGrid.RemoveAllChildren();
@@ -230,60 +192,123 @@ namespace Content.Client.Construction.UI
if (_constructionView.GridViewButtonPressed)
{
foreach (var recipe in recipes)
{
var itemButton = new TextureButton
{
TextureNormal = _spriteSystem.Frame0(recipe.Icon),
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.Name,
ToolTip = recipe.Name,
Scale = new Vector2(1.35f),
ToggleMode = true,
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Name] = itemButton;
var isCurrentButtonSelected = _selected == recipe;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
recipesList.PopulateList([]);
PopulateGrid(recipesGrid, actualRecipes);
}
else
{
foreach (var recipe in recipes)
{
recipesList.Add(GetItem(recipe, recipesList));
}
recipesList.PopulateList(actualRecipes);
}
}
private void SelectGridButton(TextureButton button, bool select)
private void PopulateGrid(GridContainer recipesGrid,
IEnumerable<ConstructionMenu.ConstructionMenuListData> actualRecipes)
{
foreach (var recipe in actualRecipes)
{
if (!recipe.Prototype.CrystallPunkAllowed) //CrystallEdge clearing recipes
continue;
var protoView = new EntityPrototypeView()
{
Scale = new Vector2(1.2f),
};
protoView.SetPrototype(recipe.TargetPrototype);
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.TargetPrototype.Name,
ToolTip = recipe.TargetPrototype.Name,
ToggleMode = true,
Children = { protoView },
};
var itemButtonPanelContainer = new PanelContainer
{
PanelOverride = new StyleBoxFlat { BackgroundColor = StyleNano.ButtonColorDefault },
Children = { itemButton },
};
itemButton.OnToggled += buttonToggledEventArgs =>
{
SelectGridButton(itemButton, buttonToggledEventArgs.Pressed);
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
}
OnGridViewRecipeSelected(this, buttonToggledEventArgs.Pressed ? recipe.Prototype : null);
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.Name!] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
}
}
private List<ConstructionMenu.ConstructionMenuListData> GetAndSortRecipes((string, string) args)
{
var recipes = new List<ConstructionMenu.ConstructionMenuListData>();
var (search, category) = args;
var isEmptyCategory = string.IsNullOrEmpty(category) || category == ForAllCategoryName;
_selectedCategory = isEmptyCategory ? string.Empty : category;
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
continue;
if (_playerManager.LocalSession == null
|| _playerManager.LocalEntity == null
|| _whitelistSystem.IsWhitelistFail(recipe.EntityWhitelist, _playerManager.LocalEntity.Value))
continue;
if (!string.IsNullOrEmpty(search) && (recipe.Name is { } name &&
!name.Contains(search.Trim(),
StringComparison.InvariantCultureIgnoreCase)))
continue;
if (!isEmptyCategory)
{
if ((category != FavoriteCatName || !_favoritedRecipes.Contains(recipe)) &&
recipe.Category != category)
continue;
}
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;
}
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
continue;
recipes.Add(new(recipe, proto));
}
recipes.Sort(
(a, b) => string.Compare(a.Prototype.Name, b.Prototype.Name, StringComparison.InvariantCulture));
return recipes;
}
private void SelectGridButton(BaseButton button, bool select)
{
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Modulate = select ? Color.Green : Color.White;
button.Modulate = select ? Color.Green : Color.Transparent;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}
@@ -305,12 +330,12 @@ namespace Content.Client.Construction.UI
// hard-coded to show all recipes
var idx = 0;
categoriesArray[idx++] = _forAllCategoryName;
categoriesArray[idx++] = ForAllCategoryName;
// hard-coded to show favorites if it need
if (isFavorites)
{
categoriesArray[idx++] = _favoriteCatName;
categoriesArray[idx++] = FavoriteCatName;
}
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
@@ -328,18 +353,31 @@ namespace Content.Client.Construction.UI
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
_constructionView.OptionCategories.SelectId(i);
}
_constructionView.Categories = categoriesArray;
}
private void PopulateInfo(ConstructionPrototype prototype)
private void PopulateInfo(ConstructionPrototype? prototype)
{
if (_constructionSystem is null)
return;
_constructionView.ClearRecipeInfo();
if (prototype is null)
return;
if (!_constructionSystem.TryGetRecipePrototype(prototype.ID, out var targetProtoId))
return;
if (!_prototypeManager.TryIndex(targetProtoId, out EntityPrototype? proto))
return;
_constructionView.SetRecipeInfo(
prototype.Name, prototype.Description, _spriteSystem.Frame0(prototype.Icon),
prototype.Name!,
prototype.Description!,
proto,
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
@@ -352,16 +390,17 @@ namespace Content.Client.Construction.UI
if (_constructionSystem?.GetGuide(prototype) is not { } guide)
return;
foreach (var entry in guide.Entries)
{
var text = entry.Arguments != null
? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization);
? Loc.GetString(entry.Localization, entry.Arguments)
: Loc.GetString(entry.Localization);
if (entry.EntryNumber is { } number)
{
text = Loc.GetString("construction-presenter-step-wrapper",
("step-number", number), ("text", text));
("step-number", number),
("text", text));
}
// The padding needs to be applied regardless of text length... (See PadLeft documentation)
@@ -372,23 +411,12 @@ namespace Content.Client.Construction.UI
}
}
private ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList)
{
return new(itemList)
{
Metadata = recipe,
Text = recipe.Name,
Icon = _spriteSystem.Frame0(recipe.Icon),
TooltipEnabled = true,
TooltipText = recipe.Description,
};
}
private void BuildButtonToggled(bool pressed)
{
if (pressed)
{
if (_selected == null) return;
if (_selected == null)
return;
// not bound to a construction system
if (_constructionSystem is null)
@@ -405,10 +433,11 @@ namespace Content.Client.Construction.UI
}
_placementManager.BeginPlacing(new PlacementInformation
{
IsTile = false,
PlacementOption = _selected.PlacementMode
}, new ConstructionPlacementHijack(_constructionSystem, _selected));
{
IsTile = false,
PlacementOption = _selected.PlacementMode
},
new ConstructionPlacementHijack(_constructionSystem, _selected));
UpdateGhostPlacement();
}
@@ -432,38 +461,39 @@ namespace Content.Client.Construction.UI
var constructSystem = _systemManager.GetEntitySystem<ConstructionSystem>();
_placementManager.BeginPlacing(new PlacementInformation()
{
IsTile = false,
PlacementOption = _selected.PlacementMode,
}, new ConstructionPlacementHijack(constructSystem, _selected));
{
IsTile = false,
PlacementOption = _selected.PlacementMode,
},
new ConstructionPlacementHijack(constructSystem, _selected));
_constructionView.BuildButtonPressed = true;
}
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem system) SystemBindingChanged(system);
if (args.System is ConstructionSystem system)
SystemBindingChanged(system);
}
private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
{
if (args.System is ConstructionSystem) SystemBindingChanged(null);
if (args.System is ConstructionSystem)
SystemBindingChanged(null);
}
private void OnViewFavoriteRecipe()
{
if (_selected is not ConstructionPrototype recipe)
if (_selected is null)
return;
if (!_favoritedRecipes.Remove(_selected))
_favoritedRecipes.Add(_selected);
if (_selectedCategory == _favoriteCatName)
if (_selectedCategory == FavoriteCatName)
{
if (_favoritedRecipes.Count > 0)
OnViewPopulateRecipes(_constructionView, (string.Empty, _favoriteCatName));
else
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
OnViewPopulateRecipes(_constructionView,
_favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty));
}
PopulateInfo(_selected);
@@ -495,6 +525,9 @@ namespace Content.Client.Construction.UI
private void BindToSystem(ConstructionSystem system)
{
_constructionSystem = system;
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
system.ToggleCraftingWindow += SystemOnToggleMenu;
system.FlipConstructionPrototype += SystemFlipConstructionPrototype;
system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged;
@@ -536,7 +569,8 @@ namespace Content.Client.Construction.UI
if (IsAtFront)
{
WindowOpen = false;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
_uiManager.GetActiveUIWidget<GameTopMenuBar>()
.CraftingButton.SetClickPressed(false); // This does not call CraftingButtonToggled
}
else
_constructionView.MoveToFront();
@@ -544,7 +578,8 @@ namespace Content.Client.Construction.UI
else
{
WindowOpen = true;
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
_uiManager.GetActiveUIWidget<GameTopMenuBar>()
.CraftingButton.SetClickPressed(true); // This does not call CraftingButtonToggled
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Damage.Systems;
namespace Content.Client.Damage.Systems;
public sealed partial class StaminaSystem : SharedStaminaSystem
{
}

View File

@@ -4,6 +4,7 @@ using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using System.Linq;
namespace Content.Client.Disposal.Mailing;
@@ -70,10 +71,10 @@ public sealed class MailingUnitBoundUserInterface : BoundUserInterface
//UnitTag.Text = state.Tag;
MailingUnitWindow.Target.Text = entity.Comp.Target;
MailingUnitWindow.TargetListContainer.Clear();
foreach (var target in entity.Comp.TargetList)
{
MailingUnitWindow.TargetListContainer.AddItem(target);
}
var entries = entity.Comp.TargetList.Select(target => new ItemList.Item(MailingUnitWindow.TargetListContainer) {
Text = target,
Selected = target == entity.Comp.Target
}).ToList();
MailingUnitWindow.TargetListContainer.SetItems(entries);
}
}

View File

@@ -22,6 +22,7 @@ using Content.Client.Replay;
using Content.Client.Screenshot;
using Content.Client.Singularity;
using Content.Client.Stylesheets;
using Content.Client.UserInterface;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared._CP14.Sponsor;
@@ -82,6 +83,7 @@ namespace Content.Client.Entry
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
public override void Init()
{
@@ -240,6 +242,15 @@ namespace Content.Client.Entry
{
_debugMonitorManager.FrameUpdate();
}
if (level == ModUpdateLevel.PreEngine)
{
if (_baseClient.RunLevel is ClientRunLevel.InGame or ClientRunLevel.SinglePlayerGame)
{
var updateSystem = _entitySystemManager.GetEntitySystem<BuiPreTickUpdateSystem>();
updateSystem.RunUpdates();
}
}
}
}
}

View File

@@ -37,7 +37,6 @@ namespace Content.Client.Examine
public const string StyleClassEntityTooltip = "entity-tooltip";
private EntityUid _examinedEntity;
private EntityUid _lastExaminedEntity;
private Popup? _examineTooltipOpen;
private ScreenCoordinates _popupPos;
private CancellationTokenSource? _requestCancelTokenSource;
@@ -416,15 +415,14 @@ namespace Content.Client.Examine
if (!IsClientSide(entity))
{
// Ask server for extra examine info.
if (entity != _lastExaminedEntity)
unchecked
{
_idCounter += 1;
if (_idCounter == int.MaxValue)
_idCounter = 0;
}
RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(GetNetEntity(entity), _idCounter, true));
}
RaiseLocalEvent(entity, new ClientExaminedEvent(entity, playerEnt.Value));
_lastExaminedEntity = entity;
}
private void CloseTooltip()

View File

@@ -93,7 +93,7 @@ public sealed partial class TriggerSystem
break;
case ProximityTriggerVisuals.Active:
if (_player.HasRunningAnimation(uid, player, AnimKey)) return;
_player.Play(uid, player, _flasherAnimation, AnimKey);
_player.Play((uid, player), _flasherAnimation, AnimKey);
break;
case ProximityTriggerVisuals.Off:
default:

View File

@@ -34,7 +34,7 @@ public sealed class EyeLerpingSystem : EntitySystem
SubscribeLocalEvent<LerpingEyeComponent, LocalPlayerDetachedEvent>(OnDetached);
UpdatesAfter.Add(typeof(TransformSystem));
UpdatesAfter.Add(typeof(PhysicsSystem));
UpdatesAfter.Add(typeof(Robust.Client.Physics.PhysicsSystem));
UpdatesBefore.Add(typeof(SharedEyeSystem));
UpdatesOutsidePrediction = true;
}

View File

@@ -46,7 +46,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!HasComp<HolopadUserComponent>(uid))
return;
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping);
var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.State);
RaiseNetworkEvent(netEv);
}

View File

@@ -167,11 +167,14 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
private void UpdateRendererMaster(InstrumentComponent instrument)
{
if (instrument.Renderer == null || instrument.Master == null)
if (instrument.Renderer == null)
return;
if (!TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
if (instrument.Master == null || !TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
{
instrument.Renderer.Master = null;
return;
}
instrument.Renderer.Master = masterInstrument.Renderer;
}
@@ -196,15 +199,16 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return;
}
instrument.Renderer?.SystemReset();
instrument.Renderer?.ClearAllEvents();
if (instrument.Renderer is { } renderer)
{
renderer.Master = null;
renderer.SystemReset();
renderer.ClearAllEvents();
var renderer = instrument.Renderer;
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
// Don't use timers bound to the entity in case it is getting deleted.
if (renderer != null)
// We dispose of the synth two seconds from now to allow the last notes to stop from playing.
// Don't use timers bound to the entity in case it is getting deleted.
Timer.Spawn(2000, () => { renderer.Dispose(); });
}
instrument.Renderer = null;
instrument.MidiEventBuffer.Clear();

View File

@@ -100,7 +100,27 @@
<controls:HSpacer Spacing="10" />
<widgets:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50" />
</BoxContainer>
<TextureButton Name="CollapseButton"
TexturePath="filled_right_arrow.svg.192dpi"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0 2 2 0"
ModulateSelfOverride="#DEDEDE"
Scale="0.5 0.5"/>
</PanelContainer>
</SplitContainer>
<PanelContainer Name="ExpandPanel"
StyleClasses="AngleRect"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0 2 2 0"
Visible="False">
<TextureButton Name="ExpandButton"
TexturePath="filled_left_arrow.svg.192dpi"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ModulateSelfOverride="#DEDEDE"
Scale="0.5 0.5"/>
</PanelContainer>
</BoxContainer>
</lobbyUi:LobbyGui>

View File

@@ -23,6 +23,9 @@ namespace Content.Client.Lobby.UI
LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
CollapseButton.OnPressed += _ => TogglePanel(false);
ExpandButton.OnPressed += _ => TogglePanel(true);
}
public void SwitchState(LobbyGuiState state)
@@ -53,6 +56,12 @@ namespace Content.Client.Lobby.UI
}
}
private void TogglePanel(bool value)
{
RightSide.Visible = value;
ExpandPanel.Visible = !value;
}
public enum LobbyGuiState : byte
{
/// <summary>

View File

@@ -3,11 +3,10 @@ using Content.Shared.Emag.Systems;
using Content.Shared.Medical.Cryogenics;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Medical.Cryogenics;
public sealed class CryoPodSystem: SharedCryoPodSystem
public sealed class CryoPodSystem : SharedCryoPodSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
@@ -63,11 +62,9 @@ public sealed class CryoPodSystem: SharedCryoPodSystem
{
args.Sprite.LayerSetState(CryoPodVisualLayers.Base, "pod-open");
args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, false);
args.Sprite.DrawDepth = (int) DrawDepth.Objects;
}
else
{
args.Sprite.DrawDepth = (int) DrawDepth.Mobs;
args.Sprite.LayerSetState(CryoPodVisualLayers.Base, isOn ? "pod-on" : "pod-off");
args.Sprite.LayerSetState(CryoPodVisualLayers.Cover, isOn ? "cover-on" : "cover-off");
args.Sprite.LayerSetVisible(CryoPodVisualLayers.Cover, true);

View File

@@ -1,5 +1,6 @@
using System.Numerics;
using Content.Client.Physics.Controllers;
using Content.Client.PhysicsSystem.Controllers;
using Content.Shared.Movement.Components;
using Content.Shared.NPC;
using Content.Shared.NPC.Events;

View File

@@ -0,0 +1,47 @@
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed partial class BlackAndWhiteOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _greyscaleShader;
public BlackAndWhiteOverlay()
{
IoCManager.InjectDependencies(this);
_greyscaleShader = _prototypeManager.Index<ShaderPrototype>("GreyscaleFullscreen").InstanceUnique();
ZIndex = 10; // draw this over the DamageOverlay, RainbowOverlay etc.
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
return false;
if (args.Viewport.Eye != eyeComp.Eye)
return false;
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null)
return;
var handle = args.WorldHandle;
_greyscaleShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
handle.UseShader(_greyscaleShader);
handle.DrawRect(args.WorldBounds, Color.White);
handle.UseShader(null);
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays;
using Robust.Client.Graphics;
using Robust.Client.Player;
namespace Content.Client.Overlays;
public sealed partial class BlackAndWhiteOverlaySystem : EquipmentHudSystem<BlackAndWhiteOverlayComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private BlackAndWhiteOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
_overlay = new();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<BlackAndWhiteOverlayComponent> component)
{
base.UpdateInternal(component);
_overlayMan.AddOverlay(_overlay);
}
protected override void DeactivateInternal()
{
base.DeactivateInternal();
_overlayMan.RemoveOverlay(_overlay);
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Robust.Shared.Utility;
namespace Content.Client.Paper.UI;
@@ -55,6 +56,24 @@ public sealed partial class PaperVisualsComponent : Component
[DataField("headerMargin")]
public Box2 HeaderMargin = default;
/// <summary>
/// A path to an image which will be used as a footer on the paper
/// </summary>
[DataField]
public ResPath? FooterImagePath;
/// <summary>
/// Modulate the footer image by this color
/// </summary>
[DataField]
public Color FooterImageModulate = Color.White;
/// <summary>
/// Any additional margin to add around the footer
/// </summary>
[DataField]
public Box2 FooterMargin = default;
/// <summary>
/// Path to an image to use as the background to the "content" of the paper
/// The header and actual written text will use this as a background. The

View File

@@ -22,6 +22,7 @@
</PanelContainer>
<Label Name="FillStatus" StyleClasses="LabelSecondaryColor"/>
</BoxContainer>
<TextureButton Name="FooterImage" HorizontalAlignment="Center" VerticalAlignment="Top" MouseFilter="Ignore"/>
</BoxContainer>
<paper:StampCollection Name="StampDisplay" VerticalAlignment="Bottom" Margin="6"/>

View File

@@ -149,6 +149,16 @@ namespace Content.Client.Paper.UI
HeaderImage.Margin = new Thickness(visuals.HeaderMargin.Left, visuals.HeaderMargin.Top,
visuals.HeaderMargin.Right, visuals.HeaderMargin.Bottom);
// Then the footer
if (visuals.FooterImagePath is {} path)
{
FooterImage.TexturePath = path.ToString();
FooterImage.MinSize = FooterImage.TextureNormal?.Size ?? Vector2.Zero;
}
FooterImage.ModulateSelfOverride = visuals.FooterImageModulate;
FooterImage.Margin = new Thickness(visuals.FooterMargin.Left, visuals.FooterMargin.Top,
visuals.FooterMargin.Right, visuals.FooterMargin.Bottom);
PaperContent.ModulateSelfOverride = visuals.ContentImageModulate;
WrittenTextLabel.ModulateSelfOverride = visuals.FontAccentColor;

View File

@@ -3,15 +3,13 @@ using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Physics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client.Physics.Controllers;
namespace Content.Client.PhysicsSystem.Controllers;
public sealed class MoverController : SharedMoverController
{
@@ -101,39 +99,13 @@ public sealed class MoverController : SharedMoverController
private void HandleClientsideMovement(EntityUid player, float frameTime)
{
if (!MoverQuery.TryGetComponent(player, out var mover) ||
!XformQuery.TryGetComponent(player, out var xform))
{
return;
}
var physicsUid = player;
PhysicsComponent? body;
var xformMover = xform;
if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
{
if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
!XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
{
return;
}
physicsUid = xform.ParentUid;
}
else if (!PhysicsQuery.TryGetComponent(player, out body))
if (!MoverQuery.TryGetComponent(player, out var mover))
{
return;
}
// Server-side should just be handled on its own so we'll just do this shizznit
HandleMobMovement(
player,
mover,
physicsUid,
body,
xformMover,
frameTime);
HandleMobMovement((player, mover), frameTime);
}
protected override bool CanSound()

View File

@@ -0,0 +1,85 @@
using Content.Client.UserInterface;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
namespace Content.Client.Power.Battery;
/// <summary>
/// BUI for <see cref="BatteryUiKey.Key"/>.
/// </summary>
/// <seealso cref="BoundUserInterfaceState"/>
/// <seealso cref="BatteryMenu"/>
[UsedImplicitly]
public sealed class BatteryBoundUserInterface : BoundUserInterface, IBuiPreTickUpdate
{
[Dependency] private readonly IClientGameTiming _gameTiming = null!;
[ViewVariables]
private BatteryMenu? _menu;
private BuiPredictionState? _pred;
private InputCoalescer<float> _chargeRateCoalescer;
private InputCoalescer<float> _dischargeRateCoalescer;
public BatteryBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_pred = new BuiPredictionState(this, _gameTiming);
_menu = this.CreateWindow<BatteryMenu>();
_menu.SetEntity(Owner);
_menu.OnInBreaker += val => _pred!.SendMessage(new BatterySetInputBreakerMessage(val));
_menu.OnOutBreaker += val => _pred!.SendMessage(new BatterySetOutputBreakerMessage(val));
_menu.OnChargeRate += val => _chargeRateCoalescer.Set(val);
_menu.OnDischargeRate += val => _dischargeRateCoalescer.Set(val);
}
void IBuiPreTickUpdate.PreTickUpdate()
{
if (_chargeRateCoalescer.CheckIsModified(out var chargeRateValue))
_pred!.SendMessage(new BatterySetChargeRateMessage(chargeRateValue));
if (_dischargeRateCoalescer.CheckIsModified(out var dischargeRateValue))
_pred!.SendMessage(new BatterySetDischargeRateMessage(dischargeRateValue));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not BatteryBuiState batteryState)
return;
foreach (var replayMsg in _pred!.MessagesToReplay())
{
switch (replayMsg)
{
case BatterySetInputBreakerMessage setInputBreaker:
batteryState.CanCharge = setInputBreaker.On;
break;
case BatterySetOutputBreakerMessage setOutputBreaker:
batteryState.CanDischarge = setOutputBreaker.On;
break;
case BatterySetChargeRateMessage setChargeRate:
batteryState.MaxChargeRate = setChargeRate.Rate;
break;
case BatterySetDischargeRateMessage setDischargeRate:
batteryState.MaxSupply = setDischargeRate.Rate;
break;
}
}
_menu?.Update(batteryState);
}
}

View File

@@ -0,0 +1,146 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
SetSize="650 330"
Resizable="False">
<BoxContainer Orientation="Vertical">
<!-- Top row: main content -->
<BoxContainer Name="MainContent" Orientation="Horizontal" VerticalExpand="True" Margin="4">
<!-- Left pane: I/O, passthrough, sprite view -->
<BoxContainer Name="IOPane" Orientation="Vertical" HorizontalExpand="True">
<!-- Top row: input -->
<BoxContainer Name="InputRow" Orientation="Horizontal">
<!-- Input power line -->
<PanelContainer Name="InPowerLine" SetHeight="2" VerticalAlignment="Top" SetWidth="32"
Margin="2 16" />
<!-- Box with breaker, label, values -->
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-in'}" HorizontalExpand="True" VerticalAlignment="Top"
StyleClasses="LabelKeyText" />
<controls:OnOffButton Name="InBreaker" />
</BoxContainer>
<Label Name="InValue" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
<!-- Middle row: Entity view & passthrough -->
<BoxContainer Name="MiddleRow" Orientation="Horizontal" VerticalExpand="True">
<SpriteView Name="EntityView" SetSize="64 64" Scale="2 2" OverrideDirection="South" Margin="15" />
<BoxContainer Orientation="Vertical" VerticalAlignment="Center" HorizontalExpand="True"
HorizontalAlignment="Right">
<Label HorizontalAlignment="Right" Text="{Loc 'battery-menu-passthrough'}" StyleClasses="StatusFieldTitle" />
<Label HorizontalAlignment="Right" Name="PassthroughValue" />
</BoxContainer>
</BoxContainer>
<!-- Bottom row: output -->
<BoxContainer Name="OutputRow" Orientation="Horizontal">
<!-- Output power line -->
<PanelContainer Name="OutPowerLine" SetHeight="2" VerticalAlignment="Bottom" SetWidth="32"
Margin="2 16" />
<!-- Box with breaker, label, values -->
<PanelContainer HorizontalExpand="True" StyleClasses="Inset">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-out'}" HorizontalExpand="True" VerticalAlignment="Top"
StyleClasses="LabelKeyText" />
<controls:OnOffButton Name="OutBreaker" />
</BoxContainer>
<Label Name="OutValue" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
<!-- Separator connecting panes with some wires -->
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
<PanelContainer Name="InSecondPowerLine" SetHeight="2" />
<PanelContainer Name="PassthroughPowerLine" SetWidth="2" HorizontalAlignment="Center" VerticalExpand="True" />
<PanelContainer Name="OutSecondPowerLine" SetHeight="2" />
</BoxContainer>
<!-- Middle pane: charge/discharge -->
<BoxContainer Name="ChargeDischarge" Orientation="Vertical" HorizontalExpand="True">
<!-- Charge -->
<PanelContainer VerticalExpand="True" StyleClasses="Inset" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'battery-menu-charge-header'}" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<Slider Name="ChargeRateSlider" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
<Label Name="ChargeMaxValue" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
<Label Name="ChargeCurrentValue" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
<!-- Discharge -->
<PanelContainer VerticalExpand="True" StyleClasses="Inset">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'battery-menu-discharge-header'}" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
<Slider Name="DischargeRateSlider" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-max'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
<Label Name="DischargeMaxValue" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'battery-menu-current'}" StyleClasses="StatusFieldTitle" HorizontalExpand="True" />
<Label Name="DischargeCurrentValue" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<!-- Separator connecting panes with some wires -->
<BoxContainer Orientation="Vertical" SetWidth="22" Margin="2 16">
<PanelContainer Name="ChargePowerLine" SetHeight="2" VerticalAlignment="Top" VerticalExpand="True" />
<PanelContainer Name="DischargePowerLine" SetHeight="2" VerticalAlignment="Bottom" VerticalExpand="True" />
</BoxContainer>
<!-- Right pane: storage -->
<PanelContainer Name="Storage" StyleClasses="Inset" HorizontalExpand="True">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'battery-menu-storage-header'}" StyleClasses="LabelKeyText" />
<GridContainer Columns="2">
<Label Text="{Loc 'battery-menu-stored'}" StyleClasses="StatusFieldTitle" />
<Label Name="StoredPercentageValue" HorizontalAlignment="Right" HorizontalExpand="True" />
<Label Text="{Loc 'battery-menu-energy'}" StyleClasses="StatusFieldTitle" />
<Label Name="StoredEnergyValue" HorizontalAlignment="Right" />
<Label Name="EtaLabel" StyleClasses="StatusFieldTitle" />
<Label Name="EtaValue" HorizontalAlignment="Right" />
</GridContainer>
<!-- Charge meter -->
<GridContainer Name="ChargeMeter" Columns="3" VerticalExpand="True" Margin="0 24 0 0">
</GridContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Name="Footer" Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'battery-menu-footer-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'battery-menu-footer-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,280 @@
using System.Diagnostics.CodeAnalysis;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Power;
using Content.Shared.Rounding;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Power.Battery;
/// <summary>
/// Interface control for batteries.
/// </summary>
/// <seealso cref="BatteryBoundUserInterface"/>
[GenerateTypedNameReferences]
public sealed partial class BatteryMenu : FancyWindow
{
// Cutoff for the ETA time to switch from "~" to ">" and cap out.
private const float MaxEtaValueMinutes = 60;
// Cutoff where ETA times likely don't make sense and it's better to just say "N/A".
private const float NotApplicableEtaHighCutoffMinutes = 1000;
private const float NotApplicableEtaLowCutoffMinutes = 0.01f;
// Fudge factor to ignore small charge/discharge values, that are likely caused by floating point rounding errors.
private const float PrecisionRoundFactor = 100_000;
// Colors used for the storage cell bar graphic.
private static readonly Color[] StorageColors =
[
StyleNano.DangerousRedFore,
Color.FromHex("#C49438"),
Color.FromHex("#B3BF28"),
StyleNano.GoodGreenFore,
];
// StorageColors but dimmed for "off" bars.
private static readonly Color[] DimStorageColors =
[
DimStorageColor(StorageColors[0]),
DimStorageColor(StorageColors[1]),
DimStorageColor(StorageColors[2]),
DimStorageColor(StorageColors[3]),
];
// Parameters for the sine wave pulsing animations for active power lines in the UI.
private static readonly Color ActivePowerLineHighColor = Color.FromHex("#CCC");
private static readonly Color ActivePowerLineLowColor = Color.FromHex("#888");
private const float PowerPulseFactor = 4;
// Dependencies
[Dependency] private readonly IEntityManager _entityManager = null!;
[Dependency] private readonly ILocalizationManager _loc = null!;
// Active and inactive style boxes for power lines.
// We modify _activePowerLineStyleBox's properties programmatically to implement the pulsing animation.
private readonly StyleBoxFlat _activePowerLineStyleBox = new();
private readonly StyleBoxFlat _inactivePowerLineStyleBox = new() { BackgroundColor = Color.FromHex("#555") };
// Style boxes for the storage cell bar graphic.
// We modify the properties of these to change the bars' colors.
private StyleBoxFlat[] _chargeMeterBoxes;
// State for the powerline pulsing animation.
private float _powerPulseValue;
// State for the storage cell bar graphic and its blinking effect.
private float _blinkPulseValue;
private bool _blinkPulse;
private int _storageLevel;
private bool _hasStorageDelta;
// The entity that this UI is for.
private EntityUid _entity;
// Used to avoid sending input events when updating slider values.
private bool _suppressSliderEvents;
// Events for the BUI to subscribe to.
public event Action<bool>? OnInBreaker;
public event Action<bool>? OnOutBreaker;
public event Action<float>? OnChargeRate;
public event Action<float>? OnDischargeRate;
public BatteryMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
InitChargeMeter();
InBreaker.StateChanged += val => OnInBreaker?.Invoke(val);
OutBreaker.StateChanged += val => OnOutBreaker?.Invoke(val);
ChargeRateSlider.OnValueChanged += _ =>
{
if (!_suppressSliderEvents)
OnChargeRate?.Invoke(ChargeRateSlider.Value);
};
DischargeRateSlider.OnValueChanged += _ =>
{
if (!_suppressSliderEvents)
OnDischargeRate?.Invoke(DischargeRateSlider.Value);
};
}
public void SetEntity(EntityUid entity)
{
_entity = entity;
this.SetInfoFromEntity(_entityManager, _entity);
EntityView.SetEntity(entity);
}
[MemberNotNull(nameof(_chargeMeterBoxes))]
public void InitChargeMeter()
{
_chargeMeterBoxes = new StyleBoxFlat[StorageColors.Length];
for (var i = StorageColors.Length - 1; i >= 0; i--)
{
var styleBox = new StyleBoxFlat();
_chargeMeterBoxes[i] = styleBox;
for (var j = 0; j < ChargeMeter.Columns; j++)
{
var control = new PanelContainer
{
Margin = new Thickness(2),
PanelOverride = styleBox,
HorizontalExpand = true,
VerticalExpand = true,
};
ChargeMeter.AddChild(control);
}
}
}
public void Update(BatteryBuiState msg)
{
var inValue = msg.CurrentReceiving;
var outValue = msg.CurrentSupply;
var storageDelta = inValue - outValue;
// Mask rounding errors in power code.
if (Math.Abs(storageDelta) < msg.Capacity / PrecisionRoundFactor)
storageDelta = 0;
// Update power lines based on a ton of parameters.
SetPowerLineState(InPowerLine, msg.SupplyingNetworkHasPower);
SetPowerLineState(OutPowerLine, msg.LoadingNetworkHasPower);
SetPowerLineState(InSecondPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge);
SetPowerLineState(ChargePowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && storageDelta > 0);
SetPowerLineState(PassthroughPowerLine, msg.SupplyingNetworkHasPower && msg.CanCharge && msg.CanDischarge);
SetPowerLineState(OutSecondPowerLine,
msg.CanDischarge && (msg.Charge > 0 || msg.SupplyingNetworkHasPower && msg.CanCharge));
SetPowerLineState(DischargePowerLine, storageDelta < 0);
// Update breakers.
InBreaker.IsOn = msg.CanCharge;
OutBreaker.IsOn = msg.CanDischarge;
// Update various power values.
InValue.Text = FormatPower(inValue);
OutValue.Text = FormatPower(outValue);
PassthroughValue.Text = FormatPower(Math.Min(msg.CurrentReceiving, msg.CurrentSupply));
ChargeMaxValue.Text = FormatPower(msg.MaxChargeRate);
DischargeMaxValue.Text = FormatPower(msg.MaxSupply);
ChargeCurrentValue.Text = FormatPower(Math.Max(0, storageDelta));
DischargeCurrentValue.Text = FormatPower(Math.Max(0, -storageDelta));
// Update charge/discharge rate sliders.
_suppressSliderEvents = true;
ChargeRateSlider.MaxValue = msg.MaxMaxChargeRate;
ChargeRateSlider.MinValue = msg.MinMaxChargeRate;
ChargeRateSlider.Value = msg.MaxChargeRate;
DischargeRateSlider.MaxValue = msg.MaxMaxSupply;
DischargeRateSlider.MinValue = msg.MinMaxSupply;
DischargeRateSlider.Value = msg.MaxSupply;
_suppressSliderEvents = false;
// Update ETA display.
var storageEtaDiff = storageDelta > 0 ? (msg.Capacity - msg.Charge) * (1 / msg.Efficiency) : -msg.Charge;
var etaTimeSeconds = storageEtaDiff / storageDelta;
var etaTimeMinutes = etaTimeSeconds / 60.0;
EtaLabel.Text = _loc.GetString(
storageDelta > 0 ? "battery-menu-eta-full" : "battery-menu-eta-empty");
if (!double.IsFinite(etaTimeMinutes)
|| Math.Abs(etaTimeMinutes) > NotApplicableEtaHighCutoffMinutes
|| Math.Abs(etaTimeMinutes) < NotApplicableEtaLowCutoffMinutes)
{
EtaValue.Text = _loc.GetString("battery-menu-eta-value-na");
}
else
{
EtaValue.Text = _loc.GetString(
etaTimeMinutes > MaxEtaValueMinutes ? "battery-menu-eta-value-max" : "battery-menu-eta-value",
("minutes", Math.Min(Math.Ceiling(etaTimeMinutes), MaxEtaValueMinutes)));
}
// Update storage display.
StoredPercentageValue.Text = _loc.GetString(
"battery-menu-stored-percent-value",
("value", msg.Charge / msg.Capacity));
StoredEnergyValue.Text = _loc.GetString(
"battery-menu-stored-energy-value",
("value", msg.Charge));
// Update charge meter.
_storageLevel = ContentHelpers.RoundToNearestLevels(msg.Charge, msg.Capacity, _chargeMeterBoxes.Length);
_hasStorageDelta = Math.Abs(storageDelta) > 0;
}
private static Color DimStorageColor(Color color)
{
var hsv = Color.ToHsv(color);
hsv.Z /= 5;
return Color.FromHsv(hsv);
}
private void SetPowerLineState(PanelContainer control, bool value)
{
control.PanelOverride = value ? _activePowerLineStyleBox : _inactivePowerLineStyleBox;
}
private string FormatPower(float value)
{
return _loc.GetString("battery-menu-power-value", ("value", value));
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// Pulse power lines.
_powerPulseValue += args.DeltaSeconds * PowerPulseFactor;
var color = Color.InterpolateBetween(
ActivePowerLineLowColor,
ActivePowerLineHighColor,
MathF.Sin(_powerPulseValue) / 2 + 1);
_activePowerLineStyleBox.BackgroundColor = color;
// Update storage indicator and blink it.
for (var i = 0; i < _chargeMeterBoxes.Length; i++)
{
var box = _chargeMeterBoxes[i];
if (_storageLevel > i)
{
// On
box.BackgroundColor = StorageColors[i];
}
else
{
box.BackgroundColor = DimStorageColors[i];
}
}
_blinkPulseValue += args.DeltaSeconds;
if (_blinkPulseValue > 1)
{
_blinkPulseValue -= 1;
_blinkPulse ^= true;
}
// If there is a storage delta (charging or discharging), we want to blink the highest bar.
if (_hasStorageDelta)
{
// If there is no highest bar (UI completely at 0), then blink bar 0.
var toBlink = Math.Max(0, _storageLevel - 1);
_chargeMeterBoxes[toBlink].BackgroundColor =
_blinkPulse ? StorageColors[toBlink] : DimStorageColors[toBlink];
}
}
}

View File

@@ -66,10 +66,60 @@ namespace Content.Client.Stack
if (!_appearanceSystem.TryGetData<bool>(uid, StackVisuals.Hide, out var hidden, args.Component))
hidden = false;
if (comp.LayerFunction != StackLayerFunction.None)
ApplyLayerFunction((uid, comp), ref actual, ref maxCount);
if (comp.IsComposite)
_counterSystem.ProcessCompositeSprite(uid, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
else
_counterSystem.ProcessOpaqueSprite(uid, comp.BaseLayer, actual, maxCount, comp.LayerStates, hidden, sprite: args.Sprite);
}
/// <summary>
/// Adjusts the actual and maxCount to change how stack amounts are displayed.
/// </summary>
/// <param name="ent">The entity considered.</param>
/// <param name="actual">The actual number of items in the stack. Altered depending on the function to run.</param>
/// <param name="maxCount">The maximum number of items in the stack. Altered depending on the function to run.</param>
/// <returns>Whether or not a function was applied.</returns>
private bool ApplyLayerFunction(Entity<StackComponent> ent, ref int actual, ref int maxCount)
{
switch (ent.Comp.LayerFunction)
{
case StackLayerFunction.Threshold:
if (TryComp<StackLayerThresholdComponent>(ent, out var threshold))
{
ApplyThreshold(threshold, ref actual, ref maxCount);
return true;
}
break;
}
// No function applied.
return false;
}
/// <summary>
/// Selects which layer a stack applies based on a list of thresholds.
/// Each threshold passed results in the next layer being selected.
/// </summary>
/// <param name="comp">The threshold parameters to apply.</param>
/// <param name="actual">The number of items in the stack. Will be set to the index of the layer to use.</param>
/// <param name="maxCount">The maximum possible number of items in the stack. Will be set to the number of selectable layers.</param>
private static void ApplyThreshold(StackLayerThresholdComponent comp, ref int actual, ref int maxCount)
{
// We must stop before we run out of thresholds or layers, whichever's smaller.
maxCount = Math.Min(comp.Thresholds.Count + 1, maxCount);
var newActual = 0;
foreach (var threshold in comp.Thresholds)
{
//If our value exceeds threshold, the next layer should be displayed.
//Note: we must ensure actual <= MaxCount.
if (actual >= threshold && newActual < maxCount)
newActual++;
else
break;
}
actual = newActual;
}
}
}

View File

@@ -5,7 +5,7 @@ namespace Content.Client.Station;
/// <summary>
/// This handles letting the client know stations are a thing. Only really used by an admin menu.
/// </summary>
public sealed class StationSystem : EntitySystem
public sealed partial class StationSystem : SharedStationSystem
{
private readonly List<(string Name, NetEntity Entity)> _stations = new();
@@ -15,11 +15,14 @@ public sealed class StationSystem : EntitySystem
/// <remarks>
/// I'd have this just invoke an entity query, but we're on the client and the client barely knows about stations.
/// </remarks>
// TODO: Stations have a global PVS override now, this can probably be changed into a query.
public IReadOnlyList<(string Name, NetEntity Entity)> Stations => _stations;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<StationsUpdatedEvent>(StationsUpdated);
}

View File

@@ -66,6 +66,11 @@ namespace Content.Client.Stylesheets
public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton";
public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton";
public const string StyleClassStorageButton = "storageButton";
public const string StyleClassInset = "Inset";
public const string StyleClassConsoleHeading = "ConsoleHeading";
public const string StyleClassConsoleSubHeading = "ConsoleSubHeading";
public const string StyleClassConsoleText = "ConsoleText";
public const string StyleClassSliderRed = "Red";
public const string StyleClassSliderGreen = "Green";
@@ -179,6 +184,10 @@ namespace Content.Client.Stylesheets
var notoSansBold18 = resCache.NotoStack(variation: "Bold", size: 18);
var notoSansBold20 = resCache.NotoStack(variation: "Bold", size: 20);
var notoSansMono = resCache.GetFont("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf", size: 12);
var robotoMonoBold11 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 11);
var robotoMonoBold12 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 12);
var robotoMonoBold14 = resCache.GetFont("/Fonts/RobotoMono/RobotoMono-Bold.ttf", size: 14);
var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png");
var windowHeader = new StyleBoxTexture
{
@@ -413,9 +422,60 @@ namespace Content.Client.Stylesheets
};
progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
// Monotone (unfilled)
var monotoneButton = new StyleBoxTexture
{
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button.svg.96dpi.png"),
};
monotoneButton.SetPatchMargin(StyleBox.Margin.All, 11);
monotoneButton.SetPadding(StyleBox.Margin.All, 1);
monotoneButton.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
monotoneButton.SetContentMarginOverride(StyleBox.Margin.Horizontal, 14);
var monotoneButtonOpenLeft = new StyleBoxTexture(monotoneButton)
{
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_left.svg.96dpi.png"),
};
var monotoneButtonOpenRight = new StyleBoxTexture(monotoneButton)
{
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_right.svg.96dpi.png"),
};
var monotoneButtonOpenBoth = new StyleBoxTexture(monotoneButton)
{
Texture = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_button_open_both.svg.96dpi.png"),
};
// Monotone (filled)
var monotoneFilledButton = new StyleBoxTexture(monotoneButton)
{
Texture = buttonTex,
};
var monotoneFilledButtonOpenLeft = new StyleBoxTexture(monotoneButton)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
};
monotoneFilledButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0);
var monotoneFilledButtonOpenRight = new StyleBoxTexture(monotoneButton)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
};
monotoneFilledButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0);
var monotoneFilledButtonOpenBoth = new StyleBoxTexture(monotoneButton)
{
Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
};
monotoneFilledButtonOpenBoth.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
// CheckBox
var checkBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_checked.svg.96dpi.png");
var checkBoxTextureUnchecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_unchecked.svg.96dpi.png");
var monotoneCheckBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_checkbox_checked.svg.96dpi.png");
var monotoneCheckBoxTextureUnchecked = resCache.GetTexture("/Textures/Interface/Nano/Monotone/monotone_checkbox_unchecked.svg.96dpi.png");
// Tooltip box
var tooltipTexture = resCache.GetTexture("/Textures/Interface/Nano/tooltip.png");
@@ -945,6 +1005,17 @@ namespace Content.Client.Stylesheets
new StyleProperty(BoxContainer.StylePropertySeparation, 10),
}),
// MonotoneCheckBox
new StyleRule(new SelectorElement(typeof(TextureRect), new [] { MonotoneCheckBox.StyleClassMonotoneCheckBox }, null, null), new[]
{
new StyleProperty(TextureRect.StylePropertyTexture, monotoneCheckBoxTextureUnchecked),
}),
new StyleRule(new SelectorElement(typeof(TextureRect), new [] { MonotoneCheckBox.StyleClassMonotoneCheckBox, CheckBox.StyleClassCheckBoxChecked }, null, null), new[]
{
new StyleProperty(TextureRect.StylePropertyTexture, monotoneCheckBoxTextureChecked),
}),
// Tooltip
new StyleRule(new SelectorElement(typeof(Tooltip), null, null, null), new[]
{
@@ -1143,6 +1214,22 @@ namespace Content.Client.Stylesheets
new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray),
}),
// Console text
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleText}, null, null), new[]
{
new StyleProperty(Label.StylePropertyFont, robotoMonoBold11)
}),
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleSubHeading}, null, null), new[]
{
new StyleProperty(Label.StylePropertyFont, robotoMonoBold12)
}),
new StyleRule(new SelectorElement(typeof(Label), new[] {StyleClassConsoleHeading}, null, null), new[]
{
new StyleProperty(Label.StylePropertyFont, robotoMonoBold14)
}),
// Big Button
new StyleRule(new SelectorChild(
new SelectorElement(typeof(Button), new[] {StyleClassButtonBig}, null, null),
@@ -1242,6 +1329,64 @@ namespace Content.Client.Stylesheets
new StyleProperty(Label.StylePropertyFont, notoSansDisplayBold14),
}),
// MonotoneButton (unfilled)
new StyleRule(
new SelectorElement(typeof(MonotoneButton), null, null, null),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneButton),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenLeft }, null, null),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenLeft),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenRight }, null, null),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenRight),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenBoth }, null, null),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneButtonOpenBoth),
}),
// MonotoneButton (filled)
new StyleRule(
new SelectorElement(typeof(MonotoneButton), null, null, new[] { Button.StylePseudoClassPressed }),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButton),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenLeft }, null, new[] { Button.StylePseudoClassPressed }),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenLeft),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenRight }, null, new[] { Button.StylePseudoClassPressed }),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenRight),
}),
new StyleRule(
new SelectorElement(typeof(MonotoneButton), new[] { ButtonOpenBoth }, null, new[] { Button.StylePseudoClassPressed }),
new[]
{
new StyleProperty(Button.StylePropertyStyleBox, monotoneFilledButtonOpenBoth),
}),
// NanoHeading
new StyleRule(
@@ -1675,7 +1820,11 @@ namespace Content.Client.Stylesheets
new[]
{
new StyleProperty(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Bwoink/un_pinned.png"))
})
}),
Element<PanelContainer>()
.Class(StyleClassInset)
.Prop(PanelContainer.StylePropertyPanel, insetBack),
}).ToList());
}
}

View File

@@ -0,0 +1,75 @@
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface;
/// <summary>
/// Interface for <see cref="BoundUserInterface"/>s that need some updating logic
/// ran in the <see cref="ModUpdateLevel.PreEngine"/> stage.
/// </summary>
/// <remarks>
/// <para>
/// This is called on all open <see cref="BoundUserInterface"/>s that implement this interface.
/// </para>
/// <para>
/// One intended use case is coalescing input events (e.g. via <see cref="InputCoalescer{T}"/>) to send them to the
/// server only once per tick.
/// </para>
/// </remarks>
/// <seealso cref="BuiPreTickUpdateSystem"/>
public interface IBuiPreTickUpdate
{
void PreTickUpdate();
}
/// <summary>
/// Implements <see cref="BuiPreTickUpdateSystem"/>.
/// </summary>
public sealed class BuiPreTickUpdateSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
[Dependency] private readonly IGameTiming _gameTiming = null!;
private EntityQuery<UserInterfaceUserComponent> _userQuery;
public override void Initialize()
{
base.Initialize();
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
}
public void RunUpdates()
{
if (!_gameTiming.IsFirstTimePredicted)
return;
var localSession = _playerManager.LocalSession;
if (localSession?.AttachedEntity is not { } localEntity)
return;
if (!_userQuery.TryGetComponent(localEntity, out var userUIComp))
return;
foreach (var (entity, uis) in userUIComp.OpenInterfaces)
{
foreach (var key in uis)
{
if (!_uiSystem.TryGetOpenUi(entity, key, out var ui))
{
DebugTools.Assert("Unable to find UI that was in the open UIs list??");
continue;
}
if (ui is IBuiPreTickUpdate tickUpdate)
{
tickUpdate.PreTickUpdate();
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System.Linq;
using Robust.Client.Timing;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface;
/// <summary>
/// A local buffer for <see cref="BoundUserInterface"/>s to manually implement prediction.
/// </summary>
/// <remarks>
/// <para>
/// In many current (and future) cases, it is not practically possible to implement prediction for UIs
/// by implementing the logic in shared. At the same time, we want to implement prediction for the best user experience
/// (and it is sometimes the easiest way to make even a middling user experience).
/// </para>
/// <para>
/// You can queue predicted messages into this class with <see cref="SendMessage"/>,
/// and then call <see cref="MessagesToReplay"/> later from <see cref="BoundUserInterface.UpdateState"/>
/// to get all messages that are still "ahead" of the latest server state.
/// These messages can then manually be "applied" to the latest state received from the server.
/// </para>
/// <para>
/// Note that this system only works if the server is guaranteed to send some kind of update in response to UI messages,
/// or at a regular schedule. If it does not, there is no opportunity to error correct the prediction.
/// </para>
/// </remarks>
public sealed class BuiPredictionState
{
private readonly BoundUserInterface _parent;
private readonly IClientGameTiming _gameTiming;
private readonly Queue<MessageData> _queuedMessages = new();
public BuiPredictionState(BoundUserInterface parent, IClientGameTiming gameTiming)
{
_parent = parent;
_gameTiming = gameTiming;
}
public void SendMessage(BoundUserInterfaceMessage message)
{
if (_gameTiming.IsFirstTimePredicted)
{
var messageData = new MessageData
{
TickSent = _gameTiming.CurTick,
Message = message,
};
_queuedMessages.Enqueue(messageData);
}
_parent.SendPredictedMessage(message);
}
public IEnumerable<BoundUserInterfaceMessage> MessagesToReplay()
{
var curTick = _gameTiming.LastRealTick;
while (_queuedMessages.TryPeek(out var data) && data.TickSent <= curTick)
{
_queuedMessages.Dequeue();
}
if (_queuedMessages.Count == 0)
return [];
return _queuedMessages.Select(c => c.Message);
}
private struct MessageData
{
public GameTick TickSent;
public required BoundUserInterfaceMessage Message;
public override string ToString()
{
return $"{Message} @ {TickSent}";
}
}
}

View File

@@ -81,4 +81,54 @@ namespace Content.Client.UserInterface.Controls
return mode;
}
}
/// <summary>
/// Helper functions for working with <see cref="FancyWindow"/>.
/// </summary>
public static class FancyWindowExt
{
/// <summary>
/// Sets information for a window (title and guidebooks) based on an entity.
/// </summary>
/// <param name="window">The window to modify.</param>
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
/// <param name="entity">The entity that this window represents.</param>
/// <seealso cref="SetTitleFromEntity"/>
/// <seealso cref="SetGuidebookFromEntity"/>
public static void SetInfoFromEntity(this FancyWindow window, IEntityManager entityManager, EntityUid entity)
{
window.SetTitleFromEntity(entityManager, entity);
window.SetGuidebookFromEntity(entityManager, entity);
}
/// <summary>
/// Set a window's title to the name of an entity.
/// </summary>
/// <param name="window">The window to modify.</param>
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
/// <param name="entity">The entity that this window represents.</param>
/// <seealso cref="SetInfoFromEntity"/>
public static void SetTitleFromEntity(
this FancyWindow window,
IEntityManager entityManager,
EntityUid entity)
{
window.Title = entityManager.GetComponent<MetaDataComponent>(entity).EntityName;
}
/// <summary>
/// Set a window's guidebook IDs to those of an entity.
/// </summary>
/// <param name="window">The window to modify.</param>
/// <param name="entityManager">Entity manager used to retrieve the information.</param>
/// <param name="entity">The entity that this window represents.</param>
/// <seealso cref="SetInfoFromEntity"/>
public static void SetGuidebookFromEntity(
this FancyWindow window,
IEntityManager entityManager,
EntityUid entity)
{
window.HelpGuidebookIds = entityManager.GetComponentOrNull<GuideHelpComponent>(entity)?.Guides;
}
}
}

View File

@@ -264,12 +264,6 @@ public class ListContainer : Control
_updateChildren = false;
var toRemove = new Dictionary<ListData, ListContainerButton>(_buttons);
foreach (var child in Children.ToArray())
{
if (child == _vScrollBar)
continue;
RemoveChild(child);
}
if (_data.Count > 0)
{
@@ -292,8 +286,9 @@ public class ListContainer : Control
if (Toggle && data == _selected)
button.Pressed = true;
AddChild(button);
}
AddChild(button);
button.SetPositionInParent(i - _topIndex);
button.Measure(finalSize);
}
}

View File

@@ -0,0 +1,79 @@
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
using static Robust.Client.UserInterface.Controls.Label;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A button intended for use with a monotone color palette
/// </summary>
public sealed class MonotoneButton : ContainerButton
{
/// <summary>
/// Specifies the color of the label text when the button is pressed.
/// </summary>
[ViewVariables]
public Color AltTextColor { set; get; } = new Color(0.2f, 0.2f, 0.2f);
/// <summary>
/// The label that holds the button text.
/// </summary>
public Label Label { get; }
/// <summary>
/// The text displayed by the button.
/// </summary>
[PublicAPI, ViewVariables]
public string? Text { get => Label.Text; set => Label.Text = value; }
/// <summary>
/// How to align the text inside the button.
/// </summary>
[PublicAPI, ViewVariables]
public AlignMode TextAlign { get => Label.Align; set => Label.Align = value; }
/// <summary>
/// If true, the button will allow shrinking and clip text
/// to prevent the text from going outside the bounds of the button.
/// If false, the minimum size will always fit the contained text.
/// </summary>
[PublicAPI, ViewVariables]
public bool ClipText
{
get => Label.ClipText;
set => Label.ClipText = value;
}
public MonotoneButton()
{
Label = new Label
{
StyleClasses = { StyleClassButton }
};
AddChild(Label);
UpdateAppearance();
}
private void UpdateAppearance()
{
// Recolor the label
if (Label != null)
Label.ModulateSelfOverride = DrawMode == DrawModeEnum.Pressed ? AltTextColor : null;
// Modulate the button if disabled
Modulate = Disabled ? Color.Gray : Color.White;
}
protected override void StylePropertiesChanged()
{
base.StylePropertiesChanged();
UpdateAppearance();
}
protected override void DrawModeChanged()
{
base.DrawModeChanged();
UpdateAppearance();
}
}

View File

@@ -0,0 +1,24 @@
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A check box intended for use with a monotone color palette
/// </summary>
public sealed class MonotoneCheckBox : CheckBox
{
public const string StyleClassMonotoneCheckBox = "monotoneCheckBox";
public MonotoneCheckBox()
{
TextureRect.AddStyleClass(StyleClassMonotoneCheckBox);
}
protected override void DrawModeChanged()
{
base.DrawModeChanged();
// Appearance modulations
Modulate = Disabled ? Color.Gray : Color.White;
}
}

View File

@@ -0,0 +1,6 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal">
<Button Name="OffButton" StyleClasses="OpenRight" Text="{Loc 'ui-button-off'}" />
<Button Name="OnButton" StyleClasses="OpenLeft" Text="{Loc 'ui-button-on'}" />
</BoxContainer>
</Control>

View File

@@ -0,0 +1,48 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// A simple control that displays a toggleable on/off button.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class OnOffButton : Control
{
/// <summary>
/// Whether the control is currently in the "on" state.
/// </summary>
public bool IsOn
{
get => OnButton.Pressed;
set
{
if (value)
OnButton.Pressed = true;
else
OffButton.Pressed = true;
}
}
/// <summary>
/// Raised when the user changes the state of the control.
/// </summary>
/// <remarks>
/// This does not get raised if state is changed with <see cref="set_IsOn"/>.
/// </remarks>
public event Action<bool>? StateChanged;
public OnOffButton()
{
RobustXamlLoader.Load(this);
var group = new ButtonGroup(isNoneSetAllowed: false);
OffButton.Group = group;
OnButton.Group = group;
OffButton.OnPressed += _ => StateChanged?.Invoke(false);
OnButton.OnPressed += _ => StateChanged?.Invoke(true);
}
}

View File

@@ -0,0 +1,40 @@
using System.Diagnostics.CodeAnalysis;
namespace Content.Client.UserInterface;
/// <summary>
/// A simple utility class to "coalesce" multiple input events into a single one, fired later.
/// </summary>
/// <typeparam name="T"></typeparam>
public struct InputCoalescer<T>
{
public bool IsModified;
public T LastValue;
/// <summary>
/// Replace the value in the <see cref="InputCoalescer{T}"/>. This sets <see cref="IsModified"/> to true.
/// </summary>
public void Set(T value)
{
LastValue = value;
IsModified = true;
}
/// <summary>
/// Check if the <see cref="InputCoalescer{T}"/> has been modified.
/// If it was, return the value and clear <see cref="IsModified"/>.
/// </summary>
/// <returns>True if the value was modified since the last check.</returns>
public bool CheckIsModified([MaybeNullWhen(false)] out T value)
{
if (IsModified)
{
value = LastValue;
IsModified = false;
return true;
}
value = default;
return IsModified;
}
}

View File

@@ -12,6 +12,8 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
{
public sealed class AlertControl : BaseButton
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public AlertPrototype Alert { get; }
/// <summary>
@@ -33,8 +35,7 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
private (TimeSpan Start, TimeSpan End)? _cooldown;
private short? _severity;
private readonly IGameTiming _gameTiming;
private readonly IEntityManager _entityManager;
private readonly SpriteView _icon;
private readonly CooldownGraphic _cooldownGraphic;
@@ -47,8 +48,10 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
public AlertControl(AlertPrototype alert, short? severity)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
_entityManager = IoCManager.Resolve<IEntityManager>();
// Alerts will handle this.
MuteSounds = true;
IoCManager.InjectDependencies(this);
TooltipSupplier = SupplyTooltip;
Alert = alert;
_severity = severity;

View File

@@ -1,4 +1,5 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
@@ -17,7 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
public void SetOutputPressure(float value)
{
SendMessage(new GasTankSetPressureMessage
SendPredictedMessage(new GasTankSetPressureMessage
{
Pressure = value
});
@@ -25,13 +26,14 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
public void ToggleInternals()
{
SendMessage(new GasTankToggleInternalsMessage());
SendPredictedMessage(new GasTankToggleInternalsMessage());
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<GasTankWindow>();
_window.Entity = Owner;
_window.SetTitle(EntMan.GetComponent<MetaDataComponent>(Owner).EntityName);
_window.OnOutputPressure += SetOutputPressure;
_window.OnToggleInternals += ToggleInternals;
@@ -41,6 +43,12 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
{
base.UpdateState(state);
if (EntMan.TryGetComponent(Owner, out GasTankComponent? component))
{
var canConnect = EntMan.System<SharedGasTankSystem>().CanConnectToInternals((Owner, component));
_window?.Update(canConnect, component.IsConnected, component.OutputPressure);
}
if (state is GasTankBoundUserInterfaceState cast)
_window?.UpdateState(cast);
}

View File

@@ -3,11 +3,14 @@ using Content.Client.Message;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Timing;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
@@ -15,6 +18,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank;
public sealed class GasTankWindow
: BaseWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IResourceCache _cache = default!;
private readonly RichTextLabel _lblPressure;
@@ -23,6 +27,8 @@ public sealed class GasTankWindow
private readonly Button _btnInternals;
private readonly Label _topLabel;
public EntityUid Entity;
public event Action<float>? OnOutputPressure;
public event Action? OnToggleInternals;
@@ -194,12 +200,30 @@ public sealed class GasTankWindow
public void UpdateState(GasTankBoundUserInterfaceState state)
{
_lblPressure.SetMarkup(Loc.GetString("gas-tank-window-tank-pressure-text", ("tankPressure", $"{state.TankPressure:0.##}")));
_btnInternals.Disabled = !state.CanConnectInternals;
}
public void Update(bool canConnectInternals, bool internalsConnected, float outputPressure)
{
_btnInternals.Disabled = !canConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
("status", Loc.GetString(state.InternalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
if (state.OutputPressure.HasValue)
("status", Loc.GetString(internalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
_spbPressure.Value = outputPressure;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// Easier than managing state on any ent changes. Previously this was just ticked on server's GasTankSystem.
if (_entManager.TryGetComponent(Entity, out GasTankComponent? tank))
{
_spbPressure.Value = state.OutputPressure.Value;
var canConnectInternals = _entManager.System<SharedGasTankSystem>().CanConnectToInternals((Entity, tank));
_btnInternals.Disabled = !canConnectInternals;
}
if (!_btnInternals.Disabled)
{
_btnInternals.Disabled = _entManager.System<UseDelaySystem>().IsDelayed(Entity, id: SharedGasTankSystem.GasTankDelay);
}
}

View File

@@ -918,6 +918,11 @@ public sealed class ChatUIController : UIController
_typingIndicator?.ClientChangedChatText();
}
public void NotifyChatFocus(bool isFocused)
{
_typingIndicator?.ClientChangedChatFocus(isFocused);
}
public void Repopulate()
{
foreach (var chat in _chats)

View File

@@ -34,6 +34,8 @@ public partial class ChatBox : UIWidget
ChatInput.Input.OnTextEntered += OnTextEntered;
ChatInput.Input.OnKeyBindDown += OnInputKeyBindDown;
ChatInput.Input.OnTextChanged += OnTextChanged;
ChatInput.Input.OnFocusEnter += OnFocusEnter;
ChatInput.Input.OnFocusExit += OnFocusExit;
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter;
@@ -174,6 +176,18 @@ public partial class ChatBox : UIWidget
_controller.NotifyChatTextChange();
}
private void OnFocusEnter(LineEditEventArgs args)
{
// Warn typing indicator about focus
_controller.NotifyChatFocus(true);
}
private void OnFocusExit(LineEditEventArgs args)
{
// Warn typing indicator about focus
_controller.NotifyChatFocus(false);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -10,6 +10,7 @@ using Robust.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Content.Shared.IdentityManagement;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
namespace Content.Client.VendingMachines.UI
{
@@ -162,7 +163,9 @@ namespace Content.Client.VendingMachines.UI
continue;
var dummy = _dummies[proto];
var amount = cachedInventory.First(o => o.ID == proto).Amount;
if (!cachedInventory.TryFirstOrDefault(o => o.ID == proto, out var entry))
continue;
var amount = entry.Amount;
// Could be better? Problem is all inventory entries get squashed.
var text = GetItemText(dummy, amount);

View File

@@ -1,31 +0,0 @@
using Content.Client.Xenoarchaeology.Ui;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.GameObjects;
namespace Content.Client.Xenoarchaeology.Equipment;
/// <inheritdoc cref="SharedNodeScannerSystem"/>
public sealed class NodeScannerSystem : SharedNodeScannerSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeScannerComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
}
protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
{
_ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true);
}
private void OnAnalysisConsoleAfterAutoHandleState(Entity<NodeScannerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_ui.TryGetOpenUi<NodeScannerBoundUserInterface>(ent.Owner, NodeScannerUiKey.Key, out var bui))
bui.Update(ent);
}
}

View File

@@ -1,4 +1,3 @@
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
@@ -18,15 +17,6 @@ public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) :
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
_scannerDisplay.SetOwner(Owner);
_scannerDisplay.OnClose += Close;
}
/// <summary>
/// Update UI state based on corresponding component.
/// </summary>
public void Update(Entity<NodeScannerComponent> ent)
{
_scannerDisplay?.Update(ent);
}
/// <inheritdoc />

View File

@@ -1,8 +1,12 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Xenoarchaeology.Ui;
@@ -10,12 +14,21 @@ namespace Content.Client.Xenoarchaeology.Ui;
public sealed partial class NodeScannerDisplay : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IGameTiming _timing= default!;
private readonly SharedXenoArtifactSystem _artifact;
private TimeSpan? _nextUpdate;
private EntityUid _owner;
private TimeSpan _updateFromAttachedFrequency;
private readonly HashSet<string> _triggeredNodeNames = new();
public NodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
}
/// <summary>
@@ -30,30 +43,80 @@ public sealed partial class NodeScannerDisplay : FancyWindow
return;
}
Update((scannerEntityUid, scannerComponent));
_updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval;
_owner = scannerEntityUid;
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if(_nextUpdate != null && _timing.CurTime < _nextUpdate)
return;
_nextUpdate = _timing.CurTime + _updateFromAttachedFrequency;
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
{
Update(false, ArtifactState.None);
return;
}
var attachedArtifactEnt = connectedScanner.AttachedTo;
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
return;
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
_triggeredNodeNames.Clear();
ArtifactState artifactState;
if (unlockingComponent == null)
{
var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
artifactState = timeToUnlockAvailable > TimeSpan.Zero
? ArtifactState.Cooldown
: ArtifactState.Ready;
}
else
{
var triggeredIndexes = unlockingComponent.TriggeredNodeIndexes;
foreach (var triggeredIndex in triggeredIndexes)
{
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
_triggeredNodeNames.Add(triggeredNodeName);
}
artifactState = ArtifactState.Unlocking;
}
Update(true, artifactState, _triggeredNodeNames);
}
/// <summary>
/// Updates labels with scanned artifact data and list of triggered nodes from component.
/// </summary>
public void Update(Entity<NodeScannerComponent> ent)
private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? triggeredNodeNames = null)
{
ArtifactStateLabel.Text = GetState(ent);
var scannedAt = ent.Comp.ScannedAt;
NodeScannerState.Text = scannedAt > TimeSpan.Zero
? Loc.GetString("node-scanner-artifact-scanned-time", ("time", scannedAt.Value.ToString(@"hh\:mm\:ss")))
: Loc.GetString("node-scanner-artifact-scanned-time-none");
ArtifactStateLabel.Text = GetStateText(artifactState);
NodeScannerState.Text = isConnected
? Loc.GetString("node-scanner-artifact-connected")
: Loc.GetString("node-scanner-artifact-non-connected");
ActiveNodesList.Children.Clear();
var triggeredNodesSnapshot = ent.Comp.TriggeredNodesSnapshot;
if (triggeredNodesSnapshot.Count > 0)
if (triggeredNodeNames == null)
return;
if (triggeredNodeNames.Count > 0)
{
// show list of triggered nodes instead of 'no data' placeholder
NoActiveNodeDataLabel.Visible = false;
ActiveNodesList.Visible = true;
foreach (var nodeId in triggeredNodesSnapshot)
foreach (var nodeId in triggeredNodeNames)
{
var nodeLabel = new Button
{
@@ -73,9 +136,9 @@ public sealed partial class NodeScannerDisplay : FancyWindow
}
}
private string GetState(Entity<NodeScannerComponent> ent)
private string GetStateText(ArtifactState state)
{
return ent.Comp.ArtifactState switch
return state switch
{
ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),

View File

@@ -219,6 +219,7 @@ public sealed class CargoTest
- type: stack
id: StackProto
name: stack-steel
spawn: A
- type: entity

View File

@@ -181,7 +181,7 @@ public sealed class NukeOpsTest
}
Assert.That(!entMan.EntityExists(nukieStationEnt)); // its not supposed to be a station!
Assert.That(server.MapMan.MapExists(gridsRule.Map));
Assert.That(mapSys.MapExists(gridsRule.Map));
var nukieMap = mapSys.GetMap(gridsRule.Map!.Value);
var targetStation = entMan.GetComponent<StationDataComponent>(ruleComp.TargetStation!.Value);

View File

@@ -6,6 +6,7 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Nodes;
using Content.Shared.Coordinates;
using Content.Shared.NodeContainer;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;

View File

@@ -182,9 +182,9 @@ namespace Content.IntegrationTests.Tests
var server = pair.Server;
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var mapSystem = server.System<SharedMapSystem>();
EntityUid packageRight;
EntityUid packageWrong;
@@ -255,7 +255,7 @@ namespace Content.IntegrationTests.Tests
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent), Has.Count.GreaterThan(0),
"Machine available inventory count is not greater than zero after restock.");
mapManager.DeleteMap(testMap.MapId);
mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();
@@ -269,9 +269,9 @@ namespace Content.IntegrationTests.Tests
await server.WaitIdleAsync();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
var mapSystem = server.System<SharedMapSystem>();
var damageableSystem = entitySystemManager.GetEntitySystem<DamageableSystem>();
@@ -319,7 +319,7 @@ namespace Content.IntegrationTests.Tests
Assert.That(totalRamen, Is.EqualTo(2),
"Did not find enough ramen after destroying restock box.");
mapManager.DeleteMap(testMap.MapId);
mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();

View File

@@ -197,7 +197,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
if (!PrivilegedIdIsAuthorized(uid, component))
return;
if (!_interactionSystem.InRangeUnobstructed(uid, component.TargetAccessReaderId))
if (!_interactionSystem.InRangeUnobstructed(player, component.TargetAccessReaderId))
{
_popupSystem.PopupEntity(Loc.GetString("access-overrider-out-of-range"), player, player);

View File

@@ -18,6 +18,7 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Construction.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;

View File

@@ -5,6 +5,8 @@ using Content.Server.Chat.Managers;
using Content.Server.Explosion.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;

View File

@@ -10,6 +10,7 @@ using Content.Shared.Ame.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Mind.Components;
using Content.Shared.NodeContainer;
using Content.Shared.Power;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;

View File

@@ -84,10 +84,6 @@ public sealed class TileAnomalySystem : SharedTileAnomalySystem
private void SpawnTiles(Entity<TileSpawnAnomalyComponent> anomaly, TileSpawnSettingsEntry entry, float stability, float severity, float powerMod)
{
var xform = Transform(anomaly);
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
var tiles = _anomaly.GetSpawningPoints(anomaly, stability, severity, entry.Settings, powerMod);
if (tiles == null)
return;

View File

@@ -1,21 +0,0 @@
using Content.Shared.Inventory;
namespace Content.Server.Atmos.Components
{
/// <summary>
/// Used in internals as breath tool.
/// </summary>
[RegisterComponent]
[ComponentProtoName("BreathMask")]
public sealed partial class BreathToolComponent : Component
{
/// <summary>
/// Tool is functional only in allowed slots
/// </summary>
[DataField]
public SlotFlags AllowedSlots = SlotFlags.MASK | SlotFlags.HEAD;
public bool IsFunctional;
public EntityUid? ConnectedInternalsEntity;
}
}

View File

@@ -1,121 +0,0 @@
using Content.Shared.Atmos;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Components
{
[RegisterComponent]
public sealed partial class GasTankComponent : Component, IGasMixtureHolder
{
public const float MaxExplosionRange = 26f;
private const float DefaultLowPressure = 0f;
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
public int Integrity = 3;
public bool IsLowPressure => (Air?.Pressure ?? 0F) <= TankLowPressure;
[ViewVariables(VVAccess.ReadWrite), DataField("ruptureSound")]
public SoundSpecifier RuptureSound = new SoundPathSpecifier("/Audio/Effects/spray.ogg");
[ViewVariables(VVAccess.ReadWrite), DataField("connectSound")]
public SoundSpecifier? ConnectSound =
new SoundPathSpecifier("/Audio/Effects/internals.ogg")
{
Params = AudioParams.Default.WithVolume(5f),
};
[ViewVariables(VVAccess.ReadWrite), DataField("disconnectSound")]
public SoundSpecifier? DisconnectSound;
// Cancel toggles sounds if we re-toggle again.
public EntityUid? ConnectStream;
public EntityUid? DisconnectStream;
[DataField("air"), ViewVariables(VVAccess.ReadWrite)]
public GasMixture Air { get; set; } = new();
/// <summary>
/// Pressure at which tank should be considered 'low' such as for internals.
/// </summary>
[DataField("tankLowPressure"), ViewVariables(VVAccess.ReadWrite)]
public float TankLowPressure = DefaultLowPressure;
/// <summary>
/// Distributed pressure.
/// </summary>
[DataField("outputPressure"), ViewVariables(VVAccess.ReadWrite)]
public float OutputPressure = DefaultOutputPressure;
/// <summary>
/// The maximum allowed output pressure.
/// </summary>
[DataField("maxOutputPressure"), ViewVariables(VVAccess.ReadWrite)]
public float MaxOutputPressure = 3 * DefaultOutputPressure;
/// <summary>
/// Tank is connected to internals.
/// </summary>
[ViewVariables]
public bool IsConnected => User != null;
[ViewVariables]
public EntityUid? User;
/// <summary>
/// True if this entity was recently moved out of a container. This might have been a hand -> inventory
/// transfer, or it might have been the user dropping the tank. This indicates the tank needs to be checked.
/// </summary>
[ViewVariables]
public bool CheckUser;
/// <summary>
/// Pressure at which tanks start leaking.
/// </summary>
[DataField("tankLeakPressure"), ViewVariables(VVAccess.ReadWrite)]
public float TankLeakPressure = 30 * Atmospherics.OneAtmosphere;
/// <summary>
/// Pressure at which tank spills all contents into atmosphere.
/// </summary>
[DataField("tankRupturePressure"), ViewVariables(VVAccess.ReadWrite)]
public float TankRupturePressure = 40 * Atmospherics.OneAtmosphere;
/// <summary>
/// Base 3x3 explosion.
/// </summary>
[DataField("tankFragmentPressure"), ViewVariables(VVAccess.ReadWrite)]
public float TankFragmentPressure = 50 * Atmospherics.OneAtmosphere;
/// <summary>
/// Increases explosion for each scale kPa above threshold.
/// </summary>
[DataField("tankFragmentScale"), ViewVariables(VVAccess.ReadWrite)]
public float TankFragmentScale = 2 * Atmospherics.OneAtmosphere;
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ToggleAction = "ActionToggleInternals";
[DataField("toggleActionEntity")] public EntityUid? ToggleActionEntity;
/// <summary>
/// Valve to release gas from tank
/// </summary>
[DataField("isValveOpen"), ViewVariables(VVAccess.ReadWrite)]
public bool IsValveOpen = false;
/// <summary>
/// Gas release rate in L/s
/// </summary>
[DataField("valveOutputRate"), ViewVariables(VVAccess.ReadWrite)]
public float ValveOutputRate = 100f;
[DataField("valveSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ValveSound =
new SoundCollectionSpecifier("valveSqueak")
{
Params = AudioParams.Default.WithVolume(-5f),
};
}
}

View File

@@ -18,6 +18,7 @@ using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.NodeContainer;
namespace Content.Server.Atmos.Consoles;

View File

@@ -1,30 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
private void InitializeBreathTool()
{
SubscribeLocalEvent<BreathToolComponent, ComponentShutdown>(OnBreathToolShutdown);
}
private void OnBreathToolShutdown(Entity<BreathToolComponent> entity, ref ComponentShutdown args)
{
DisconnectInternals(entity);
}
public void DisconnectInternals(Entity<BreathToolComponent> entity)
{
var old = entity.Comp.ConnectedInternalsEntity;
entity.Comp.ConnectedInternalsEntity = null;
if (TryComp<InternalsComponent>(old, out var internalsComponent))
{
_internals.DisconnectBreathTool((old.Value, internalsComponent), entity.Owner);
}
entity.Comp.IsFunctional = false;
}
}

View File

@@ -36,7 +36,7 @@ public sealed partial class AtmosphereSystem
return;
}
var mixtures = new GasMixture[8];
var mixtures = new GasMixture[9];
for (var i = 0; i < mixtures.Length; i++)
mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
@@ -68,6 +68,10 @@ public sealed partial class AtmosphereSystem
// 7: Nitrogen (101kpa) for vox rooms
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);
// 8: Air (GM)
mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))

View File

@@ -44,8 +44,6 @@ public sealed partial class AtmosphereSystem
private void OnGridAtmosphereInit(EntityUid uid, GridAtmosphereComponent component, ComponentInit args)
{
base.Initialize();
EnsureComp<GasTileOverlayComponent>(uid);
foreach (var tile in component.Tiles.Values)
{

View File

@@ -28,7 +28,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly GasTileOverlaySystem _gasTileOverlaySystem = default!;
@@ -56,7 +55,6 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
UpdatesAfter.Add(typeof(NodeGroupSystem));
InitializeBreathTool();
InitializeGases();
InitializeCommands();
InitializeCVars();

View File

@@ -7,6 +7,7 @@ using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.NodeContainer;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;

View File

@@ -1,21 +1,13 @@
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Cargo.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Actions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Examine;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Throwing;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Configuration;
using Content.Shared.CCVar;
@@ -23,14 +15,11 @@ using Content.Shared.CCVar;
namespace Content.Server.Atmos.EntitySystems
{
[UsedImplicitly]
public sealed class GasTankSystem : EntitySystem
public sealed class GasTankSystem : SharedGasTankSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ExplosionSystem _explosions = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedAudioSystem _audioSys = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
@@ -44,17 +33,9 @@ namespace Content.Server.Atmos.EntitySystems
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasTankComponent, ComponentShutdown>(OnGasShutdown);
SubscribeLocalEvent<GasTankComponent, BeforeActivatableUIOpenEvent>(BeforeUiOpen);
SubscribeLocalEvent<GasTankComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<GasTankComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
SubscribeLocalEvent<GasTankComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<GasTankComponent, GasTankSetPressureMessage>(OnGasTankSetPressure);
SubscribeLocalEvent<GasTankComponent, GasTankToggleInternalsMessage>(OnGasTankToggleInternals);
SubscribeLocalEvent<GasTankComponent, GasAnalyzerScanEvent>(OnAnalyzed);
SubscribeLocalEvent<GasTankComponent, PriceCalculationEvent>(OnGasTankPrice);
SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
}
@@ -63,44 +44,16 @@ namespace Content.Server.Atmos.EntitySystems
_maxExplosionRange = value;
}
private void OnGasShutdown(Entity<GasTankComponent> gasTank, ref ComponentShutdown args)
{
DisconnectFromInternals(gasTank);
}
private void OnGasTankToggleInternals(Entity<GasTankComponent> ent, ref GasTankToggleInternalsMessage args)
{
ToggleInternals(ent);
}
private void OnGasTankSetPressure(Entity<GasTankComponent> ent, ref GasTankSetPressureMessage args)
{
var pressure = Math.Clamp(args.Pressure, 0f, ent.Comp.MaxOutputPressure);
ent.Comp.OutputPressure = pressure;
UpdateUserInterface(ent, true);
}
public void UpdateUserInterface(Entity<GasTankComponent> ent, bool initialUpdate = false)
public override void UpdateUserInterface(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
_ui.SetUiState(owner, SharedGasTankUiKey.Key,
new GasTankBoundUserInterfaceState
{
TankPressure = component.Air?.Pressure ?? 0,
OutputPressure = initialUpdate ? component.OutputPressure : null,
InternalsConnected = component.IsConnected,
CanConnectInternals = CanConnectToInternals(ent)
});
}
private void BeforeUiOpen(Entity<GasTankComponent> ent, ref BeforeActivatableUIOpenEvent args)
{
// Only initial update includes output pressure information, to avoid overwriting client-input as the updates come in.
UpdateUserInterface(ent, true);
}
private void OnParentChange(EntityUid uid, GasTankComponent component, ref EntParentChangedMessage args)
{
// When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor.
@@ -109,30 +62,6 @@ namespace Content.Server.Atmos.EntitySystems
component.CheckUser = true;
}
private void OnGetActions(EntityUid uid, GasTankComponent component, GetItemActionsEvent args)
{
args.AddAction(ref component.ToggleActionEntity, component.ToggleAction);
}
private void OnExamined(EntityUid uid, GasTankComponent component, ExaminedEvent args)
{
using var _ = args.PushGroup(nameof(GasTankComponent));
if (args.IsInDetailsRange)
args.PushMarkup(Loc.GetString("comp-gas-tank-examine", ("pressure", Math.Round(component.Air?.Pressure ?? 0))));
if (component.IsConnected)
args.PushMarkup(Loc.GetString("comp-gas-tank-connected"));
args.PushMarkup(Loc.GetString(component.IsValveOpen ? "comp-gas-tank-examine-open-valve" : "comp-gas-tank-examine-closed-valve"));
}
private void OnActionToggle(Entity<GasTankComponent> gasTank, ref ToggleActionEvent args)
{
if (args.Handled)
return;
ToggleInternals(gasTank);
args.Handled = true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
@@ -167,8 +96,10 @@ namespace Content.Server.Atmos.EntitySystems
{
_atmosphereSystem.React(comp.Air, comp);
}
CheckStatus(gasTank);
if (_ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
if ((comp.IsConnected || comp.IsValveOpen) && _ui.IsUiOpen(uid, SharedGasTankUiKey.Key))
{
UpdateUserInterface(gasTank);
}
@@ -190,18 +121,6 @@ namespace Content.Server.Atmos.EntitySystems
_audioSys.PlayPvs(gasTank.Comp.RuptureSound, gasTank);
}
private void ToggleInternals(Entity<GasTankComponent> ent)
{
if (ent.Comp.IsConnected)
{
DisconnectFromInternals(ent);
}
else
{
ConnectToInternals(ent);
}
}
public GasMixture? RemoveAir(Entity<GasTankComponent> gasTank, float amount)
{
var gas = gasTank.Comp.Air?.Remove(amount);
@@ -227,95 +146,6 @@ namespace Content.Server.Atmos.EntitySystems
return air;
}
public bool CanConnectToInternals(Entity<GasTankComponent> ent)
{
TryGetInternalsComp(ent, out _, out var internalsComp, ent.Comp.User);
return internalsComp != null && internalsComp.BreathTools.Count != 0 && !ent.Comp.IsValveOpen;
}
public void ConnectToInternals(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
if (component.IsConnected || !CanConnectToInternals(ent))
return;
TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, ent.Comp.User);
if (internalsUid == null || internalsComp == null)
return;
if (_internals.TryConnectTank((internalsUid.Value, internalsComp), owner))
component.User = internalsUid.Value;
_actions.SetToggled(component.ToggleActionEntity, component.IsConnected);
// Couldn't toggle!
if (!component.IsConnected)
return;
component.ConnectStream = _audioSys.Stop(component.ConnectStream);
component.ConnectStream = _audioSys.PlayPvs(component.ConnectSound, owner)?.Entity;
UpdateUserInterface(ent);
}
public void DisconnectFromInternals(Entity<GasTankComponent> ent)
{
var (owner, component) = ent;
if (component.User == null)
return;
TryGetInternalsComp(ent, out var internalsUid, out var internalsComp, component.User);
component.User = null;
_actions.SetToggled(component.ToggleActionEntity, false);
if (internalsUid != null && internalsComp != null)
_internals.DisconnectTank((internalsUid.Value, internalsComp));
component.DisconnectStream = _audioSys.Stop(component.DisconnectStream);
component.DisconnectStream = _audioSys.PlayPvs(component.DisconnectSound, owner)?.Entity;
UpdateUserInterface(ent);
}
/// <summary>
/// Tries to retrieve the internals component of either the gas tank's user,
/// or the gas tank's... containing container
/// </summary>
/// <param name="user">The user of the gas tank</param>
/// <returns>True if internals comp isn't null, false if it is null</returns>
private bool TryGetInternalsComp(Entity<GasTankComponent> ent, out EntityUid? internalsUid, out InternalsComponent? internalsComp, EntityUid? user = null)
{
internalsUid = default;
internalsComp = default;
// If the gas tank doesn't exist for whatever reason, don't even bother
if (TerminatingOrDeleted(ent.Owner))
return false;
user ??= ent.Comp.User;
// Check if the gas tank's user actually has the component that allows them to use a gas tank and mask
if (TryComp<InternalsComponent>(user, out var userInternalsComp) && userInternalsComp != null)
{
internalsUid = user;
internalsComp = userInternalsComp;
return true;
}
// Yeah I have no clue what this actually does, I appreciate the lack of comments on the original function
if (_containers.TryGetContainingContainer((ent.Owner, Transform(ent.Owner)), out var container) && container != null)
{
if (TryComp<InternalsComponent>(container.Owner, out var containerInternalsComp) && containerInternalsComp != null)
{
internalsUid = container.Owner;
internalsComp = containerInternalsComp;
return true;
}
}
return false;
}
public void AssumeAir(Entity<GasTankComponent> ent, GasMixture giver)
{
_atmosphereSystem.Merge(ent.Comp.Air, giver);
@@ -404,21 +234,5 @@ namespace Content.Server.Atmos.EntitySystems
{
args.Price += _atmosphereSystem.GetPrice(component.Air);
}
private void OnGetAlternativeVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return;
args.Verbs.Add(new AlternativeVerb()
{
Text = component.IsValveOpen ? Loc.GetString("comp-gas-tank-close-valve") : Loc.GetString("comp-gas-tank-open-valve"),
Act = () =>
{
component.IsValveOpen = !component.IsValveOpen;
_audioSys.PlayPvs(component.ValveSound, uid);
},
Disabled = component.IsConnected,
});
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Construction.Components;
using Content.Shared.NodeContainer;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;

View File

@@ -1,9 +0,0 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos
{
public interface IGasMixtureHolder
{
public GasMixture Air { get; set; }
}
}

View File

@@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.NodeContainer;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.Piping.EntitySystems;

View File

@@ -7,6 +7,7 @@ using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Construction.Components;
using Content.Shared.Destructible;
using Content.Shared.NodeContainer;
using Content.Shared.Popups;
using JetBrains.Annotations;

View File

@@ -1,73 +0,0 @@
using Content.Shared.Atmos;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Guidebook;
using Robust.Shared.Audio;
namespace Content.Server.Atmos.Piping.Unary.Components
{
[RegisterComponent]
public sealed partial class GasCanisterComponent : Component, IGasMixtureHolder
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("port")]
public string PortName { get; set; } = "port";
/// <summary>
/// Container name for the gas tank holder.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("container")]
public string ContainerName { get; set; } = "tank_slot";
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ItemSlot GasTankSlot = new();
[ViewVariables(VVAccess.ReadWrite)]
[DataField("gasMixture")]
public GasMixture Air { get; set; } = new();
/// <summary>
/// Last recorded pressure, for appearance-updating purposes.
/// </summary>
public float LastPressure { get; set; } = 0f;
/// <summary>
/// Minimum release pressure possible for the release valve.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("minReleasePressure")]
public float MinReleasePressure { get; set; } = Atmospherics.OneAtmosphere / 10;
/// <summary>
/// Maximum release pressure possible for the release valve.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxReleasePressure")]
public float MaxReleasePressure { get; set; } = Atmospherics.OneAtmosphere * 10;
/// <summary>
/// Valve release pressure.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("releasePressure")]
public float ReleasePressure { get; set; } = Atmospherics.OneAtmosphere;
/// <summary>
/// Whether the release valve is open on the canister.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("releaseValve")]
public bool ReleaseValve { get; set; } = false;
[DataField("accessDeniedSound")]
public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
#region GuidebookData
[GuidebookData]
public float Volume => Air.Volume;
#endregion
}
}

View File

@@ -1,55 +1,32 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Cargo.Systems;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Atmos.Piping.Unary.Systems;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Lock;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Content.Shared.NodeContainer;
using GasCanisterComponent = Content.Shared.Atmos.Piping.Unary.Components.GasCanisterComponent;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems;
public sealed class GasCanisterSystem : EntitySystem
public sealed class GasCanisterSystem : SharedGasCanisterSystem
{
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly ItemSlotsSystem _slots = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasCanisterComponent, ComponentStartup>(OnCanisterStartup);
SubscribeLocalEvent<GasCanisterComponent, AtmosDeviceUpdateEvent>(OnCanisterUpdated);
SubscribeLocalEvent<GasCanisterComponent, ActivateInWorldEvent>(OnCanisterActivate, after: new[] { typeof(LockSystem) });
SubscribeLocalEvent<GasCanisterComponent, InteractHandEvent>(OnCanisterInteractHand);
SubscribeLocalEvent<GasCanisterComponent, ItemSlotInsertAttemptEvent>(OnCanisterInsertAttempt);
SubscribeLocalEvent<GasCanisterComponent, EntInsertedIntoContainerMessage>(OnCanisterContainerInserted);
SubscribeLocalEvent<GasCanisterComponent, EntRemovedFromContainerMessage>(OnCanisterContainerRemoved);
SubscribeLocalEvent<GasCanisterComponent, PriceCalculationEvent>(CalculateCanisterPrice);
SubscribeLocalEvent<GasCanisterComponent, GasAnalyzerScanEvent>(OnAnalyzed);
// Bound UI subscriptions
SubscribeLocalEvent<GasCanisterComponent, GasCanisterHoldingTankEjectMessage>(OnHoldingTankEjectMessage);
SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleasePressureMessage>(OnCanisterChangeReleasePressure);
SubscribeLocalEvent<GasCanisterComponent, GasCanisterChangeReleaseValveMessage>(OnCanisterChangeReleaseValve);
}
/// <summary>
@@ -65,24 +42,16 @@ public sealed class GasCanisterSystem : EntitySystem
if (environment is not null)
_atmos.Merge(environment, canister.Air);
_adminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
AdminLogger.Add(LogType.CanisterPurged, LogImpact.Medium, $"Canister {ToPrettyString(uid):canister} purged its contents of {canister.Air:gas} into the environment.");
canister.Air.Clear();
}
private void OnCanisterStartup(EntityUid uid, GasCanisterComponent comp, ComponentStartup args)
{
// Ensure container
_slots.AddItemSlot(uid, comp.ContainerName, comp.GasTankSlot);
}
private void DirtyUI(EntityUid uid,
GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
protected override void DirtyUI(EntityUid uid, GasCanisterComponent? canister = null, NodeContainerComponent? nodeContainer = null)
{
if (!Resolve(uid, ref canister, ref nodeContainer))
return;
var portStatus = false;
string? tankLabel = null;
var tankPressure = 0f;
if (_nodeContainer.TryGetNode(nodeContainer, canister.PortName, out PipeNode? portNode) && portNode.NodeGroup?.Nodes.Count > 1)
@@ -92,62 +61,11 @@ public sealed class GasCanisterSystem : EntitySystem
{
var tank = canister.GasTankSlot.Item.Value;
var tankComponent = Comp<GasTankComponent>(tank);
tankLabel = Name(tank);
tankPressure = tankComponent.Air.Pressure;
}
_ui.SetUiState(uid, GasCanisterUiKey.Key,
new GasCanisterBoundUserInterfaceState(Name(uid),
canister.Air.Pressure, portStatus, tankLabel, tankPressure, canister.ReleasePressure,
canister.ReleaseValve, canister.MinReleasePressure, canister.MaxReleasePressure));
}
private void OnHoldingTankEjectMessage(EntityUid uid, GasCanisterComponent canister, GasCanisterHoldingTankEjectMessage args)
{
if (canister.GasTankSlot.Item == null)
return;
var item = canister.GasTankSlot.Item;
_slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
if (canister.ReleaseValve)
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
}
else
{
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
}
}
private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
{
var pressure = Math.Clamp(args.Pressure, canister.MinReleasePressure, canister.MaxReleasePressure);
_adminLogger.Add(LogType.CanisterPressure, LogImpact.Medium, $"{ToPrettyString(args.Actor):player} set the release pressure on {ToPrettyString(uid):canister} to {args.Pressure}");
canister.ReleasePressure = pressure;
DirtyUI(uid, canister);
}
private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
{
// filling a jetpack with plasma is less important than filling a room with it
var hasItem = canister.GasTankSlot.HasItem;
var impact = hasItem ? LogImpact.Medium : LogImpact.High;
_adminLogger.Add(
LogType.CanisterValve,
impact,
$"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
canister.ReleaseValve = args.Valve;
DirtyUI(uid, canister);
}
private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
{
return string.Join(", ", canister.Comp.Air);
UI.SetUiState(uid, GasCanisterUiKey.Key,
new GasCanisterBoundUserInterfaceState(canister.Air.Pressure, portStatus, tankPressure));
}
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
@@ -207,76 +125,6 @@ public sealed class GasCanisterSystem : EntitySystem
}
}
private void OnCanisterActivate(EntityUid uid, GasCanisterComponent component, ActivateInWorldEvent args)
{
if (!args.Complex)
return;
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
if (CheckLocked(uid, component, args.User))
return;
// Needs to be here so the locked check still happens if the canister
// is locked and you don't have permissions
if (args.Handled)
return;
_ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
args.Handled = true;
}
private void OnCanisterInteractHand(EntityUid uid, GasCanisterComponent component, InteractHandEvent args)
{
if (!TryComp<ActorComponent>(args.User, out var actor))
return;
if (CheckLocked(uid, component, args.User))
return;
_ui.OpenUi(uid, GasCanisterUiKey.Key, actor.PlayerSession);
args.Handled = true;
}
private void OnCanisterInsertAttempt(EntityUid uid, GasCanisterComponent component, ref ItemSlotInsertAttemptEvent args)
{
if (args.Slot.ID != component.ContainerName || args.User == null)
return;
if (!TryComp<GasTankComponent>(args.Item, out var gasTank) || gasTank.IsValveOpen)
{
args.Cancelled = true;
return;
}
// Preventing inserting a tank since if its locked you cant remove it.
if (!CheckLocked(uid, component, args.User.Value))
return;
args.Cancelled = true;
}
private void OnCanisterContainerInserted(EntityUid uid, GasCanisterComponent component, EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != component.ContainerName)
return;
DirtyUI(uid, component);
_appearance.SetData(uid, GasCanisterVisuals.TankInserted, true);
}
private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.ContainerName)
return;
DirtyUI(uid, component);
_appearance.SetData(uid, GasCanisterVisuals.TankInserted, false);
}
/// <summary>
/// Mix air from a gas container into a pipe net.
/// Useful for anything that uses connector ports.
@@ -317,23 +165,4 @@ public sealed class GasCanisterSystem : EntitySystem
args.GasMixtures.Add((Name(tank), tankComponent.Air));
}
}
/// <summary>
/// Check if the canister is locked, playing its sound and popup if so.
/// </summary>
/// <returns>
/// True if locked, false otherwise.
/// </returns>
private bool CheckLocked(EntityUid uid, GasCanisterComponent comp, EntityUid user)
{
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked)
{
_popup.PopupEntity(Loc.GetString("gas-canister-popup-denied"), uid, user);
_audio.PlayPvs(comp.AccessDeniedSound, uid);
return true;
}
return false;
}
}

View File

@@ -22,11 +22,12 @@ using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DoAfter;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Power;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
@@ -41,7 +42,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly WeldableSystem _weldable = default!;
[Dependency] private readonly SharedToolSystem _toolSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -60,7 +60,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent<GasVentPumpComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<GasVentPumpComponent, GasAnalyzerScanEvent>(OnAnalyzed);
SubscribeLocalEvent<GasVentPumpComponent, WeldableChangedEvent>(OnWeldChanged);
SubscribeLocalEvent<GasVentPumpComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<GasVentPumpComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<GasVentPumpComponent, VentScrewedDoAfterEvent>(OnVentScrewed);
}
@@ -379,23 +379,43 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
{
UpdateState(uid, component);
}
private void OnInteractUsing(EntityUid uid, GasVentPumpComponent component, InteractUsingEvent args)
private void OnGetVerbs(Entity<GasVentPumpComponent> ent, ref GetVerbsEvent<Verb> args)
{
if (args.Handled
|| component.UnderPressureLockout == false
|| !_toolSystem.HasQuality(args.Used, "Screwing")
|| !Transform(uid).Anchored
)
{
if (ent.Comp.UnderPressureLockout == false || !Transform(ent).Anchored)
return;
}
args.Handled = true;
var user = args.User;
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ManualLockoutDisableDoAfter, new VentScrewedDoAfterEvent(), uid, uid, args.Used));
var v = new Verb
{
Priority = 1,
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png")),
Text = Loc.GetString("gas-vent-pump-release-lockout"),
Impact = LogImpact.Low,
DoContactInteraction = true,
Act = () =>
{
var doAfter = new DoAfterArgs(EntityManager, user, ent.Comp.ManualLockoutDisableDoAfter, new VentScrewedDoAfterEvent(), ent, ent)
{
BreakOnDamage = true,
NeedHand = true,
BreakOnMove = true,
BreakOnWeightlessMove = true,
};
_doAfterSystem.TryStartDoAfter(doAfter);
},
};
args.Verbs.Add(v);
}
private void OnVentScrewed(EntityUid uid, GasVentPumpComponent component, VentScrewedDoAfterEvent args)
{
if (args.Cancelled || args.Handled)
return;
component.ManualLockoutReenabledAt = _timing.CurTime + component.ManualLockoutDisabledDuration;
component.IsPressureLockoutManuallyDisabled = true;
}

View File

@@ -1,29 +0,0 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
namespace Content.Server.Body.Components
{
/// <summary>
/// Handles hooking up a mask (breathing tool) / gas tank together and allowing the Owner to breathe through it.
/// </summary>
[RegisterComponent]
public sealed partial class InternalsComponent : Component
{
[ViewVariables]
public EntityUid? GasTankEntity;
[ViewVariables]
public HashSet<EntityUid> BreathTools { get; set; } = new();
/// <summary>
/// Toggle Internals delay when the target is not you.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(3);
[DataField]
public ProtoId<AlertPrototype> InternalsAlert = "Internals";
}
}

View File

@@ -1,28 +1,21 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Internals;
using Content.Shared.Inventory;
using Content.Shared.Roles;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
namespace Content.Server.Body.Systems;
public sealed class InternalsSystem : EntitySystem
public sealed class InternalsSystem : SharedInternalsSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly GasTankSystem _gasTank = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly RespiratorSystem _respirator = default!;
private EntityQuery<InternalsComponent> _internalsQuery;
@@ -34,12 +27,6 @@ public sealed class InternalsSystem : EntitySystem
_internalsQuery = GetEntityQuery<InternalsComponent>();
SubscribeLocalEvent<InternalsComponent, InhaleLocationEvent>(OnInhaleLocation);
SubscribeLocalEvent<InternalsComponent, ComponentStartup>(OnInternalsStartup);
SubscribeLocalEvent<InternalsComponent, ComponentShutdown>(OnInternalsShutdown);
SubscribeLocalEvent<InternalsComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
SubscribeLocalEvent<InternalsComponent, InternalsDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<InternalsComponent, ToggleInternalsAlertEvent>(OnToggleInternalsAlert);
SubscribeLocalEvent<InternalsComponent, StartingGearEquippedEvent>(OnStartingGear);
}
@@ -66,120 +53,6 @@ public sealed class InternalsSystem : EntitySystem
ToggleInternals(uid, uid, force: false, component);
}
private void OnGetInteractionVerbs(
Entity<InternalsComponent> ent,
ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands is null)
return;
if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
return;
var user = args.User;
InteractionVerb verb = new()
{
Act = () =>
{
ToggleInternals(ent, user, force: false, ent);
},
Message = Loc.GetString("action-description-internals-toggle"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/dot.svg.192dpi.png")),
Text = Loc.GetString("action-name-internals-toggle"),
};
args.Verbs.Add(verb);
}
public void ToggleInternals(
EntityUid uid,
EntityUid user,
bool force,
InternalsComponent? internals = null)
{
if (!Resolve(uid, ref internals, logMissing: false))
return;
// Toggle off if they're on
if (AreInternalsWorking(internals))
{
if (force)
{
DisconnectTank((uid, internals));
return;
}
StartToggleInternalsDoAfter(user, (uid, internals));
return;
}
// If they're not on then check if we have a mask to use
if (internals.BreathTools.Count == 0)
{
_popupSystem.PopupEntity(Loc.GetString("internals-no-breath-tool"), uid, user);
return;
}
var tank = FindBestGasTank(uid);
if (tank is null)
{
_popupSystem.PopupEntity(Loc.GetString("internals-no-tank"), uid, user);
return;
}
if (!force)
{
StartToggleInternalsDoAfter(user, (uid, internals));
return;
}
_gasTank.ConnectToInternals(tank.Value);
}
private void StartToggleInternalsDoAfter(EntityUid user, Entity<InternalsComponent> targetEnt)
{
// Is the target not you? If yes, use a do-after to give them time to respond.
var isUser = user == targetEnt.Owner;
var delay = !isUser ? targetEnt.Comp.Delay : TimeSpan.Zero;
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, delay, new InternalsDoAfterEvent(), targetEnt, target: targetEnt)
{
BreakOnDamage = true,
BreakOnMove = true,
MovementThreshold = 0.1f,
});
}
private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent args)
{
if (args.Cancelled || args.Handled)
return;
ToggleInternals(ent, args.User, force: true, ent);
args.Handled = true;
}
private void OnToggleInternalsAlert(Entity<InternalsComponent> ent, ref ToggleInternalsAlertEvent args)
{
if (args.Handled)
return;
ToggleInternals(ent, ent, false, internals: ent.Comp);
args.Handled = true;
}
private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
{
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
{
_alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
}
private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
{
if (AreInternalsWorking(ent))
@@ -190,110 +63,4 @@ public sealed class InternalsSystem : EntitySystem
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
}
public void DisconnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
{
ent.Comp.BreathTools.Remove(toolEntity);
if (TryComp(toolEntity, out BreathToolComponent? breathTool))
_atmos.DisconnectInternals((toolEntity, breathTool));
if (ent.Comp.BreathTools.Count == 0)
DisconnectTank(ent);
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
{
if (!ent.Comp.BreathTools.Add(toolEntity))
return;
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
}
public void DisconnectTank(Entity<InternalsComponent> ent)
{
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
_gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
ent.Comp.GasTankEntity = null;
_alerts.ShowAlert(ent.Owner, ent.Comp.InternalsAlert, GetSeverity(ent.Comp));
}
public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
{
if (ent.Comp.BreathTools.Count == 0)
return false;
if (TryComp(ent.Comp.GasTankEntity, out GasTankComponent? tank))
_gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
ent.Comp.GasTankEntity = tankEntity;
_alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
return true;
}
public bool AreInternalsWorking(EntityUid uid, InternalsComponent? component = null)
{
return Resolve(uid, ref component, logMissing: false)
&& AreInternalsWorking(component);
}
public bool AreInternalsWorking(InternalsComponent component)
{
return TryComp(component.BreathTools.FirstOrNull(), out BreathToolComponent? breathTool)
&& breathTool.IsFunctional
&& HasComp<GasTankComponent>(component.GasTankEntity);
}
private short GetSeverity(InternalsComponent component)
{
if (component.BreathTools.Count == 0 || !AreInternalsWorking(component))
return 2;
// If pressure in the tank is below low pressure threshold, flash warning on internals UI
if (TryComp<GasTankComponent>(component.GasTankEntity, out var gasTank)
&& gasTank.IsLowPressure)
{
return 0;
}
return 1;
}
public Entity<GasTankComponent>? FindBestGasTank(
Entity<HandsComponent?, InventoryComponent?, ContainerManagerComponent?> user)
{
// TODO use _respirator.CanMetabolizeGas() to prioritize metabolizable gasses
// Prioritise
// 1. back equipped tanks
// 2. exo-slot tanks
// 3. in-hand tanks
// 4. pocket/belt tanks
if (!Resolve(user, ref user.Comp2, ref user.Comp3))
return null;
if (_inventory.TryGetSlotEntity(user, "back", out var backEntity, user.Comp2, user.Comp3) &&
TryComp<GasTankComponent>(backEntity, out var backGasTank) &&
_gasTank.CanConnectToInternals((backEntity.Value, backGasTank)))
{
return (backEntity.Value, backGasTank);
}
if (_inventory.TryGetSlotEntity(user, "suitstorage", out var entity, user.Comp2, user.Comp3) &&
TryComp<GasTankComponent>(entity, out var gasTank) &&
_gasTank.CanConnectToInternals((entity.Value, gasTank)))
{
return (entity.Value, gasTank);
}
foreach (var item in _inventory.GetHandOrInventoryEntities((user.Owner, user.Comp1, user.Comp2)))
{
if (TryComp(item, out gasTank) && _gasTank.CanConnectToInternals((item, gasTank)))
return (item, gasTank);
}
return null;
}
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
@@ -6,6 +5,8 @@ using Content.Shared.Atmos;
using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent;
using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
namespace Content.Server.Body.Systems;
@@ -23,7 +24,6 @@ public sealed class LungSystem : EntitySystem
SubscribeLocalEvent<LungComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<BreathToolComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<BreathToolComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
}
private void OnGotUnequipped(Entity<BreathToolComponent> ent, ref GotUnequippedEvent args)
@@ -38,8 +38,6 @@ public sealed class LungSystem : EntitySystem
return;
}
ent.Comp.IsFunctional = true;
if (TryComp(args.Equipee, out InternalsComponent? internals))
{
ent.Comp.ConnectedInternalsEntity = args.Equipee;
@@ -56,24 +54,6 @@ public sealed class LungSystem : EntitySystem
}
}
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
{
if (args.Mask.Comp.IsToggled)
{
_atmos.DisconnectInternals(ent);
}
else
{
ent.Comp.IsFunctional = true;
if (TryComp(args.Wearer, out InternalsComponent? internals))
{
ent.Comp.ConnectedInternalsEntity = args.Wearer;
_internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
}
}
}
public void GasToReagent(EntityUid uid, LungComponent lung)
{
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))

View File

@@ -1,5 +1,6 @@
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
namespace Content.Server.Botany.Systems;
@@ -37,4 +38,23 @@ public sealed partial class BotanySystem
solutionContainer.AddReagent(chem, amount);
}
}
public void OnProduceExamined(EntityUid uid, ProduceComponent comp, ExaminedEvent args)
{
if (comp.Seed == null)
return;
using (args.PushGroup(nameof(ProduceComponent)))
{
foreach (var m in comp.Seed.Mutations)
{
// Don't show mutations that have no effect on produce (sentience)
if (!m.AppliesToProduce)
continue;
if (m.Description != null)
args.PushMarkup(Loc.GetString(m.Description));
}
}
}
}

View File

@@ -36,6 +36,7 @@ public sealed partial class BotanySystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<SeedComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ProduceComponent, ExaminedEvent>(OnProduceExamined);
}
public bool TryGetSeed(SeedComponent comp, [NotNullWhen(true)] out SeedData? seed)

View File

@@ -116,6 +116,20 @@ public sealed class PlantHolderSystem : EntitySystem
? "plant-holder-component-plant-old-adjective"
: "plant-holder-component-plant-unhealthy-adjective"))));
}
// For future reference, mutations should only appear on examine if they apply to a plant, not to produce.
if (component.Seed.Ligneous)
args.PushMarkup(Loc.GetString("mutation-plant-ligneous"));
if (component.Seed.TurnIntoKudzu)
args.PushMarkup(Loc.GetString("mutation-plant-kudzu"));
if (component.Seed.CanScream)
args.PushMarkup(Loc.GetString("mutation-plant-scream"));
if (component.Seed.Viable == false)
args.PushMarkup(Loc.GetString("mutation-plant-unviable"));
}
else
{

View File

@@ -1,10 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Cargo.Components;
[RegisterComponent]
public sealed partial class CargoShuttleConsoleComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("soundDeny")]
public SoundSpecifier DenySound = new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_two.ogg");
}

View File

@@ -93,7 +93,11 @@ public sealed partial class CargoSystem
if (TryComp<AccessReaderComponent>(uid, out var accessReaderComponent) &&
!_accessReaderSystem.IsAllowed(mob, uid, accessReaderComponent))
{
_audio.PlayPvs(component.DenySound, uid);
if (Timing.CurTime >= component.NextDenySoundTime)
{
component.NextDenySoundTime = Timing.CurTime + component.DenySoundDelay;
_audio.PlayPvs(component.DenySound, uid);
}
return;
}

View File

@@ -13,8 +13,10 @@ using Content.Shared.Interaction;
using Content.Shared.Labels.Components;
using Content.Shared.Paper;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems
@@ -23,6 +25,7 @@ namespace Content.Server.Cargo.Systems
{
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private void InitializeConsole()
{
@@ -35,11 +38,8 @@ namespace Content.Server.Cargo.Systems
SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
private void OnInteractUsingCash(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
{
if (!HasComp<CashComponent>(args.Used))
return;
var price = _pricing.GetPrice(args.Used);
if (price == 0)
@@ -56,6 +56,55 @@ namespace Content.Server.Cargo.Systems
args.Handled = true;
}
private void OnInteractUsingSlip(Entity<CargoOrderConsoleComponent> ent, ref InteractUsingEvent args, CargoSlipComponent slip)
{
if (slip.OrderQuantity <= 0)
return;
var stationUid = _station.GetOwningStation(ent);
if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
return;
if (!_protoMan.TryIndex(slip.Product, out var product))
{
Log.Error($"Tried to add invalid cargo product {slip.Product} as order!");
return;
}
if (!ent.Comp.AllowedGroups.Contains(product.Group))
return;
var orderId = GenerateOrderId(orderDatabase);
var data = new CargoOrderData(orderId, product.Product, product.Name, product.Cost, slip.OrderQuantity, slip.Requester, slip.Reason, slip.Account);
if (!TryAddOrder(stationUid.Value, ent.Comp.Account, data, orderDatabase))
{
PlayDenySound(ent, ent.Comp);
return;
}
// Log order addition
_audio.PlayPvs(ent.Comp.ScanSound, ent);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.User):user} inserted order slip [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]");
QueueDel(args.Used);
args.Handled = true;
}
private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
{
if (HasComp<CashComponent>(args.Used))
{
OnInteractUsingCash(uid, component, ref args);
}
else if (TryComp<CargoSlipComponent>(args.Used, out var slip) && !component.SlipPrinter)
{
OnInteractUsingSlip((uid, component), ref args, slip);
}
}
private void OnInit(EntityUid uid, CargoOrderConsoleComponent orderConsole, ComponentInit args)
{
var station = _station.GetOwningStation(uid);
@@ -94,6 +143,9 @@ namespace Content.Server.Cargo.Systems
if (args.Actor is not { Valid: true } player)
return;
if (component.SlipPrinter)
return;
if (!_accessReaderSystem.IsAllowed(player, uid))
{
ConsolePopup(args.Actor, Loc.GetString("cargo-console-order-not-allowed"));
@@ -115,7 +167,7 @@ namespace Content.Server.Cargo.Systems
// Find our order again. It might have been dispatched or approved already
var order = orderDatabase.Orders[component.Account].Find(order => args.OrderId == order.OrderId && !order.Approved);
if (order == null)
if (order == null || !_protoMan.TryIndex(order.Account, out var account))
{
return;
}
@@ -128,7 +180,7 @@ namespace Content.Server.Cargo.Systems
return;
}
var amount = GetOutstandingOrderCount(orderDatabase, component.Account);
var amount = GetOutstandingOrderCount(orderDatabase, order.Account);
var capacity = orderDatabase.Capacity;
// Too many orders, avoid them getting spammed in the UI.
@@ -150,7 +202,7 @@ namespace Content.Server.Cargo.Systems
}
var cost = order.Price * order.OrderQuantity;
var accountBalance = GetBalanceFromAccount((station.Value, bank), component.Account);
var accountBalance = GetBalanceFromAccount((station.Value, bank), order.Account);
// Not enough balance
if (cost > accountBalance)
@@ -166,7 +218,7 @@ namespace Content.Server.Cargo.Systems
if (!ev.Handled)
{
ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), component.Account, order, orderDatabase);
ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order.Account, order, orderDatabase);
if (ev.FulfillmentEntity == null)
{
@@ -190,8 +242,8 @@ namespace Content.Server.Cargo.Systems
("orderAmount", order.OrderQuantity),
("approver", order.Approver ?? string.Empty),
("cost", cost));
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
if (CargoOrderConsoleComponent.BaseAnnouncementChannel != component.AnnouncementChannel)
_radio.SendRadioMessage(uid, message, account.RadioChannel, uid, escapeMarkup: false);
if (CargoOrderConsoleComponent.BaseAnnouncementChannel != account.RadioChannel)
_radio.SendRadioMessage(uid, message, CargoOrderConsoleComponent.BaseAnnouncementChannel, uid, escapeMarkup: false);
}
@@ -200,10 +252,10 @@ namespace Content.Server.Cargo.Systems
// Log order approval
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {component.Account} with balance at {accountBalance}");
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] on account {order.Account} with balance at {accountBalance}");
orderDatabase.Orders[component.Account].Remove(order);
UpdateBankAccount((station.Value, bank), -cost, component.Account);
UpdateBankAccount((station.Value, bank), -cost, order.Account);
UpdateOrders(station.Value);
}
@@ -259,12 +311,48 @@ namespace Content.Server.Cargo.Systems
{
var station = _station.GetOwningStation(uid);
if (component.SlipPrinter)
return;
if (!TryGetOrderDatabase(station, out var orderDatabase))
return;
RemoveOrder(station.Value, component.Account, args.OrderId, orderDatabase);
}
private void OnAddOrderMessageSlipPrinter(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args, CargoProductPrototype product)
{
if (!_protoMan.TryIndex(component.Account, out var account))
return;
if (Timing.CurTime < component.NextPrintTime)
return;
var label = Spawn(account.AcquisitionSlip, Transform(uid).Coordinates);
component.NextPrintTime = Timing.CurTime + component.PrintDelay;
_audio.PlayPvs(component.PrintSound, uid);
var paper = EnsureComp<PaperComponent>(label);
var msg = new FormattedMessage();
msg.AddMarkupPermissive(Loc.GetString("cargo-acquisition-slip-body",
("product", product.Name),
("description", product.Description),
("unit", product.Cost),
("amount", args.Amount),
("cost", product.Cost * args.Amount),
("orderer", args.Requester),
("reason", args.Reason)));
_paperSystem.SetContent((label, paper), msg.ToMarkup());
var slip = EnsureComp<CargoSlipComponent>(label);
slip.Product = product.ID;
slip.Requester = args.Requester;
slip.Reason = args.Reason;
slip.OrderQuantity = args.Amount;
slip.Account = component.Account;
}
private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
{
if (args.Actor is not { Valid: true } player)
@@ -287,7 +375,13 @@ namespace Content.Server.Cargo.Systems
if (!component.AllowedGroups.Contains(product.Group))
return;
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
if (component.SlipPrinter)
{
OnAddOrderMessageSlipPrinter(uid, component, args, product);
return;
}
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase), component.Account);
if (!TryAddOrder(stationUid.Value, component.Account, data, orderDatabase))
{
@@ -339,12 +433,16 @@ namespace Content.Server.Cargo.Systems
private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component)
{
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
if (_timing.CurTime >= component.NextDenySoundTime)
{
component.NextDenySoundTime = _timing.CurTime + component.DenySoundDelay;
_audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
}
}
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id, ProtoId<CargoAccountPrototype> account)
{
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason, account);
}
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component, ProtoId<CargoAccountPrototype> account)
@@ -378,16 +476,6 @@ namespace Content.Server.Cargo.Systems
UpdateOrderState(uid, station);
}
var consoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
while (consoleQuery.MoveNext(out var uid, out var _))
{
var station = _station.GetOwningStation(uid);
if (station != dbUid)
continue;
UpdateShuttleState(uid, station);
}
}
public bool AddAndApproveOrder(
@@ -407,7 +495,7 @@ namespace Content.Server.Cargo.Systems
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
// Make an order
var id = GenerateOrderId(component);
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description);
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description, account);
// Approve it now
order.SetApproverData(dest, sender);

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