Upstream sync (#1464)

* Fix soap recipe so it actually works (#37923)

Change soap recipe

* Automatic changelog update

* Automatic changelog update

* Added security mime mask (#37890)

* Added security mime mask

* Improved security mime mask equipped sprites

* Improved security mime mask icon sprite

* meta.json spacing fix

* Reorganized Turret Prototypes (#37863)

* Reorganized turret prototypes

* Added circuit board to migrations

* Fixed frequencies

* Fixed typos and set the base control panel to abstract since it's technically not constructable yet

* Fixed the typo in the migrations

* Addressed changes

* Whitespace fixes

* Whitespace check fix

---------

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

* Automatic changelog update

* Sellable syndicate contraband (#37835)

* Sellable syndicate contraband

* 1984 tests, laughible

* Balancing

* Standardized running and walking speed debuffs for equipped gear. (#37828)

Co-authored-by: RedBookcase <Usualmoves@gmail.com>

* Add hair back to hardhats (#38086)

adds hair back to hardhats

* Automatic changelog update

* Access Reader Refactor (#37772)

* Initial commit

* Integration test fix

* Removed redundant dirtying of accessreader

* Adds individual Bulldog magazines to the syndicate uplink (#37917)

* Brief commit to update branch

* added more magazines to uplink, corrected spelling in ftl file for uplink, corrected sprite state for incendiary bulldog mag

* removed incendiary drum, increased cost of slug drum

* Emogarbage swoop

---------

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

* Automatic changelog update

* Improve Potted Plant Movability and Availability (#37591)

* Added Purchasable Potted Plant Crate

* Removing some whitespace that made it in elsewhere

* Making Potted Plants Items and pickupable

* Revert Making potted plants not fade

This reverts part of commit a50a7def80c06e5be68a37263306007739952a28.

* Adding self to credits

* Fixing sus spacing

* Automatic changelog update

* Rifle crate order (#35535)

* lecter crate and gift

* revert cargo gift security armor change

* rifle cost down to 8000 spesos

* Automatic changelog update

* Muzzles reduce emote sound (#34444)

* Muzzles reduce emote sound

* update based on review comments

* review comments

* Automatic changelog update

* Added Scrap Armor [Reupload]. (#37601)

* Added Scrap Armor v2.

* Added a note to scrap helmet about vision reduction.

* Removed protection from step triggers.

* The patient needs broken kneecaps to live.

* Add in missing tag

* Missing tags pt 2

---------

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

* Automatic changelog update

* Make improvised explosive devices minor contra (#38088)

pipe bomb and fire bombs are minor contra

* Automatic changelog update

* emergency shuttle emergency lockers (#38095)

* Cleanup unused/redundant dependencies (#38099)

* Shark toys use the bite animation. (#38113)

* Automatic changelog update

* Fix throwing items into disposal units (#38106)

* Increase throwing into disposal chance (#38116)

* Automatic changelog update

* Don't show climb verb for objects that cannot be climbed (#38115)

fix climb verb

* Reduce Liquid Frezon Duration (#38100)

testing

* Add player autofill to rename command. (#38074)

* simple enough

* switch to CompletionResult.FromOptions and CompletionHelper.SessionNames

* cleanup

* Light Up Landmines (#38092)

* Adjust landmines to have unshaded lights.

* Someday I will give myself credit for the work I do.

* Adjust animation speed.

* Automatic changelog update

* fix wallmount vending machine dispense direction (#38112)

fix wallmount vending machine direction

* Automatic changelog update

* Fix serialization of `SunShadowCycleComponent` (#38002)

* Use a struct to hold the items for `Directions` in `SunShadowComponent`, fix serialization.

* actually make them datafields...

* Add NetSerializable to datadef

* this is why we can't have nice things

* ShowEmergencyShuttleCommand and MeleeSpreadCommand LEC conversion. (#37993)

progress

* Add name to chameleon controller implanter (#38117)

Update implanters.yml

* Automatic changelog update

* chem.yml cleanup (#38129)

Update chem.yml

* Reduce disabler capacity to 16 shots (#36019)

* 14 shots

* 14 to 16

* Automatic changelog update

* Genpop wall lockers (#38102)

* sprites

* genpop wall lockers

* number 5 alive

* slight boilerplate code, thank you slarti

* fix: allow ejecting diamonds from lathes/silos (#38132)

* Automatic changelog update

* make PacifiedComponent session specific (#38137)

* Fix stripping window for more than 2 hands (#37577)

* Automatic changelog update

* Forbid string literals for `NpcFactionSystem` methods (#38140)

* Add ForbidLiteral attribute to NpcFactionSystem methods

* Cleanup resulting warnings

* Update Credits (#38143)

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

* No raffle for space carp (#38101)

No raffle for carp

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

* Automatic changelog update

* adjreagent now accepts float (#38134)

* adjreagent now accepts float

* var

* Automatic changelog update

* MapManager warnings cleanup Server Edition 2003 (#36781)

* now you see me

* unused depen

* test fail fix attempt 1

* test fail fix attempt 2

* fix test fail attempt 3

* shot in the dark.

* Does this work?

* import cleanup

* taking a shot at this.

* Convert PersistenceSaveCommand to LocalizedEntityCommands.

* requested changes

* requested changes. also dealt with improperly named private const

* Update Content.Server/GameTicking/GameTicker.Spawning.cs

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

* Update Content.Server/GameTicking/GameTicker.Spawning.cs

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

* Convert PlanetCommand to LocalizedEntityCommand

* Update BiomeSystem.cs

* Update Content.Server/Salvage/SalvageSystem.Runner.cs

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

* Update Content.Server/Procedural/DungeonSystem.Rooms.cs

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

* Update Content.Server/Salvage/SpawnSalvageMissionJob.cs

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

* Update Content.Server/Station/Systems/StationBiomeSystem.cs

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

* revert to latest master.

* slartis suggestion.

* Update SetMapAtmosCommand.cs

* cleanup

* Update PersistenceSaveCommand.cs

* finish localizing persistencesavecommand

* this is icky, I change.

* :sigh:

* revert whatever I did here?

* oh I see, some inconsistencies.

* revert this

* Update PlanetCommand.cs

* move this ftl to the commands folder

---------

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

* Missing contraband tags on explosives (#37521)

* Firebombs as minor contraband

* Pipebombs as minor contraband

* Seismic charge as cargo/sec contra (like crushers)

* Modular payloads

* Automatic changelog update

* Improved Health examination text (#38126)

* more health examinations

* better blood

* fix

* tweaks

* sizeable

* silicon fix

* tweaks

* bleeding color tweaks

* fix

* Automatic changelog update

* Assorted device and core inhands (#38162)

* Assorted device and core inhands

* whoops forgot the thief objective

* i missed literally on pixel on the fulton argh

* inhand pinpointer update

* Automatic changelog update

* Add FixedPoint2TypeParser (#38169)

* Update submodule to 262.0.0 (#38174)

* Fix eating and drinking verbs showing up after a short delay and making your verb UI bounce (#38164)

* Fix eating and drinking verbs showing up after a short delay and making your verb UI bounce

* Usings fix

* Usings fix

* Usings fix

* Usings fix

* CVar fix

* Predicted ppups

* Openable predicted popup

* Fix audio prediction

* Automatic changelog update

* Allow cargo request computers to send orders to the primary account (#37943)

* Automatic changelog update

* Revert "Add trailing whitespace check workflow" (#38179)

Revert "Add trailing whitespace check workflow (#37367)"

This reverts commit be35f6e9ce.

* Give unique names to all curtain construction recipes (#38182)

* Give unique names to all curtain construction recipes

* Duplicate entry

* Cleanup unused fields in `DrinkSystem` (#38181)

Cleanup unused fields in DrinkSystem

* New Recipes: Breakfasts (#37279)

* Adds full english and american breakfasts and respective recipes

* sprite tweak and added random spawner

* reagent rebalance

* reagent re-rebalance

* spawner ID fix

* ACTUALLY actually fixed the spawner, also description changes

* reagent re-re-rebalance

* spawner fix so the linter stops screaming at me

* different check failed kekw

* trailing whitespace fix attempt #1

* identation fix? i think?

* indetation fix part 2, also vox cant eat this anymore

* more yaml fixes

---------

Co-authored-by: DispenserDev <mariousjoaquin@gmail.com>

* Typo fix in coffin description (#38193)

* Automatic changelog update

* Derotate and remove several maps, remove unused maps (#38190)

* Automatic changelog update

* Emergency Shuttles Update (#38186)

* Automatic changelog update

* hi-viz vest laser reflect (#38180)

* Update vests.yml

* Update vests.yml

* Update Resources/Prototypes/Entities/Clothing/OuterClothing/vests.yml

---------

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

* Automatic changelog update

* Bring back low pressure damage to its unnerfed value (#38196)

* Automatic changelog update

* Remade the AI Camera Static Shader (#38093)

Redid the camera static shader

* Automatic changelog update

* stop amber AI core from inducing seizures (#38209)

* Automatic changelog update

* HOTFIX: Stop AI turrets from causing flickering lights (#38212)

no power draw

* Add missing locale in Criminal Records Computer (#38217)

add missing filter locale

* WHYYYYYYY (cleanup) (#38220)

* WHYYYYYYYY

* Delete Resources/Locale/en-US/actions/actions/rootable.ftl

* cleanup mirror.yml (#38222)

* Add stamina damage resists to the bloodred raid/hard-suits (and ERT hardsuits). [TEST MERGE] (#37040)

Add stamina damage resists to the bloodred suits.

* Automatic changelog update

* Fix missing bike horn implant cooldown (#38237)

* Automatic changelog update

* Cleanup `ChameleonJobLoadoutTest` (#38229)

Cleanup ChameleonJobLoadoutTest

* Fix construction menu grid view quirky button selection (#38214)

* make hover and info use construction proto name, not entity name

* recipeButtons uses IDs as keys, not names

* return making item green when selected

* Replace Speech bubble time accumulator with TimeSpan (#38241)

* Replace SpeechBubble time accumulator with TimeSpan

* CurTime -> RealTime

* Add test that entity prototypes don't use LocIds for name/description (#38234)

* Add test that entity prototypes don't use LocIds for name/description

* Fix violation

* Guitar Sprite Fix (#38207)

* Modified Guitar Sprites

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

* Attributions

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

---------

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

* Add technology to tech disk names (#38157)

* Automatic changelog update

* Forbid string literals for `SharedToolSystem` methods (#38250)

* Forbid string literals for ToolSystem methods

* Cleanup violations

* Sort usings

* Fix mops, rags, and towels interacting with drains (#38252)

added the stuff

* Automatic changelog update

* Cherrypick 38241

* Makes the grenadine bottle start closed. (#38219)

* whywhywhy

* removes unneeded line

* Automatic changelog update

* Fix reinforced plasma window blocking laser shots (#38041)

* init

* stuff

* Automatic changelog update

* Improve some `BinSystem` functionality (#38262)

* BinSystem improvements

* Prevent double-add

* Cleanup 2 `SpriteComponent.GetPrototypeTextures` uses (#38263)

Cleanup 2 SpriteComponent.GetPrototypeTextures uses

* Add the instrument names to the MIDI channel selector (#38083)

* Add the instrument to the MIDI channel selector

* Reviews

Adds support for chained masters
Makes the channel UI update on its own when the midi changes (Works with bands too!)

* add to admin logs and limit track count

* Limit track names by length too

* remove left over comment

* Requested changes

* Reviews

* Automatic changelog update

* Add disambiguation to most construction menu recipe names (#38197)

* Magazine rsi Cleanup + Magazine Type Recolor (#37709)

* Automatic changelog update

* Basilisk Nerfs for Gameplay (#38089)

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

* Automatic changelog update

* Launchers require wielding (#38110)

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

* Automatic changelog update

* Cleanup exosuit fab recipes, add modules category (#38069)

* Fix `StomachSystem` PVS error (#38268)

Fix StomachSystem PVS error

* scar availability for more species (#35645)

* :D

* updated for new scars

* small eye scar

* Automatic changelog update

* Minor Relic improvements (#38269)

* Automatic changelog update

* Removed Toys.rsi, reorganize toy RSIs, broad /Fun/ cleanup. (#37189)

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

* Add propeller hat & swirl lollipop (#38260)

* Automatic changelog update

* Cleanup chat highlighting code (#38235)

* Cache regexes

* Convert static LocManager to resolved

* Use Any instead of Count > 0

* Use var instead of explicit types

* Make _highlights readonly

* Fix some popup issues with pipe layer changes (#38244)

* fix: only show pipe layer popups to the user changing them

* fix: don't show pipe layer subfloor popup when not using a screwdriver

* fix: properly pass down user and used for serverside pipe layer changes

Technically this doesn't matter as the Shared codepath for this method
doesn't actually use these on the server: only the user is used for a
clientside popup. Still, will be good to have these for future changes
that might need them.

* Make BODAs start with Russian accent. (#38275)

long hand of Kremlin

* fix: Correct respath for lizard smite icon (#38288)

* Fix gas analyzer and anom scanner wrong state (#38285)

* Fix gas analyzer and anomaly scanner UI activation issue

* save

* fix comment

* milkalyzer

* Automatic changelog update

* Use correct on-head rainbow lizard plushie sprite (#38286)

* Prediction for use of utensil on incompatible food (#38297)

Prediction our beloved

* Automatic changelog update

* Fix crafting steps with modular machine parts (#38299)

fixed lines that were messing with construction menu entries for machine parts

* Automatic changelog update

* Transparent tear gas (#36440)

* tweaked tear gas

* changed color of tear gas

* sped up metabolism rate of tear gas

* sped up tear gas passing more

* reduced tear gas content in grenades

* added threshold for better effect end timing

* removed clickable component from foams

* Suggestion changes

* Hide in spawn menu

---------

Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>

* Automatic changelog update

* Fix visitor mindshields (#36537)

* Make visitor mindshields removable, and give disaster victims mindshields.

* Fix whitespace

* Fix ALL the whitespace

* Automatic changelog update

* Rat King changed to Antag (#38124)

* Changed rat king to be antag
Rat King guide updated to show antag rules

Initial infected given zombie faction
Zombies given zombie faction too (not sure if redundant or not)

* Given zombie faction to Initial Infected and Zombies in mind_roles

* Changed MindRoleGhostRoleAntag to MindRoleGhostRoleSoloAntagonist

removed components from antag listings

* Removed changes for zombies

* Update Resources/Prototypes/Roles/Antags/zombie.yml

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

* Update Resources/Prototypes/ai_factions.yml

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

* Changed mob prototype to match

* Forgot solo again

---------

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

* Automatic changelog update

* fix missing locale for Undergarment (#38304)

* Automatic changelog update

* Fix puncturase cauterizing bleeding (#38289)

fix puncturase cauterizing bleeding

* Automatic changelog update

* Added monkey/kobold emergency EVA sprites & the ability for them to wear them. (#37660)

* Added monkey/kobold emergency eva suit sprites, and the ability for them to wear it

* Added tag, fixed indentation

* Ok actually fixed the indentation this time

* Automatic changelog update

* Elkridge - Major Update (#38259)

* swap HOP and vault, fix cams, new genpop, shit ton of stuff

* fixed some initialised open doors, added posters to HOP/EVA

* implemented suggestions and feedback, lots of things

* Automatic changelog update

* Zombies won't attack Initial Infected (#38128)

* Added Zombie faction roles everywhere that makes sense for initial infected to be associated with Zombies.

* Adding faction component to round start zombie. (not hopeful)

* Removed stupid attempts and added zombie faction component to the zombie system when the player is given the ability to force themself to zombify.

* Changed use of "zombie" for ProtoId<NpcFactionPrototype>

* Update Content.Server/Zombies/ZombieSystem.cs

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

* Update Content.Server/Zombies/ZombieSystem.cs

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

* Removed faction component from ZombieOutbreak event. Shouldn't cause problems with the event as II gets component with their ability to self zombify.

---------

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

* Added warden's uniforms to their dresser. (#38283)

* Added warden's uniforms to their dresser.

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

* Added warden's guitar

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

* Removed armored coats and guitar from warden dresser.

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

---------

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

* Automatic changelog update

* Add corgium (#37951)

* Add corgi essence

* Adjust spacing concerns

* Renamed 'corgi essence' to 'corgium' based on maintainer discussion

* Automatic changelog update

* Add pet bag - wearable backpack for pets (#37440)

* Add pet bag

* Trailing whitespace moment

* make the linter happy

* Fix trailing whitespace concern.

* Fix Construction component oopsy-daisy

* yml guideline moment

* remove weird extra whitespace

* make the maintainer happy

* Adjusted bag visuals based on feedback

* Allow the fox aghost admin to use pet bags

* Automatic changelog update

* Fix for holoparasite's ability to attack the host through mech (#36659)

* Automatic changelog update

* Retractable items system + Arm Blade action (#38150)

* Automatic changelog update

* Give Syndie-Cats Syndie Access (#38318)

* Update animals.yml

* Update animals.yml

* Automatic changelog update

* Update Credits (#38321)

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

* Ensure large entities don't get stuck on conveyor belt corners (#37466)

* Initial commit

* Also avoid the friction change doesn't make turned-off conveyor belts continue going

* Ensurecomp on conveyor starting

* i give up

* Minor brackets edit

* Documentation

* Automatic changelog update

* Armblade Tweaks (#38337)

init

* Exo Station - Upstream Playtest Version (#33144)

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

* Automatic changelog update

* Randomized Markings (#34765)

randomized markings

* Automatic changelog update

* Add hints towards flash protection to all items which provide it (#37267)

* Update welding.yml

* Update FlashSystem.cs

Add subscriber event for flash immunity examine text

* Create flash-immunity-component.ftl

* Removed changed description text

* change to use entity version

* fixed mistake

* Revert "Randomized Markings" (#38345)

* Add group for loadouts (#36951)

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

* Automatic changelog update

* Commands for adding and removing actions (#38317)

* Automatic changelog update

* Fix Smart Corgis not being targeted by hostile faction entities (#38349)

* Automatic changelog update

* Exo - Day 1 Patch (#38348)

* Automatic changelog update

* Cleanup duplicate dependency in `AddActionCommand` (#38360)

Cleanup duplicate dependency in AddActionCommand

* Gun Inhand Sprite Fixes (#38294)

* cobra, viper, pulse pistol

* c20 and wt550

* l6 + attributions

* Thieves can innately pickpocket - thieving gloves rework (#38123)

* Thieves can innately pickpocket (#107)

Pickpocketing a skyrim guard's armor off

(cherry picked from commit 21b9f1ddb251ea3c7c6803e78871abefcaecbfb4)

* this isnt moff

* Make predicted, cleanup

* !skating-basketball

* orks are NOT the best trollface

* Implement much more sensible component replication prevention

---------

Co-authored-by: DuckManZach <144298822+duckmanzach@users.noreply.github.com>

* Automatic changelog update

* Objectives that do not fit requirements are now deleted (#38365)

* Fix

* Update Content.Server/Objectives/ObjectivesSystem.cs

Thanks Tayrathn!

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

---------

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

* predicted hyposprays (#38046)

* G O I D A

* how

* now proper

* a

* Update HypospraySystem.cs

* good catch

* Apply suggestions from code review

---------

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

* Automatic changelog update

* Adds the Estoc DMR to the syndicate uplink (#38122)

* Initial commit, adds the prototype, sprites and attributions, entries into syndicate uplink for weapon bundle and magazines

* Reduced weapon spread when wielded

* minor visual updates to sprites

* slightly adjusted burst rate down

* Update duffelbag.yml

---------

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

* Automatic changelog update

* Spider web visuals update (#38175)

* web update

* Update Resources/Prototypes/Entities/Structures/spider_web.yml

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

* Update Resources/Prototypes/Entities/Structures/spider_web.yml

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

* review

---------

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

* fix: no longer accidentally rotate held items (#38331)

Signed-off-by: little-meow-meow <204685920+little-meow-meow@users.noreply.github.com>

* Fix SharedActionSystem spamming errors (#38358)

* Fixed mis-labeled component filter (#38383)

* Automatic changelog update

* Fix adjacent north/south cable terminals breaking east/west HV cable (#38377)

* Fix some crafting menu oddities (#38372)

* Automatic changelog update

* Command resolve and LEC conversion batch 3 (#38378)

* I'm just a silly goober

* requested changes

* Update Content.Server/Interaction/TilePryCommand.cs

---------

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

* remove explosions from destroying a explosive payload (#38049)

* Automatic changelog update

* Increased Borg Density (Drag Speeds) (#38232)

Raised borg drag speeds

* Automatic changelog update

* Thief objectives now verify animal stealing objectives if they actually exist (#38332)

* Holy shit it's real. Theres actually animals in this joint

* WHITESPACE SPOTTD KILL KILL KILL

* Accidentally did a bad yaml oopsie

* Apply suggestions from code review

* when the stars kiss the sky

* Apply suggestions from code review

---------

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

* Automatic changelog update

* fix: erase expedition disks when their map is deleted (#38322)

* fix: erase expedition disks when their map is deleted

* fix: include paused entities in disk query, add dirty

* Scale down 4k parallax textures (#38390)

* Scale down 4k textures, add false preload to new parallaxes

* Add filter effect, also make sure the preload yml actually has the correct name...

* Switch Discord integration to use NetCord instead of Discord.Net (#38400)

* Add directionality to cable terminal port nodes (#38394)

* Command resolve mega pr 6 (#38398)

commit progress

* Command resolve killing and LEC conversions batch 2 (#38367)

commit progress

* Command resolve killing, LEC conversions, and general cleanup. (#38338)

* i'm just gonna put this here.

* I'm just gonna do it.

* Update ShowHTNCommand.cs

* I feel dumb.

* may as well with this too.

* this does in fact not work

* :/

* fix polymorph errors (#38409)

fix polymorph error

* Command resolve mega pr batch 5 (#38389)

* commit progress

* requested changes

* Convert `AtmosphereSystem` sound path strings to `SoundSpecifier`s (#38403)

* Convert AtmosphereSystem sound path strings to SoundSpecifiers

* Convert to SoundCollections

* fix: deglitchify drowsiness effect on macOS (#37817)

* Command resolves (#38415)

touchups

* show reflection prob on examine (#38199)

* goaaaaaaaal

* hmmmmmmmmmm... sorry aeshus, but thats sucks

* adwaswa

* reviev

* awdasdws

* redo

* should work

* mistake

* change
Co-authored-by: Tayrtahn Tayrtahn@users.noreply.github.com

* Update Resources/Locale/en-US/reflect/reflect-component.ftl

---------

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

* Automatic changelog update

* Fixed vapes unintentionally dealing all burn damage types (#38423)

Fixed vapes dealing all burn damage types

* Automatic changelog update

* Remove `SalvageShuttleCircuitboardStealObjective` from the objective group (#38428)

So this was causing vulture to fail to round start because it could not find the prototype... this is being migrated into null anyway so no point in it being here

* Cleanup static `Logger` use in `ChatBox` (#38405)

Convert static logger to resolved, resolve entman

* Random empty commit to be able to publish again

* Fix stacks only recycling one instead of the stack amount (#38433)

* fix

* Fixes

* Automatic changelog update

* Relocate SetOutfit method to ServerClothingSystem and LEC setoutfit command. (#38391)

* that'll do

* requested changes.

* missed this.

* Command resolves and LEC conversions batch 4 (#38382)

* reeeecolllaaaaaaaa

* gonna convert these to public while I'm here for consistency sake

* requested changes.

* Playtime Reminders - Raising awareness of addiction by highlighting excessive playtime (#36483)

* grass touch protocol - Rebases to latest master to fix conflicts

* aight local tests are passing lets see if our golf works

* It is 5 am and our ass COMPLETELY overcomplicated this lmaooo

* Addresses feedback - Clarifies comments, swaps internal var names for grasstouchless and selfdestructive, makes the third tier a little less demanding, and fixes 1 hours

* Addresses review - conflict fix

* This too

* Axes playtime exclusion for ghosts

* Use switch expression

code style nit

* Refactor/cleanup

Use IGameTiming.RealTime to track time instead of DateTime. Use nullable instead of magic values.

Expose the current day value through a property that is always up to date, instead of making the API to read the CVar that updates at inconsistent times. This also makes it trivial to debug with VV.

Other minor cleanup like using string interp, code style fixes, comments, etc.

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>

* Automatic changelog update

* Exo - Minor balance changes and fixes (#38435)

* Automatic changelog update

* Add advertised reagents to puddles (#38432)

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

* Give APCs wirepanels (#38443)

init

* Automatic changelog update

* Revert "Add stamina damage resists to the bloodred raid/hard-suits (and ERT hardsuits). [TEST MERGE]" (#38447)

Revert "Add stamina damage resists to the bloodred raid/hard-suits (and ERT h…"

This reverts commit 8c6b288853.

* Adds dog-shaped sprite variants for the most common hardsuits (#36875)

Co-authored-by: dandy <203235481+casiliuscestus@users.noreply.github.com>
Co-authored-by: Raccoononi <70777222+raccoononi@users.noreply.github.com>
Co-authored-by: Scrubdelub <196583332+scrubdelub@users.noreply.github.com>
Co-authored-by: Tiniest Shark <176858593+TiniestShark@users.noreply.github.com>

* Automatic changelog update

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Signed-off-by: little-meow-meow <204685920+little-meow-meow@users.noreply.github.com>
Co-authored-by: Gentleman-Bird <dcgreen406@gmail.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: Hitlinemoss <209321380+Hitlinemoss@users.noreply.github.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: RedBookcase <crazykid1590@gmail.com>
Co-authored-by: RedBookcase <Usualmoves@gmail.com>
Co-authored-by: vmzd <33641966+vmzd@users.noreply.github.com>
Co-authored-by: August Sun <45527070+august-sun@users.noreply.github.com>
Co-authored-by: august-sun <45527070+august.sun@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: Xeri Marai <8867601+Xeri7@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: jkwookee <157201244+jkwookee@users.noreply.github.com>
Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
Co-authored-by: Spanky <180730777+spanky-spanky@users.noreply.github.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: hoshizora <willhornbeck12@gmail.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nox <nebulousnox38@gmail.com>
Co-authored-by: Archee <archee3@hotmail.co.uk>
Co-authored-by: Orsoniks <orsoniksstuff@gmail.com>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: Disp-Dev <91643998+Disp-Dev@users.noreply.github.com>
Co-authored-by: DispenserDev <mariousjoaquin@gmail.com>
Co-authored-by: cnv41 <cnv41g@gmail.com>
Co-authored-by: nabegator <stunprod228@outlook.com>
Co-authored-by: qrwas <aleksandr.vernigora93@gmail.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: qwerltaz <69696513+qwerltaz@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: eoineoineoin <helloworld@eoinrul.es>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
Co-authored-by: Seam_Less <kai.julian.watson@gmail.com>
Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: āda <ss.adasts@gmail.com>
Co-authored-by: Vortebo <64214314+Vortebo@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: kosticia <kosticia46@gmail.com>
Co-authored-by: Qerd <73325910+BigfootBravo@users.noreply.github.com>
Co-authored-by: Nikovnik <116634167+nkokic@users.noreply.github.com>
Co-authored-by: M1tht1c <mytht1c@proton.me>
Co-authored-by: SyaoranFox <99053675+SyaoranFox@users.noreply.github.com>
Co-authored-by: Kittygyat <202250949+Kittygyat@users.noreply.github.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: Sparlight <twiksparlight@gmail.com>
Co-authored-by: nikitosych <boriszyn@gmail.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: beck <163376292+widgetbeck@users.noreply.github.com>
Co-authored-by: Helix-ctrl <kylemccann08@gmail.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: DuckManZach <144298822+duckmanzach@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: little-meow-meow <204685920+little-meow-meow@users.noreply.github.com>
Co-authored-by: Citrea <citrea.lingua@gmx.net>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: KamTheSythe <kamil.dolowiec01@gmail.com>
Co-authored-by: TheFlyingSentry <AFlyingSentry@gmail.com>
Co-authored-by: deathride58 <deathride58@users.noreply.github.com>
Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: dandy <203235481+casiliuscestus@users.noreply.github.com>
Co-authored-by: Raccoononi <70777222+raccoononi@users.noreply.github.com>
Co-authored-by: Scrubdelub <196583332+scrubdelub@users.noreply.github.com>
Co-authored-by: Tiniest Shark <176858593+TiniestShark@users.noreply.github.com>
This commit is contained in:
Red
2025-06-24 01:05:19 +03:00
committed by GitHub
1455 changed files with 168869 additions and 1121592 deletions

View File

@@ -1,59 +0,0 @@
name: Trailing Whitespace Check
on:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
jobs:
build:
name: Trailing Whitespace Check
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Get changed text files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
with:
files: |
**.cs
**.yml
**.swsl
**.json
**.py
- name: Check for trailing whitespace and EOF newline
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
has_trailing_whitespace=0
has_missing_eof_newline=0
for file in ${ALL_CHANGED_FILES}; do
# Ignore vanilla not CrystallEdge files
if [[ "$file" != *CP14* ]]; then
continue
fi
echo "Checking $file"
# Check for trailing whitespace
if grep -qP '[ \t]+$' "$file"; then
echo "::error file=$file::Trailing whitespace found"
has_trailing_whitespace=1
fi
# Check for missing EOF newline
if [ -f "$file" ] && [ -s "$file" ]; then
last_char=$(tail -c 1 "$file")
if [ "$last_char" != "" ] && [ "$last_char" != $'\n' ]; then
echo "::error file=$file::Missing newline at end of file"
has_missing_eof_newline=1
fi
fi
done
if [ "$has_trailing_whitespace" -eq 1 ] || [ "$has_missing_eof_newline" -eq 1 ]; then
echo "Issues found: trailing whitespace or missing EOF newline."
echo "We recommend using an IDE to prevent this from happening."
exit 1
fi

View File

@@ -4,39 +4,20 @@ using Robust.Shared.Console;
namespace Content.Client.Access.Commands;
public sealed class ShowAccessReadersCommand : IConsoleCommand
public sealed class ShowAccessReadersCommand : LocalizedEntityCommands
{
public string Command => "showaccessreaders";
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
public string Description => "Toggles showing access reader permissions on the map";
public string Help => """
Overlay Info:
-Disabled | The access reader is disabled
+Unrestricted | The access reader has no restrictions
+Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
+Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
-Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
""";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "showaccessreaders";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
var existing = _overlay.RemoveOverlay<AccessOverlay>();
if (!existing)
_overlay.AddOverlay(new AccessOverlay(EntityManager, _cache, _xform));
if (collection == null)
return;
var overlay = collection.Resolve<IOverlayManager>();
if (overlay.RemoveOverlay<AccessOverlay>())
{
shell.WriteLine($"Set access reader debug overlay to false");
return;
}
var entManager = collection.Resolve<IEntityManager>();
var cache = collection.Resolve<IResourceCache>();
var xform = entManager.System<SharedTransformSystem>();
overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
shell.WriteLine($"Set access reader debug overlay to true");
shell.WriteLine(Loc.GetString($"cmd-showaccessreaders-status", ("status", !existing)));
}
}

View File

@@ -32,7 +32,6 @@ namespace Content.Client.Actions
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public event Action<EntityUid>? OnActionAdded;

View File

@@ -20,6 +20,7 @@ public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
_menu = new AnomalyScannerMenu();
_menu.OpenCentered();
_menu.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)

View File

@@ -1,4 +1,4 @@
using Robust.Client.GameObjects;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
@@ -18,6 +18,7 @@ namespace Content.Client.Atmos.UI
base.Open();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
@@ -29,15 +30,6 @@ namespace Content.Client.Atmos.UI
_window.Populate(cast);
}
/// <summary>
/// Closes UI and tells the server to disable the analyzer
/// </summary>
private void OnClose()
{
SendMessage(new GasAnalyzerDisableMessage());
Close();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -2,16 +2,16 @@ using Robust.Shared.Console;
namespace Content.Client.Audio;
public sealed class AmbientOverlayCommand : IConsoleCommand
public sealed class AmbientOverlayCommand : LocalizedEntityCommands
{
public string Command => "showambient";
public string Description => "Shows all AmbientSoundComponents in the viewport";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AmbientSoundSystem>();
system.OverlayEnabled ^= true;
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
shell.WriteLine($"Ambient sound overlay set to {system.OverlayEnabled}");
public override string Command => "showambient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_ambient.OverlayEnabled ^= true;
shell.WriteLine(Loc.GetString($"cmd-showambient-status", ("status", _ambient.OverlayEnabled)));
}
}

View File

@@ -203,6 +203,9 @@ namespace Content.Client.Cargo.UI
/// </summary>
public void PopulateOrders(IEnumerable<CargoOrderData> orders)
{
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
return;
Requests.DisposeAllChildren();
foreach (var order in orders)
@@ -237,6 +240,7 @@ namespace Content.Client.Cargo.UI
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
// TODO: Disable based on access.
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
Requests.AddChild(row);
}
@@ -290,8 +294,8 @@ namespace Content.Client.Cargo.UI
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
OrdersSpacer.Visible = !orderConsole.SlipPrinter;
Orders.Visible = !orderConsole.SlipPrinter;
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
}
}
}

View File

@@ -14,5 +14,15 @@ namespace Content.Client.Cargo.UI
{
RobustXamlLoader.Load(this);
}
public void SetApproveVisible(bool visible)
{
Approve.Visible = visible;
if (visible)
Cancel.AddStyleClass("OpenLeft");
else
Cancel.RemoveStyleClass("OpenLeft");
}
}
}

View File

@@ -15,8 +15,8 @@ namespace Content.Client.Changelog
[GenerateTypedNameReferences]
public sealed partial class ChangelogWindow : FancyWindow
{
[Dependency] private readonly ChangelogManager _changelog = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly ChangelogManager _changelog = default!;
public ChangelogWindow()
{
@@ -112,15 +112,15 @@ namespace Content.Client.Changelog
}
[UsedImplicitly, AnyCommand]
public sealed class ChangelogCommand : IConsoleCommand
public sealed class ChangelogCommand : LocalizedCommands
{
public string Command => "changelog";
public string Description => "Opens the changelog";
public string Help => "Usage: changelog";
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "changelog";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ChangelogUIController>().OpenWindow();
_uiManager.GetUIController<ChangelogUIController>().OpenWindow();
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Content.Client.Chat.UI
{
public abstract class SpeechBubble : Control
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] protected readonly IConfigurationManager ConfigManager = default!;
@@ -30,12 +31,12 @@ namespace Content.Client.Chat.UI
/// <summary>
/// The total time a speech bubble stays on screen.
/// </summary>
private const float TotalTime = 4;
private static readonly TimeSpan TotalTime = TimeSpan.FromSeconds(4);
/// <summary>
/// The amount of time at the end of the bubble's life at which it starts fading.
/// </summary>
private const float FadeTime = 0.25f;
private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f);
/// <summary>
/// The distance in world space to offset the speech bubble from the center of the entity.
@@ -50,7 +51,10 @@ namespace Content.Client.Chat.UI
private readonly EntityUid _senderEntity;
private float _timeLeft = TotalTime;
/// <summary>
/// The time at which this bubble will die.
/// </summary>
private TimeSpan _deathTime;
public float VerticalOffset { get; set; }
private float _verticalOffsetAchieved;
@@ -99,6 +103,7 @@ namespace Content.Client.Chat.UI
bubble.Measure(Vector2Helpers.Infinity);
ContentSize = bubble.DesiredSize;
_verticalOffsetAchieved = -ContentSize.Y;
_deathTime = _timing.RealTime + TotalTime;
}
protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
@@ -107,8 +112,8 @@ namespace Content.Client.Chat.UI
{
base.FrameUpdate(args);
_timeLeft -= args.DeltaSeconds;
if (_entityManager.Deleted(_senderEntity) || _timeLeft <= 0)
var timeLeft = (float)(_deathTime - _timing.RealTime).TotalSeconds;
if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0)
{
// Timer spawn to prevent concurrent modification exception.
Timer.Spawn(0, Die);
@@ -131,10 +136,10 @@ namespace Content.Client.Chat.UI
return;
}
if (_timeLeft <= FadeTime)
if (timeLeft <= FadeTime.TotalSeconds)
{
// Update alpha if we're fading.
Modulate = Color.White.WithAlpha(_timeLeft / FadeTime);
Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds);
}
else
{
@@ -144,7 +149,7 @@ namespace Content.Client.Chat.UI
var baseOffset = 0f;
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
if (_entityManager.TryGetComponent<SpeechComponent>(_senderEntity, out var speech))
baseOffset = speech.SpeechBubbleOffset;
var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset);
@@ -175,9 +180,9 @@ namespace Content.Client.Chat.UI
/// </summary>
public void FadeNow()
{
if (_timeLeft > FadeTime)
if (_deathTime > _timing.RealTime)
{
_timeLeft = FadeTime;
_deathTime = _timing.RealTime + FadeTime;
}
}

View File

@@ -5,8 +5,9 @@ using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class HypospraySystem : SharedHypospraySystem
public sealed class HyposprayStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -11,8 +11,6 @@ namespace Content.Client.Clothing.Systems;
// All valid items for chameleon are calculated on client startup and stored in dictionary.
public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -38,7 +38,7 @@ namespace Content.Client.Construction.UI
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private Dictionary<string, ContainerButton> _recipeButtons = new();
private readonly Dictionary<string, ContainerButton> _recipeButtons = new();
private string _selectedCategory = string.Empty;
private const string FavoriteCatName = "construction-category-favorites";
@@ -217,8 +217,8 @@ namespace Content.Client.Construction.UI
var itemButton = new ContainerButton()
{
VerticalAlignment = Control.VAlignment.Center,
Name = recipe.TargetPrototype.Name,
ToolTip = recipe.TargetPrototype.Name,
Name = recipe.Prototype.Name,
ToolTip = recipe.Prototype.Name,
ToggleMode = true,
Children = { protoView },
};
@@ -235,7 +235,7 @@ namespace Content.Client.Construction.UI
if (buttonToggledEventArgs.Pressed &&
_selected != null &&
_recipeButtons.TryGetValue(_selected.Name!, out var oldButton))
_recipeButtons.TryGetValue(_selected.ID, out var oldButton))
{
oldButton.Pressed = false;
SelectGridButton(oldButton, false);
@@ -245,7 +245,7 @@ namespace Content.Client.Construction.UI
};
recipesGrid.AddChild(itemButtonPanelContainer);
_recipeButtons[recipe.Prototype.Name!] = itemButton;
_recipeButtons[recipe.Prototype.ID] = itemButton;
var isCurrentButtonSelected = _selected == recipe.Prototype;
itemButton.Pressed = isCurrentButtonSelected;
SelectGridButton(itemButton, isCurrentButtonSelected);
@@ -310,7 +310,7 @@ namespace Content.Client.Construction.UI
if (button.Parent is not PanelContainer buttonPanel)
return;
button.Modulate = select ? Color.Green : Color.Transparent;
button.Children.Single().Modulate = select ? Color.Green : Color.White;
var buttonColor = select ? StyleNano.ButtonColorDefault : Color.Transparent;
buttonPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = buttonColor };
}

View File

@@ -1,17 +1,15 @@
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
namespace Content.Client.Decals;
public sealed class ToggleDecalCommand : IConsoleCommand
public sealed class ToggleDecalCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly DecalSystem _decal = default!;
public string Command => "toggledecals";
public string Description => "Toggles decaloverlay";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "toggledecals";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<DecalSystem>().ToggleOverlay();
_decal.ToggleOverlay();
}
}

View File

@@ -18,6 +18,7 @@ using Content.Client.MainMenu;
using Content.Client.Overlays;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Playtime;
using Content.Client.Radiation.Overlays;
using Content.Client.Replay;
using Content.Client.Screenshot;
@@ -85,6 +86,7 @@ namespace Content.Client.Entry
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
public override void Init()
{
@@ -134,6 +136,8 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
_prototypeManager.RegisterIgnore("codewordGenerator");
_prototypeManager.RegisterIgnore("codewordFaction");
_componentFactory.GenerateNetIds();
_adminManager.Initialize();
@@ -145,6 +149,7 @@ namespace Content.Client.Entry
_extendedDisconnectInformation.Initialize();
_jobRequirements.Initialize();
_playbackMan.Initialize();
_clientsidePlaytimeManager.Initialize();
//AUTOSCALING default Setup!
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);

View File

@@ -2,25 +2,17 @@
namespace Content.Client.Ghost.Commands;
public sealed class ToggleGhostVisibilityCommand : IConsoleCommand
public sealed class ToggleGhostVisibilityCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntitySystemManager _entSysMan = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
public string Command => "toggleghostvisibility";
public string Description => "Toggles ghost visibility on the client.";
public string Help => "toggleghostvisibility [bool]";
public override string Command => "toggleghostvisibility";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ghostSystem = _entSysMan.GetEntitySystem<GhostSystem>();
if (args.Length != 0 && bool.TryParse(args[0], out var visibility))
{
ghostSystem.ToggleGhostVisibility(visibility);
}
_ghost.ToggleGhostVisibility(visibility);
else
{
ghostSystem.ToggleGhostVisibility();
}
_ghost.ToggleGhostVisibility();
}
}

View File

@@ -4,28 +4,27 @@ using Robust.Shared.Console;
namespace Content.Client.Ghost;
public sealed class GhostToggleSelfVisibility : IConsoleCommand
public sealed class GhostToggleSelfVisibility : LocalizedEntityCommands
{
public string Command => "toggleselfghost";
public string Description => "Toggles seeing your own ghost.";
public string Help => "toggleselfghost";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly SpriteSystem _sprite = default!;
public override string Command => "toggleselfghost";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var attachedEntity = shell.Player?.AttachedEntity;
if (!attachedEntity.HasValue)
return;
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.HasComponent<GhostComponent>(attachedEntity))
if (!EntityManager.HasComponent<GhostComponent>(attachedEntity))
{
shell.WriteError("Entity must be a ghost.");
shell.WriteError(Loc.GetString($"cmd-toggleselfghost-must-be-ghost"));
return;
}
if (!entityManager.TryGetComponent(attachedEntity, out SpriteComponent? spriteComponent))
if (!EntityManager.TryGetComponent(attachedEntity, out SpriteComponent? spriteComponent))
return;
var spriteSys = entityManager.System<SpriteSystem>();
spriteSys.SetVisible((attachedEntity.Value, spriteComponent), !spriteComponent.Visible);
_sprite.SetVisible((attachedEntity.Value, spriteComponent), !spriteComponent.Visible);
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using Content.Shared.Instruments;
using Robust.Shared.Audio.Midi;
namespace Content.Client.Instruments;
public sealed partial class InstrumentSystem
{
/// <summary>
/// Tries to parse the input data as a midi and set the channel names respectively.
/// </summary>
/// <remarks>
/// Thank you to http://www.somascape.org/midi/tech/mfile.html for providing an awesome resource for midi files.
/// </remarks>
/// <remarks>
/// This method has exception tolerance and does not throw, even if the midi file is invalid.
/// </remarks>
private bool TrySetChannels(EntityUid uid, byte[] data)
{
if (!MidiParser.MidiParser.TryGetMidiTracks(data, out var tracks, out var error))
{
Log.Error(error);
return false;
}
var resolvedTracks = new List<MidiTrack?>();
for (var index = 0; index < tracks.Length; index++)
{
var midiTrack = tracks[index];
if (midiTrack is { TrackName: null, ProgramName: null, InstrumentName: null})
continue;
switch (midiTrack)
{
case { TrackName: not null, ProgramName: not null }:
case { TrackName: not null, InstrumentName: not null }:
case { TrackName: not null }:
case { ProgramName: not null }:
resolvedTracks.Add(midiTrack);
break;
default:
resolvedTracks.Add(null); // Used so the channel still displays as MIDI Channel X and doesn't just take the next valid one in the UI
break;
}
Log.Debug($"Channel name: {resolvedTracks.Last()}");
}
RaiseNetworkEvent(new InstrumentSetChannelsEvent(GetNetEntity(uid), resolvedTracks.Take(RobustMidiEvent.MaxChannels).ToArray()));
Log.Debug($"Resolved {resolvedTracks.Count} channels.");
return true;
}
}

View File

@@ -1,3 +1,4 @@
using System.IO;
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Instruments;
@@ -12,7 +13,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Instruments;
public sealed class InstrumentSystem : SharedInstrumentSystem
public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IMidiManager _midiManager = default!;
@@ -23,6 +24,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
public int MaxMidiEventsPerBatch { get; private set; }
public int MaxMidiEventsPerSecond { get; private set; }
public event Action? OnChannelsUpdated;
public override void Initialize()
{
base.Initialize();
@@ -38,6 +41,26 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
SubscribeLocalEvent<InstrumentComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<InstrumentComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<ActiveInstrumentComponent, AfterAutoHandleStateEvent>(OnActiveInstrumentAfterHandleState);
}
private bool _isUpdateQueued = false;
private void OnActiveInstrumentAfterHandleState(Entity<ActiveInstrumentComponent> ent, ref AfterAutoHandleStateEvent args)
{
// Called in the update loop so that the components update client side for resolving them in TryComps.
_isUpdateQueued = true;
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!_isUpdateQueued)
return;
_isUpdateQueued = false;
OnChannelsUpdated?.Invoke();
}
private void OnHandleState(EntityUid uid, SharedInstrumentComponent component, ref ComponentHandleState args)
@@ -252,7 +275,13 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
}
[Obsolete("Use overload that takes in byte[] instead.")]
public bool OpenMidi(EntityUid uid, ReadOnlySpan<byte> data, InstrumentComponent? instrument = null)
{
return OpenMidi(uid, data.ToArray(), instrument);
}
public bool OpenMidi(EntityUid uid, byte[] data, InstrumentComponent? instrument = null)
{
if (!Resolve(uid, ref instrument))
return false;
@@ -263,6 +292,8 @@ public sealed class InstrumentSystem : SharedInstrumentSystem
return false;
SetMaster(uid, null);
TrySetChannels(uid, data);
instrument.MidiEventBuffer.Clear();
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
return true;

View File

@@ -0,0 +1,147 @@
using Robust.Shared.Utility;
namespace Content.Client.Instruments.MidiParser;
// This file was autogenerated. Based on https://www.ccarh.org/courses/253/handout/gminstruments/
public enum MidiInstrument : byte
{
AcousticGrandPiano = 0,
BrightAcousticPiano = 1,
ElectricGrandPiano = 2,
HonkyTonkPiano = 3,
RhodesPiano = 4,
ChorusedPiano = 5,
Harpsichord = 6,
Clavinet = 7,
Celesta = 8,
Glockenspiel = 9,
MusicBox = 10,
Vibraphone = 11,
Marimba = 12,
Xylophone = 13,
TubularBells = 14,
Dulcimer = 15,
HammondOrgan = 16,
PercussiveOrgan = 17,
RockOrgan = 18,
ChurchOrgan = 19,
ReedOrgan = 20,
Accordion = 21,
Harmonica = 22,
TangoAccordion = 23,
AcousticNylonGuitar = 24,
AcousticSteelGuitar = 25,
ElectricJazzGuitar = 26,
ElectricCleanGuitar = 27,
ElectricMutedGuitar = 28,
OverdrivenGuitar = 29,
DistortionGuitar = 30,
GuitarHarmonics = 31,
AcousticBass = 32,
FingeredElectricBass = 33,
PluckedElectricBass = 34,
FretlessBass = 35,
SlapBass1 = 36,
SlapBass2 = 37,
SynthBass1 = 38,
SynthBass2 = 39,
Violin = 40,
Viola = 41,
Cello = 42,
Contrabass = 43,
TremoloStrings = 44,
PizzicatoStrings = 45,
OrchestralHarp = 46,
Timpani = 47,
StringEnsemble1 = 48,
StringEnsemble2 = 49,
SynthStrings1 = 50,
SynthStrings2 = 51,
ChoirAah = 52,
VoiceOoh = 53,
SynthChoir = 54,
OrchestraHit = 55,
Trumpet = 56,
Trombone = 57,
Tuba = 58,
MutedTrumpet = 59,
FrenchHorn = 60,
BrassSection = 61,
SynthBrass1 = 62,
SynthBrass2 = 63,
SopranoSax = 64,
AltoSax = 65,
TenorSax = 66,
BaritoneSax = 67,
Oboe = 68,
EnglishHorn = 69,
Bassoon = 70,
Clarinet = 71,
Piccolo = 72,
Flute = 73,
Recorder = 74,
PanFlute = 75,
BottleBlow = 76,
Shakuhachi = 77,
Whistle = 78,
Ocarina = 79,
SquareWaveLead = 80,
SawtoothWaveLead = 81,
CalliopeLead = 82,
ChiffLead = 83,
CharangLead = 84,
VoiceLead = 85,
FithsLead = 86,
BassLead = 87,
NewAgePad = 88,
WarmPad = 89,
PolysynthPad = 90,
ChoirPad = 91,
BowedPad = 92,
MetallicPad = 93,
HaloPad = 94,
SweepPad = 95,
RainEffect = 96,
SoundtrackEffect = 97,
CrystalEffect = 98,
AtmosphereEffect = 99,
BrightnessEffect = 100,
GoblinsEffect = 101,
EchoesEffect = 102,
SciFiEffect = 103,
Sitar = 104,
Banjo = 105,
Shamisen = 106,
Koto = 107,
Kalimba = 108,
Bagpipe = 109,
Fiddle = 110,
Shanai = 111,
TinkleBell = 112,
Agogo = 113,
SteelDrums = 114,
Woodblock = 115,
TaikoDrum = 116,
MelodicTom = 117,
SynthDrum = 118,
ReverseCymbal = 119,
GuitarFretNoise = 120,
BreathNoise = 121,
Seashore = 122,
BirdTweet = 123,
TelephoneRing = 124,
Helicopter = 125,
Applause = 126,
Gunshot = 127,
}
public static class MidiInstrumentExt
{
/// <summary>
/// Turns the given enum value into it's string representation to be used in localization.
/// </summary>
public static string GetStringRep(this MidiInstrument instrument)
{
return CaseConversion.PascalToKebab(instrument.ToString());
}
}

View File

@@ -0,0 +1,184 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Content.Shared.Instruments;
namespace Content.Client.Instruments.MidiParser;
public static class MidiParser
{
// Thanks again to http://www.somascape.org/midi/tech/mfile.html
public static bool TryGetMidiTracks(
byte[] data,
[NotNullWhen(true)] out MidiTrack[]? tracks,
[NotNullWhen(false)] out string? error)
{
tracks = null;
error = null;
var stream = new MidiStreamWrapper(data);
if (stream.ReadString(4) != "MThd")
{
error = "Invalid file header";
return false;
}
var headerLength = stream.ReadUInt32();
// MIDI specs define that the header is 6 bytes, we only look at the 6 bytes, if its more, we skip ahead.
stream.Skip(2); // format
var trackCount = stream.ReadUInt16();
stream.Skip(2); // time div
// We now skip ahead if we still have any header length left
stream.Skip((int)(headerLength - 6));
var parsedTracks = new List<MidiTrack>();
for (var i = 0; i < trackCount; i++)
{
if (stream.ReadString(4) != "MTrk")
{
tracks = null;
error = "Track contains invalid header";
return false;
}
var track = new MidiTrack();
var trackLength = stream.ReadUInt32();
var trackEnd = stream.StreamPosition + trackLength;
var hasMidiEvent = false;
byte? lastStatusByte = null;
while (stream.StreamPosition < trackEnd)
{
stream.ReadVariableLengthQuantity();
/*
* If the first (status) byte is less than 128 (hex 80), this implies that running status is in effect,
* and that this byte is actually the first data byte (the status carrying over from the previous MIDI event).
* This can only be the case if the immediately previous event was also a MIDI event,
* i.e. SysEx and Meta events interrupt (clear) running status.
* See http://www.somascape.org/midi/tech/mfile.html#events
*/
var firstByte = stream.ReadByte();
if (firstByte >= 0x80)
{
lastStatusByte = firstByte;
}
else
{
// Running status: push byte back for reading as data
stream.Skip(-1);
}
// The first event in each MTrk chunk must specify status.
if (lastStatusByte == null)
{
tracks = null;
error = "Track data not valid, expected status byte, got nothing.";
return false;
}
var eventType = (byte)(lastStatusByte & 0xF0);
switch (lastStatusByte)
{
// Meta events
case 0xFF:
{
var metaType = stream.ReadByte();
var metaLength = stream.ReadVariableLengthQuantity();
var metaData = stream.ReadBytes((int)metaLength);
if (metaType == 0x00) // SequenceNumber event
continue;
// Meta event types 01 through 0F are reserved for text and all follow the basic FF 01 len text format
if (metaType is < 0x01 or > 0x0F)
break;
// 0x03 is TrackName,
// 0x04 is InstrumentName
var text = Encoding.ASCII.GetString(metaData, 0, (int)metaLength);
switch (metaType)
{
case 0x03 when track.TrackName == null:
track.TrackName = text;
break;
case 0x04 when track.InstrumentName == null:
track.InstrumentName = text;
break;
}
// still here? then we dont care about the event
break;
}
// SysEx events
case 0xF0:
case 0xF7:
{
var sysexLength = stream.ReadVariableLengthQuantity();
stream.Skip((int)sysexLength);
// Sysex events and meta-events cancel any running status which was in effect.
// Running status does not apply to and may not be used for these messages.
lastStatusByte = null;
break;
}
default:
switch (eventType)
{
// Program Change
case 0xC0:
{
var programNumber = stream.ReadByte();
if (track.ProgramName == null)
{
if (programNumber < Enum.GetValues<MidiInstrument>().Length)
track.ProgramName = Loc.GetString($"instruments-component-menu-midi-channel-{((MidiInstrument)programNumber).GetStringRep()}");
}
break;
}
case 0x80: // Note Off
case 0x90: // Note On
case 0xA0: // Polyphonic Key Pressure
case 0xB0: // Control Change
case 0xE0: // Pitch Bend
{
hasMidiEvent = true;
stream.Skip(2);
break;
}
case 0xD0: // Channel Pressure
{
hasMidiEvent = true;
stream.Skip(1);
break;
}
default:
error = $"Unknown MIDI event type {lastStatusByte:X2}";
tracks = null;
return false;
}
break;
}
}
if (hasMidiEvent)
parsedTracks.Add(track);
}
tracks = parsedTracks.ToArray();
return true;
}
}

View File

@@ -0,0 +1,103 @@
using System.IO;
using System.Text;
namespace Content.Client.Instruments.MidiParser;
public sealed class MidiStreamWrapper
{
private readonly MemoryStream _stream;
private byte[] _buffer;
public long StreamPosition => _stream.Position;
public MidiStreamWrapper(byte[] data)
{
_stream = new MemoryStream(data, writable: false);
_buffer = new byte[4];
}
/// <summary>
/// Skips X number of bytes in the stream.
/// </summary>
/// <param name="count">The number of bytes to skip. If 0, no operations on the stream are performed.</param>
public void Skip(int count)
{
if (count == 0)
return;
_stream.Seek(count, SeekOrigin.Current);
}
public byte ReadByte()
{
var b = _stream.ReadByte();
if (b == -1)
throw new Exception("Unexpected end of stream");
return (byte)b;
}
/// <summary>
/// Reads N bytes using the buffer.
/// </summary>
public byte[] ReadBytes(int count)
{
if (_buffer.Length < count)
{
Array.Resize(ref _buffer, count);
}
var read = _stream.Read(_buffer, 0, count);
if (read != count)
throw new Exception("Unexpected end of stream");
return _buffer;
}
/// <summary>
/// Reads a 4 byte big-endian uint.
/// </summary>
public uint ReadUInt32()
{
var bytes = ReadBytes(4);
return (uint)((bytes[0] << 24) |
(bytes[1] << 16) |
(bytes[2] << 8) |
(bytes[3]));
}
/// <summary>
/// Reads a 2 byte big-endian ushort.
/// </summary>
public ushort ReadUInt16()
{
var bytes = ReadBytes(2);
return (ushort)((bytes[0] << 8) | bytes[1]);
}
public string ReadString(int count)
{
var bytes = ReadBytes(count);
return Encoding.UTF8.GetString(bytes, 0, count);
}
public uint ReadVariableLengthQuantity()
{
uint value = 0;
// variable-length-quantities encode ints using 7 bits per byte
// the highest bit (7) is used for a continuation flag. We read until the high bit is 0
while (true)
{
var b = ReadByte();
value = (value << 7) | (uint)(b & 0x7f); // Shift current value and add 7 bits
// value << 7, make room for the next 7 bits
// b & 0x7F mask out the high bit to just get the 7 bit payload
if ((b & 0x80) == 0)
break; // This was the last bit.
}
return value;
}
}

View File

@@ -7,5 +7,7 @@
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
</BoxContainer>
<CheckButton Name="DisplayTrackNames"
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
</BoxContainer>
</DefaultWindow>

View File

@@ -1,26 +1,56 @@
using Content.Shared.Instruments;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Instruments.UI;
[GenerateTypedNameReferences]
public sealed partial class ChannelsMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = null!;
private readonly InstrumentBoundUserInterface _owner;
public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_owner = owner;
ChannelList.OnItemSelected += OnItemSelected;
ChannelList.OnItemDeselected += OnItemDeselected;
AllButton.OnPressed += OnAllPressed;
ClearButton.OnPressed += OnClearPressed;
DisplayTrackNames.OnPressed += OnDisplayTrackNamesPressed;
}
protected override void EnteredTree()
{
base.EnteredTree();
_owner.Instruments.OnChannelsUpdated += UpdateChannelList;
}
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
{
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
Populate();
}
private void UpdateChannelList()
{
Populate(); // This is kind of in-efficent because we don't filter for which instrument updated its channels, but idc
}
protected override void ExitedTree()
{
base.ExitedTree();
_owner.Instruments.OnChannelsUpdated -= UpdateChannelList;
}
private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
@@ -51,15 +81,71 @@ public sealed partial class ChannelsMenu : DefaultWindow
}
}
public void Populate(InstrumentComponent? instrument)
/// <summary>
/// Walks up the tree of instrument masters to find the truest master of them all.
/// </summary>
private ActiveInstrumentComponent ResolveActiveInstrument(InstrumentComponent? comp)
{
comp ??= _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
var instrument = new Entity<InstrumentComponent>(_owner.Owner, comp);
while (true)
{
if (instrument.Comp.Master == null)
break;
instrument = new Entity<InstrumentComponent>((EntityUid)instrument.Comp.Master,
_entityManager.GetComponent<InstrumentComponent>((EntityUid)instrument.Comp.Master));
}
return _entityManager.GetComponent<ActiveInstrumentComponent>(instrument.Owner);
}
public void Populate()
{
ChannelList.Clear();
var instrument = _entityManager.GetComponent<InstrumentComponent>(_owner.Owner);
var activeInstrument = ResolveActiveInstrument(instrument);
for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
{
var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
("number", i)), null, true, i);
var label = _owner.Loc.GetString("instrument-component-channel-name",
("number", i));
if (activeInstrument != null
&& activeInstrument.Tracks.TryGetValue(i, out var resolvedMidiChannel)
&& resolvedMidiChannel != null)
{
if (DisplayTrackNames.Pressed)
{
label = resolvedMidiChannel switch
{
{ TrackName: not null, InstrumentName: not null } =>
Loc.GetString("instruments-component-channels-multi",
("channel", i),
("name", resolvedMidiChannel.TrackName),
("other", resolvedMidiChannel.InstrumentName)),
{ TrackName: not null } =>
Loc.GetString("instruments-component-channels-single",
("channel", i),
("name", resolvedMidiChannel.TrackName)),
_ => label,
};
}
else
{
label = resolvedMidiChannel switch
{
{ ProgramName: not null } =>
Loc.GetString("instruments-component-channels-single",
("channel", i),
("name", resolvedMidiChannel.ProgramName)),
_ => label,
};
}
}
var item = ChannelList.AddItem(label, null, true, i);
item.Selected = !instrument?.FilteredChannels[i] ?? false;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Instruments;
using Content.Shared.Instruments.UI;
using Content.Shared.Interaction;
using Robust.Client.Audio.Midi;
@@ -101,9 +102,7 @@ namespace Content.Client.Instruments.UI
public void OpenChannelsMenu()
{
_channelsMenu ??= new ChannelsMenu(this);
EntMan.TryGetComponent(Owner, out InstrumentComponent? instrument);
_channelsMenu.Populate(instrument);
_channelsMenu.Populate();
_channelsMenu.OpenCenteredRight();
}

View File

@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Containers;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Range = Robust.Client.UserInterface.Controls.Range;
@@ -145,10 +146,6 @@ namespace Content.Client.Instruments.UI
if (!PlayCheck())
return;
await using var memStream = new MemoryStream((int) file.Length);
await file.CopyToAsync(memStream);
if (!_entManager.TryGetComponent<InstrumentComponent>(Entity, out var instrument))
{
return;
@@ -156,7 +153,7 @@ namespace Content.Client.Instruments.UI
if (!_entManager.System<InstrumentSystem>()
.OpenMidi(Entity,
memStream.GetBuffer().AsSpan(0, (int) memStream.Length),
file.CopyToArray(),
instrument))
{
return;

View File

@@ -50,6 +50,18 @@ namespace Content.Client.Inventory
[ViewVariables]
private readonly EntityUid _virtualHiddenEntity;
/// <summary>
/// The current amount of added hand buttons.
/// </summary>
[ViewVariables]
private int _handCount;
/// <summary>
/// The current shape of the inventory, needed to calculate the window size.
/// </summary>
[ViewVariables]
private Vector2i _inventoryDimensions;
public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_examine = EntMan.System<ExamineSystem>();
@@ -93,6 +105,8 @@ namespace Content.Client.Inventory
return;
_strippingMenu.ClearButtons();
_handCount = 0;
_inventoryDimensions = Vector2i.Zero;
if (EntMan.TryGetComponent<InventoryComponent>(Owner, out var inv))
{
@@ -152,9 +166,15 @@ namespace Content.Client.Inventory
// TODO allow windows to resize based on content's desired size
// for now: shit-code
// this breaks for drones (too many hands, lots of empty vertical space), and looks shit for monkeys and the like.
// but the window is realizable, so eh.
_strippingMenu.SetSize = new Vector2(220, snare?.IsEnsnared == true ? 550 : 530);
// calculate the window size manually
// +20 horizontally and vertically from the ContentsContainer margin
// +16 vertically from the BoxContainer margin
// +27 vertically from the window header
var horizontalMenuSize = Math.Max(200, Math.Max(_handCount, _inventoryDimensions.X + 1) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 20);
var verticalMenuSize = Math.Max(200, (_inventoryDimensions.Y + (_handCount > 0 ? 2 : 1)) * (SlotControl.DefaultButtonSize + ButtonSeparation) + 53);
if (snare?.IsEnsnared == true)
verticalMenuSize += 20;
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
}
private void AddHandButton(Hand hand)
@@ -172,6 +192,8 @@ namespace Content.Client.Inventory
UpdateEntityIcon(button, hand.HeldEntity);
_strippingMenu!.HandsContainer.AddChild(button);
LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
_handCount++;
}
private void SlotPressed(GUIBoundKeyEventArgs ev, SlotControl slot)
@@ -220,6 +242,10 @@ namespace Content.Client.Inventory
UpdateEntityIcon(button, entity);
LayoutContainer.SetPosition(button, slotDef.StrippingWindowPos * (SlotControl.DefaultButtonSize + ButtonSeparation));
if (slotDef.StrippingWindowPos.X > _inventoryDimensions.X)
_inventoryDimensions = new Vector2i(slotDef.StrippingWindowPos.X, _inventoryDimensions.Y);
if (slotDef.StrippingWindowPos.Y > _inventoryDimensions.Y)
_inventoryDimensions = new Vector2i(_inventoryDimensions.X, slotDef.StrippingWindowPos.Y);
}
private void UpdateEntityIcon(SlotControl button, EntityUid? entity)

View File

@@ -15,6 +15,7 @@ using Content.Client.Launcher;
using Content.Client.Mapping;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Playtime;
using Content.Client.Replay;
using Content.Client.Screenshot;
using Content.Client.Stylesheets;
@@ -69,6 +70,7 @@ namespace Content.Client.IoC
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
collection.Register<ClientsidePlaytimeTrackingManager>();
}
}
}

View File

@@ -3,6 +3,7 @@ using Content.Client.GameTicking.Managers;
using Content.Client.LateJoin;
using Content.Client.Lobby.UI;
using Content.Client.Message;
using Content.Client.Playtime;
using Content.Client.UserInterface.Systems.Chat;
using Content.Client.Voting;
using Content.Shared.CCVar;
@@ -26,6 +27,7 @@ namespace Content.Client.Lobby
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IVoteManager _voteManager = default!;
[Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!;
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
@@ -195,6 +197,26 @@ namespace Content.Client.Lobby
{
Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
}
var minutesToday = _playtimeTracking.PlaytimeMinutesToday;
if (minutesToday > 60)
{
Lobby!.PlaytimeComment.Visible = true;
var hoursToday = Math.Round(minutesToday / 60f, 1);
var chosenString = minutesToday switch
{
< 180 => "lobby-state-playtime-comment-normal",
< 360 => "lobby-state-playtime-comment-concerning",
< 720 => "lobby-state-playtime-comment-grasstouchless",
_ => "lobby-state-playtime-comment-selfdestructive"
};
Lobby.PlaytimeComment.SetMarkup(Loc.GetString(chosenString, ("hours", hoursToday)));
}
else
Lobby!.PlaytimeComment.Visible = false;
}
private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)

View File

@@ -20,6 +20,12 @@ public sealed partial class LoadoutContainer : BoxContainer
public Button Select => SelectButton;
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
{
RobustXamlLoader.Load(this);
@@ -54,22 +60,9 @@ public sealed partial class LoadoutContainer : BoxContainer
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_entity);
}
public bool Pressed
{
get => SelectButton.Pressed;
set => SelectButton.Pressed = value;
}
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
}

View File

@@ -1,10 +1,14 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True"/>
</PanelContainer>
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True" Margin="5">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'loadout-restrictions'}"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Shared.Clothing;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
@@ -7,12 +6,21 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer
{
private const string ClosedGroupMark = "▶";
private const string OpenedGroupMark = "▼";
/// <summary>
/// A dictionary that stores open groups
/// </summary>
private Dictionary<string, bool> _openedGroups = new();
private readonly LoadoutGroupPrototype _groupProto;
public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
@@ -21,6 +29,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
public LoadoutGroupContainer(HumanoidCharacterProfile profile, RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_groupProto = groupProto;
RefreshLoadouts(profile, loadout, session, collection);
@@ -63,32 +72,165 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
}
LoadoutsContainer.DisposeAllChildren();
// Didn't use options because this is more robust in future.
var selected = loadout.SelectedLoadouts[_groupProto.ID];
// Get all loadout prototypes for this group.
var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));
foreach (var loadoutProto in _groupProto.Loadouts)
/*
* Group the prototypes based on their GroupBy field.
* - If GroupBy is null or empty, fallback to grouping by the prototype ID itself.
* - The result is a dictionary where:
* - The key is either GroupBy or ID (if GroupBy is not set).
* - The value is the list of prototypes that belong to that group.
*
* This allows grouping loadouts into sub-categories within the group.
*/
var groups = validProtos
.GroupBy(p => string.IsNullOrEmpty(p.GroupBy)
? p.ID
: p.GroupBy)
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var kvp in groups)
{
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
continue;
var protos = kvp.Value;
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(profile, session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
loadoutContainer.Select.OnPressed += args =>
if (protos.Count > 1)
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(loadoutProto);
else
OnLoadoutUnpressed?.Invoke(loadoutProto);
};
/*
* Build the list of UI elements for each loadout prototype:
* - For each prototype, create its corresponding LoadoutContainer UI element.
* - Set HorizontalExpand to true so elements properly stretch in layout.
* - Collect all UI elements into a list for further processing.
*/
var uiElements = protos
.Select(proto =>
{
var elem = CreateLoadoutUI(proto, profile, loadout, session, collection, loadoutSystem);
elem.HorizontalExpand = true;
return elem;
})
.ToList();
LoadoutsContainer.AddChild(loadoutContainer);
/*
* Determine which element should be displayed first:
* - If any element is currently selected (its button is pressed), use it.
* - Otherwise, fallback to the first element in the list.
*
* This moves the selected item outside of the sublist for better usability,
* making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
* without having to search inside expanded subgroups.
*/
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
/*
* Get all remaining elements except the first one:
* - Use ReferenceEquals to ensure we exclude the exact instance used as firstElement.
*/
var otherElements = uiElements.Where(e => !ReferenceEquals(e, firstElement)).ToList();
firstElement.HorizontalExpand = true;
var subContainer = new SubLoadoutContainer()
{
Visible = _openedGroups.GetValueOrDefault(kvp.Key, false)
};
var toggle = CreateToggleButton(kvp, firstElement, subContainer);
LoadoutsContainer.AddChild(firstElement);
LoadoutsContainer.AddChild(subContainer);
var subList = subContainer.Grid;
foreach (var proto in otherElements)
{
subList.AddChild(proto);
}
UpdateToggleColor(toggle, subList);
}
else
{
LoadoutsContainer.AddChild(
CreateLoadoutUI(protos[0], profile, loadout, session, collection, loadoutSystem)
);
}
}
}
private ToggleLoadoutButton CreateToggleButton(KeyValuePair<string, List<LoadoutPrototype>> kvp, LoadoutContainer firstElement, SubLoadoutContainer subContainer)
{
var toggle = new ToggleLoadoutButton
{
Text = ClosedGroupMark
};
toggle.Text = subContainer.Visible ? OpenedGroupMark : ClosedGroupMark;
toggle.OnPressed += _ =>
{
var willOpen = !subContainer.Visible;
subContainer.Visible = willOpen;
toggle.Text = willOpen ? OpenedGroupMark : ClosedGroupMark;
_openedGroups[kvp.Key] = willOpen;
};
firstElement.AddChild(toggle);
toggle.SetPositionFirst();
return toggle;
}
private void UpdateToggleColor(Button toggle, BoxContainer subList)
{
var anyActive = subList.Children
.OfType<LoadoutContainer>()
.Any(c => c.Select.Pressed);
toggle.Modulate = anyActive
? Color.Green
: Color.White;
}
/// <summary>
/// Creates a UI container for a single Loadout item.
///
/// This method was extracted from RefreshLoadouts because the logic for creating
/// individual loadout items is used multiple times inside that method, and duplicating
/// the code made it harder to maintain.
///
/// Logic:
/// - Checks if the item is currently selected in the loadout.
/// - Checks if the item is valid for selection (IsValid).
/// - Creates a LoadoutContainer with the appropriate status (disabled / active).
/// - Subscribes to button press events to handle selection and deselection.
/// </summary>
/// <param name="proto">The loadout item prototype.</param>
/// <param name="profile">The humanoid character profile.</param>
/// <param name="loadout">The current role loadout for the user.</param>
/// <param name="session">The user's session.</param>
/// <param name="collection">The dependency injection container.</param>
/// <param name="loadoutSystem">The loadout system instance.</param>
/// <returns>A fully initialized LoadoutContainer for UI display.</returns>
private LoadoutContainer CreateLoadoutUI(LoadoutPrototype proto, HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, LoadoutSystem loadoutSystem)
{
var selected = loadout.SelectedLoadouts[_groupProto.ID];
var pressed = selected.Any(e => e.Prototype == proto.ID);
var enabled = loadout.IsValid(profile, session, proto.ID, collection, out var reason);
var cont = new LoadoutContainer(proto, !enabled, reason);
cont.Text = loadoutSystem.GetName(proto);
cont.Select.Pressed = pressed;
cont.Select.OnPressed += args =>
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(proto.ID);
else
OnLoadoutUnpressed?.Invoke(proto.ID);
};
return cont;
}
}

View File

@@ -0,0 +1,8 @@
<PanelContainer Name="SubContainer"
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<BoxContainer Name="SubGridContainer"
Orientation="Vertical"
HorizontalExpand="true"/>
</PanelContainer>

View File

@@ -0,0 +1,21 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Lobby.UI.Loadouts;
/// <summary>
/// A simple container used to group additional loadout UI elements
/// that are part of the same GroupBy subgroup.
///
/// - Used when a loadout group contains multiple prototypes.
/// - The first prototype is shown directly; the remaining ones are placed inside this container.
/// - Allows toggling visibility of the subgroup via expandable UI (collapsible behavior).
///
/// Internally inherits from PanelContainer to allow for border/background styling if needed.
/// Exposes its internal BoxContainer (SubGridContainer) via the <see cref="Grid"/> property for adding children.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SubLoadoutContainer : PanelContainer
{
public BoxContainer Grid => SubGridContainer;
}

View File

@@ -0,0 +1,9 @@
<Button Name="ToggleButton"
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
VerticalExpand="False"
HorizontalExpand="False"
SetSize="64 64"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0 0 5 0"/>

View File

@@ -0,0 +1,10 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Lobby.UI.Loadouts;
/// <summary>
/// A button that toggles the loadout groups. Needs for override default styles.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class ToggleLoadoutButton : Button;

View File

@@ -41,6 +41,7 @@
StyleClasses="ButtonBig" MinWidth="137" />
</BoxContainer>
</controls:StripeBack>
<RichTextLabel Name="PlaytimeComment" Visible="False" Access="Public" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
<!-- Voting Popups -->

View File

@@ -454,7 +454,7 @@ public sealed class MappingState : GameplayStateBase
switch (prototype)
{
case EntityPrototype entity:
textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).Select(t => t.Default));
textures.AddRange(_sprite.GetPrototypeTextures(entity).Select(t => t.Default));
break;
case DecalPrototype decal:
textures.Add(_sprite.Frame0(decal.Sprite));

View File

@@ -16,7 +16,6 @@ public sealed class JetpackSystem : SharedJetpackSystem
[Dependency] private readonly ClothingSystem _clothing = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{

View File

@@ -3,14 +3,14 @@ using Robust.Shared.Console;
namespace Content.Client.NPC;
public sealed class ShowHTNCommand : IConsoleCommand
public sealed class ShowHtnCommand : LocalizedEntityCommands
{
public string Command => "showhtn";
public string Description => "Shows the current status for HTN NPCs";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly HTNSystem _htnSystem = default!;
public override string Command => "showhtn";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var npcs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<HTNSystem>();
npcs.EnableOverlay ^= true;
_htnSystem.EnableOverlay ^= true;
}
}

View File

@@ -137,15 +137,14 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
}
}
public sealed class ClearAllNetworkLinkOverlays : IConsoleCommand
public sealed class ClearAllNetworkLinkOverlays : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly NetworkConfiguratorSystem _network = default!;
public string Command => "clearnetworklinkoverlays";
public string Description => "Clear all network link overlays.";
public string Help => Command;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "clearnetworklinkoverlays";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<NetworkConfiguratorSystem>().ClearAllOverlays();
_network.ClearAllOverlays();
}
}

View File

@@ -1,48 +1,39 @@
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.NodeContainer
{
public sealed class NodeVisCommand : IConsoleCommand
public sealed class NodeVisCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly NodeGroupSystem _nodeSystem = default!;
public string Command => "nodevis";
public string Description => "Toggles node group visualization";
public string Help => "";
public override string Command => "nodevis";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var adminMan = IoCManager.Resolve<IClientAdminManager>();
if (!adminMan.HasFlag(AdminFlags.Debug))
if (!_adminManager.HasFlag(AdminFlags.Debug))
{
shell.WriteError("You need +DEBUG for this command");
shell.WriteError(Loc.GetString($"shell-missing-required-permission", ("perm", "+DEBUG")));
return;
}
var sys = _e.System<NodeGroupSystem>();
sys.SetVisEnabled(!sys.VisEnabled);
_nodeSystem.SetVisEnabled(!_nodeSystem.VisEnabled);
}
}
public sealed class NodeVisFilterCommand : IConsoleCommand
public sealed class NodeVisFilterCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly NodeGroupSystem _nodeSystem = default!;
public string Command => "nodevisfilter";
public string Description => "Toggles showing a specific group on nodevis";
public string Help => "Usage: nodevis [filter]\nOmit filter to list currently masked-off";
public override string Command => "nodevisfilter";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var sys = _e.System<NodeGroupSystem>();
if (args.Length == 0)
{
foreach (var filtered in sys.Filtered)
foreach (var filtered in _nodeSystem.Filtered)
{
shell.WriteLine(filtered);
}
@@ -50,10 +41,8 @@ namespace Content.Client.NodeContainer
else
{
var filter = args[0];
if (!sys.Filtered.Add(filter))
{
sys.Filtered.Remove(filter);
}
if (!_nodeSystem.Filtered.Add(filter))
_nodeSystem.Filtered.Remove(filter);
}
}
}

View File

@@ -0,0 +1,108 @@
using Content.Shared.CCVar;
using Robust.Client.Player;
using Robust.Shared.Network;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
namespace Content.Client.Playtime;
/// <summary>
/// Keeps track of how long the player has played today.
/// </summary>
/// <remarks>
/// <para>
/// Playtime is treated as any time in which the player is attached to an entity.
/// This notably excludes scenarios like the lobby.
/// </para>
/// </remarks>
public sealed class ClientsidePlaytimeTrackingManager
{
[Dependency] private readonly IClientNetManager _clientNetManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private ISawmill _sawmill = default!;
private const string InternalDateFormat = "yyyy-MM-dd";
[ViewVariables]
private TimeSpan? _mobAttachmentTime;
/// <summary>
/// The total amount of time played today, in minutes.
/// </summary>
[ViewVariables]
public float PlaytimeMinutesToday
{
get
{
var cvarValue = _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday);
if (_mobAttachmentTime == null)
return cvarValue;
return cvarValue + (float)(_gameTiming.RealTime - _mobAttachmentTime.Value).TotalMinutes;
}
}
public void Initialize()
{
_sawmill = _logManager.GetSawmill("clientplaytime");
_clientNetManager.Connected += OnConnected;
// The downside to relying on playerattached and playerdetached is that unsaved playtime won't be saved in the event of a crash
// But then again, the config doesn't get saved in the event of a crash, either, so /shrug
// Playerdetached gets called on quit, though, so at least that's covered.
_playerManager.LocalPlayerAttached += OnPlayerAttached;
_playerManager.LocalPlayerDetached += OnPlayerDetached;
}
private void OnConnected(object? sender, NetChannelArgs args)
{
var datatimey = DateTime.Now;
_sawmill.Info($"Current day: {datatimey.Day} Current Date: {datatimey.Date.ToString(InternalDateFormat)}");
var recordedDateString = _configurationManager.GetCVar(CCVars.PlaytimeLastConnectDate);
var formattedDate = datatimey.Date.ToString(InternalDateFormat);
if (formattedDate == recordedDateString)
return;
_configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, 0);
_configurationManager.SetCVar(CCVars.PlaytimeLastConnectDate, formattedDate);
}
private void OnPlayerAttached(EntityUid entity)
{
_mobAttachmentTime = _gameTiming.RealTime;
}
private void OnPlayerDetached(EntityUid entity)
{
if (_mobAttachmentTime == null)
return;
var newTimeValue = PlaytimeMinutesToday;
_mobAttachmentTime = null;
var timeDiffMinutes = newTimeValue - _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday);
if (timeDiffMinutes < 0)
{
_sawmill.Error("Time differential on player detachment somehow less than zero!");
return;
}
// At less than 1 minute of time diff, there's not much point, and saving regardless will brick tests
// The reason this isn't checking for 0 is because TotalMinutes is fractional, rather than solely whole minutes
if (timeDiffMinutes < 1)
return;
_configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, newTimeValue);
_sawmill.Info($"Recorded {timeDiffMinutes} minutes of living playtime!");
_configurationManager.SaveToFile(); // We don't like that we have to save the entire config just to store playtime stats '^'
}
}

View File

@@ -3,15 +3,15 @@ using Robust.Shared.Console;
namespace Content.Client.Shuttles.Commands;
public sealed class ShowEmergencyShuttleCommand : IConsoleCommand
public sealed class ShowEmergencyShuttleCommand : LocalizedEntityCommands
{
public string Command => "showemergencyshuttle";
public string Description => "Shows the expected position of the emergency shuttle";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly ShuttleSystem _shuttle = default!;
public override string Command => "showemergencyshuttle";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var tstalker = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ShuttleSystem>();
tstalker.EnableShuttlePosition ^= true;
shell.WriteLine($"Set emergency shuttle debug to {tstalker.EnableShuttlePosition}");
_shuttle.EnableShuttlePosition ^= true;
shell.WriteLine(Loc.GetString($"cmd-showemergencyshuttle-status", ("status", _shuttle.EnableShuttlePosition)));
}
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.Strip
public sealed class StrippingMenu : DefaultWindow
{
public LayoutContainer InventoryContainer = new();
public BoxContainer HandsContainer = new() { Orientation = LayoutOrientation.Horizontal };
public LayoutContainer HandsContainer = new();
public BoxContainer SnareContainer = new();
public bool Dirty = true;

View File

@@ -14,12 +14,17 @@ namespace Content.Client.UserInterface.Systems.Chat;
/// </summary>
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
private static readonly Regex StartDoubleQuote = new("\"$");
private static readonly Regex EndDoubleQuote = new("^\"|(?<=^@)\"");
private static readonly Regex StartAtSign = new("^@");
/// <summary>
/// The list of words to be highlighted in the chatbox.
/// </summary>
private List<string> _highlights = new();
private readonly List<string> _highlights = new();
/// <summary>
/// The string holding the hex color used to highlight words.
@@ -42,7 +47,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
// Load highlights if any were saved.
string highlights = _config.GetCVar(CCVars.ChatHighlights);
var highlights = _config.GetCVar(CCVars.ChatHighlights);
if (!string.IsNullOrEmpty(highlights))
{
@@ -84,12 +89,12 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
// We first subdivide the highlights based on newlines to prevent replacing
// a valid "\n" tag and adding it to the final regex.
string[] splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var splittedHighlights = newHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
for (int i = 0; i < splittedHighlights.Length; i++)
for (var i = 0; i < splittedHighlights.Length; i++)
{
// Replace every "\" character with a "\\" to prevent "\n", "\0", etc...
string keyword = splittedHighlights[i].Replace(@"\", @"\\");
var keyword = splittedHighlights[i].Replace(@"\", @"\\");
// Escape the keyword to prevent special characters like "(" and ")" to be considered valid regex.
keyword = Regex.Escape(keyword);
@@ -102,17 +107,17 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
// that make sure the words to match are separated by spaces or punctuation.
// NOTE: The reason why we don't use \b tags is that \b doesn't match reverse slash characters "\" so
// a pre-sanitized (see 1.) string like "\[test]" wouldn't get picked up by the \b.
if (keyword.Count(c => (c == '"')) > 0)
if (keyword.Any(c => c == '"'))
{
// Matches the last double quote character.
keyword = Regex.Replace(keyword, "\"$", "(?!\\w)");
keyword = StartDoubleQuote.Replace(keyword, "(?!\\w)");
// When matching for the first double quote character we also consider the possibility
// of the double quote being preceded by a @ character.
keyword = Regex.Replace(keyword, "^\"|(?<=^@)\"", "(?<!\\w)");
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
}
// Make sure any name tagged as ours gets highlighted only when others say it.
keyword = Regex.Replace(keyword, "^@", "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
keyword = StartAtSign.Replace(keyword, "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
_highlights.Add(keyword);
}
@@ -132,7 +137,7 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
var (_, job, _, _, entityName) = data;
// Mark this entity's name as our character name for the "UpdateHighlights" function.
string newHighlights = "@" + entityName;
var newHighlights = "@" + entityName;
// Subdivide the character's name based on spaces or hyphens so that every word gets highlighted.
if (newHighlights.Count(c => (c == ' ' || c == '-')) == 1)
@@ -144,9 +149,9 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
newHighlights = newHighlights.Split('-')[0] + "\n@" + newHighlights.Split('-')[^1];
// Convert the job title to kebab-case and use it as a key for the loc file.
string jobKey = job.Replace(' ', '-').ToLower();
var jobKey = job.Replace(' ', '-').ToLower();
if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
if (_loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
newHighlights += '\n' + jobMatches.Replace(", ", "\n");
UpdateHighlights(newHighlights);

View File

@@ -19,8 +19,11 @@ namespace Content.Client.UserInterface.Systems.Chat.Widgets;
[Virtual]
public partial class ChatBox : UIWidget
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _log = default!;
private readonly ISawmill _sawmill;
private readonly ChatUIController _controller;
private readonly IEntityManager _entManager;
public bool Main { get; set; }
@@ -29,7 +32,7 @@ public partial class ChatBox : UIWidget
public ChatBox()
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_sawmill = _log.GetSawmill("chat");
ChatInput.Input.OnTextEntered += OnTextEntered;
ChatInput.Input.OnKeyBindDown += OnInputKeyBindDown;
@@ -52,7 +55,7 @@ public partial class ChatBox : UIWidget
private void OnMessageAdded(ChatMessage msg)
{
Logger.DebugS("chat", $"{msg.Channel}: {msg.Message}");
_sawmill.Debug($"{msg.Channel}: {msg.Message}");
if (!ChatInput.FilterButton.Popup.IsActive(msg.Channel))
{
return;

View File

@@ -1,7 +1,7 @@
using Content.Client.Eui;
using Content.Server.Ghost.Roles.Raffles;
using Content.Shared.Eui;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Raffles;
using JetBrains.Annotations;
using Robust.Client.Console;
using Robust.Client.Player;

View File

@@ -1,6 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Server.Ghost.Roles.Raffles;
using System.Numerics;
using Content.Shared.Ghost.Roles.Raffles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;

View File

@@ -225,6 +225,10 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
if (!IsDragging && EntityManager.System<HandsSystem>().GetActiveHandEntity() == null)
return;
// Do not rotate items unless we are either dragging them or hovering over a storage window.
if (DraggingGhost is null && UIManager.CurrentlyHovered is not StorageWindow)
return;
//clamp it to a cardinal.
DraggingRotation = (DraggingRotation + Math.PI / 2f).GetCardinalDir().ToAngle();
if (DraggingGhost != null)

View File

@@ -274,13 +274,11 @@ namespace Content.Client.Voting.UI
}
[UsedImplicitly, AnyCommand]
public sealed class VoteMenuCommand : IConsoleCommand
public sealed class VoteMenuCommand : LocalizedCommands
{
public string Command => "votemenu";
public string Description => Loc.GetString("ui-vote-menu-command-description");
public string Help => Loc.GetString("ui-vote-menu-command-help-text");
public override string Command => "votemenu";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
new VoteCallMenu().OpenCentered();
}

View File

@@ -3,39 +3,33 @@ using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Shared.Console;
using Robust.Shared.Map;
namespace Content.Client.Weapons.Melee;
public sealed class MeleeSpreadCommand : IConsoleCommand
public sealed class MeleeSpreadCommand : LocalizedEntityCommands
{
public string Command => "showmeleespread";
public string Description => "Shows the current weapon's range and arc for debugging";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly MeleeWeaponSystem _meleeSystem = default!;
[Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public override string Command => "showmeleespread";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var collection = IoCManager.Instance;
if (collection == null)
if (_overlay.RemoveOverlay<MeleeArcOverlay>())
return;
var overlayManager = collection.Resolve<IOverlayManager>();
if (overlayManager.RemoveOverlay<MeleeArcOverlay>())
{
return;
}
var sysManager = collection.Resolve<IEntitySystemManager>();
overlayManager.AddOverlay(new MeleeArcOverlay(
collection.Resolve<IEntityManager>(),
collection.Resolve<IEyeManager>(),
collection.Resolve<IInputManager>(),
collection.Resolve<IPlayerManager>(),
sysManager.GetEntitySystem<MeleeWeaponSystem>(),
sysManager.GetEntitySystem<SharedCombatModeSystem>(),
sysManager.GetEntitySystem<SharedTransformSystem>()));
_overlay.AddOverlay(new MeleeArcOverlay(
EntityManager,
_eyeManager,
_inputManager,
_playerManager,
_meleeSystem,
_combatSystem,
_transformSystem));
}
}

View File

@@ -1,18 +1,18 @@
using Content.Client.Weapons.Ranged.Systems;
using Robust.Shared.Console;
namespace Content.Client.Weapons.Ranged;
namespace Content.Client.Weapons.Ranged.Commands;
public sealed class ShowSpreadCommand : IConsoleCommand
public sealed class ShowSpreadCommand : LocalizedEntityCommands
{
public string Command => "showgunspread";
public string Description => $"Shows gun spread overlay for debugging";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GunSystem>();
system.SpreadOverlay ^= true;
[Dependency] private readonly GunSystem _gunSystem = default!;
shell.WriteLine($"Set spread overlay to {system.SpreadOverlay}");
public override string Command => "showgunspread";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_gunSystem.SpreadOverlay ^= true;
shell.WriteLine(Loc.GetString($"cmd-showgunspread-status", ("status", _gunSystem.SpreadOverlay)));
}
}

View File

@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Access
@@ -13,6 +13,15 @@ namespace Content.IntegrationTests.Tests.Access
public sealed class AccessReaderTest
{
/*
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: TestAccessReader
name: access reader
components:
- type: AccessReader
";
[Test]
public async Task TestTags()
{
@@ -20,13 +29,13 @@ namespace Content.IntegrationTests.Tests.Access
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var system = entityManager.System<AccessReaderSystem>();
var ent = entityManager.SpawnEntity("TestAccessReader", MapCoordinates.Nullspace);
var reader = new Entity<AccessReaderComponent>(ent, entityManager.GetComponent<AccessReaderComponent>(ent));
// test empty
var reader = new AccessReaderComponent();
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -35,8 +44,7 @@ namespace Content.IntegrationTests.Tests.Access
});
// test deny
reader = new AccessReaderComponent();
reader.DenyTags.Add("A");
system.AddDenyTag(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "Foo" }, reader), Is.True);
@@ -44,10 +52,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "Foo" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.True);
});
system.ClearDenyTags(reader);
// test one list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.AddAccess(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -55,10 +63,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test one list - two items
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
@@ -66,11 +74,14 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test two list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "B", "C" });
var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
};
system.AddAccesses(reader, accesses);
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -80,11 +91,11 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
// test deny list
reader = new AccessReaderComponent();
reader.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
reader.DenyTags.Add("B");
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.AddDenyTag(reader, "B");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -92,6 +103,8 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.ClearDenyTags(reader);
});
await pair.CleanReturnAsync();
}

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Text;
using Content.Client.Implants;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Clothing;
using Content.Shared.Implants;
@@ -11,17 +9,17 @@ using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Chameleon;
/// <summary>
/// Ensures all round <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
/// Ensures all <see cref="IsProbablyRoundStartJob">"round start jobs"</see> have an associated chameleon loadout.
/// </summary>
public sealed class ChameleonJobLoadoutTest : InteractionTest
{/* //CP14 we dont wanna chameleons, disabled test
private readonly List<ProtoId<JobPrototype>> JobBlacklist =
private static readonly List<ProtoId<JobPrototype>> JobBlacklist =
[
];
[Test]
public async Task CheckAllJobs()
public Task CheckAllJobs()
{
var alljobs = ProtoMan.EnumeratePrototypes<JobPrototype>();
@@ -47,24 +45,16 @@ public sealed class ChameleonJobLoadoutTest : InteractionTest
validJobs[chameleon.Job.Value] += 1;
}
var errorMessage = new StringBuilder();
errorMessage.AppendLine("The following job(s) have no chameleon prototype(s):");
var invalid = false;
// All round start jobs have a chameleon loadout
foreach (var job in validJobs)
Assert.Multiple(() =>
{
if (job.Value != 0)
continue;
foreach (var job in validJobs)
{
Assert.That(job.Value, Is.Not.Zero,
$"{job.Key} has no chameleonOutfit prototype.");
}
});
errorMessage.AppendLine(job.Key + " has no chameleonOutfit prototype.");
invalid = true;
}
if (!invalid)
return;
Assert.Fail(errorMessage.ToString());
return Task.CompletedTask;
}
/// <summary>

View File

@@ -16,6 +16,7 @@ namespace Content.IntegrationTests.Tests
var client = pair.Client;
var prototypeManager = client.ResolveDependency<IPrototypeManager>();
var resourceCache = client.ResolveDependency<IResourceCache>();
var spriteSys = client.System<SpriteSystem>();
await client.WaitAssertion(() =>
{
@@ -26,7 +27,7 @@ namespace Content.IntegrationTests.Tests
Assert.DoesNotThrow(() =>
{
var _ = SpriteComponent.GetPrototypeTextures(proto, resourceCache).ToList();
var _ = spriteSys.GetPrototypeTextures(proto).ToList();
}, "Prototype {0} threw an exception when getting its textures.",
proto.ID);
}

View File

@@ -16,6 +16,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.NukeOps;
using Content.Shared.Pinpointer;
@@ -23,12 +24,16 @@ using Content.Shared.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.GameRules;
[TestFixture]
public sealed class NukeOpsTest
{/*
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
/// <summary>
/// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
/// </summary>
@@ -119,8 +124,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(player));
Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
var roles = roleSys.MindGetAllRoleInfo(mind);
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
@@ -130,8 +135,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(dummyEnts[1]));
Assert.That(roleSys.MindIsAntagonist(dummyMind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(dummyEnts[1], SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(dummyEnts[1], NanotrasenFaction), Is.False);
roles = roleSys.MindGetAllRoleInfo(dummyMind);
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
@@ -146,8 +151,8 @@ public sealed class NukeOpsTest
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
Assert.That(factionSys.IsMember(ent, SyndicateFaction), Is.False);
Assert.That(factionSys.IsMember(ent, NanotrasenFaction), Is.True);
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}

View File

@@ -8,6 +8,7 @@ using Content.Server.Roles;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Objectives.Components;
using Robust.Shared.GameObjects;
@@ -20,6 +21,8 @@ public sealed class TraitorRuleTest
{/*
private const string TraitorGameRuleProtoId = "Traitor";
private const string TraitorAntagRoleName = "Traitor";
private static readonly ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
private static readonly ProtoId<NpcFactionPrototype> NanotrasenFaction = "NanoTrasen";
[Test]
public async Task TestTraitorObjectives()
@@ -108,8 +111,8 @@ public sealed class TraitorRuleTest
// Make sure the player is a traitor.
var mind = mindSys.GetMind(player)!.Value;
Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
Assert.That(factionSys.IsMember(player, SyndicateFaction), Is.True);
Assert.That(factionSys.IsMember(player, NanotrasenFaction), Is.False);
Assert.That(traitorRule.TotalTraitors, Is.EqualTo(1));
Assert.That(traitorRule.TraitorMinds[0], Is.EqualTo(mind));

View File

@@ -0,0 +1,50 @@
using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Localization;
public sealed class EntityPrototypeLocalizationTest
{
/// <summary>
/// An explanation of why LocIds should not be used for entity prototype names/descriptions.
/// Appended to the error message when the test is failed.
/// </summary>
private const string NoLocIdExplanation = "Entity prototypes should not use LocIds for names/descriptions, as localization IDs are automated for entity prototypes. See https://docs.spacestation14.com/en/ss14-by-example/fluent-and-localization.html#localizing-prototypes for more information.";
/// <summary>
/// Checks that no entity prototypes have a LocId as their name or description.
/// See <see href="https://docs.spacestation14.com/en/ss14-by-example/fluent-and-localization.html#localizing-prototypes"/> for why this is important.
/// </summary>
[Test]
public async Task TestNoManualEntityLocStrings()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ProtoMan;
var locMan = server.ResolveDependency<ILocalizationManager>();
var protos = protoMan.EnumeratePrototypes<EntityPrototype>();
Assert.Multiple(() =>
{
foreach (var proto in protos)
{
// Check name
if (!string.IsNullOrEmpty(proto.SetName))
{
Assert.That(locMan.HasString(proto.SetName), Is.False,
$"Entity prototype {proto.ID} has a LocId ({proto.SetName}) as a name. {NoLocIdExplanation}");
}
// Check description
if (!string.IsNullOrEmpty(proto.SetDesc))
{
Assert.That(locMan.HasString(proto.SetDesc), Is.False,
$"Entity prototype {proto.ID} has a LocId ({proto.SetDesc}) as a description. {NoLocIdExplanation}");
}
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -55,8 +55,6 @@ namespace Content.IntegrationTests.Tests
//CrystallEdge Maps end
"/Maps/centcomm.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/gate.yml", // Contains positronic brain and LSE-1200c "Perforator"
"/Maps/meta.yml", // Contains warden's rubber stamp
"/Maps/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp

View File

@@ -47,7 +47,7 @@ public static class ServerPackaging
// Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql",
"Microsoft",
"Discord",
"NetCord",
};
private static readonly List<string> ServerNotExtraAssemblies = new()

View File

@@ -1,6 +1,7 @@
using Content.Server.Wires;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Wires;
namespace Content.Server.Access;
@@ -23,23 +24,21 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
{
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
comp.Enabled = false;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.Enabled = true;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.Enabled = false;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, comp), false);
WiresSystem.StartWireAction(wire.Owner, _pulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
}
@@ -57,8 +56,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
{
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
{
access.Enabled = true;
EntityManager.Dirty(wire.Owner, access);
EntityManager.System<AccessReaderSystem>().SetActive((wire.Owner, access), true);
}
}
}

View File

@@ -1,8 +1,8 @@
using Content.Server.Administration;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
namespace Content.Server.Access;
@@ -19,7 +19,7 @@ public sealed class AddAccessLogCommand : ToolshedCommand
ctx.WriteLine($"WARNING: Surpassing the limit of the log by {accessLogCount - accessReader.AccessLogLimit+1} entries!");
var accessTime = TimeSpan.FromSeconds(seconds);
accessReader.AccessLog.Enqueue(new AccessRecord(accessTime, accessor));
EntityManager.System<AccessReaderSystem>().LogAccess((input, accessReader), accessor, accessTime, true);
ctx.WriteLine($"Successfully added access log to {input} with this information inside:\n " +
$"Time of access: {accessTime}\n " +
$"Accessed by: {accessor}");

View File

@@ -37,21 +37,21 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
{
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
comp.LoggingDisabled = true;
EntityManager.Dirty(wire.Owner, comp);
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.LoggingDisabled = false;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
{
_access.LogAccess((wire.Owner, comp), Loc.GetString(PulseLog));
comp.LoggingDisabled = true;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), false);
WiresSystem.StartWireAction(wire.Owner, PulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
}
@@ -64,7 +64,7 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
private void AwaitPulseCancel(Wire wire)
{
if (!wire.IsCut && EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var comp))
comp.LoggingDisabled = false;
EntityManager.System<AccessReaderSystem>().SetLoggingActive((wire.Owner, comp), true);
}
private enum PulseTimeoutKey : byte

View File

@@ -168,21 +168,6 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
return accessList;
}
private List<HashSet<ProtoId<AccessLevelPrototype>>> ConvertAccessListToHashSet(List<ProtoId<AccessLevelPrototype>> accessList)
{
List<HashSet<ProtoId<AccessLevelPrototype>>> accessHashsets = new List<HashSet<ProtoId<AccessLevelPrototype>>>();
if (accessList != null && accessList.Any())
{
foreach (ProtoId<AccessLevelPrototype> access in accessList)
{
accessHashsets.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
}
}
return accessHashsets;
}
/// <summary>
/// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader.
/// </summary>
@@ -244,12 +229,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
_accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
Dirty(accessReaderEnt.Value);
}
/// <summary>

View File

@@ -15,7 +15,6 @@ namespace Content.Server.Actions;
public sealed class ActionOnInteractSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;

View File

@@ -0,0 +1,74 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Administration;
using Content.Shared.Prototypes;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Actions.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class AddActionCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Command => "addaction";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString(Loc.GetString("cmd-addaction-invalid-args")));
return;
}
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
{
shell.WriteLine(Loc.GetString("shell-entity-uid-must-be-number"));
return;
}
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
{
shell.WriteError(Loc.GetString("cmd-addaction-actions-not-found"));
return;
}
if (!_prototypeManager.TryIndex<EntityPrototype>(args[1], out var proto) ||
!proto.HasComponent<ActionComponent>())
{
shell.WriteError(Loc.GetString("cmd-addaction-action-not-found", ("action", args[1])));
return;
}
if (_actions.AddAction(targetEntity.Value, args[1]) == null)
{
shell.WriteError(Loc.GetString("cmd-addaction-adding-failed"));
}
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<ActionsComponent>(args[0]),
Loc.GetString("cmd-addaction-player-completion"));
}
if (args.Length != 2)
return CompletionResult.Empty;
var actionPrototypes = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
.Where(p => p.HasComponent<ActionComponent>())
.Select(p => p.ID)
.Order();
return CompletionResult.FromHintOptions(
actionPrototypes,
Loc.GetString("cmd-addaction-action-completion"));
}
}

View File

@@ -0,0 +1,84 @@
using Content.Server.Administration;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Actions.Commands;
[AdminCommand(AdminFlags.Debug)]
public sealed class RemoveActionCommand : LocalizedEntityCommands
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
public override string Command => "rmaction";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString(Loc.GetString("cmd-rmaction-invalid-args")));
return;
}
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
{
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
if (!NetEntity.TryParse(args[1], out var targetActionUidNet) || !EntityManager.TryGetEntity(targetActionUidNet, out var targetActionEntity))
{
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[1])));
return;
}
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
{
shell.WriteError(Loc.GetString("cmd-rmaction-actions-not-found"));
return;
}
if (_actions.GetAction(targetActionEntity) is not { } ent)
{
shell.WriteError(Loc.GetString("cmd-rmaction-not-an-action"));
return;
}
_actions.SetTemporary(ent.Owner, true);
_actions.RemoveAction(ent.Owner);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.Components<ActionsComponent>(args[0]),
Loc.GetString("cmd-rmaction-player-completion"));
}
if (args.Length == 2)
{
if (!NetEntity.TryParse(args[0], out var targetUidNet) || !EntityManager.TryGetEntity(targetUidNet, out var targetEntity))
return CompletionResult.Empty;
if (!EntityManager.HasComponent<ActionsComponent>(targetEntity))
return CompletionResult.Empty;
var actions = _actions.GetActions(targetEntity.Value);
var options = new List<CompletionOption>();
foreach (var action in actions)
{
var hint = Loc.GetString("cmd-rmaction-action-info", ("action", action));
options.Add(new CompletionOption(action.Owner.ToString(), hint));
}
return CompletionResult.FromHintOptions(options, Loc.GetString("cmd-rmaction-action-completion"));
}
return CompletionResult.Empty;
}
}

View File

@@ -8,32 +8,30 @@ using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.AdminWho)]
public sealed class AdminWhoCommand : IConsoleCommand
public sealed class AdminWhoCommand : LocalizedCommands
{
public string Command => "adminwho";
public string Description => "Returns a list of all admins on the server";
public string Help => "Usage: adminwho";
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "adminwho";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var adminMgr = IoCManager.Resolve<IAdminManager>();
var afk = IoCManager.Resolve<IAfkManager>();
var seeStealth = true;
// If null it (hopefully) means it is being called from the console.
if (shell.Player != null)
{
var playerData = adminMgr.GetAdminData(shell.Player);
var playerData = _adminManager.GetAdminData(shell.Player);
seeStealth = playerData != null && playerData.CanStealth();
}
var sb = new StringBuilder();
var first = true;
foreach (var admin in adminMgr.ActiveAdmins)
foreach (var admin in _adminManager.ActiveAdmins)
{
var adminData = adminMgr.GetAdminData(admin)!;
var adminData = _adminManager.GetAdminData(admin)!;
DebugTools.AssertNotNull(adminData);
if (adminData.Stealth && !seeStealth)
@@ -50,9 +48,9 @@ public sealed class AdminWhoCommand : IConsoleCommand
if (adminData.Stealth)
sb.Append(" (S)");
if (shell.Player is { } player && adminMgr.HasAdminFlag(player, AdminFlags.Admin))
if (shell.Player is { } player && _adminManager.HasAdminFlag(player, AdminFlags.Admin))
{
if (afk.IsAfk(admin))
if (_afkManager.IsAfk(admin))
sb.Append(" [AFK]");
}
}

View File

@@ -6,26 +6,23 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceUiCommand : IConsoleCommand
public sealed class AnnounceUiCommand : LocalizedEntityCommands
{
public string Command => "announceui";
[Dependency] private readonly EuiManager _euiManager = default!;
public string Description => "Opens the announcement UI";
public override string Command => "announceui";
public string Help => $"{Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("This does not work from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminAnnounceEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
}

View File

@@ -7,23 +7,22 @@ namespace Content.Server.Administration.Commands
{
[UsedImplicitly]
[AdminCommand(AdminFlags.None)]
public sealed class DeAdminCommand : IConsoleCommand
public sealed class DeAdminCommand : LocalizedCommands
{
public string Command => "deadmin";
public string Description => "Temporarily de-admins you so you can experience the round as a normal player.";
public string Help => "Usage: deadmin\nUse readmin to re-admin after using this.";
[Dependency] private readonly IAdminManager _admin = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "deadmin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("You cannot use this command from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
mgr.DeAdmin(player);
_admin.DeAdmin(player);
}
}
}

View File

@@ -4,42 +4,40 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Spawn)]
public sealed class DeleteComponent : IConsoleCommand
public sealed class DeleteComponent : LocalizedEntityCommands
{
public string Command => "deletecomponent";
public string Description => "Deletes all instances of the specified component.";
public string Help => $"Usage: {Command} <name>";
[Dependency] private readonly IComponentFactory _compFactory = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "deletecomponent";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteLine($"Not enough arguments.\n{Help}");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
break;
default:
var name = string.Join(" ", args);
var componentFactory = IoCManager.Resolve<IComponentFactory>();
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!componentFactory.TryGetRegistration(name, out var registration))
if (!_compFactory.TryGetRegistration(name, out var registration))
{
shell.WriteLine($"No component exists with name {name}.");
shell.WriteLine(Loc.GetString($"cmd-deletecomponent-no-component-exists", ("name", name)));
break;
}
var componentType = registration.Type;
var components = entityManager.GetAllComponents(componentType, true);
var components = EntityManager.GetAllComponents(componentType, true);
var i = 0;
foreach (var (uid, component) in components)
{
entityManager.RemoveComponent(uid, component);
EntityManager.RemoveComponent(uid, component);
i++;
}
shell.WriteLine($"Removed {i} components with name {name}.");
shell.WriteLine(Loc.GetString($"cmd-deletecomponent-success", ("count", i), ("name", name)));
break;
}

View File

@@ -13,72 +13,73 @@ using Robust.Server.GameObjects;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class OpenExplosionEui : IConsoleCommand
public sealed class OpenExplosionEui : LocalizedEntityCommands
{
public string Command => "explosionui";
public string Description => "Opens a window for easy access to station destruction";
public string Help => $"Usage: {Command}";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "explosionui";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteError("This does not work from the server console.");
shell.WriteError(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new SpawnExplosionEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
[AdminCommand(AdminFlags.Fun)] // for the admin. Not so much for anyone else.
public sealed class ExplosionCommand : IConsoleCommand
public sealed class ExplosionCommand : LocalizedEntityCommands
{
public string Command => "explosion";
public string Description => "Train go boom";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ExplosionSystem _explosion = default!;
[Dependency] private readonly TransformSystem _transform = default!;
public override string Command => "explosion";
// Note that if you change the arguments, you should also update the client-side SpawnExplosionWindow, as that just
// uses this command.
public string Help => "Usage: explosion [intensity] [slope] [maxIntensity] [x y] [mapId] [prototypeId]";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0 || args.Length == 4 || args.Length > 7)
{
shell.WriteError("Wrong number of arguments.");
shell.WriteError(Loc.GetString($"shell-wrong-arguments-number"));
return;
}
if (!float.TryParse(args[0], out var intensity))
{
shell.WriteError($"Failed to parse intensity: {args[0]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-intensity", ("value", args[0])));
return;
}
float slope = 5;
if (args.Length > 1 && !float.TryParse(args[1], out slope))
{
shell.WriteError($"Failed to parse float: {args[1]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-float", ("value", args[1])));
return;
}
float maxIntensity = 100;
if (args.Length > 2 && !float.TryParse(args[2], out maxIntensity))
{
shell.WriteError($"Failed to parse float: {args[2]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-float", ("value", args[2])));
return;
}
float x = 0, y = 0;
if (args.Length > 4)
{
if (!float.TryParse(args[3], out x) ||
!float.TryParse(args[4], out y))
if (!float.TryParse(args[3], out x) || !float.TryParse(args[4], out y))
{
shell.WriteError($"Failed to parse coordinates: {(args[3], args[4])}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-coords",
("value1", args[3]),
("value2", args[4])));
return;
}
}
@@ -88,7 +89,7 @@ public sealed class ExplosionCommand : IConsoleCommand
{
if (!int.TryParse(args[5], out var parsed))
{
shell.WriteError($"Failed to parse map ID: {args[5]}");
shell.WriteError(Loc.GetString($"cmd-explosion-failed-to-parse-map-id", ("value", args[5])));
return;
}
coords = new MapCoordinates(new Vector2(x, y), new(parsed));
@@ -96,42 +97,39 @@ public sealed class ExplosionCommand : IConsoleCommand
else
{
// attempt to find the player's current position
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent(shell.Player?.AttachedEntity, out TransformComponent? xform))
if (!EntityManager.TryGetComponent(shell.Player?.AttachedEntity, out TransformComponent? xform))
{
shell.WriteError($"Failed get default coordinates/map via player's transform. Need to specify explicitly.");
shell.WriteError(Loc.GetString($"cmd-explosion-need-coords-explicit"));
return;
}
if (args.Length > 4)
coords = new MapCoordinates(new Vector2(x, y), xform.MapID);
else
coords = entMan.System<TransformSystem>().GetMapCoordinates(shell.Player.AttachedEntity.Value, xform: xform);
coords = _transform.GetMapCoordinates(shell.Player.AttachedEntity.Value, xform: xform);
}
ExplosionPrototype? type;
var protoMan = IoCManager.Resolve<IPrototypeManager>();
if (args.Length > 6)
{
if (!protoMan.TryIndex(args[6], out type))
if (!_prototypeManager.TryIndex(args[6], out type))
{
shell.WriteError($"Unknown explosion prototype: {args[6]}");
shell.WriteError(Loc.GetString($"cmd-explosion-unknown-prototype", ("value", args[6])));
return;
}
}
else if (!protoMan.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
else if (!_prototypeManager.TryIndex(ExplosionSystem.DefaultExplosionPrototypeId, out type))
{
// no prototype was specified, so lets default to whichever one was defined first
type = protoMan.EnumeratePrototypes<ExplosionPrototype>().FirstOrDefault();
type = _prototypeManager.EnumeratePrototypes<ExplosionPrototype>().FirstOrDefault();
if (type == null)
{
shell.WriteError($"Prototype manager has no explosion prototypes?");
shell.WriteError(Loc.GetString($"cmd-explosion-no-prototypes"));
return;
}
}
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
_explosion.QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
}
}

View File

@@ -6,14 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Fun)]
public sealed class FaxUiCommand : IConsoleCommand
public sealed class FaxUiCommand : LocalizedEntityCommands
{
public string Command => "faxui";
[Dependency] private readonly EuiManager _euiManager = default!;
public string Description => Loc.GetString("cmd-faxui-desc");
public string Help => Loc.GetString("cmd-faxui-help");
public override string Command => "faxui";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -21,8 +20,7 @@ public sealed class FaxUiCommand : IConsoleCommand
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminFaxEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}

View File

@@ -3,42 +3,36 @@ using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.EntitySerialization;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Round | AdminFlags.Spawn)]
public sealed class LoadGameMapCommand : IConsoleCommand
public sealed class LoadGameMapCommand : LocalizedEntityCommands
{
public string Command => "loadgamemap";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public string Description => "Loads the given game map at the given coordinates.";
public override string Command => "loadgamemap";
public string Help => "loadgamemap <mapid> <gamemap> [<x> <y> [<name>]] ";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
var mapSys = entityManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
if (args.Length is not (2 or 4 or 5))
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
if (!_prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
{
shell.WriteError($"The given map prototype {args[0]} is invalid.");
return;
}
if (!int.TryParse(args[0], out var mapId))
return;
return;
var stationName = args.Length == 5 ? args[4] : null;
@@ -48,14 +42,14 @@ namespace Content.Server.Administration.Commands
var id = new MapId(mapId);
var grids = mapSys.MapExists(id)
? gameTicker.MergeGameMap(gameMap, id, stationName: stationName, offset: offset)
: gameTicker.LoadGameMapWithId(gameMap, id, stationName: stationName, offset: offset);
var grids = _mapSystem.MapExists(id)
? _gameTicker.MergeGameMap(gameMap, id, stationName: stationName, offset: offset)
: _gameTicker.LoadGameMapWithId(gameMap, id, stationName: stationName, offset: offset);
shell.WriteLine($"Loaded {grids.Count} grids.");
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
@@ -79,26 +73,20 @@ namespace Content.Server.Administration.Commands
}
[AdminCommand(AdminFlags.Round | AdminFlags.Spawn)]
public sealed class ListGameMaps : IConsoleCommand
public sealed class ListGameMaps : LocalizedCommands
{
public string Command => "listgamemaps";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public string Description => "Lists the game maps that can be used by loadgamemap";
public override string Command => "listgamemaps";
public string Help => "listgamemaps";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
if (args.Length != 0)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
foreach (var prototype in prototypeManager.EnumeratePrototypes<GameMapPrototype>())
foreach (var prototype in _prototypeManager.EnumeratePrototypes<GameMapPrototype>())
{
shell.WriteLine($"{prototype.ID} - {prototype.MapName}");
}

View File

@@ -6,13 +6,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Logs)]
public sealed class OpenAdminLogsCommand : IConsoleCommand
public sealed class OpenAdminLogsCommand : LocalizedEntityCommands
{
public string Command => "adminlogs";
public string Description => "Opens the admin logs panel.";
public string Help => $"Usage: {Command}";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "adminlogs";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
@@ -20,8 +20,7 @@ public sealed class OpenAdminLogsCommand : IConsoleCommand
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new AdminLogsEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}

View File

@@ -6,24 +6,23 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Permissions)]
public sealed class OpenPermissionsCommand : IConsoleCommand
public sealed class OpenPermissionsCommand : LocalizedEntityCommands
{
public string Command => "permissions";
public string Description => "Opens the admin permissions panel.";
public string Help => "Usage: permissions";
[Dependency] private readonly EuiManager _euiManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "permissions";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("This does not work from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new PermissionsEui();
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
}
}
}

View File

@@ -1,21 +1,19 @@
using System.Text;
using Content.Server.Database;
using Content.Server.Database;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Ban)]
public sealed class PardonCommand : IConsoleCommand
public sealed class PardonCommand : LocalizedCommands
{
public string Command => "pardon";
public string Description => "Pardons somebody's ban";
public string Help => $"Usage: {Command} <ban id>";
[Dependency] private readonly IServerDbManager _dbManager = default!;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "pardon";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
var dbMan = IoCManager.Resolve<IServerDbManager>();
if (args.Length != 1)
{
@@ -25,11 +23,11 @@ namespace Content.Server.Administration.Commands
if (!int.TryParse(args[0], out var banId))
{
shell.WriteLine($"Unable to parse {args[0]} as a ban id integer.\n{Help}");
shell.WriteLine(Loc.GetString($"cmd-pardon-unable-to-parse", ("id", args[0]), ("help", Help)));
return;
}
var ban = await dbMan.GetServerBanAsync(banId);
var ban = await _dbManager.GetServerBanAsync(banId);
if (ban == null)
{
@@ -39,22 +37,22 @@ namespace Content.Server.Administration.Commands
if (ban.Unban != null)
{
var response = new StringBuilder("This ban has already been pardoned");
if (ban.Unban.UnbanningAdmin != null)
{
response.Append($" by {ban.Unban.UnbanningAdmin.Value}");
shell.WriteLine(Loc.GetString($"cmd-pardon-already-pardoned-specific",
("admin", ban.Unban.UnbanningAdmin.Value),
("time", ban.Unban.UnbanTime)));
}
response.Append($" in {ban.Unban.UnbanTime}.");
else
shell.WriteLine(Loc.GetString($"cmd-pardon-already-pardoned"));
shell.WriteLine(response.ToString());
return;
}
await dbMan.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now));
await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now));
shell.WriteLine($"Pardoned ban with id {banId}");
shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId)));
}
}
}

View File

@@ -12,12 +12,10 @@ namespace Content.Server.Administration.Commands;
public sealed class PersistenceSave : LocalizedEntityCommands
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
public override string Command => "persistencesave";
public override string Description => "Saves server data to a persistence file to be loaded later.";
public override string Help => "persistencesave [mapId] [filePath - default: game.map (CCVar) ]";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -47,8 +45,7 @@ public sealed class PersistenceSave : LocalizedEntityCommands
return;
}
var mapLoader = _system.GetEntitySystem<MapLoaderSystem>();
mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
_mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
}
}

View File

@@ -6,29 +6,28 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[UsedImplicitly]
public sealed class PromoteHostCommand : IConsoleCommand
public sealed class PromoteHostCommand : LocalizedCommands
{
public string Command => "promotehost";
public string Description => "Grants client temporary full host admin privileges. Use this to bootstrap admins.";
public string Help => "Usage promotehost <player>";
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "promotehost";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine("Expected exactly one argument.");
shell.WriteLine(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
var plyMgr = IoCManager.Resolve<IPlayerManager>();
if (!plyMgr.TryGetSessionByUsername(args[0], out var targetPlayer))
if (!_playerManager.TryGetSessionByUsername(args[0], out var targetPlayer))
{
shell.WriteLine("Unable to find a player by that name.");
shell.WriteLine(Loc.GetString($"shell-target-player-does-not-exist"));
return;
}
var adminMgr = IoCManager.Resolve<IAdminManager>();
adminMgr.PromoteHost(targetPlayer);
_adminManager.PromoteHost(targetPlayer);
}
}
}

View File

@@ -5,30 +5,28 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AnyCommand]
public sealed class ReAdminCommand : IConsoleCommand
public sealed class ReAdminCommand : LocalizedCommands
{
public string Command => "readmin";
public string Description => "Re-admins you if you previously de-adminned.";
public string Help => "Usage: readmin";
[Dependency] private readonly IAdminManager _adminManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "readmin";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("You cannot use this command from the server console.");
shell.WriteLine(Loc.GetString($"shell-cannot-run-command-from-server"));
return;
}
var mgr = IoCManager.Resolve<IAdminManager>();
if (mgr.GetAdminData(player, includeDeAdmin: true) == null)
if (_adminManager.GetAdminData(player, includeDeAdmin: true) == null)
{
shell.WriteLine("You're not an admin.");
shell.WriteLine(Loc.GetString($"cmd-readmin-not-an-admin"));
return;
}
mgr.ReAdmin(player);
_adminManager.ReAdmin(player);
}
}
}

View File

@@ -5,46 +5,43 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Mapping)]
public sealed class RemoveExtraComponents : IConsoleCommand
public sealed class RemoveExtraComponents : LocalizedEntityCommands
{
public string Command => "removeextracomponents";
public string Description => "Removes all components from all entities of the specified id if that component is not in its prototype.\nIf no id is specified, it matches all entities.";
public string Help => $"{Command} <entityId> / {Command}";
public void Execute(IConsoleShell shell, string argStr, string[] args)
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override string Command => "removeextracomponents";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var id = args.Length == 0 ? null : string.Join(" ", args);
var entityManager = IoCManager.Resolve<IEntityManager>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var fac = IoCManager.Resolve<IComponentFactory>();
EntityPrototype? prototype = null;
var checkPrototype = !string.IsNullOrEmpty(id);
if (checkPrototype && !prototypeManager.TryIndex(id!, out prototype))
if (checkPrototype && !_prototypeManager.TryIndex(id!, out prototype))
{
shell.WriteError($"Can't find entity prototype with id \"{id}\"!");
shell.WriteError(Loc.GetString($"cmd-removeextracomponents-invalid-prototype-id", ("id", $"{id}")));
return;
}
var entities = 0;
var components = 0;
foreach (var entity in entityManager.GetEntities())
foreach (var entity in EntityManager.GetEntities())
{
var metaData = entityManager.GetComponent<MetaDataComponent>(entity);
var metaData = EntityManager.GetComponent<MetaDataComponent>(entity);
if (checkPrototype && metaData.EntityPrototype != prototype || metaData.EntityPrototype == null)
{
continue;
}
var modified = false;
foreach (var component in entityManager.GetComponents(entity))
foreach (var component in EntityManager.GetComponents(entity))
{
if (metaData.EntityPrototype.Components.ContainsKey(fac.GetComponentName(component.GetType())))
if (metaData.EntityPrototype.Components.ContainsKey(_compFactory.GetComponentName(component.GetType())))
continue;
entityManager.RemoveComponent(entity, component);
EntityManager.RemoveComponent(entity, component);
components++;
modified = true;
@@ -54,7 +51,18 @@ namespace Content.Server.Administration.Commands
entities++;
}
shell.WriteLine($"Removed {components} components from {entities} entities{(id == null ? "." : $" with id {id}")}");
if (id != null)
{
shell.WriteLine(Loc.GetString($"cmd-removeextracomponents-success-with-id",
("count", components),
("entities", entities),
("id", id)));
return;
}
shell.WriteLine(Loc.GetString($"cmd-removeextracomponents-success",
("count", components),
("entities", entities)));
}
}
}

View File

@@ -5,13 +5,13 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
public sealed class RoleUnbanCommand : IConsoleCommand
public sealed class RoleUnbanCommand : LocalizedCommands
{
public string Command => "roleunban";
public string Description => Loc.GetString("cmd-roleunban-desc");
public string Help => Loc.GetString("cmd-roleunban-help");
[Dependency] private readonly IBanManager _banManager = default!;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "roleunban";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
@@ -21,16 +21,15 @@ public sealed class RoleUnbanCommand : IConsoleCommand
if (!int.TryParse(args[0], out var banId))
{
shell.WriteLine($"Unable to parse {args[0]} as a ban id integer.\n{Help}");
shell.WriteLine(Loc.GetString($"cmd-roleunban-unable-to-parse-id", ("id", args[0]), ("help", Help)));
return;
}
var banManager = IoCManager.Resolve<IBanManager>();
var response = await banManager.PardonRoleBan(banId, shell.Player?.UserId, DateTimeOffset.Now);
var response = await _banManager.PardonRoleBan(banId, shell.Player?.UserId, DateTimeOffset.Now);
shell.WriteLine(response);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
// Can't think of good way to do hint options for this
return args.Length switch

View File

@@ -6,13 +6,14 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.NameColor)]
internal sealed class SetAdminOOC : IConsoleCommand
internal sealed class SetAdminOOC : LocalizedCommands
{
public string Command => "setadminooc";
public string Description => Loc.GetString("set-admin-ooc-command-description", ("command", Command));
public string Help => Loc.GetString("set-admin-ooc-command-help-text", ("command", Command));
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly IServerPreferencesManager _preferenceManager = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "setadminooc";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player == null)
{
@@ -36,11 +37,9 @@ namespace Content.Server.Administration.Commands
var userId = shell.Player.UserId;
// Save the DB
var dbMan = IoCManager.Resolve<IServerDbManager>();
dbMan.SaveAdminOOCColorAsync(userId, color.Value);
_dbManager.SaveAdminOOCColorAsync(userId, color.Value);
// Update the cached preference
var prefManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = prefManager.GetPreferences(userId);
var prefs = _preferenceManager.GetPreferences(userId);
prefs.AdminOOCColor = color.Value;
}
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Players;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
@@ -9,17 +8,16 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
sealed class SetMindCommand : IConsoleCommand
public sealed class SetMindCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public string Command => "setmind";
public override string Command => "setmind";
public string Description => Loc.GetString("set-mind-command-description", ("requiredComponent", nameof(MindContainerComponent)));
public override string Description => Loc.GetString("cmd-setmind-desc", ("requiredComponent", nameof(MindContainerComponent)));
public string Help => Loc.GetString("set-mind-command-help-text", ("command", Command));
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
@@ -33,7 +31,7 @@ namespace Content.Server.Administration.Commands
return;
}
bool ghostOverride = true;
var ghostOverride = true;
if (args.Length > 2)
{
ghostOverride = bool.Parse(args[2]);
@@ -41,19 +39,19 @@ namespace Content.Server.Administration.Commands
var nent = new NetEntity(entInt);
if (!_entManager.TryGetEntity(nent, out var eUid))
if (!EntityManager.TryGetEntity(nent, out var eUid))
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-id"));
return;
}
if (!_entManager.HasComponent<MindContainerComponent>(eUid))
if (!EntityManager.HasComponent<MindContainerComponent>(eUid))
{
shell.WriteLine(Loc.GetString("set-mind-command-target-has-no-mind-message"));
shell.WriteLine(Loc.GetString("cmd-setmind-target-has-no-mind-message"));
return;
}
if (!IoCManager.Resolve<IPlayerManager>().TryGetSessionByUsername(args[1], out var session))
if (!_playerManager.TryGetSessionByUsername(args[1], out var session))
{
shell.WriteLine(Loc.GetString("shell-target-player-does-not-exist"));
return;
@@ -63,24 +61,21 @@ namespace Content.Server.Administration.Commands
var playerCData = session.ContentData();
if (playerCData == null)
{
shell.WriteLine(Loc.GetString("set-mind-command-target-has-no-content-data-message"));
shell.WriteLine(Loc.GetString("cmd-setmind-target-has-no-content-data-message"));
return;
}
var mindSystem = _entManager.System<SharedMindSystem>();
var metadata = _entManager.GetComponent<MetaDataComponent>(eUid.Value);
var metadata = EntityManager.GetComponent<MetaDataComponent>(eUid.Value);
var mind = playerCData.Mind ?? mindSystem.CreateMind(session.UserId, metadata.EntityName);
var mind = playerCData.Mind ?? _mindSystem.CreateMind(session.UserId, metadata.EntityName);
mindSystem.TransferTo(mind, eUid, ghostOverride);
_mindSystem.TransferTo(mind, eUid, ghostOverride);
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 2)
{
return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-mind-command-hint"));
}
return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Help);
return CompletionResult.Empty;
}

View File

@@ -1,36 +1,22 @@
using Content.Server.Administration.UI;
using Content.Server.Clothing.Systems;
using Content.Server.EUI;
using Content.Server.Hands.Systems;
using Content.Server.Preferences.Managers;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
using Content.Shared.Clothing;
using Content.Shared.Hands.Components;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Admin)]
public sealed class SetOutfitCommand : IConsoleCommand
public sealed class SetOutfitCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
public string Command => "setoutfit";
public override string Command => "setoutfit";
public override string Description => Loc.GetString("cmd-setoutfit-desc", ("requiredComponent", nameof(InventoryComponent)));
public string Description => Loc.GetString("set-outfit-command-description", ("requiredComponent", nameof(InventoryComponent)));
public string Help => Loc.GetString("set-outfit-command-help-text", ("command", Command));
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 1)
{
@@ -46,13 +32,13 @@ namespace Content.Server.Administration.Commands
var nent = new NetEntity(entInt);
if (!_entities.TryGetEntity(nent, out var target))
if (!EntityManager.TryGetEntity(nent, out var target))
{
shell.WriteLine(Loc.GetString("shell-invalid-entity-id"));
return;
}
if (!_entities.HasComponent<InventoryComponent>(target))
if (!EntityManager.HasComponent<InventoryComponent>(target))
{
shell.WriteLine(Loc.GetString("shell-target-entity-does-not-have-message", ("missing", "inventory")));
return;
@@ -62,109 +48,17 @@ namespace Content.Server.Administration.Commands
{
if (shell.Player is not { } player)
{
shell.WriteError(Loc.GetString("set-outfit-command-is-not-player-error"));
shell.WriteError(Loc.GetString("cmd-setoutfit-is-not-player-error"));
return;
}
var eui = IoCManager.Resolve<EuiManager>();
var ui = new SetOutfitEui(nent);
eui.OpenEui(ui, player);
_euiManager.OpenEui(ui, player);
return;
}
if (!SetOutfit(target.Value, args[1], _entities))
shell.WriteLine(Loc.GetString("set-outfit-command-invalid-outfit-id-error"));
}
public static bool SetOutfit(EntityUid target, string gear, IEntityManager entityManager, Action<EntityUid, EntityUid>? onEquipped = null)
{
if (!entityManager.TryGetComponent(target, out InventoryComponent? inventoryComponent))
return false;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<StartingGearPrototype>(gear, out var startingGear))
return false;
HumanoidCharacterProfile? profile = null;
ICommonSession? session = null;
// Check if we are setting the outfit of a player to respect the preferences
if (entityManager.TryGetComponent(target, out ActorComponent? actorComponent))
{
session = actorComponent.PlayerSession;
var userId = actorComponent.PlayerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);
profile = prefs.SelectedCharacter as HumanoidCharacterProfile;
}
var invSystem = entityManager.System<InventorySystem>();
if (invSystem.TryGetSlots(target, out var slots))
{
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = ((IEquipmentLoadout) startingGear).GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;
}
var equipmentEntity = entityManager.SpawnEntity(gearStr, entityManager.GetComponent<TransformComponent>(target).Coordinates);
if (slot.Name == "id" &&
entityManager.TryGetComponent(equipmentEntity, out PdaComponent? pdaComponent) &&
entityManager.TryGetComponent<IdCardComponent>(pdaComponent.ContainedId, out var id))
{
id.FullName = entityManager.GetComponent<MetaDataComponent>(target).EntityName;
}
invSystem.TryEquip(target, equipmentEntity, slot.Name, silent: true, force: true, inventory: inventoryComponent);
onEquipped?.Invoke(target, equipmentEntity);
}
}
if (entityManager.TryGetComponent(target, out HandsComponent? handsComponent))
{
var handsSystem = entityManager.System<HandsSystem>();
var coords = entityManager.GetComponent<TransformComponent>(target).Coordinates;
foreach (var prototype in startingGear.Inhand)
{
var inhandEntity = entityManager.SpawnEntity(prototype, coords);
handsSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent);
}
}
// See if this starting gear is associated with a job
var jobs = prototypeManager.EnumeratePrototypes<JobPrototype>();
foreach (var job in jobs)
{
if (job.StartingGear != gear)
continue;
var jobProtoId = LoadoutSystem.GetJobPrototype(job.ID);
if (!prototypeManager.TryIndex<RoleLoadoutPrototype>(jobProtoId, out var jobProto))
break;
// Don't require a player, so this works on Urists
profile ??= entityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
: new HumanoidCharacterProfile();
// Try to get the user's existing loadout for the role
profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
if (roleLoadout == null)
{
// If they don't have a loadout for the role, make a default one
roleLoadout = new RoleLoadout(jobProtoId);
roleLoadout.SetDefault(profile, session, prototypeManager);
}
// Equip the target with the job loadout
var stationSpawning = entityManager.System<SharedStationSpawningSystem>();
stationSpawning.EquipRoleLoadout(target, roleLoadout, jobProto);
}
return true;
if (!_outfitSystem.SetOutfit(target.Value, args[1]))
shell.WriteLine(Loc.GetString("cmd-setoutfit-invalid-outfit-id-error"));
}
}
}

View File

@@ -6,46 +6,36 @@ using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Round)]
public sealed class CallShuttleCommand : IConsoleCommand
public sealed class CallShuttleCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
public string Command => "callshuttle";
public string Description => Loc.GetString("call-shuttle-command-description");
public string Help => Loc.GetString("call-shuttle-command-help-text", ("command",Command));
public override string Command => "callshuttle";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var loc = IoCManager.Resolve<ILocalizationManager>();
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (args.Length == 1 && TimeSpan.TryParseExact(args[0], ContentLocalizationManager.TimeSpanMinutesFormats, loc.DefaultCulture, out var timeSpan))
{
_e.System<RoundEndSystem>().RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, false);
}
if (args.Length == 1 && TimeSpan.TryParseExact(args[0], ContentLocalizationManager.TimeSpanMinutesFormats, LocalizationManager.DefaultCulture, out var timeSpan))
_roundEndSystem.RequestRoundEnd(timeSpan, shell.Player?.AttachedEntity, false);
else if (args.Length == 1)
{
shell.WriteLine(Loc.GetString("shell-timespan-minutes-must-be-correct"));
}
else
{
_e.System<RoundEndSystem>().RequestRoundEnd(shell.Player?.AttachedEntity, false);
}
_roundEndSystem.RequestRoundEnd(shell.Player?.AttachedEntity, false);
}
}
[AdminCommand(AdminFlags.Round)]
public sealed class RecallShuttleCommand : IConsoleCommand
public sealed class RecallShuttleCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _e = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
public string Command => "recallshuttle";
public string Description => Loc.GetString("recall-shuttle-command-description");
public string Help => Loc.GetString("recall-shuttle-command-help-text", ("command",Command));
public override string Command => "recallshuttle";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_e.System<RoundEndSystem>().CancelRoundEndCountdown(shell.Player?.AttachedEntity, false);
_roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, false);
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Server.Clothing.Systems;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
@@ -21,6 +22,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly OutfitSystem _outfit = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
@@ -148,7 +150,7 @@ public sealed partial class AdminVerbSystem
Act = () =>
{
// pirates just get an outfit because they don't really have logic associated with them
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
_outfit.SetOutfit(args.Target, PirateGearId);
},
Impact = LogImpact.High,
Message = string.Join(": ", pirateName, Loc.GetString("admin-verb-make-pirate")),

View File

@@ -1,10 +1,10 @@
using System.Threading;
using Content.Server.Administration.Commands;
using Content.Server.Administration.Components;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@@ -42,7 +42,6 @@ using Content.Shared.Slippery;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
@@ -80,6 +79,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SuperBonkSystem _superBonkSystem = default!;
[Dependency] private readonly SlipperySystem _slipperySystem = default!;
[Dependency] private readonly OutfitSystem _outfitSystem = default!;
// All smite verbs have names so invokeverb works.
private void AddSmiteVerbs(GetVerbsEvent<Verb> args)
@@ -416,7 +416,7 @@ public sealed partial class AdminVerbSystem
{
Text = pinballName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Balls/basketball.rsi"), "icon"),
Act = () =>
{
var xform = Transform(args.Target);
@@ -587,7 +587,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
Act = () =>
{
SetOutfitCommand.SetOutfit(args.Target, "JanitorMaidGear", EntityManager, (_, clothing) =>
_outfitSystem.SetOutfit(args.Target, "JanitorMaidGear", (_, clothing) =>
{
if (HasComp<ClothingComponent>(clothing))
EnsureComp<UnremoveableComponent>(clothing);
@@ -685,7 +685,7 @@ public sealed partial class AdminVerbSystem
{
Text = reptilianName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Plushies/lizard.rsi"), "icon"),
Act = () =>
{
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");

View File

@@ -38,18 +38,21 @@ public sealed class SolutionCommand : ToolshedCommand
public SolutionRef AdjReagent(
[PipedArgument] SolutionRef input,
ProtoId<ReagentPrototype> proto,
FixedPoint2 amount
float amount
)
{
_solutionContainer ??= GetSys<SharedSolutionContainerSystem>();
if (amount > 0)
// Convert float to FixedPoint2
var amountFixed = FixedPoint2.New(amount);
if (amountFixed > 0)
{
_solutionContainer.TryAddReagent(input.Solution, proto, amount, out _);
_solutionContainer.TryAddReagent(input.Solution, proto, amountFixed, out _);
}
else if (amount < 0)
else if (amountFixed < 0)
{
_solutionContainer.RemoveReagent(input.Solution, proto, -amount);
_solutionContainer.RemoveReagent(input.Solution, proto, -amountFixed);
}
return input;
@@ -59,7 +62,7 @@ public sealed class SolutionCommand : ToolshedCommand
public IEnumerable<SolutionRef> AdjReagent(
[PipedArgument] IEnumerable<SolutionRef> input,
ProtoId<ReagentPrototype> name,
FixedPoint2 amount
float amount
)
=> input.Select(x => AdjReagent(x, name, amount));
}

View File

@@ -6,34 +6,31 @@ using Robust.Shared.Console;
namespace Content.Server.Afk
{
[AdminCommand(AdminFlags.Admin)]
public sealed class IsAfkCommand : IConsoleCommand
public sealed class IsAfkCommand : LocalizedCommands
{
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IPlayerManager _players = default!;
public string Command => "isafk";
public string Description => "Checks if a specified player is AFK";
public string Help => "Usage: isafk <playerName>";
public override string Command => "isafk";
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var afkManager = IoCManager.Resolve<IAfkManager>();
if (args.Length == 0)
{
shell.WriteError("Need one argument");
shell.WriteError(Loc.GetString($"shell-need-exactly-one-argument"));
return;
}
if (!_players.TryGetSessionByUsername(args[0], out var player))
{
shell.WriteError("Unable to find that player");
shell.WriteError(Loc.GetString($"shell-target-player-does-not-exist"));
return;
}
shell.WriteLine(afkManager.IsAfk(player) ? "They are indeed AFK" : "They are not AFK");
shell.WriteLine(Loc.GetString(_afkManager.IsAfk(player) ? "cmd-isafk-true" : "cmd-isafk-false"));
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{

View File

@@ -38,7 +38,7 @@ public sealed partial class AtmosPipeLayersSystem : SharedAtmosPipeLayersSystem
if (ent.Comp.PipeLayersLocked)
return;
base.SetPipeLayer(ent, layer);
base.SetPipeLayer(ent, layer, user, used);
if (!TryComp<NodeContainerComponent>(ent, out var nodeContainer))
return;

View File

@@ -7,6 +7,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
@@ -14,12 +15,14 @@ namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpaceWindSounds = "SpaceWind";
private const int SpaceWindSoundCooldownCycles = 75;
private int _spaceWindSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public string? SpaceWindSound { get; private set; } = "/Audio/Effects/space_wind.ogg";
public SoundSpecifier? SpaceWindSound { get; private set; } = new SoundCollectionSpecifier(DefaultSpaceWindSounds, AudioParams.Default.WithVariation(0.125f));
private readonly HashSet<Entity<MovedByPressureComponent>> _activePressures = new(8);
@@ -105,10 +108,10 @@ namespace Content.Server.Atmos.EntitySystems
// Don't play the space wind sound on tiles that are on fire...
if (tile.PressureDifference > 15 && !tile.Hotspot.Valid)
{
if (_spaceWindSoundCooldown == 0 && !string.IsNullOrEmpty(SpaceWindSound))
if (_spaceWindSoundCooldown == 0 && SpaceWindSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
_audio.PlayPvs(SpaceWindSound, coordinates, AudioParams.Default.WithVariation(0.125f).WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100)));
_audio.PlayPvs(SpaceWindSound, coordinates, SpaceWindSound.Params.WithVolume(MathHelper.Clamp(tile.PressureDifference / 10, 10, 100)));
}
}

View File

@@ -7,12 +7,15 @@ using Content.Shared.Database;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
private static readonly ProtoId<SoundCollectionPrototype> DefaultHotspotSounds = "AtmosHotspot";
[Dependency] private readonly DecalSystem _decalSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@@ -21,7 +24,7 @@ namespace Content.Server.Atmos.EntitySystems
private int _hotspotSoundCooldown = 0;
[ViewVariables(VVAccess.ReadWrite)]
public string? HotspotSound { get; private set; } = "/Audio/Effects/fire.ogg";
public SoundSpecifier? HotspotSound { get; private set; } = new SoundCollectionSpecifier(DefaultHotspotSounds);
private void ProcessHotspot(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
@@ -105,14 +108,14 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Temperature > tile.MaxFireTemperatureSustained)
tile.MaxFireTemperatureSustained = tile.Hotspot.Temperature;
if (_hotspotSoundCooldown++ == 0 && !string.IsNullOrEmpty(HotspotSound))
if (_hotspotSoundCooldown++ == 0 && HotspotSound != null)
{
var coordinates = _mapSystem.ToCenterCoordinates(tile.GridIndex, tile.GridIndices);
// A few details on the audio parameters for fire.
// The greater the fire state, the lesser the pitch variation.
// The greater the fire state, the greater the volume.
_audio.PlayPvs(HotspotSound, coordinates, AudioParams.Default.WithVariation(0.15f/tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
_audio.PlayPvs(HotspotSound, coordinates, HotspotSound.Params.WithVariation(0.15f / tile.Hotspot.State).WithVolume(-5f + 5f * tile.Hotspot.State));
}
if (_hotspotSoundCooldown > HotspotSoundCooldownCycles)

View File

@@ -33,9 +33,12 @@ public sealed class GasAnalyzerSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
Subs.BuiEvents<GasAnalyzerComponent>(GasAnalyzerUiKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(OnBoundUIOpened);
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
});
}
public override void Update(float frameTime)
@@ -72,21 +75,6 @@ public sealed class GasAnalyzerSystem : EntitySystem
args.Handled = true;
}
/// <summary>
/// Activates the analyzer with no target, so it only scans the tile the user was on when activated
/// </summary>
private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
{
// Not checking for Handled because ActivatableUISystem already marks it as such.
if (!entity.Comp.Enabled)
ActivateAnalyzer(entity, args.User);
else
DisableAnalyzer(entity, args.User);
args.Handled = true;
}
/// <summary>
/// Handles analyzer activation logic
/// </summary>
@@ -104,16 +92,6 @@ public sealed class GasAnalyzerSystem : EntitySystem
UpdateAnalyzer(entity.Owner, entity.Comp);
}
/// <summary>
/// Close the UI, turn the analyzer off, and don't update when it's dropped
/// </summary>
private void OnDropped(Entity<GasAnalyzerComponent> entity, ref DroppedEvent args)
{
if (args.User is var userId && entity.Comp.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(entity, args.User);
}
/// <summary>
/// Closes the UI, sets the icon to off, and removes it from the update list
/// </summary>
@@ -121,6 +99,9 @@ public sealed class GasAnalyzerSystem : EntitySystem
{
_userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user);
if (user.HasValue && entity.Comp.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), user.Value, user.Value);
entity.Comp.Enabled = false;
Dirty(entity);
_appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
@@ -130,9 +111,25 @@ public sealed class GasAnalyzerSystem : EntitySystem
/// <summary>
/// Disables the analyzer when the user closes the UI
/// </summary>
private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
private void OnBoundUIClosed(Entity<GasAnalyzerComponent> entity, ref BoundUIClosedEvent args)
{
DisableAnalyzer(entity);
if (HasComp<ActiveGasAnalyzerComponent>(entity.Owner)
&& !_userInterface.IsUiOpen(entity.Owner, args.UiKey))
{
DisableAnalyzer(entity, args.Actor);
}
}
/// <summary>
/// Enables the analyzer when the user opens the UI
/// </summary>
private void OnBoundUIOpened(Entity<GasAnalyzerComponent> entity, ref BoundUIOpenedEvent args)
{
if (!HasComp<ActiveGasAnalyzerComponent>(entity.Owner)
&& _userInterface.IsUiOpen(entity.Owner, args.UiKey))
{
ActivateAnalyzer(entity, args.Actor);
}
}
/// <summary>

View File

@@ -1,11 +1,10 @@
using Content.Server.Actions;
using Content.Server.Bed.Components;
using Content.Server.Body.Systems;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
using Content.Shared.Bed.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Emag.Systems;

View File

@@ -3,23 +3,23 @@ using Content.Server.Body.Systems;
using Content.Shared.Administration;
using Content.Shared.Body.Components;
using Robust.Shared.Console;
using Robust.Shared.Random;
namespace Content.Server.Body.Commands
{
[AdminCommand(AdminFlags.Fun)]
sealed class DestroyMechanismCommand : IConsoleCommand
internal sealed class DestroyMechanismCommand : LocalizedEntityCommands
{
public string Command => "destroymechanism";
public string Description => "Destroys a mechanism from your entity";
public string Help => $"Usage: {Command} <mechanism>";
[Dependency] private readonly IComponentFactory _compFactory = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
public void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "destroymechanism";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = shell.Player;
if (player == null)
{
shell.WriteLine("Only a player can run this command.");
shell.WriteLine(Loc.GetString($"shell-only-players-can-run-this-command"));
return;
}
@@ -31,36 +31,29 @@ namespace Content.Server.Body.Commands
if (player.AttachedEntity is not {} attached)
{
shell.WriteLine("You have no entity.");
shell.WriteLine(Loc.GetString($"shell-must-be-attached-to-entity"));
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
var fac = IoCManager.Resolve<IComponentFactory>();
if (!entityManager.TryGetComponent(attached, out BodyComponent? body))
if (!EntityManager.TryGetComponent(attached, out BodyComponent? body))
{
var random = IoCManager.Resolve<IRobustRandom>();
var text = $"You have no body{(random.Prob(0.2f) ? " and you must scream." : ".")}";
shell.WriteLine(text);
shell.WriteLine(Loc.GetString($"shell-must-have-body"));
return;
}
var mechanismName = string.Join(" ", args).ToLowerInvariant();
var bodySystem = entityManager.System<BodySystem>();
foreach (var organ in bodySystem.GetBodyOrgans(attached, body))
foreach (var organ in _bodySystem.GetBodyOrgans(attached, body))
{
if (fac.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName)
if (_compFactory.GetComponentName(organ.Component.GetType()).ToLowerInvariant() == mechanismName)
{
entityManager.QueueDeleteEntity(organ.Id);
shell.WriteLine($"Mechanism with name {mechanismName} has been destroyed.");
EntityManager.QueueDeleteEntity(organ.Id);
shell.WriteLine(Loc.GetString($"cmd-destroymechanism-success", ("name", mechanismName)));
return;
}
}
shell.WriteLine($"No mechanism was found with name {mechanismName}.");
shell.WriteLine(Loc.GetString($"cmd-destroymechanism-no-mechanism-found", ("name", mechanismName)));
}
}
}

View File

@@ -1,8 +1,8 @@
using Content.Server.Body.Components;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
@@ -10,6 +10,7 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Drunk;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
@@ -204,10 +205,13 @@ public sealed class BloodstreamSystem : EntitySystem
}
// TODO probably cache this or something. humans get hurt a lot
if (!_prototypeManager.TryIndex<DamageModifierSetPrototype>(ent.Comp.DamageBleedModifiers, out var modifiers))
if (!_prototypeManager.TryIndex(ent.Comp.DamageBleedModifiers, out var modifiers))
return;
var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);
// some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true
// but we only want to consider the dealt damage when causing bleeding
var damage = DamageSpecifier.GetPositive(args.DamageDelta);
var bloodloss = DamageSpecifier.ApplyModifierSet(damage, modifiers);
if (bloodloss.Empty)
return;
@@ -226,7 +230,7 @@ public sealed class BloodstreamSystem : EntitySystem
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && _robustRandom.Prob(prob))
{
TryModifyBloodLevel(ent, (-total) / 5, ent);
TryModifyBloodLevel(ent, -total / 5, ent);
_audio.PlayPvs(ent.Comp.InstantBloodSound, ent);
}
@@ -247,18 +251,30 @@ public sealed class BloodstreamSystem : EntitySystem
/// </summary>
private void OnHealthBeingExamined(Entity<BloodstreamComponent> ent, ref HealthBeingExaminedEvent args)
{
// Shows profusely bleeding at half the max bleed rate.
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
// Shows massively bleeding at 0.75x the max bleed rate.
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding, but less than profusely.
else if (ent.Comp.BleedAmount > 0)
// Shows bleeding message when bleeding above half the max rate, but less than massively.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
}
// Shows bleeding message when bleeding below 0.25x the max cap
else if (ent.Comp.BleedAmount > 0)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)

View File

@@ -1,9 +1,12 @@
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Ghost;
using Content.Server.Humanoid;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
using Content.Shared.Damage.Components;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
@@ -11,8 +14,6 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using System.Numerics;
using Content.Shared.Damage.Components;
namespace Content.Server.Body.Systems;

View File

@@ -1,9 +1,10 @@
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
@@ -231,29 +232,4 @@ namespace Content.Server.Body.Systems
_solutionContainerSystem.UpdateChemicals(soln.Value);
}
}
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}
}

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