Upstream sync (#1509)

* Fix the tail wagging action being blocked by cuffs. (#38454)

Update types.yml

* Automatic changelog update

* Remove redundant IoC Resolve in `EmptyOrWindowValidInTile` (#38446)

Remove redundant IoC Resolve in EmptyOrWindowValidInTile

* fix: trim five minutes of silence from bottle_clunk_2 (#38463)

* Update patrons (#38470)

* Forbid string literals for `SharedActionsSystem` methods (#38472)

Forbid string literals for SharedActionsSystem methods

* Chameleon Controller Implants can be deimplanted (#38439)

* Automatic changelog update

* Cleanup duplicate dependency in `AdminVerbSystem.Smites` (#38473)

Cleanup duplicate dependency in AdminVerbSystem.Smites

* fix: set correct layer visibility for power cell sprites (#38458)

* fix: set correct layer visibility for power cell sprites

* fix: use a default charge level for power cell sprites' appearance data

* Remove Icon Smoothing for Airlocks, Doors, and Plastic Flaps, and Shutters from Walls (#38456)

* Nuke Icon Smoothing

* Update highsec.yml

* Update plastic_flaps.yml

* Scurrets (#38218)

* Scurrets.

* Add missing equipment YAML

* Fix count of NamesFirstScurret

* Resolve PR comments, wa.

Also add like a bunch more wa replacements

* ed

* Update Resources/Textures/Mobs/Animals/scurret/displacement.rsi/meta.json

* :rivflabbergasted:

* Fixed spacings in scurret_last.ftl

* Fix mangled endings of some last names

* wawa

* the scug has a spear for self defence

* Automatic changelog update

* Fail if we attempt to run publish on master (#38431)

* Fail if we attempt to run publish on master

BREAKING: FORKS REMOVE THIS

* Update publish.yml

* Fix styling for Admin Menu lists (#38261)

* remove styleclass from playertab 'button' lines

* cvar, fix button-style header alignment

* Fix requested changes

And also the extra cvar things I was meant to remove in the upstream merge

* Tiny tweaks

---------

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

* Automatic changelog update

* Add interaction test for retractable arm blade (#38452)

* Add interaction test for retractable arm blade

* Update for HandsSystem refactor

* Revert "Update for HandsSystem refactor"

This reverts commit e01bb9a7e318dd916240d57b30f48af9594bff1f.

* Combine WaitAssertion blocks

* Make... sloths... speak... slowly... (#38142)

* Add SlowAccent

* Apply SlowAccent to sloths

* xmldocs

* Automatic changelog update

* Make gas tank UI a bit more network-happy (#38184)

* fix: network gas tank output pressure

* fix: don't overwrite gas tank output pressure during editing

* allow to publish news without ui (#35262)

* allow to publish news without ui

* the hell was that

* apply

* apply review

* sure

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

* okay

---------

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

* add some documentation for visiting entity (#38475)

* add some documentation for visiting entity

to prevent someone from spending 5 minutes diving into the codebase

* reviews

Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>

---------

Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>

* SignalSwitchSystem: Check button is locked before toggling (#38474)

Check button is locked before toggling

* Automatic changelog update

* Fix compile (#38477)

* Fix wallmount interaction (#38111)

* Automatic changelog update

* Fix debug asserts when unequipping items (#38274)

* Flask Visual Overhaul & YML Organizing (#38032)

* Adding Back Baseline V1 Changes

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

* Readability Change

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

* icon_open implementation, tidy up & attributions (fix lith/shiny flask ugly af open sprites)

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

* good enough

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

* No mo closed spills

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

* Inhands & Attributions Part 1

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

* Inhands & Attributions Part 2

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

* Flask Sounds & Attributions

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

* Cap flask inhands change

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

* Lith flask Inhand Changes

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

* Spacing

* Fix?

* Man...

* Attributions Back in...

* Im loosing it..

* REAL!

* :/

* Again...

* again :/

* Plz???

---------

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

* Automatic changelog update

* Kills TurfHelpers (#37939)

* Create TurfSystem equivalent for and obsolete TurfHelpers.GetTileRef

* Fix EntitySystem uses of TurfHelpers.GetTileRef

* Fix EntitySystem uses of TurfHelpers.TryGetTileRef

* Fix construction condition uses of TurfHelpers.GetTileRef

* Fix last use of TurfHelpers.IsBlockedTurf

* Create TurfSystem equivalent to and obsolete TurfHelpers.GetContentTileDefinition

* Fix uses of TurfHelpers.GetContentTileDefinition(TileRef)

* Fix uses of TurfHelpers.GetContentTileDefinition(Tile)

* Create TurfSystem equivalent to and obsolete TurfHelpers.IsSpace

* Fix EntitySystem uses of TurfHelpers.IsSpace(Tile)

* Fix EntitySystem uses of TurfHelpers.IsSpace(TileRef)

* Fix remaining uses of TurfHelpers.IsSpace

* Fix uses of TurfHelpers.GetEntitiesInTile

* Delete TurfHelpers.cs

* Add GetEntitiesInTile lookup methods

* Convert some GetEntitiesInTile methods to LookupSystem extension methods

* Use new GetEntitiesInTile methods

* Recycle spiderweb hashset

* Recycle floor tile hashset

* Allow Maintainers to use customvote command (#38385)

Changed from Moderator perm to Round perm

* Automatic changelog update

* Make role ban pannel pretty (#37952)

* Make role ban pannel pretty

* Removed unused depencency

* refactor: wider panel (no jumping due to scroll in english lang) minor readability improvements

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>

* Automatic changelog update

* Generic Numeric Alerts (#38370)

* Fix styles for group button in loadout menu (#38488)

* Fix styles color for group button in loadout menu

* Show selected item count in loadout group UI

Updated the loadout group container to display the number of selected items in a group. Added a new localization string to support this feature in the UI.

* Resolve required changes

* Automatic changelog update

* fix: mops with liquid can hit people again (#38486)

* Automatic changelog update

* Treat duplicate dependency warnings as errors (#38480)

* Treat duplicate dependency warnings as errors

* GitHub workflows

* commas

* Maybe we just don't use these?

* I think we can get rid of these?

* Retractable items get removed by handcuffs (#38441)

* init

* oops

* happens

* review

* fix

* Update Credits (#38493)

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

* Fixed dirt tiles having identical names (#38500)

* added prefix to planet dirt/grass

* prefixes don't work, just doing renaming instead.

* Readds the Hypereutactic Blade for traitors, adds Hypereutatic blade for Nukies (#37182)

* adds hypereutactic back into the uplinks

* Disables hum

* Adds the Hypereutatic Blade for Nukies

* Actually makes the sound so quiet no one can hear it

* Apply suggestions from code review

* Update Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml

---------

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

* Automatic changelog update

* Allow admins to export round logs to CSV files (#38206)

* Automatic changelog update

* MINOR SPELLING ERROR!!1!!1! (#38510)

Spelling fix in the code of conduct

Comunity ->Community

* Staff of Healing for Pacifists (#38509)

* Automatic changelog update

* Command resolves. (#38519)

* banlist-command

* open-admin-notes-command

* stealthmin-command

* set-alert-level-command

* remove unused usings

* whitelist commands

* Emotional Support Scurret locstrings (#38518)

Missed during the atomization of the Wa Wa PR.

* Content fix for `LocalizedCommands` `Loc` change (#38133)

* Content fix for LocalizedCommands Loc change

* Switch to LocalizationManager

* Predict Flashes (#37640)

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

* Automatic changelog update

* Remove excess `SingularityLevelChangedEvent` subscriptions (#38536)

* Remove excess SingularityLevelChangedEvent subscriptions

* RadiationSourceComponent too

* Cleanup commented out code

* Revisions and cleanup to dock and shuttle commands. (#38533)

commit

* Add wall-based ambient occlusion (#38276)

* Add wall ambient occlusion

* wawawewa

* Work

* cvars

* Comment to make slart happy

* Automatic changelog update

* Update submodule to 263.0.0 (#38524)

* update oasis (#37770)

* Add uplink command minor revision and cleanup (#38532)

* commit

* Update AddUplinkCommand.cs

* Fix IsSpace call (#38548)

* Crew manifest command cleanup and LEC (#38544)

commit

* Re-Add Stamina Damage Resistances to Nukie & ERT Suits after the Test Merge. (#38526)

* Add stamina damage resists to the bloodred suits.

* Apply suggestions from code review

---------

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

* Automatic changelog update

* Update cmd-adminwho-desc (#38539)

Update admin-who-command.ftl

* Added a handheld station map to the cyborg thruster module (#38541)

* Added a station map to the borg thruster module

* Removed HandheldStationMapBorg & replaced it with HandheldStationMapUnpowered

* Automatic changelog update

* fix: correct wall AO having weird sharp edges (macOS only?) (#38552)

* Fix displacement map rendering in UI (#38042)

Fix displacement map rendering

918709cb47 changed the way humans work to completely remove the stencil mask thing for clothing cutouts, in favor of displacement maps. Except it didn't actually remove the stencil parameters from the shader prototype, so this caused the shader to render incorrectly in SpriteView instances.

I introduced a new DisplacedDraw shader that doesn't have the stencil in use, as removing the old one would probably be a more annoying breaking change.

Fixes #37629

* Automatic changelog update

* remove suffix from ChameleonControllerImplanter (#38554)

* Update implanters.yml

* Update Resources/Prototypes/Entities/Objects/Misc/implanters.yml

---------

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

* Bulldog desc fix (#38558)

Co-authored-by: Vasilis The Pikachu <vasilis@pikachu.systems>

* Automatic changelog update

* cleanup air_alarm.yml and fire_alarm.yml (#38559)

* Update air_alarm.yml

* that too

* Update air_alarm.yml

* Fix cutter machine having free techmaint floor tiles (#38557)

Fix BaseMaintTileRecipe
Changed BaseTileRecipe to BaseMaintTileRecipe

* Automatic changelog update

* Lobby Audio from static to CVar (#38375)

* Changed LobbyMusiccollection over to a Cvar and edited ContentAudioSystem.cs to use Cvar Values

* Addedd Ability to modify the lobbyMusiccollection from the command line

* Fixed changing lobby music while in the round

* Deleted uneeded duplicate line

* Removed additional duplicate lobbyplaylist line

* Alphabatized imports and refactored to use Subs.CVar

* Added error checking and default behaviour to CVar sub.

* Refactored to use TryIndex and Allowed for a empty soundcollection when a sound collection is not found. Edited Cvar comment to reflect changes.

* Made _lobbyMusicCollection nullable and addedd handling for null case where used. Also Changed LobbyMusicCollection Cvar over to audio rather than ambience.

* Update Content.Server/Audio/ContentAudioSystem.cs

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Electrocute command cleanup and localization (#38563)

* hey look I knocked out a todo :shockedface:

* Update Resources/Locale/en-US/electrocution/electrocute-command.ftl

---------

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

* allow combat mode toggling when unable to interact (#38573)

allow combat mode toggling when cuffed or crit

* update xeno techmaint floor name (#38555)

Update tiles.yml

* New status effect system (#37238)

* spectra

* documentation

* added into liquid anomaly

* Update TemporaryStealthComponent.cs

* Update TemporaryStealthComponent.cs

* integrated

* new system

* mark old status effect system as obsolete

* ForcedSleeping new status effect

* work with reagents

* networking???

* Revert "integrated"

This reverts commit bca02b82bae18ae131af593d7eb86e6de2745157.

* Revert "Update TemporaryStealthComponent.cs"

This reverts commit 4a5be8c4b704a0d1ff9544b2e245d8b2701ec580.

* Revert "Update TemporaryStealthComponent.cs"

This reverts commit a4875bcb41347638854bd723d96a51c3e6d38034.

* Revert "added into liquid anomaly"

This reverts commit df5086b14bb35f1467158a36807c0f2163a16d99.

* Revert "documentation"

This reverts commit 3629b9466758cbdfa4dd5e67ece122fa2f181138.

* Revert "spectra"

This reverts commit 2d03d88c16d16ad6831c19a7921b84600daeb284.

* drowsiness status effect remove

* reagents work

* polish, remove test changes

* first Fildrance review part

* Update misc.yml

* more fildrance review

* final part

* fix trailing spaces

* sleeping status effect

* drowsiness status effect

* Create ModifyStatusEffect.cs

* some tweak

* Yay!!! Manual networking

* minor nitpick

* oopsie

* refactor: xml-docs, notnullwhen attributes, whitespaces

* fildrance and emo review

* refactor: simplify check in SharedStatusEffectsSystem by using pattern matching, TryEffectsWithComp now returns set of Entity<T, StatusEffectComponent>

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>

* Properly evaluate publish condition for master branch (#38556)

* HandsSystem Refactor (#38438)

* checkpoint

* pt 2

* pt... i forgot

* pt 4

* patch

* More test fixes

* optimization!!!

* the REAL hand system

* fix RetractableItemActionSystem.cs oversight

* the review

* test

* remove test usage of body prototype

* Update Content.IntegrationTests/Tests/Interaction/InteractionTest.cs

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

* hellcode

* hellcode 2

* Minor cleanup

* test

* Chasing the last of the bugs

* changes

---------

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

* Fix PowerCellDrawComponent draw rate (#38562)

* fix power cell draw rate

* comment

* Hand refactor bugfix (#38576)

* Fix hand refactor 2 (#38578)

* fix unwielding

* hand refactor bugfix 2

* Added a ConfirmableAction component to ActionRevertPolymorph (#38570)

* Added a ConfirmableAction component to ActionRevertPolymorph

* Update Resources/Locale/en-US/actions/actions/polymorph.ftl

---------

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

* Automatic changelog update

* Fixup Sericulture to be clonable (#38516)

* Add CloningEvent and an action entity prototype

* Remove redundant action prototype from Yaml

* Add a field that might be changed

* CR

* CR - guard statement and Dirty

* Convert DumpReagentGuideText command to LEC. (#38569)

* commit

* revert linq conversion

* fix: EyeOffset when eyes are closed (#38534)

* fix: EyeOffset when eyes are closed

* fix: Relay only blocked on eyes closed action

* cleanup: whitespace

* fix: missing cancel on PVS, dependencies

* remove: namespace import

* change: apply from review

* Apply suggestions from code review

---------

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

* Automatic changelog update

* Updated hat descriptions (#38156)

* Updated hat descriptions

* final touches

* typo fix

* final touchups

* Quote fixes

* Automatic changelog update

* Add /showaccessreaders to +MAPPING and +DEBUG (#37759)

feat: add /showaccessreaders to +MAPPING and +DEBUG

* ShowHealthBarsCommand to LEC. (#38588)

* mfw

* Update Content.Client/Commands/ShowHealthBarsCommand.cs

---------

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

* Forgor changelog for permission change (#38593)

* Automatic changelog update

* MapRenderer code fixes (#38357)

* Fix MapRenderer integration test usage to properly show output.

Added an ITestContextLike interface that can be used to properly run the integration test infrastructure OUTSIDE A TEST.

* Use System.Test.Json instead of Newtonsoft.Json for MapRenderer

* Fix map renderer JSON output being broken

I love not testing or even reading the surrounding code.

* Fix un-reusable integration instances getting leaked.

The pair state was always getting set to Ready even if the instance was killed, meaning it was getting put back into the pool even if killed.

* Mark map renderer integration instances as destructive to avoid memory leak.

* Fix file specification handling.

Map file specification is now backwards compatible again (loose filename match to search prototypes). It also supports proper direct OS filename arguments. The former is the fallback scenario is extremely important for the map server still.

Cleaned up the way that target map files are passed through the application, so mixed file/prototype specifications are now handled properly (which can be caused by the fallback behavior).

Fixes JSON data export to use the proper user-facing map name. This only works if a prototype ID is specified *or* the legacy file behavior is used.

Restructured MapPainter into an instance that has multiple functions called on it, so not all data has to be passed through a single Paint() call.

Clean up the godawful map/grid detection code. Now we just load both in a single call, because yes you can do that. This relies on LogOrphanedGrids = false in the map loader options, which I think is fine for our purposes.

Improved error handling in much of the program.

* Fix duplicate map names in map renderer output

I'm not sure *what* this output is used for, but I'm sure having it duplicated per grid isn't intentional.

* Make maprenderer command line parsing bail on unknown - options

* Fix incorrect docs for --viewer maprenderer argument

It doesn't change directory layout

* Fix parallax layer specification to not use imgur as a fucking CDN

Files are now copied to a separate folder _parallax, and these files are referenced by the parallax configuration.

Parallax data is only output when instructed to via --parallax.

This will break parallax on current map server builds, but it should be graceful. Also, that's fucking good considering we shouldn't be using imgur links. Purge it.

* Fix incorrect assert in test pair clean return

* Restore other map viewer parallax layers, fix attribution.

* This isn't a valid copyright statement but the validator forces me to enter something here.

* Hide mechanism and show mechanism commands to LEC (#38587)

commit

* Fix multi handed items (#38603)

* Automatic changelog update

* fix smart equip (#38605)

* Automatic changelog update

* New Status Effects system: Relay events (#38579)

* Fixed Snacks Mispredicting on Clients (#38522)

* Well this works

* Removed merge conflict bait

* PredictedDel

* Apply suggestions from code review

---------

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

* Improve lathe queue performance (#38583)

* Use an actual Queue

* Store ProtoIds instead of prototypes

* Network as NetListAsArray

* Remove Serializable & NetSerializable from LatheRecipePrototype

* Convert CurrentlyProducing too

* No point using NetListAsArray<T> if you're going to .ToArray() it anyways.

---------

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

* Quartermaster's PDA has AstroNav preinstalled (#38445)

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

* Automatic changelog update

* Bladed flatcaps are minor contraband (#38597)

* Automatic changelog update

* Stop network serializing prototypes (#38602)

* Stop network serializing prototypes

Send the damn proto ID instead.

* Fix sandbox violation

* Clean up all missing `EntitySystem` proxy method uses (#38353)

* Cleanup SharedRoleCodewordSystem (#38310)

* cleanup

* Update Content.Shared/Roles/RoleCodeword/SharedRoleCodewordSystem.cs

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

* Apply suggestions from code review

---------

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

* Toy/Plushie Inhands and Wearables (#38514)

* Automatic changelog update

* Thief Guidebook Refresh (#38586)

* Automatic changelog update

* RespiratorSystem Cleanup (#38572)

* Respirator Debodied

* Forgot about alerts (also respirator testa and events)

* Fix Urist eating air and not giving it back

* Stop nuke ops from taking in a breath then taking in a second breath causing them to get a headache from carbon dioxide poisoning and failing TryStopNukeOpsFromConstantlyFailing();

* Consts are smelly,

* Actually we don't need to raise the entity, just the component

* Don't forget to remove the unused code today, said me yesterday

* Remove all fallbacks

* Debody that too

---------

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

* Fix the sensor monitoring console forcing a GC every 3 seconds (#38146)

* Optimize sensor monitoring window graph drawing

* Add shared static Vector2 pool for all GraphView instances

* Address requested changes

* remove lock

* New Status Effects API Overload (#38617)

* I love API changes

* Update Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs

* fix

---------

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

* Fix silicons looking at their laws crashing the server (#38623)

* fix RCD ghosts not disappearing when changing hand to empty (#38622)

* Automatic changelog update

* Refactor SeeingRainbows to new status effect system (#38620)

* Make Review Requested label get applied to all opened PRs (#38625)

It previously only applied automatically to PRs that have reviews requested, which happens if the PR touches files owned by code owners. Now it applies to *all* opened PRs.

* Fix vocalization emotes (#38627)

* Update submodule to 264.0.0 (#38629)

* HoP's beret (#38601)

* added HoP beret

* added beret to loadout

* added craft to machine

* added beret to hop ddresser

* Updated the authorship

* Change on request

* added 4-spaced

* Hot fix of dresser

* patchfor broken NetworkConfiguratorLinkMenu (#38632)

fix

* Fix solutions flickering when transferring contents (#34838)

* Use Solution clones when applying SolutionComponent states

* Revert "Use Solution clones when applying SolutionComponent states"

This reverts commit 013fd111cf92b22562e00f98a7aaa49bc4b4ed62.

* Make Solution implement ICloneable and rename Clone method.

* Copy CanReact value when cloning a Solution

* Convert to IRobustCloneable

* Automatic changelog update

* Fix typos in guidebook: Buisness → Business (#38636)

* Fix typo in `MinorAntagonists.xml`

Buisness → Business

* Fix typo in `YourFirstCharacter.xml`

Buisness → Business

* AddBodyPartCommand localization. (#38612)

commit

* Automatic changelog update

* Various Headphones Fixes and Tweaks (#38479)

* sprites, wearables

* neck

* icon-on sprite

* Automatic changelog update

* Cleanup prototype instantiation in `DamageTest` (#38639)

Cleanup prototype instantiation in DamageTest

* Cleanup prototype instantiation in `ExplosionSystem` (#38642)

Cleanup prototype instantiation in ExplosionSystem

* Update Credits (#38646)

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

* fix ItemSlotsSystem debug assert (#38655)

* Automatic changelog update

* Allow the Command & Super door remotes to use the access of their user. (Re-creation of PR due to changes to game balance) (#35536)

* Automatic changelog update

* Added directional beacons (#38284)

* Added directional beacons

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

* Fixed names

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

---------

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

* Power stat and nuke codes commands get some LEC love. (#38585)

* commit

* requested changes.

* Tweaks to admin CSV exporting (#38531)

* Automatic changelog update

* Dsay Dirty and Follow commands converted to LEC and localized. (#38666)

* commit

* whoopwhoopwhoop

* Retro laser sprite fix (#38676)

* Fixed everything except the icon

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

* fixed icon

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

---------

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

* fix water coolers (#38681)

* Automatic changelog update

* Monochromacy typo fix (#38686)

* fixes the typo

* Fixed cloning looking for the trait, not the component, RE https://github.com/space-wizards/space-station-14/pull/38686#issuecomment-3025093504

* Apply suggestions from code review

---------

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

* Validate `CloningSettingsPrototype`s (#38688)

* Validate CloningSettingsPrototypes

* Update Content.IntegrationTests/Tests/Cloning/CloningSettingsPrototypeTest.cs

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

* Check EventComponents too

---------

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

* Add test of objective-related console commands (#36400)

* Add test of objective add/list/remove commands

* Not sure why we're validating test prototypes, but sure

* We don't need a map

* CC Genpop and other misc fixes (#37494)

* First commit: genpop and AI satellite

* Revert AI satellite, add representative locker.

* Automatic changelog update

* Improvements and fixups for New Status Effect API (#38660)

* ci: include pull request id in changelog link (#38504)

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

* Resolving Wizard casting recall on nuke disk making it impossible to disarm (#38661)

* Resolving Wizard Recall on Nuke disk making it impossible to disarm
- Adding a DisarmBomb case to nuke status update loop
- Changing a few methods and parameters to properly follow formatting standards
- Updating some names to follow camelCase

* Updating missed tag

* Reverting DataField change

Should prevent this preventative bugfix being a breaking change.

* Automatic changelog update

* Predict healing and bloodstream (#38690)

* initial commit

* reapply 38126

* fix rootable

* someone missed an important minus sign here

* try this

* fix

* fix

* reenable crit hits

* cleanup

* fix status time dirtying

* fix

* camelCase

* make telesci wreck easier (#37569)

rel

* Reduce most salvage mob health, reduce most salvage weapon damage. (#38131)

* Automatic changelog update

* Switch HSV to the default colorspace for character customization (#38434)

* Made HSV default for character editor

* Adds/fixes comments to HSV defaulting

* Added dropbox fix, potentially cursed

* Revert "Added dropbox fix, potentially cursed"

This reverts commit a709883366fbee813e839742125e70844672af29.

---------

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

* Automatic changelog update

* Centcomm update (#38697)

* Biome rework (#37735)

* DungeonData rework

Back to fields, serializes better, just make new layers dumby.

* wawawewa

* Fix this

* Fixes

* Port the work over

* wawawewa

* zoom

* Kinda workin

* Adjust wawa

* Unloading work

* Ore + entitytable fixes

Iterate every dungeon not just last.

* Big shot

* wawawewa

* Fixes

* true

* Fixes

# Conflicts:
#	Content.Server/Procedural/DungeonJob/DungeonJob.cs

* wawawewa

* Fixes

* Fix

* Lot of work

* wawawewa

* Fixing

* eh?

* a

* Fix a heap of stuff

* Better ignored check

* Reserve tile changes

* biome

* changes

* wawawewa

* Fixes & snow

* Shadow fixes

* wawawewa

* smol

* Add layer API

* More work

* wawawewa

* Preloads and running again

* wawawewa

* Modified

* Replacements and command

* Runtime support

* werk

* Fix expeds + dungeon alltiles

* reh

---------

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

* Fix: Don't deploy foldables when clicking on items inside containers (#38709)

* Fix

* Apply suggestions from code review

---------

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

* Fix world generation (#38713)

* Fix world generation

* Remove unused impoty

* Scurrets - Audio Improvements (#38482)

* Scurret audio tuning

* Add new sfx

* Update sneezing sfx

* YAML support

* Rename a folder

* Pressure Relief Valve (#36708)

* initial system (this math is probably WRONG)

* General code cleanup and OnExamined support
(holy moly this code sucks)

* UICode and related events foundation
TODO:
- Actually write the XAML UI and the underlying system
- Un-shitcode the entire thing
- Actually test everything...

* Working UI code
TODO: Make predicted, as this certainly isn't predicted. Even though I said it was. It isn't.

* Remove one TODO for unshitcoding the examine code

* Add reminder
yea

* Make predicted (defenitely isn't)
(also defenitely isn't a copypaste from pressure pump code)

* It's predicted!
TODO:
- Give it snazzy predicted visuals!
- Have a different field for pressure entry, lest it gets bulldozed every UI update.

* Improve gas pressure relief valve UI
TODO: Reminder to reduce amount of dirties using deltafields

* Implement DirtyField prediction

* Entity<T> cleanup
A lot of Entity<T> conversions and lukewarm cleanup.

Also got caught copy pasting code in 4K UHD but it's not like you couldn't tell.

* More cleanup and comments

* Remove TODO comment on bulldozing window title

* """refactoring"""
- Move appearance out of shared and finally fix it. Pointless to predict appearance in this instance.
- More Entity<T> conversions because I like them.
- Move UI creation handling over entirely to the ActivatableUI system.
- Fix a hardcoded locale string (why????).

* Add visuals

* Revert debugging variable replacememt
yea

* Revert skissue

* Remove unused using directives and remove TODO

* Localize, cleanup, document

* Fix adminlogging discrepancy

* Add ability to construct, add guidebook entry

* Clear up comment

* Add guidebook tooltip to valve

* Convert GasPressureReliefValveBoundUserInterface declaration into primary constructor

* Adds more input handling and adds autofill on open

* Un-deepfry input validator shitcode
Genuinely what was I smoking

* improve visuals logic

* Refactor again
- Update math to the correct implementation
- Moved code that could be re-used in the future into a helper method under AtmosphereSystem.Gases.cs

* I'm sorry but I hate warnings

* Remove unused using directive in AtmosphereSystem.Gases.cs

* Review and cleanup

* Lukewarm UI glossup

* Maintainer for the upstream project btw

* Remove redundant state sets and messy logic

* Unduplicate valve updater code

* Redo UI (im sorry Slarti)

* run tests

* Test refactored UI messaging

* Second round of UI improvements
- God please find a way to improve this system. Feels bad.

* Update loop implementation

* Further predict UI

* Clear up SetToCurrentThreshold

* cleanup

* Update to master + pipe layers and bug fixes
want to run tests

* fixes

* Deploy rename pipebomb

* Documentation and requested changes

* Rename the method that wiggled away

* Undo rounding changes

* Fix comment

* Rename and cleanup

* Apply suggestions from code review

---------

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

* Automatic changelog update

* Revert biome rework (#38724)

* Revert "Fix world generation (#38713)"

This reverts commit 10fa6ff4af.

* Revert "Biome rework (#37735)"

This reverts commit fe7b96147c.

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

* Automatic changelog update

* Fix TextLinkTag (#32203)

* Scurret PDA slot fix (#38777)

* Scurrets get emergency EVA suit access (#38778)

* Scurrets - spacing protection via adorable outfits

* ALPHABETIZATION!!

* Revert "HoP's beret (#38601)"

This reverts commit e9c90fe66d.

Please check the maintainer meeting for details

* Revert "Readds the Hypereutactic Blade for traitors, adds Hypereutatic blade for Nukies (#37182)"

This reverts commit c03afeb29c.

Please check the maintainer meeting for details

* Changelog removal

* Scurret naming fixes (#38776)

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

* Scurret displacement map fixes (#38775)

* sanitize MIDI parser (#38806)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>

* Add Breach of Permanent Confinement to Space Law (#38663)

* Initial commit

* Fix category in text

* ADAPTATION

---------

Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com>
Signed-off-by: Nox38 <nebulousnox38@gmail.com>
Signed-off-by: little-meow-meow <204685920+little-meow-meow@users.noreply.github.com>
Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Qerd <73325910+BigfootBravo@users.noreply.github.com>
Co-authored-by: Ps3Moira <113228053+ps3moira@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: crazybrain23 <44417085+crazybrain23@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Just-a-Unity-Dev <67359748+Just-a-Unity-Dev@users.noreply.github.com>
Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com>
Co-authored-by: TemporalOroboros <TemporalOroboros@gmail.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: qrwas <aleksandr.vernigora93@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Moomoobeef <62638182+Moomoobeef@users.noreply.github.com>
Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com>
Co-authored-by: Skybailey-dev <skybailey.01@gmail.com>
Co-authored-by: Cojoke <83733158+Cojoke-dot@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Kittygyat <202250949+Kittygyat@users.noreply.github.com>
Co-authored-by: John <50085876+AreYouConfused@users.noreply.github.com>
Co-authored-by: Dragonjspider <140023808+Dragonjspider@users.noreply.github.com>
Co-authored-by: beesterman <72782957+beesterman@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: qwerltaz <69696513+qwerltaz@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: rumaks <me@rumaks.xyz>
Co-authored-by: poklj <compgeek223@gmail.com>
Co-authored-by: Łukasz Mędrek <lukasz@lukaszm.xyz>
Co-authored-by: Hitlinemoss <209321380+Hitlinemoss@users.noreply.github.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: Andrew Malcolm O'Neill <105134723+maland1@users.noreply.github.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: 96flo <163569033+96flo@users.noreply.github.com>
Co-authored-by: Łukasz Lindert <lukasz.lindert@protonmail.com>
Co-authored-by: Stefano Pigozzi <me@steffo.eu>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: Nox <nebulousnox38@gmail.com>
Co-authored-by: Kowlin <10947836+Kowlin@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: little-meow-meow <204685920+little-meow-meow@users.noreply.github.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: Mora <46364955+TrixxedHeart@users.noreply.github.com>
Co-authored-by: TrixxedHeart <46364955+TrixxedBit@users.noreply.github.com>
Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>
Co-authored-by: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
This commit is contained in:
Red
2025-07-08 11:59:20 +03:00
committed by GitHub
parent 6fc57804cd
commit a35c718734
935 changed files with 19977 additions and 9423 deletions

View File

@@ -27,7 +27,7 @@ jobs:
run: dotnet restore
- name: Build Project
run: dotnet build --no-restore /p:WarningsAsErrors=nullable
run: dotnet build --no-restore
- name: Build DocFX
uses: nikeee/docfx-action@v1.0.0

View File

@@ -42,7 +42,7 @@ jobs:
run: dotnet restore
- name: Build Project
run: dotnet build Content.MapRenderer --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
run: dotnet build Content.MapRenderer --configuration Release --no-restore /m
- name: Run Map Renderer
run: dotnet run --project Content.MapRenderer Dev

View File

@@ -42,7 +42,7 @@ jobs:
run: dotnet restore
- name: Build Project
run: dotnet build --configuration DebugOpt --no-restore /p:WarningsAsErrors=nullable /m
run: dotnet build --configuration DebugOpt --no-restore /m
- name: Run Content.Tests
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0

View File

@@ -2,7 +2,7 @@
on:
pull_request_target:
types: [review_requested]
types: [review_requested, opened]
jobs:
add_label:

View File

@@ -14,6 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fail if we are attempting to run on the master branch
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
run: exit 1
- name: Install dependencies
run: sudo apt-get install -y python3-paramiko python3-lxml

View File

@@ -27,7 +27,7 @@ If you believe someone is violating the code of conduct, we ask that you report
Original text courtesy of the [Speak Up! project](http://web.archive.org/web/20141109123859/http://speakup.io/coc.html).
## On Comunity Moderation
## On Community Moderation
Deviating from the Code of Conduct on the Github repository may result in moderative actions taken by project Maintainers. This can involve your content being edited or deleted, and may result in a temporary or permanent block from the repository.

View File

@@ -211,7 +211,7 @@ namespace Content.Client.Actions
else
{
var request = new RequestPerformActionEvent(GetNetEntity(action));
EntityManager.RaisePredictiveEvent(request);
RaisePredictiveEvent(request);
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Frozen;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Systems;
@@ -24,6 +25,7 @@ internal sealed class AdminNameOverlay : Overlay
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly IPrototypeManager _prototypeManager;
private readonly Font _font;
private readonly Font _fontBold;
private AdminOverlayAntagFormat _overlayFormat;
@@ -36,8 +38,9 @@ internal sealed class AdminNameOverlay : Overlay
private float _overlayMergeDistance;
//TODO make this adjustable via GUI?
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private static readonly FrozenSet<ProtoId<RoleTypePrototype>> Filter =
new ProtoId<RoleTypePrototype>[] {"SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"}
.ToFrozenSet();
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
@@ -49,7 +52,8 @@ internal sealed class AdminNameOverlay : Overlay
EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config,
SharedRoleSystem roles)
SharedRoleSystem roles,
IPrototypeManager prototypeManager)
{
_system = system;
_entityManager = entityManager;
@@ -57,6 +61,7 @@ internal sealed class AdminNameOverlay : Overlay
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
_roles = roles;
_prototypeManager = prototypeManager;
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
@@ -125,6 +130,14 @@ internal sealed class AdminNameOverlay : Overlay
foreach (var info in sortable.OrderBy(s => s.Item4.Y).ToList())
{
var playerInfo = info.Item1;
var rolePrototype = playerInfo.RoleProto == null
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
var aabb = info.Item2;
var entity = info.Item3;
var screenCoordinatesCenter = info.Item4;
@@ -209,7 +222,7 @@ internal sealed class AdminNameOverlay : Overlay
switch (_overlaySymbolStyle)
{
case AdminOverlayAntagSymbolStyle.Specific:
symbol = playerInfo.RoleProto.Symbol;
symbol = roleSymbol;
break;
case AdminOverlayAntagSymbolStyle.Basic:
symbol = Loc.GetString("player-tab-antag-prefix");
@@ -225,17 +238,17 @@ internal sealed class AdminNameOverlay : Overlay
switch (_overlayFormat)
{
case AdminOverlayAntagFormat.Roletype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? Loc.GetString(playerInfo.RoleProto.Name).ToUpper()
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? roleName.ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(playerInfo.RoleProto.Name, playerInfo.Subtype).ToUpper()
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(roleName, playerInfo.Subtype).ToUpper()
: string.Empty;
break;
default:
@@ -258,4 +271,12 @@ internal sealed class AdminNameOverlay : Overlay
drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
}
}
private static bool IsFiltered(ProtoId<RoleTypePrototype>? roleProtoId)
{
if (roleProtoId == null)
return false;
return Filter.Contains(roleProtoId.Value);
}
}

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.Systems
{
@@ -17,6 +18,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@@ -33,7 +35,8 @@ namespace Content.Client.Administration.Systems
_entityLookup,
_userInterfaceManager,
_configurationManager,
_roles);
_roles,
_proto);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}

View File

@@ -1,7 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc ban-panel-title}" MinSize="350 500">
Title="{Loc ban-panel-title}" MinSize="410 500">
<BoxContainer Orientation="Vertical">
<TabContainer Name="Tabs" VerticalExpand="True">
<!-- Basic info -->

View File

@@ -1,12 +1,14 @@
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -31,14 +33,21 @@ public sealed partial class BanPanel : DefaultWindow
private uint Multiplier { get; set; }
private bool HasBanFlag { get; set; }
private TimeSpan? ButtonResetOn { get; set; }
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
private readonly List<CheckBox> _roleCheckboxes = new();
// Role group name -> the role buttons themselves.
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
private readonly ISawmill _banpanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
private const string ExpandedArrow = "▼";
private const string ContractedArrow = "▶";
private enum TabNumbers
{
@@ -144,47 +153,90 @@ public sealed partial class BanPanel : DefaultWindow
ReasonTextEdit.Placeholder = new Rope.Leaf(Loc.GetString("ban-panel-reason"));
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach (var proto in prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
var departmentJobs = _protoMan.EnumeratePrototypes<DepartmentPrototype>()
.OrderBy(x => x.Weight);
foreach (var proto in departmentJobs)
{
CreateRoleGroup(proto.ID, proto.Roles.Select(p => p.Id), proto.Color);
var roles = proto.Roles.Select(x => _protoMan.Index(x))
.OrderBy(x => x.ID);
CreateRoleGroup(proto.ID, proto.Color, roles);
}
CreateRoleGroup("Antagonist", prototypeManager.EnumeratePrototypes<AntagPrototype>().Select(p => p.ID), Color.Red);
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
.OrderBy(x => x.ID);
CreateRoleGroup("Antagonist", Color.Red, antagRoles);
}
private void CreateRoleGroup(string roleName, IEnumerable<string> roleList, Color color)
/// <summary>
/// Creates a "Role group" which stores information and logic for one "group" of roll bans.
/// For example, all antags are one group, logi is a group, medical is a group, etc...
/// </summary>
private void CreateRoleGroup<T>(string groupName, Color color, IEnumerable<T> roles) where T : class, IPrototype
{
var outerContainer = new BoxContainer
{
Name = $"{roleName}GroupOuterBox",
Name = $"{groupName}GroupOuterBox",
HorizontalExpand = true,
VerticalExpand = true,
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(4)
Margin = new Thickness(4),
};
var departmentCheckbox = new CheckBox
// Stores stuff like ban all and expand buttons.
var roleGroupHeader = new BoxContainer
{
Name = $"{roleName}GroupCheckbox",
Text = roleName,
Modulate = color,
HorizontalAlignment = HAlignment.Left
Orientation = BoxContainer.LayoutOrientation.Horizontal,
};
outerContainer.AddChild(departmentCheckbox);
var innerContainer = new BoxContainer
// Stores the role checkboxes themselves.
var innerContainer = new GridContainer
{
Name = $"{roleName}GroupInnerBox",
Name = $"{groupName}GroupInnerBox",
HorizontalExpand = true,
Orientation = BoxContainer.LayoutOrientation.Horizontal
Columns = 2,
Visible = false,
Margin = new Thickness(15, 5, 0, 5),
};
departmentCheckbox.OnToggled += args =>
var roleGroupCheckbox = CreateRoleGroupHeader(groupName, roleGroupHeader, color, innerContainer);
outerContainer.AddChild(roleGroupHeader);
// Add the roles themselves
foreach (var role in roles)
{
foreach (var child in innerContainer.Children)
AddRoleCheckbox(groupName, role.ID, innerContainer, roleGroupCheckbox);
}
outerContainer.AddChild(innerContainer);
RolesContainer.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat
{
if (child is CheckBox c)
{
c.Pressed = args.Pressed;
}
BackgroundColor = color
}
});
RolesContainer.AddChild(outerContainer);
RolesContainer.AddChild(new HSeparator());
}
private Button CreateRoleGroupHeader(string groupName, BoxContainer header, Color color, GridContainer innerContainer)
{
var roleGroupCheckbox = new Button
{
Name = $"{groupName}GroupCheckbox",
Text = "Ban all",
Margin = new Thickness(0, 0, 5, 0),
ToggleMode = true,
};
// When this is toggled, toggle all buttons in this group so they match.
roleGroupCheckbox.OnToggled += args =>
{
foreach (var role in _roleCheckboxes[groupName])
{
role.Pressed = args.Pressed;
}
if (args.Pressed)
@@ -199,15 +251,12 @@ public sealed partial class BanPanel : DefaultWindow
}
else
{
foreach (var childContainer in RolesContainer.Children)
foreach (var roleButtons in _roleCheckboxes.Values)
{
if (childContainer is Container)
foreach (var button in roleButtons)
{
foreach (var child in childContainer.Children)
{
if (child is CheckBox { Pressed: true })
return;
}
if (button.Pressed)
return;
}
}
@@ -220,38 +269,72 @@ public sealed partial class BanPanel : DefaultWindow
SeverityOption.SelectId((int) newSeverity);
}
};
outerContainer.AddChild(innerContainer);
foreach (var role in roleList)
var hideButton = new Button
{
AddRoleCheckbox(role, innerContainer, departmentCheckbox);
}
RolesContainer.AddChild(new PanelContainer
Text = Loc.GetString("role-bans-expand-roles") + " " + ContractedArrow,
ToggleMode = true,
};
hideButton.OnPressed += args =>
{
PanelOverride = new StyleBoxFlat
{
BackgroundColor = color
}
innerContainer.Visible = args.Button.Pressed;
((Button)args.Button).Text = args.Button.Pressed
? Loc.GetString("role-bans-contract-roles") + " " + ExpandedArrow
: Loc.GetString("role-bans-expand-roles") + " " + ContractedArrow;
};
header.AddChild(new Label
{
Text = groupName,
Modulate = color,
Margin = new Thickness(0, 0, 5, 0),
});
RolesContainer.AddChild(outerContainer);
RolesContainer.AddChild(new HSeparator());
header.AddChild(roleGroupCheckbox);
header.AddChild(hideButton);
return roleGroupCheckbox;
}
private void AddRoleCheckbox(string role, Control container, CheckBox header)
/// <summary>
/// Adds a checkbutton specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
/// </summary>
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
{
var roleCheckbox = new CheckBox
var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button
{
Name = $"{role}RoleCheckbox",
Text = role
Text = role,
ToggleMode = true,
};
roleCheckbox.OnToggled += args =>
roleCheckButton.OnToggled += args =>
{
if (args is { Pressed: true, Button.Parent: { } } && args.Button.Parent.Children.Where(e => e is CheckBox).All(e => ((CheckBox) e).Pressed))
header.Pressed = args.Pressed;
// Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
roleGroupCheckbox.Pressed = args.Pressed;
else
header.Pressed = false;
roleGroupCheckbox.Pressed = false;
};
container.AddChild(roleCheckbox);
_roleCheckboxes.Add(roleCheckbox);
// This is adding the icon before the role name
// Yeah, this is sus, but having to split the functions up and stuff is worse imo.
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
Texture = _entMan.System<SpriteSystem>().Frame0(iconProto.Icon),
TextureScale = new Vector2(2.5f, 2.5f),
Stretch = TextureRect.StretchMode.KeepCentered,
Margin = new Thickness(5, 0, 0, 0),
};
roleCheckboxContainer.AddChild(jobIconTexture);
}
roleCheckboxContainer.AddChild(roleCheckButton);
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
_roleCheckboxes[group].Add(roleCheckButton);
}
public void UpdateBanFlag(bool newFlag)
@@ -469,7 +552,13 @@ public sealed partial class BanPanel : DefaultWindow
if (_roleCheckboxes.Count == 0)
throw new DebugAssertException("RoleCheckboxes was empty");
rolesList.AddRange(_roleCheckboxes.Where(c => c is { Pressed: true, Text: { } }).Select(c => c.Text!));
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
{
if (button is { Pressed: true, Text: not null })
{
rolesList.Add(button.Text);
}
}
if (rolesList.Count == 0)
{

View File

@@ -49,6 +49,7 @@
<Control HorizontalExpand="True"/>
<Label Name="Count" Access="Public"/>
<Control HorizontalExpand="True"/>
<Button Name="ExportLogs" Access="Public" Text="{Loc admin-logs-export}"/>
<Button Name="PopOutButton" Access="Public" Text="{Loc admin-logs-pop-out}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">

View File

@@ -1,4 +1,6 @@
using System.Linq;
using System.IO;
using System.Linq;
using Content.Client.Administration.UI.CustomControls;
using Content.Client.Eui;
using Content.Shared.Administration.Logs;
using Content.Shared.Eui;
@@ -15,6 +17,16 @@ public sealed class AdminLogsEui : BaseEui
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IFileDialogManager _dialogManager = default!;
[Dependency] private readonly ILogManager _log = default!;
private const char CsvSeparator = ',';
private const string CsvQuote = "\"";
private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message";
private ISawmill _sawmill;
private bool _currentlyExportingLogs = false;
public AdminLogsEui()
{
@@ -26,6 +38,9 @@ public sealed class AdminLogsEui : BaseEui
LogsControl.RefreshButton.OnPressed += _ => RequestLogs();
LogsControl.NextButton.OnPressed += _ => NextLogs();
LogsControl.PopOutButton.OnPressed += _ => PopOut();
LogsControl.ExportLogs.OnPressed += _ => ExportLogs();
_sawmill = _log.GetSawmill("admin.logs.ui");
}
private WindowRoot? Root { get; set; }
@@ -74,6 +89,71 @@ public sealed class AdminLogsEui : BaseEui
SendMessage(request);
}
private async void ExportLogs()
{
if (_currentlyExportingLogs)
return;
_currentlyExportingLogs = true;
LogsControl.ExportLogs.Disabled = true;
var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("csv")));
if (file == null)
return;
try
{
// Buffer is set to 4KB for performance reasons. As the average export of 1000 logs is ~200KB
await using var writer = new StreamWriter(file.Value.fileStream, bufferSize: 4096);
await writer.WriteLineAsync(CsvHeader);
foreach (var child in LogsControl.LogsContainer.Children)
{
if (child is not AdminLogLabel logLabel || !child.Visible)
continue;
var log = logLabel.Log;
// Date
// I swear to god if someone adds ,s or "s to the other fields...
await writer.WriteAsync(log.Date.ToString("s", System.Globalization.CultureInfo.InvariantCulture));
await writer.WriteAsync(CsvSeparator);
// ID
await writer.WriteAsync(log.Id.ToString());
await writer.WriteAsync(CsvSeparator);
// PlayerID
var players = log.Players;
for (var i = 0; i < players.Length; i++)
{
await writer.WriteAsync(players[i] + (i == players.Length - 1 ? "" : " "));
}
await writer.WriteAsync(CsvSeparator);
// Severity
await writer.WriteAsync(log.Impact.ToString());
await writer.WriteAsync(CsvSeparator);
// Type
await writer.WriteAsync(log.Type.ToString());
await writer.WriteAsync(CsvSeparator);
// Message
await writer.WriteAsync(CsvQuote);
await writer.WriteAsync(log.Message.Replace(CsvQuote, CsvQuote + CsvQuote));
await writer.WriteAsync(CsvQuote);
await writer.WriteLineAsync();
}
}
catch (Exception exc)
{
_sawmill.Error($"Error when exporting admin log:\n{exc.StackTrace}");
}
finally
{
await file.Value.fileStream.DisposeAsync();
_currentlyExportingLogs = false;
LogsControl.ExportLogs.Disabled = false;
}
}
private void PopOut()
{
if (LogsWindow == null)

View File

@@ -132,8 +132,8 @@ public sealed partial class ObjectsTab : Control
entry.OnTeleport += TeleportTo;
entry.OnDelete += Delete;
button.ToolTip = $"{info.Name}, {info.Entity}";
button.AddChild(entry);
button.StyleClasses.Clear();
}
private bool DataFilterCondition(string filter, ListData listData)

View File

@@ -172,6 +172,7 @@ public sealed partial class PlayerTab : Control
_playerTabSymbolSetting);
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
button.StyleClasses.Clear();
}
/// <summary>

View File

@@ -1,9 +1,11 @@
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
@@ -11,6 +13,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public sealed partial class PlayerTabEntry : PanelContainer
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public PlayerTabEntry(
PlayerInfo player,
@@ -23,6 +26,8 @@ public sealed partial class PlayerTabEntry : PanelContainer
RobustXamlLoader.Load(this);
var roles = _entMan.System<SharedRoleSystem>();
var rolePrototype = player.RoleProto == null ? null : _prototype.Index(player.RoleProto.Value);
UsernameLabel.Text = player.Username;
if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
@@ -57,19 +62,19 @@ public sealed partial class PlayerTabEntry : PanelContainer
break;
default:
case AdminPlayerTabSymbolOption.Specific:
symbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
symbol = player.Antag ? rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol : string.Empty;
break;
}
CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName));
if (player.Antag && colorAntags)
CharacterLabel.FontColorOverride = player.RoleProto.Color;
CharacterLabel.FontColorOverride = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
var roletype = RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
var subtype = roles.GetRoleSubtypeLabel(player.RoleProto.Name, player.Subtype);
var roletype = RoleTypeLabel.Text = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var subtype = roles.GetRoleSubtypeLabel(rolePrototype?.Name ?? RoleTypePrototype.FallbackName, player.Subtype);
switch (roleSetting)
{
case AdminPlayerTabRoleTypeOption.RoleTypeSubtype:
@@ -92,7 +97,7 @@ public sealed partial class PlayerTabEntry : PanelContainer
}
if (colorRoles)
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
RoleTypeLabel.FontColorOverride = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
}

View File

@@ -0,0 +1,90 @@
using System.Numerics;
using Content.Shared.Alert.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
namespace Content.Client.Alerts;
/// <summary>
/// This handles <see cref="GenericCounterAlertComponent"/>
/// </summary>
public sealed class GenericCounterAlertSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<GenericCounterAlertComponent, UpdateAlertSpriteEvent>(OnUpdateAlertSprite);
}
private void OnUpdateAlertSprite(Entity<GenericCounterAlertComponent> ent, ref UpdateAlertSpriteEvent args)
{
var sprite = args.SpriteViewEnt.Comp;
var ev = new GetGenericAlertCounterAmountEvent(args.Alert);
RaiseLocalEvent(args.ViewerEnt, ref ev);
if (!ev.Handled)
return;
// It cannot be null if its handled, but good to check to avoid ugly null ignores.
if (ev.Amount == null)
return;
// How many digits can we display
var maxDigitCount = GetMaxDigitCount((ent, ent, sprite));
// Clamp it to a positive number that we can actually display in full (no rollover to 0)
var amount = (int) Math.Clamp(ev.Amount.Value, 0, Math.Pow(10, maxDigitCount) - 1);
// This is super wack but ig it works?
var digitCount = ent.Comp.HideLeadingZeroes
? amount.ToString().Length
: maxDigitCount;
if (ent.Comp.HideLeadingZeroes)
{
for (var i = 0; i < ent.Comp.DigitKeys.Count; i++)
{
if (!_sprite.LayerMapTryGet(ent.Owner, ent.Comp.DigitKeys[i], out var layer, false))
continue;
_sprite.LayerSetVisible(ent.Owner, layer, i <= digitCount - 1);
}
}
// ReSharper disable once PossibleLossOfFraction
var baseOffset = (ent.Comp.AlertSize.X - digitCount * ent.Comp.GlyphWidth) / 2 * (1f / EyeManager.PixelsPerMeter);
for (var i = 0; i < ent.Comp.DigitKeys.Count; i++)
{
if (!_sprite.LayerMapTryGet(ent.Owner, ent.Comp.DigitKeys[i], out var layer, false))
continue;
var result = amount / (int) Math.Pow(10, i) % 10;
_sprite.LayerSetRsiState(ent.Owner, layer, result.ToString());
if (ent.Comp.CenterGlyph)
{
var offset = baseOffset + (digitCount - 1 - i) * ent.Comp.GlyphWidth * (1f / EyeManager.PixelsPerMeter);
_sprite.LayerSetOffset(ent.Owner, layer, new Vector2(offset, 0));
}
}
}
/// <summary>
/// Gets the number of digits that we can display.
/// </summary>
/// <returns>The number of digits.</returns>
private int GetMaxDigitCount(Entity<GenericCounterAlertComponent, SpriteComponent> ent)
{
for (var i = ent.Comp1.DigitKeys.Count - 1; i >= 0; i--)
{
if (_sprite.LayerExists((ent.Owner, ent.Comp2), ent.Comp1.DigitKeys[i]))
return i + 1;
}
return 0;
}
}

View File

@@ -11,11 +11,14 @@ public record struct UpdateAlertSpriteEvent
{
public Entity<SpriteComponent> SpriteViewEnt;
public EntityUid ViewerEnt;
public AlertPrototype Alert;
public UpdateAlertSpriteEvent(Entity<SpriteComponent> spriteViewEnt, AlertPrototype alert)
public UpdateAlertSpriteEvent(Entity<SpriteComponent> spriteViewEnt, EntityUid viewerEnt, AlertPrototype alert)
{
SpriteViewEnt = spriteViewEnt;
ViewerEnt = viewerEnt;
Alert = alert;
}
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping.Binary.Components;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// Represents the client system responsible for managing and updating the gas pressure regulator interface.
/// Inherits from the shared system <see cref="SharedGasPressureRegulatorSystem"/>.
/// </summary>
public sealed partial class GasPressureRegulatorSystem : SharedGasPressureRegulatorSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasPressureRegulatorComponent, AfterAutoHandleStateEvent>(OnValveUpdate);
}
private void OnValveUpdate(Entity<GasPressureRegulatorComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
protected override void UpdateUi(Entity<GasPressureRegulatorComponent> ent)
{
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, GasPressureRegulatorUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -0,0 +1,58 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Localizations;
using Robust.Client.UserInterface;
namespace Content.Client.Atmos.UI;
public sealed class GasPressureRegulatorBoundUserInterface(EntityUid owner, Enum uiKey)
: BoundUserInterface(owner, uiKey)
{
private GasPressureRegulatorWindow? _window;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<GasPressureRegulatorWindow>();
_window.SetEntity(Owner);
_window.ThresholdPressureChanged += OnThresholdChanged;
if (EntMan.TryGetComponent(Owner, out GasPressureRegulatorComponent? comp))
_window.SetThresholdPressureInput(comp.Threshold);
Update();
}
public override void Update()
{
if (_window == null)
return;
_window.Title = Identity.Name(Owner, EntMan);
if (!EntMan.TryGetComponent(Owner, out GasPressureRegulatorComponent? comp))
return;
_window.SetThresholdPressureLabel(comp.Threshold);
_window.UpdateInfo(comp.InletPressure, comp.OutletPressure, comp.FlowRate);
}
private void OnThresholdChanged(string newThreshold)
{
var sentThreshold = 0f;
if (UserInputParser.TryFloat(newThreshold, out var parsedNewThreshold) && parsedNewThreshold >= 0 &&
!float.IsInfinity(parsedNewThreshold))
{
sentThreshold = parsedNewThreshold;
}
// Autofill to zero if the user inputs an invalid value.
_window?.SetThresholdPressureInput(sentThreshold);
SendPredictedMessage(new GasPressureRegulatorChangeThresholdMessage(sentThreshold));
}
}

View File

@@ -0,0 +1,96 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="345 380"
MinSize="345 380"
Title="{Loc gas-pressure-regulator-ui-title}"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="0 10 0 10">
<BoxContainer Orientation="Vertical" Align="Center">
<Label Text="{Loc gas-pressure-regulator-ui-outlet}" Align="Center" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<Label Name="OutletPressureLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Align="Center">
<BoxContainer Orientation="Vertical" Align="Center" HorizontalExpand="True">
<Label Text="{Loc gas-pressure-regulator-ui-target}" Align="Right" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right">
<Label Name="TargetPressureLabel" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
<ProgressBar Name="ToTargetBar" MaxValue="1" SetSize="5 75" Margin="10" Vertical="True" />
<SpriteView Name="EntityView" SetSize="64 64" Scale="3 3" OverrideDirection="North" Margin="0" />
<ProgressBar Name="FlowRateBar" MaxValue="1" SetSize="5 75" Margin="10" Vertical="True" />
<BoxContainer Orientation="Vertical" Align="Center" HorizontalExpand="True">
<Label Text="{Loc gas-pressure-regulator-ui-flow}" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal">
<Label Name="CurrentFlowLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-flow-rate-unit}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Align="Center" Margin="1">
<Label Text="{Loc gas-pressure-regulator-ui-inlet}" Align="Center" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<Label Name="InletPressureLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Controls to Set Pressure -->
<controls:StripeBack Name="SetPressureStripeBack" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="10 10 10 10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<LineEdit Name="ThresholdInput" HorizontalExpand="True" MinSize="70 0" />
<Button Name="SetThresholdButton" Text="{Loc gas-pressure-regulator-ui-set-threshold}"
Disabled="True" Margin="5 0 0 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Button Name="Subtract1000Button" Text="{Loc gas-pressure-regulator-ui-subtract-1000}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Subtract100Button" Text="{Loc gas-pressure-regulator-ui-subtract-100}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Subtract10Button" Text="{Loc gas-pressure-regulator-ui-subtract-10}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add10Button" Text="{Loc gas-pressure-regulator-ui-add-10}" HorizontalExpand="True"
Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add100Button" Text="{Loc gas-pressure-regulator-ui-add-100}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add1000Button" Text="{Loc gas-pressure-regulator-ui-add-1000}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Button Name="ZeroThresholdButton" Text="{Loc gas-pressure-regulator-ui-zero-threshold}"
HorizontalExpand="True" Margin="0 0 5 0" />
<Button Name="SetToCurrentPressureButton"
Text="{Loc gas-pressure-regulator-ui-set-to-current-pressure}" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,129 @@
using System.Globalization;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Atmos.UI;
/// <summary>
/// Client-side UI for controlling a pressure regulator.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class GasPressureRegulatorWindow : FancyWindow
{
private float _flowRate;
public GasPressureRegulatorWindow()
{
RobustXamlLoader.Load(this);
ThresholdInput.OnTextChanged += _ => SetThresholdButton.Disabled = false;
SetThresholdButton.OnPressed += _ =>
{
ThresholdPressureChanged?.Invoke(ThresholdInput.Text ??= "");
SetThresholdButton.Disabled = true;
};
SetToCurrentPressureButton.OnPressed += _ =>
{
if (InletPressureLabel.Text != null)
{
ThresholdInput.Text = InletPressureLabel.Text;
}
SetThresholdButton.Disabled = false;
};
ZeroThresholdButton.OnPressed += _ =>
{
ThresholdInput.Text = "0";
SetThresholdButton.Disabled = false;
};
Add1000Button.OnPressed += _ => AdjustThreshold(1000);
Add100Button.OnPressed += _ => AdjustThreshold(100);
Add10Button.OnPressed += _ => AdjustThreshold(10);
Subtract10Button.OnPressed += _ => AdjustThreshold(-10);
Subtract100Button.OnPressed += _ => AdjustThreshold(-100);
Subtract1000Button.OnPressed += _ => AdjustThreshold(-1000);
return;
void AdjustThreshold(float adjustment)
{
if (float.TryParse(ThresholdInput.Text, out var currentValue))
{
ThresholdInput.Text = (currentValue + adjustment).ToString(CultureInfo.CurrentCulture);
SetThresholdButton.Disabled = false;
}
}
}
public event Action<string>? ThresholdPressureChanged;
/// <summary>
/// Sets the current threshold pressure label. This is not setting the threshold input box.
/// </summary>
/// <param name="threshold"> Threshold to set.</param>
public void SetThresholdPressureLabel(float threshold)
{
TargetPressureLabel.Text = threshold.ToString(CultureInfo.CurrentCulture);
}
/// <summary>
/// Sets the threshold pressure input field with the given value.
/// When the client opens the UI the field will be autofilled with the current threshold pressure.
/// </summary>
/// <param name="input">The threshold pressure value to autofill into the input field.</param>
public void SetThresholdPressureInput(float input)
{
ThresholdInput.Text = input.ToString(CultureInfo.CurrentCulture);
}
/// <summary>
/// Sets the entity to be visible in the UI.
/// </summary>
/// <param name="entity"></param>
public void SetEntity(EntityUid entity)
{
EntityView.SetEntity(entity);
}
/// <summary>
/// Updates the UI for the labels.
/// </summary>
/// <param name="inletPressure">The current pressure at the valve's inlet.</param>
/// <param name="outletPressure">The current pressure at the valve's outlet.</param>
/// <param name="flowRate">The current flow rate through the valve.</param>
public void UpdateInfo(float inletPressure, float outletPressure, float flowRate)
{
if (float.TryParse(TargetPressureLabel.Text, out var parsedfloat))
ToTargetBar.Value = inletPressure / parsedfloat;
InletPressureLabel.Text = float.Round(inletPressure).ToString(CultureInfo.CurrentCulture);
OutletPressureLabel.Text = float.Round(outletPressure).ToString(CultureInfo.CurrentCulture);
CurrentFlowLabel.Text = float.IsNaN(flowRate) ? "0" : flowRate.ToString(CultureInfo.CurrentCulture);
_flowRate = flowRate;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// Defines the flow rate at which the progress bar fills in one second.
// If the flow rate is >50 L/s, the bar will take <1 second to fill.
// If the flow rate is <50 L/s, the bar will take >1 second to fill.
const int barFillPerSecond = 50;
var maxValue = FlowRateBar.MaxValue;
// Increment the progress bar value based on elapsed time
FlowRateBar.Value += (_flowRate / barFillPerSecond) * args.DeltaSeconds;
// Reset the progress bar when it is fully filled
if (FlowRateBar.Value >= maxValue)
{
FlowRateBar.Value = 0f;
}
}
}

View File

@@ -168,7 +168,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
_targetTime = _gameTiming.CurTime + TimeSpan.FromSeconds(_cooldown);
var player = _playerManager.LocalEntity;
if (!EntityManager.TryGetComponent(player, out TransformComponent? xform))
if (!TryComp(player, out TransformComponent? xform))
{
ClearSounds();
return;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
public sealed class BloodstreamSystem : SharedBloodstreamSystem;

View File

@@ -5,32 +5,27 @@ using Robust.Shared.Containers;
namespace Content.Client.Commands;
public sealed class HideMechanismsCommand : LocalizedCommands
public sealed class HideMechanismsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
public override string Command => "hidemechanisms";
public override string Description => LocalizationManager.GetString($"cmd-{Command}-desc", ("showMechanismsCommand", ShowMechanismsCommand.CommandName));
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var containerSys = _entityManager.System<SharedContainerSystem>();
var spriteSys = _entityManager.System<SpriteSystem>();
var query = _entityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
spriteSys.SetContainerOccluded((uid, sprite), false);
_spriteSystem.SetContainerOccluded((uid, sprite), false);
var tempParent = uid;
while (containerSys.TryGetContainingContainer((tempParent, null, null), out var container))
while (_containerSystem.TryGetContainingContainer((tempParent, null, null), out var container))
{
if (!container.ShowContents)
{
spriteSys.SetContainerOccluded((uid, sprite), true);
_spriteSystem.SetContainerOccluded((uid, sprite), true);
break;
}

View File

@@ -1,57 +1,46 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.Overlays;
using Robust.Client.Player;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Client.Commands;
public sealed class ShowHealthBarsCommand : LocalizedCommands
public sealed class ShowHealthBarsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public override string Command => "showhealthbars";
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var player = _playerManager.LocalSession;
var player = shell.Player;
if (player == null)
{
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error-not-player"));
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
return;
}
var playerEntity = player?.AttachedEntity;
if (!playerEntity.HasValue)
if (player.AttachedEntity is not { } playerEntity)
{
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error-no-entity"));
shell.WriteError(Loc.GetString("shell-must-be-attached-to-entity"));
return;
}
if (!_entityManager.HasComponent<ShowHealthBarsComponent>(playerEntity))
if (!EntityManager.HasComponent<ShowHealthBarsComponent>(playerEntity))
{
var showHealthBarsComponent = new ShowHealthBarsComponent
{
DamageContainers = args.Select(arg => new ProtoId<DamageContainerPrototype>(arg)).ToList(),
HealthStatusIcon = null,
NetSyncEnabled = false
NetSyncEnabled = false,
};
_entityManager.AddComponent(playerEntity.Value, showHealthBarsComponent, true);
EntityManager.AddComponent(playerEntity, showHealthBarsComponent, true);
shell.WriteLine(LocalizationManager.GetString($"cmd-{Command}-notify-enabled", ("args", string.Join(", ", args))));
shell.WriteLine(Loc.GetString("cmd-showhealthbars-notify-enabled", ("args", string.Join(", ", args))));
return;
}
else
{
_entityManager.RemoveComponentDeferred<ShowHealthBarsComponent>(playerEntity.Value);
shell.WriteLine(LocalizationManager.GetString($"cmd-{Command}-notify-disabled"));
}
return;
EntityManager.RemoveComponentDeferred<ShowHealthBarsComponent>(playerEntity);
shell.WriteLine(Loc.GetString("cmd-showhealthbars-notify-disabled"));
}
}

View File

@@ -4,24 +4,19 @@ using Robust.Shared.Console;
namespace Content.Client.Commands;
public sealed class ShowMechanismsCommand : LocalizedCommands
public sealed class ShowMechanismsCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
public const string CommandName = "showmechanisms";
public override string Command => CommandName;
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override string Command => "showmechanisms";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var spriteSys = _entManager.System<SpriteSystem>();
var query = _entManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
var query = EntityManager.AllEntityQueryEnumerator<OrganComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out _, out var sprite))
{
spriteSys.SetContainerOccluded((uid, sprite), false);
_spriteSystem.SetContainerOccluded((uid, sprite), false);
}
}
}

View File

@@ -286,14 +286,14 @@ namespace Content.Client.Construction
if (!CheckConstructionConditions(prototype, loc, dir, user, showPopup: true))
return false;
ghost = EntityManager.SpawnEntity("constructionghost", loc);
var comp = EntityManager.GetComponent<ConstructionGhostComponent>(ghost.Value);
ghost = Spawn("constructionghost", loc);
var comp = Comp<ConstructionGhostComponent>(ghost.Value);
comp.Prototype = prototype;
comp.GhostId = ghost.GetHashCode();
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
Comp<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
_ghosts.Add(comp.GhostId, ghost.Value);
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
var sprite = Comp<SpriteComponent>(ghost.Value);
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
@@ -306,7 +306,7 @@ namespace Content.Client.Construction
else if (targetProto.Components.TryGetValue("Sprite", out _))
{
var dummy = EntityManager.SpawnEntity(targetProtoId, MapCoordinates.Nullspace);
var targetSprite = EntityManager.EnsureComponent<SpriteComponent>(dummy);
var targetSprite = EnsureComp<SpriteComponent>(dummy);
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
@@ -325,7 +325,7 @@ namespace Content.Client.Construction
_sprite.LayerSetVisible((ghost.Value, sprite), i, true);
}
EntityManager.DeleteEntity(dummy);
Del(dummy);
}
else
return false;
@@ -367,7 +367,7 @@ namespace Content.Client.Construction
{
foreach (var ghost in _ghosts)
{
if (EntityManager.GetComponent<TransformComponent>(ghost.Value).Coordinates.Equals(loc))
if (Comp<TransformComponent>(ghost.Value).Coordinates.Equals(loc))
return true;
}
@@ -384,7 +384,7 @@ namespace Content.Client.Construction
throw new ArgumentException($"Can't start construction for a ghost with no prototype. Ghost id: {ghostId}");
}
var transform = EntityManager.GetComponent<TransformComponent>(ghostId);
var transform = Comp<TransformComponent>(ghostId);
var msg = new TryStartStructureConstructionMessage(GetNetCoordinates(transform.Coordinates), ghostComp.Prototype.ID, transform.LocalRotation, ghostId.GetHashCode());
RaiseNetworkEvent(msg);
}
@@ -405,7 +405,7 @@ namespace Content.Client.Construction
if (!_ghosts.TryGetValue(ghostId, out var ghost))
return;
EntityManager.QueueDeleteEntity(ghost);
QueueDel(ghost);
_ghosts.Remove(ghostId);
}
@@ -416,7 +416,7 @@ namespace Content.Client.Construction
{
foreach (var ghost in _ghosts.Values)
{
EntityManager.QueueDeleteEntity(ghost);
QueueDel(ghost);
}
_ghosts.Clear();

View File

@@ -7,7 +7,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Client\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>nullable</WarningsAsErrors>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
<Platforms>AnyCPU</Platforms>

View File

@@ -1,5 +1,5 @@
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
@@ -15,6 +15,7 @@ public sealed class DrowsinessOverlay : Overlay
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedStatusEffectsSystem _statusEffects = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
@@ -29,6 +30,9 @@ public sealed class DrowsinessOverlay : Overlay
public DrowsinessOverlay()
{
IoCManager.InjectDependencies(this);
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
_drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique();
}
@@ -39,17 +43,11 @@ public sealed class DrowsinessOverlay : Overlay
if (playerEntity == null)
return;
if (!_entityManager.HasComponent<DrowsinessComponent>(playerEntity)
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
if (!_statusEffects.TryGetEffectsEndTimeWithComp<DrowsinessStatusEffectComponent>(playerEntity, out var endTime))
return;
var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>();
if (!statusSys.TryGetTime(playerEntity.Value, SharedDrowsinessSystem.DrowsinessKey, out var time, status))
return;
var curTime = _timing.CurTime;
var timeLeft = (float)(time.Value.Item2 - curTime).TotalSeconds;
endTime ??= TimeSpan.MaxValue;
var timeLeft = (float)(endTime - _timing.CurTime).Value.TotalSeconds;
CurrentPower += 8f * (0.5f * timeLeft - CurrentPower) * args.DeltaSeconds / (timeLeft + 1);
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
@@ -9,6 +10,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
private DrowsinessOverlay _overlay = default!;
@@ -16,35 +18,44 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
{
base.Initialize();
SubscribeLocalEvent<DrowsinessComponent, ComponentInit>(OnDrowsinessInit);
SubscribeLocalEvent<DrowsinessComponent, ComponentShutdown>(OnDrowsinessShutdown);
SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectAppliedEvent>(OnDrowsinessApply);
SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectRemovedEvent>(OnDrowsinessShutdown);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerAttachedEvent>>(OnStatusEffectPlayerAttached);
SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerDetachedEvent>>(OnStatusEffectPlayerDetached);
_overlay = new();
}
private void OnPlayerAttached(EntityUid uid, DrowsinessComponent component, LocalPlayerAttachedEvent args)
private void OnDrowsinessApply(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
{
if (_player.LocalEntity == args.Target)
_overlayMan.AddOverlay(_overlay);
}
private void OnDrowsinessShutdown(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectRemovedEvent args)
{
if (_player.LocalEntity != args.Target)
return;
if (!_statusEffects.HasEffectComp<DrowsinessStatusEffectComponent>(_player.LocalEntity.Value))
{
_overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
}
private void OnStatusEffectPlayerAttached(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
{
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(EntityUid uid, DrowsinessComponent component, LocalPlayerDetachedEvent args)
private void OnStatusEffectPlayerDetached(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
{
_overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
if (_player.LocalEntity is null)
return;
private void OnDrowsinessInit(EntityUid uid, DrowsinessComponent component, ComponentInit args)
{
if (_player.LocalEntity == uid)
_overlayMan.AddOverlay(_overlay);
}
private void OnDrowsinessShutdown(EntityUid uid, DrowsinessComponent component, ComponentShutdown args)
{
if (_player.LocalEntity == uid)
if (!_statusEffects.HasEffectComp<DrowsinessStatusEffectComponent>(_player.LocalEntity.Value))
{
_overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay);

View File

@@ -1,4 +1,5 @@
using Content.Shared.Drugs;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
@@ -17,49 +18,47 @@ public sealed class DrugOverlaySystem : EntitySystem
private RainbowOverlay _overlay = default!;
public static string RainbowKey = "SeeingRainbows";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SeeingRainbowsComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SeeingRainbowsComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SeeingRainbowsStatusEffectComponent, StatusEffectAppliedEvent>(OnApplied);
SubscribeLocalEvent<SeeingRainbowsStatusEffectComponent, StatusEffectRemovedEvent>(OnRemoved);
SubscribeLocalEvent<SeeingRainbowsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SeeingRainbowsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<SeeingRainbowsStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerAttachedEvent>>(OnPlayerAttached);
SubscribeLocalEvent<SeeingRainbowsStatusEffectComponent, StatusEffectRelayedEvent<LocalPlayerDetachedEvent>>(OnPlayerDetached);
_overlay = new();
}
private void OnPlayerAttached(EntityUid uid, SeeingRainbowsComponent component, LocalPlayerAttachedEvent args)
private void OnRemoved(Entity<SeeingRainbowsStatusEffectComponent> ent, ref StatusEffectRemovedEvent args)
{
_overlayMan.AddOverlay(_overlay);
}
if (_player.LocalEntity != args.Target)
return;
private void OnPlayerDetached(EntityUid uid, SeeingRainbowsComponent component, LocalPlayerDetachedEvent args)
{
_overlay.Intoxication = 0;
_overlay.TimeTicker = 0;
_overlayMan.RemoveOverlay(_overlay);
}
private void OnInit(EntityUid uid, SeeingRainbowsComponent component, ComponentInit args)
private void OnApplied(Entity<SeeingRainbowsStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
{
if (_player.LocalEntity == uid)
{
_overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect
_overlayMan.AddOverlay(_overlay);
}
if (_player.LocalEntity != args.Target)
return;
_overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect
_overlayMan.AddOverlay(_overlay);
}
private void OnShutdown(EntityUid uid, SeeingRainbowsComponent component, ComponentShutdown args)
private void OnPlayerAttached(Entity<SeeingRainbowsStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
{
if (_player.LocalEntity == uid)
{
_overlay.Intoxication = 0;
_overlay.TimeTicker = 0;
_overlayMan.RemoveOverlay(_overlay);
}
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(Entity<SeeingRainbowsStatusEffectComponent> ent, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
{
_overlay.Intoxication = 0;
_overlay.TimeTicker = 0;
_overlayMan.RemoveOverlay(_overlay);
}
}

View File

@@ -1,6 +1,6 @@
using Content.Shared.CCVar;
using Content.Shared.Drugs;
using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
@@ -17,6 +17,8 @@ public sealed class RainbowOverlay : Overlay
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedStatusEffectsSystem _statusEffects = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
@@ -37,6 +39,8 @@ public sealed class RainbowOverlay : Overlay
{
IoCManager.InjectDependencies(this);
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
_rainbowShader = _prototypeManager.Index<ShaderPrototype>("Rainbow").InstanceUnique();
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
}
@@ -54,18 +58,13 @@ public sealed class RainbowOverlay : Overlay
if (playerEntity == null)
return;
if (!_entityManager.HasComponent<SeeingRainbowsComponent>(playerEntity)
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
if (!_statusEffects.TryGetEffectsEndTimeWithComp<SeeingRainbowsStatusEffectComponent>(playerEntity, out var endTime))
return;
var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>();
if (!statusSys.TryGetTime(playerEntity.Value, DrugOverlaySystem.RainbowKey, out var time, status))
return;
var timeLeft = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds;
endTime ??= TimeSpan.MaxValue;
var timeLeft = (float)(endTime - _timing.CurTime).Value.TotalSeconds;
TimeTicker += args.DeltaSeconds;
if (timeLeft - TimeTicker > timeLeft / 16f)
{
Intoxication += (timeLeft - Intoxication) * args.DeltaSeconds / 16f;

View File

@@ -110,7 +110,7 @@ namespace Content.Client.Examine
{
var entity = args.EntityUid;
if (!args.EntityUid.IsValid() || !EntityManager.EntityExists(entity))
if (!args.EntityUid.IsValid() || !Exists(entity))
{
return false;
}
@@ -225,7 +225,7 @@ namespace Content.Client.Examine
vBox.AddChild(hBox);
if (EntityManager.HasComponent<SpriteComponent>(target))
if (HasComp<SpriteComponent>(target))
{
var spriteView = new SpriteView
{

View File

@@ -61,7 +61,7 @@ public sealed partial class TriggerSystem
private void OnProximityInit(EntityUid uid, TriggerOnProximityComponent component, ComponentInit args)
{
EntityManager.EnsureComponent<AnimationPlayerComponent>(uid);
EnsureComp<AnimationPlayerComponent>(uid);
}
private void OnProxAppChange(EntityUid uid, TriggerOnProximityComponent component, ref AppearanceChangeEvent args)

View File

@@ -1,6 +1,5 @@
using Content.Shared.Flash;
using Content.Shared.Flash.Components;
using Content.Shared.StatusEffect;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;

View File

@@ -6,7 +6,7 @@ using Robust.Shared.Utility;
namespace Content.Client.Guidebook.Richtext;
[UsedImplicitly]
public sealed class KeyBindTag : IMarkupTag
public sealed class KeyBindTag : IMarkupTagHandler
{
[Dependency] private readonly IInputManager _inputManager = default!;

View File

@@ -9,7 +9,7 @@ namespace Content.Client.Guidebook.RichText;
/// In order to be accessed by this tag, the desired field/property must
/// be tagged with <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
/// </summary>
public sealed class ProtodataTag : IMarkupTag
public sealed class ProtodataTag : IMarkupTagHandler
{
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IEntityManager _entMan = default!;

View File

@@ -10,16 +10,14 @@ using Content.Client.UserInterface.ControlExtensions;
namespace Content.Client.Guidebook.RichText;
[UsedImplicitly]
public sealed class TextLinkTag : IMarkupTag
public sealed class TextLinkTag : IMarkupTagHandler
{
public static Color LinkColor => Color.CornflowerBlue;
public string Name => "textlink";
public Control? Control;
/// <inheritdoc/>
public bool TryGetControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? control)
{
if (!node.Value.TryGetString(out var text)
|| !node.Attributes.TryGetValue("link", out var linkParameter)
@@ -38,22 +36,21 @@ public sealed class TextLinkTag : IMarkupTag
label.OnMouseEntered += _ => label.FontColorOverride = Color.LightSkyBlue;
label.OnMouseExited += _ => label.FontColorOverride = Color.CornflowerBlue;
label.OnKeyBindDown += args => OnKeybindDown(args, link);
label.OnKeyBindDown += args => OnKeybindDown(args, link, label);
control = label;
Control = label;
return true;
}
private void OnKeybindDown(GUIBoundKeyEventArgs args, string link)
private void OnKeybindDown(GUIBoundKeyEventArgs args, string link, Control? control)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (Control == null)
if (control == null)
return;
if (Control.TryGetParentHandler<ILinkClickHandler>(out var handler))
if (control.TryGetParentHandler<ILinkClickHandler>(out var handler))
handler.HandleClick(link);
else
Logger.Warning("Warning! No valid ILinkClickHandler found.");

View File

@@ -16,7 +16,6 @@ using Robust.Client.UserInterface;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Hands.Systems
@@ -27,16 +26,13 @@ namespace Content.Client.Hands.Systems
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly StrippableSystem _stripSys = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
public event Action<string, HandLocation>? OnPlayerAddHand;
public event Action<string>? OnPlayerRemoveHand;
public event Action<string?>? OnPlayerSetActiveHand;
public event Action<HandsComponent>? OnPlayerHandsAdded;
public event Action<Entity<HandsComponent>>? OnPlayerHandsAdded;
public event Action? OnPlayerHandsRemoved;
public event Action<string, EntityUid>? OnPlayerItemAdded;
public event Action<string, EntityUid>? OnPlayerItemRemoved;
@@ -58,67 +54,28 @@ namespace Content.Client.Hands.Systems
}
#region StateHandling
private void HandleComponentState(EntityUid uid, HandsComponent component, ref ComponentHandleState args)
private void HandleComponentState(Entity<HandsComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not HandsComponentState state)
return;
var handsModified = component.Hands.Count != state.Hands.Count;
// we need to check that, even if we have the same amount, that the individual hands didn't change.
if (!handsModified)
var newHands = state.Hands.Keys.Except(ent.Comp.Hands.Keys); // hands that were added between states
var oldHands = ent.Comp.Hands.Keys.Except(state.Hands.Keys); // hands that were removed between states
foreach (var handId in oldHands)
{
foreach (var hand in component.Hands.Values)
{
if (state.Hands.Contains(hand))
continue;
handsModified = true;
break;
}
RemoveHand(ent.AsNullable(), handId);
}
var manager = EnsureComp<ContainerManagerComponent>(uid);
if (handsModified)
foreach (var handId in state.SortedHands.Intersect(newHands))
{
List<Hand> addedHands = new();
foreach (var hand in state.Hands)
{
if (component.Hands.ContainsKey(hand.Name))
continue;
var container = _containerSystem.EnsureContainer<ContainerSlot>(uid, hand.Name, manager);
var newHand = new Hand(hand.Name, hand.Location, container);
component.Hands.Add(hand.Name, newHand);
addedHands.Add(newHand);
}
foreach (var name in component.Hands.Keys)
{
if (!state.HandNames.Contains(name))
{
RemoveHand(uid, name, component);
}
}
component.SortedHands.Clear();
component.SortedHands.AddRange(state.HandNames);
var sorted = addedHands.OrderBy(hand => component.SortedHands.IndexOf(hand.Name));
foreach (var hand in sorted)
{
AddHand(uid, hand, component);
}
AddHand(ent.AsNullable(), handId, state.Hands[handId]);
}
ent.Comp.SortedHands = new (state.SortedHands);
_stripSys.UpdateUi(uid);
SetActiveHand(ent.AsNullable(), state.ActiveHandId);
if (component.ActiveHand == null && state.ActiveHand == null)
return; //edge case
if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name)
{
SetActiveHand(uid, component.Hands[state.ActiveHand!], component);
}
_stripSys.UpdateUi(ent);
}
#endregion
@@ -129,72 +86,77 @@ namespace Content.Client.Hands.Systems
return;
}
OnPlayerHandsAdded?.Invoke(hands);
OnPlayerHandsAdded?.Invoke(hands.Value);
}
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true)
public override void DoDrop(Entity<HandsComponent?> ent,
string handId,
bool doDropInteraction = true,
bool log = true)
{
base.DoDrop(uid, hand, doDropInteraction, hands, log);
base.DoDrop(ent, handId, doDropInteraction, log);
if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
if (TryGetHeldItem(ent, handId, out var held) && TryComp(held, out SpriteComponent? sprite))
sprite.RenderOrder = EntityManager.CurrentTick.Value;
}
public EntityUid? GetActiveHandEntity()
{
return TryGetPlayerHands(out var hands) ? hands.ActiveHandEntity : null;
return TryGetPlayerHands(out var hands) ? GetActiveItem(hands.Value.AsNullable()) : null;
}
/// <summary>
/// Get the hands component of the local player
/// </summary>
public bool TryGetPlayerHands([NotNullWhen(true)] out HandsComponent? hands)
public bool TryGetPlayerHands([NotNullWhen(true)] out Entity<HandsComponent>? hands)
{
var player = _playerManager.LocalEntity;
hands = null;
return player != null && TryComp(player.Value, out hands);
if (player == null || !TryComp<HandsComponent>(player.Value, out var handsComp))
return false;
hands = (player.Value, handsComp);
return true;
}
/// <summary>
/// Called when a user clicked on their hands GUI
/// </summary>
public void UIHandClick(HandsComponent hands, string handName)
public void UIHandClick(Entity<HandsComponent> ent, string handName)
{
if (!hands.Hands.TryGetValue(handName, out var pressedHand))
var hands = ent.Comp;
if (hands.ActiveHandId == null)
return;
if (hands.ActiveHand == null)
return;
var pressedEntity = GetHeldItem(ent.AsNullable(), handName);
var activeEntity = GetActiveItem(ent.AsNullable());
var pressedEntity = pressedHand.HeldEntity;
var activeEntity = hands.ActiveHand.HeldEntity;
if (pressedHand == hands.ActiveHand && activeEntity != null)
if (handName == hands.ActiveHandId && activeEntity != null)
{
// use item in hand
// it will always be attack_self() in my heart.
EntityManager.RaisePredictiveEvent(new RequestUseInHandEvent());
RaisePredictiveEvent(new RequestUseInHandEvent());
return;
}
if (pressedHand != hands.ActiveHand && pressedEntity == null)
if (handName != hands.ActiveHandId && pressedEntity == null)
{
// change active hand
EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(handName));
RaisePredictiveEvent(new RequestSetHandEvent(handName));
return;
}
if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity != null)
if (handName != hands.ActiveHandId && pressedEntity != null && activeEntity != null)
{
// use active item on held item
EntityManager.RaisePredictiveEvent(new RequestHandInteractUsingEvent(pressedHand.Name));
RaisePredictiveEvent(new RequestHandInteractUsingEvent(handName));
return;
}
if (pressedHand != hands.ActiveHand && pressedEntity != null && activeEntity == null)
if (handName != hands.ActiveHandId && pressedEntity != null && activeEntity == null)
{
// move the item to the active hand
EntityManager.RaisePredictiveEvent(new RequestMoveHandItemEvent(pressedHand.Name));
RaisePredictiveEvent(new RequestMoveHandItemEvent(handName));
}
}
@@ -204,19 +166,18 @@ namespace Content.Client.Hands.Systems
/// </summary>
public void UIHandActivate(string handName)
{
EntityManager.RaisePredictiveEvent(new RequestActivateInHandEvent(handName));
RaisePredictiveEvent(new RequestActivateInHandEvent(handName));
}
public void UIInventoryExamine(string handName)
{
if (!TryGetPlayerHands(out var hands) ||
!hands.Hands.TryGetValue(handName, out var hand) ||
hand.HeldEntity is not { Valid: true } entity)
!TryGetHeldItem(hands.Value.AsNullable(), handName, out var heldEntity))
{
return;
}
_examine.DoExamine(entity);
_examine.DoExamine(heldEntity.Value);
}
/// <summary>
@@ -226,13 +187,12 @@ namespace Content.Client.Hands.Systems
public void UIHandOpenContextMenu(string handName)
{
if (!TryGetPlayerHands(out var hands) ||
!hands.Hands.TryGetValue(handName, out var hand) ||
hand.HeldEntity is not { Valid: true } entity)
!TryGetHeldItem(hands.Value.AsNullable(), handName, out var heldEntity))
{
return;
}
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(entity);
_ui.GetUIController<VerbMenuUIController>().OpenVerbMenu(heldEntity.Value);
}
public void UIHandAltActivateItem(string handName)
@@ -246,60 +206,67 @@ namespace Content.Client.Hands.Systems
{
base.HandleEntityInserted(uid, hands, args);
if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
if (!hands.Hands.ContainsKey(args.Container.ID))
return;
UpdateHandVisuals(uid, args.Entity, hand);
UpdateHandVisuals(uid, args.Entity, args.Container.ID);
_stripSys.UpdateUi(uid);
if (uid != _playerManager.LocalEntity)
return;
OnPlayerItemAdded?.Invoke(hand.Name, args.Entity);
OnPlayerItemAdded?.Invoke(args.Container.ID, args.Entity);
if (HasComp<VirtualItemComponent>(args.Entity))
OnPlayerHandBlocked?.Invoke(hand.Name);
OnPlayerHandBlocked?.Invoke(args.Container.ID);
}
protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args)
{
base.HandleEntityRemoved(uid, hands, args);
if (!hands.Hands.TryGetValue(args.Container.ID, out var hand))
if (!hands.Hands.ContainsKey(args.Container.ID))
return;
UpdateHandVisuals(uid, args.Entity, hand);
UpdateHandVisuals(uid, args.Entity, args.Container.ID);
_stripSys.UpdateUi(uid);
if (uid != _playerManager.LocalEntity)
return;
OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity);
OnPlayerItemRemoved?.Invoke(args.Container.ID, args.Entity);
if (HasComp<VirtualItemComponent>(args.Entity))
OnPlayerHandUnblocked?.Invoke(hand.Name);
OnPlayerHandUnblocked?.Invoke(args.Container.ID);
}
/// <summary>
/// Update the players sprite with new in-hand visuals.
/// </summary>
private void UpdateHandVisuals(EntityUid uid, EntityUid held, Hand hand, HandsComponent? handComp = null, SpriteComponent? sprite = null)
private void UpdateHandVisuals(Entity<HandsComponent?, SpriteComponent?> ent, EntityUid held, string handId)
{
if (!Resolve(uid, ref handComp, ref sprite, false))
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return;
var handComp = ent.Comp1;
var sprite = ent.Comp2;
if (!TryGetHand((ent, handComp), handId, out var hand))
return;
// visual update might involve changes to the entity's effective sprite -> need to update hands GUI.
if (uid == _playerManager.LocalEntity)
OnPlayerItemAdded?.Invoke(hand.Name, held);
if (ent == _playerManager.LocalEntity)
OnPlayerItemAdded?.Invoke(handId, held);
if (!handComp.ShowInHands)
return;
// Remove old layers. We could also just set them to invisible, but as items may add arbitrary layers, this
// may eventually bloat the player with lots of layers.
if (handComp.RevealedLayers.TryGetValue(hand.Location, out var revealedLayers))
if (handComp.RevealedLayers.TryGetValue(hand.Value.Location, out var revealedLayers))
{
foreach (var key in revealedLayers)
{
_sprite.RemoveLayer((uid, sprite), key);
_sprite.RemoveLayer((ent, sprite), key);
}
revealedLayers.Clear();
@@ -307,22 +274,22 @@ namespace Content.Client.Hands.Systems
else
{
revealedLayers = new();
handComp.RevealedLayers[hand.Location] = revealedLayers;
handComp.RevealedLayers[hand.Value.Location] = revealedLayers;
}
if (hand.HeldEntity == null)
if (HandIsEmpty((ent, handComp), handId))
{
// the held item was removed.
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
return;
}
var ev = new GetInhandVisualsEvent(uid, hand.Location);
var ev = new GetInhandVisualsEvent(ent, hand.Value.Location);
RaiseLocalEvent(held, ev);
if (ev.Layers.Count == 0)
{
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
return;
}
@@ -335,7 +302,7 @@ namespace Content.Client.Hands.Systems
continue;
}
var index = _sprite.LayerMapReserve((uid, sprite), key);
var index = _sprite.LayerMapReserve((ent, sprite), key);
// In case no RSI is given, use the item's base RSI as a default. This cuts down on a lot of unnecessary yaml entries.
if (layerData.RsiPath == null
@@ -343,35 +310,34 @@ namespace Content.Client.Hands.Systems
&& sprite[index].Rsi == null)
{
if (TryComp<ItemComponent>(held, out var itemComponent) && itemComponent.RsiPath != null)
_sprite.LayerSetRsi((uid, sprite), index, new ResPath(itemComponent.RsiPath));
_sprite.LayerSetRsi((ent, sprite), index, new ResPath(itemComponent.RsiPath));
else if (TryComp(held, out SpriteComponent? clothingSprite))
_sprite.LayerSetRsi((uid, sprite), index, clothingSprite.BaseRSI);
_sprite.LayerSetRsi((ent, sprite), index, clothingSprite.BaseRSI);
}
_sprite.LayerSetData((uid, sprite), index, layerData);
_sprite.LayerSetData((ent, sprite), index, layerData);
// Add displacement maps
var displacement = hand.Location switch
var displacement = hand.Value.Location switch
{
HandLocation.Left => handComp.LeftHandDisplacement,
HandLocation.Right => handComp.RightHandDisplacement,
_ => handComp.HandDisplacement
};
if (displacement is not null && _displacement.TryAddDisplacement(displacement, (uid, sprite), index, key, out var displacementKey))
if (displacement is not null && _displacement.TryAddDisplacement(displacement, (ent, sprite), index, key, out var displacementKey))
revealedLayers.Add(displacementKey);
}
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(ent, revealedLayers), true);
}
private void OnVisualsChanged(EntityUid uid, HandsComponent component, VisualsChangedEvent args)
{
// update hands visuals if this item is in a hand (rather then inventory or other container).
if (component.Hands.TryGetValue(args.ContainerId, out var hand))
{
UpdateHandVisuals(uid, GetEntity(args.Item), hand, component);
}
if (!component.Hands.ContainsKey(args.ContainerId))
return;
UpdateHandVisuals((uid, component), GetEntity(args.Item), args.ContainerId);
}
#endregion
@@ -379,7 +345,7 @@ namespace Content.Client.Hands.Systems
private void HandlePlayerAttached(EntityUid uid, HandsComponent component, LocalPlayerAttachedEvent args)
{
OnPlayerHandsAdded?.Invoke(component);
OnPlayerHandsAdded?.Invoke((uid, component));
}
private void HandlePlayerDetached(EntityUid uid, HandsComponent component, LocalPlayerDetachedEvent args)
@@ -390,7 +356,7 @@ namespace Content.Client.Hands.Systems
private void OnHandsStartup(EntityUid uid, HandsComponent component, ComponentStartup args)
{
if (_playerManager.LocalEntity == uid)
OnPlayerHandsAdded?.Invoke(component);
OnPlayerHandsAdded?.Invoke((uid, component));
}
private void OnHandsShutdown(EntityUid uid, HandsComponent component, ComponentShutdown args)
@@ -400,36 +366,6 @@ namespace Content.Client.Hands.Systems
}
#endregion
private void AddHand(EntityUid uid, Hand newHand, HandsComponent? handsComp = null)
{
AddHand(uid, newHand.Name, newHand.Location, handsComp);
}
public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, HandsComponent? handsComp = null)
{
base.AddHand(uid, handName, handLocation, handsComp);
if (uid == _playerManager.LocalEntity)
OnPlayerAddHand?.Invoke(handName, handLocation);
if (handsComp == null)
return;
if (handsComp.ActiveHand == null)
SetActiveHand(uid, handsComp.Hands[handName], handsComp);
}
public override void RemoveHand(EntityUid uid, string handName, HandsComponent? handsComp = null)
{
if (uid == _playerManager.LocalEntity && handsComp != null &&
handsComp.Hands.ContainsKey(handName) && uid ==
_playerManager.LocalEntity)
{
OnPlayerRemoveHand?.Invoke(handName);
}
base.RemoveHand(uid, handName, handsComp);
}
private void OnHandActivated(Entity<HandsComponent>? ent)
{
if (ent is not { } hand)
@@ -438,13 +374,7 @@ namespace Content.Client.Hands.Systems
if (_playerManager.LocalEntity != hand.Owner)
return;
if (hand.Comp.ActiveHand == null)
{
OnPlayerSetActiveHand?.Invoke(null);
return;
}
OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHand.Name);
OnPlayerSetActiveHand?.Invoke(hand.Comp.ActiveHandId);
}
}
}

View File

@@ -107,7 +107,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Remove shading from all layers (except displacement maps)
for (var i = 0; i < hologramSprite.AllLayers.Count(); i++)
{
if (_sprite.TryGetLayer((hologram, hologramSprite), i, out var layer, false) && layer.ShaderPrototype != "DisplacedStencilDraw")
if (_sprite.TryGetLayer((hologram, hologramSprite), i, out var layer, false) && layer.ShaderPrototype != "DisplacedDraw")
hologramSprite.LayerSetShader(i, "unshaded");
}

View File

@@ -27,6 +27,7 @@ public sealed class EyeColorPicker : Control
AddChild(vBox);
vBox.AddChild(_colorSelectors = new ColorSelectorSliders());
_colorSelectors.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
_colorSelectors.OnColorChanged += ColorValueChanged;
}

View File

@@ -36,7 +36,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
private void OnCvarChanged(bool value)
{
var humanoidQuery = EntityManager.AllEntityQueryEnumerator<HumanoidAppearanceComponent, SpriteComponent>();
var humanoidQuery = AllEntityQuery<HumanoidAppearanceComponent, SpriteComponent>();
while (humanoidQuery.MoveNext(out var uid, out var humanoidComp, out var spriteComp))
{
UpdateSprite((uid, humanoidComp, spriteComp));

View File

@@ -416,6 +416,7 @@ public sealed partial class MarkingPicker : Control
CMarkingColors.AddChild(colorContainer);
ColorSelectorSliders colorSelector = new ColorSelectorSliders();
colorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
colorSliders.Add(colorSelector);
colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" });

View File

@@ -15,7 +15,7 @@ public sealed partial class SingleMarkingPicker : BoxContainer
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SpriteSystem _sprite;
/// <summary>
/// What happens if a marking is selected.
/// It will send the 'slot' (marking index)
@@ -231,6 +231,7 @@ public sealed partial class SingleMarkingPicker : BoxContainer
HorizontalExpand = true
};
selector.Color = marking.MarkingColors[i];
selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
var colorIndex = i;
selector.OnColorChanged += color =>

View File

@@ -102,6 +102,8 @@ public static class MidiParser
// 0x03 is TrackName,
// 0x04 is InstrumentName
// This string can potentially contain control characters, including 0x00 which can cause problems if it ends up in database entries via admin logs
// we sanitize TrackName and InstrumentName after they have been send to the server
var text = Encoding.ASCII.GetString(metaData, 0, (int)metaLength);
switch (metaType)
{

View File

@@ -194,12 +194,12 @@ namespace Content.Client.Inventory
public void UIInventoryActivate(string slot)
{
EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot));
RaisePredictiveEvent(new UseSlotNetworkMessage(slot));
}
public void UIInventoryStorageActivate(string slot)
{
EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
}
public void UIInventoryExamine(string slot, EntityUid uid)
@@ -223,7 +223,7 @@ namespace Content.Client.Inventory
if (!TryGetSlotEntity(uid, slot, out var item))
return;
EntityManager.RaisePredictiveEvent(
RaisePredictiveEvent(
new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: false));
}
@@ -232,7 +232,7 @@ namespace Content.Client.Inventory
if (!TryGetSlotEntity(uid, slot, out var item))
return;
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
RaisePredictiveEvent(new InteractInventorySlotEvent(GetNetEntity(item.Value), altInteract: true));
}
protected override void UpdateInventoryTemplate(Entity<InventoryComponent> ent)

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using Content.Client.Examine;
using Content.Client.Hands.Systems;
using Content.Client.Strip;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
@@ -34,6 +35,7 @@ namespace Content.Client.Inventory
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private readonly ExamineSystem _examine;
private readonly HandsSystem _hands;
private readonly InventorySystem _inv;
private readonly SharedCuffableSystem _cuffable;
private readonly StrippableSystem _strippable;
@@ -65,6 +67,7 @@ namespace Content.Client.Inventory
public StrippableBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_examine = EntMan.System<ExamineSystem>();
_hands = EntMan.System<HandsSystem>();
_inv = EntMan.System<InventorySystem>();
_cuffable = EntMan.System<SharedCuffableSystem>();
_strippable = EntMan.System<StrippableSystem>();
@@ -120,28 +123,28 @@ namespace Content.Client.Inventory
{
// good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands
// and not gui hands... which are different...
foreach (var hand in handsComp.Hands.Values)
foreach (var (id, hand) in handsComp.Hands)
{
if (hand.Location != HandLocation.Right)
continue;
AddHandButton(hand);
AddHandButton((Owner, handsComp), id, hand);
}
foreach (var hand in handsComp.Hands.Values)
foreach (var (id, hand) in handsComp.Hands)
{
if (hand.Location != HandLocation.Middle)
continue;
AddHandButton(hand);
AddHandButton((Owner, handsComp), id, hand);
}
foreach (var hand in handsComp.Hands.Values)
foreach (var (id, hand) in handsComp.Hands)
{
if (hand.Location != HandLocation.Left)
continue;
AddHandButton(hand);
AddHandButton((Owner, handsComp), id, hand);
}
}
@@ -177,20 +180,21 @@ namespace Content.Client.Inventory
_strippingMenu.SetSize = new Vector2(horizontalMenuSize, verticalMenuSize);
}
private void AddHandButton(Hand hand)
private void AddHandButton(Entity<HandsComponent> ent, string handId, Hand hand)
{
var button = new HandButton(hand.Name, hand.Location);
var button = new HandButton(handId, hand.Location);
button.Pressed += SlotPressed;
if (EntMan.TryGetComponent<VirtualItemComponent>(hand.HeldEntity, out var virt))
var heldEntity = _hands.GetHeldItem(ent.AsNullable(), handId);
if (EntMan.TryGetComponent<VirtualItemComponent>(heldEntity, out var virt))
{
button.Blocked = true;
if (EntMan.TryGetComponent<CuffableComponent>(Owner, out var cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
}
UpdateEntityIcon(button, hand.HeldEntity);
UpdateEntityIcon(button, heldEntity);
_strippingMenu!.HandsContainer.AddChild(button);
LayoutContainer.SetPosition(button, new Vector2i(_handCount, 0) * (SlotControl.DefaultButtonSize + ButtonSeparation));
_handCount++;

View File

@@ -223,13 +223,14 @@ public sealed partial class LatheMenu : DefaultWindow
/// Populates the build queue list with all queued items
/// </summary>
/// <param name="queue"></param>
public void PopulateQueueList(List<LatheRecipePrototype> queue)
public void PopulateQueueList(IReadOnlyCollection<ProtoId<LatheRecipePrototype>> queue)
{
QueueList.DisposeAllChildren();
var idx = 1;
foreach (var recipe in queue)
foreach (var recipeProto in queue)
{
var recipe = _prototypeManager.Index(recipeProto);
var queuedRecipeBox = new BoxContainer();
queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
@@ -243,12 +244,14 @@ public sealed partial class LatheMenu : DefaultWindow
}
}
public void SetQueueInfo(LatheRecipePrototype? recipe)
public void SetQueueInfo(ProtoId<LatheRecipePrototype>? recipeProto)
{
FabricatingContainer.Visible = recipe != null;
if (recipe == null)
FabricatingContainer.Visible = recipeProto != null;
if (recipeProto == null)
return;
var recipe = _prototypeManager.Index(recipeProto.Value);
FabricatingDisplayContainer.Children.Clear();
FabricatingDisplayContainer.AddChild(GetRecipeDisplayControl(recipe));

View File

@@ -0,0 +1,144 @@
using System.Numerics;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Light;
/// <summary>
/// Applies ambient-occlusion to the viewport.
/// </summary>
public sealed class AmbientOcclusionOverlay : Overlay
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private IRenderTexture? _aoTarget;
private IRenderTexture? _aoBlurBuffer;
// Couldn't figure out a way to avoid this so if you can then please do.
private IRenderTexture? _aoStencilTarget;
public AmbientOcclusionOverlay()
{
IoCManager.InjectDependencies(this);
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
}
protected override void Draw(in OverlayDrawArgs args)
{
/*
* tl;dr
* - we draw a black square on each "ambient occlusion" entity.
* - we blur this.
* - We apply it to the viewport.
*
* We do this while ignoring lighting because it will wash out the actual effect.
* In 3D ambient occlusion is more complicated due top having to calculate normals but in 2D
* we don't have a concept of depth / corners necessarily.
*/
var viewport = args.Viewport;
var mapId = args.MapId;
var worldBounds = args.WorldBounds;
var worldHandle = args.WorldHandle;
var color = Color.FromHex(_cfgManager.GetCVar(CCVars.AmbientOcclusionColor));
var distance = _cfgManager.GetCVar(CCVars.AmbientOcclusionDistance);
//var color = Color.Red;
var target = viewport.RenderTarget;
var lightScale = target.Size / (Vector2) viewport.Size;
var scale = viewport.RenderScale / (Vector2.One / lightScale);
var maps = _entManager.System<SharedMapSystem>();
var lookups = _entManager.System<EntityLookupSystem>();
var query = _entManager.System<OccluderSystem>();
var xformSystem = _entManager.System<SharedTransformSystem>();
var turfSystem = _entManager.System<TurfSystem>();
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
if (_aoTarget?.Texture.Size != target.Size)
{
_aoTarget?.Dispose();
_aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
}
if (_aoBlurBuffer?.Texture.Size != target.Size)
{
_aoBlurBuffer?.Dispose();
_aoBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
}
if (_aoStencilTarget?.Texture.Size != target.Size)
{
_aoStencilTarget?.Dispose();
_aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
}
// Draw the texture data to the texture.
args.WorldHandle.RenderInRenderTarget(_aoTarget,
() =>
{
worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
foreach (var entry in query.QueryAabb(mapId, worldBounds))
{
DebugTools.Assert(entry.Component.Enabled);
var matrix = xformSystem.GetWorldMatrix(entry.Transform);
var localMatrix = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(localMatrix);
// 4 pixels
worldHandle.DrawRect(Box2.UnitCentered.Enlarged(distance / EyeManager.PixelsPerMeter), Color.White);
}
}, Color.Transparent);
_clyde.BlurRenderTarget(viewport, _aoTarget, _aoBlurBuffer, viewport.Eye!, 14f);
// Need to do stencilling after blur as it will nuke it.
// Draw stencil for the grid so we don't draw in space.
args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
() =>
{
// Don't want lighting affecting it.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
var transform = xformSystem.GetWorldMatrix(grid.Owner);
var worldToTextureMatrix = Matrix3x2.Multiply(transform, invMatrix);
var tiles = maps.GetTilesEnumerator(grid.Owner, grid, worldBounds);
worldHandle.SetTransform(worldToTextureMatrix);
while (tiles.MoveNext(out var tileRef))
{
if (turfSystem.IsSpace(tileRef))
continue;
var bounds = lookups.GetLocalBounds(tileRef, grid.TileSize);
worldHandle.DrawRect(bounds, Color.White);
}
}
}, Color.Transparent);
// Draw the stencil texture to depth buffer.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
// Draw the Blurred AO texture finally.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilEqualDraw").Instance());
worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
args.WorldHandle.UseShader(null);
}
}

View File

@@ -63,7 +63,7 @@ public sealed class LightBehaviorSystem : EntitySystem
/// </summary>
private void CopyLightSettings(Entity<LightBehaviourComponent> entity, string property)
{
if (EntityManager.TryGetComponent(entity, out PointLightComponent? light))
if (TryComp(entity, out PointLightComponent? light))
{
var propertyValue = AnimationHelper.GetAnimatableProperty(light, property);
if (propertyValue != null)
@@ -73,7 +73,7 @@ public sealed class LightBehaviorSystem : EntitySystem
}
else
{
Log.Warning($"{EntityManager.GetComponent<MetaDataComponent>(entity).EntityName} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!");
Log.Warning($"{Comp<MetaDataComponent>(entity).EntityName} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!");
}
}
@@ -84,7 +84,7 @@ public sealed class LightBehaviorSystem : EntitySystem
/// </summary>
public void StartLightBehaviour(Entity<LightBehaviourComponent> entity, string id = "")
{
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
if (!TryComp(entity, out AnimationPlayerComponent? animation))
{
return;
}
@@ -113,7 +113,7 @@ public sealed class LightBehaviorSystem : EntitySystem
/// <param name="resetToOriginalSettings">Should the light have its original settings applied?</param>
public void StopLightBehaviour(Entity<LightBehaviourComponent> entity, string id = "", bool removeBehaviour = false, bool resetToOriginalSettings = false)
{
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
if (!TryComp(entity, out AnimationPlayerComponent? animation))
{
return;
}
@@ -143,7 +143,7 @@ public sealed class LightBehaviorSystem : EntitySystem
comp.Animations.Remove(container);
}
if (resetToOriginalSettings && EntityManager.TryGetComponent(entity, out PointLightComponent? light))
if (resetToOriginalSettings && TryComp(entity, out PointLightComponent? light))
{
foreach (var (property, value) in comp.OriginalPropertyValues)
{
@@ -161,7 +161,7 @@ public sealed class LightBehaviorSystem : EntitySystem
public bool HasRunningBehaviours(Entity<LightBehaviourComponent> entity)
{
//var uid = Owner;
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
if (!TryComp(entity, out AnimationPlayerComponent? animation))
{
return false;
}

View File

@@ -1,17 +1,51 @@
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
namespace Content.Client.Light.EntitySystems;
public sealed class PlanetLightSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
/// <summary>
/// Enables / disables the ambient occlusion overlay.
/// </summary>
public bool AmbientOcclusion
{
get => _ambientOcclusion;
set
{
if (_ambientOcclusion == value)
return;
_ambientOcclusion = value;
if (value)
{
_overlayMan.AddOverlay(new AmbientOcclusionOverlay());
}
else
{
_overlayMan.RemoveOverlay<AmbientOcclusionOverlay>();
}
}
}
private bool _ambientOcclusion;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GetClearColorEvent>(OnClearColor);
_cfgManager.OnValueChanged(CCVars.AmbientOcclusion, val =>
{
AmbientOcclusion = val;
}, true);
_overlayMan.AddOverlay(new BeforeLightTargetOverlay());
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));

View File

@@ -62,6 +62,8 @@ public sealed class RoofOverlay : Overlay
worldHandle.RenderInRenderTarget(target,
() =>
{
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
for (var i = 0; i < _grids.Count; i++)
{
var grid = _grids[i];
@@ -69,8 +71,6 @@ public sealed class RoofOverlay : Overlay
if (!_entManager.TryGetComponent(grid.Owner, out ImplicitRoofComponent? roof))
continue;
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
@@ -94,13 +94,13 @@ public sealed class RoofOverlay : Overlay
worldHandle.RenderInRenderTarget(target,
() =>
{
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
foreach (var grid in _grids)
{
if (!_entManager.TryGetComponent(grid.Owner, out RoofComponent? roof))
continue;
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);

View File

@@ -241,6 +241,7 @@ namespace Content.Client.Lobby.UI
};
RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
_rgbSkinColorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
_rgbSkinColorSelector.OnColorChanged += _ =>
{
OnSkinColorOnValueChanged();

View File

@@ -144,8 +144,8 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
{
subList.AddChild(proto);
}
UpdateToggleColor(toggle, subList);
var itemName = firstElement.Text ?? "";
UpdateSubGroupSelectedInfo(firstElement, itemName, subList);
}
else
{
@@ -164,12 +164,14 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
};
toggle.Text = subContainer.Visible ? OpenedGroupMark : ClosedGroupMark;
toggle.Pressed = subContainer.Visible;
toggle.OnPressed += _ =>
{
var willOpen = !subContainer.Visible;
subContainer.Visible = willOpen;
toggle.Text = willOpen ? OpenedGroupMark : ClosedGroupMark;
toggle.Pressed = willOpen;
_openedGroups[kvp.Key] = willOpen;
};
@@ -178,15 +180,16 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
return toggle;
}
private void UpdateToggleColor(Button toggle, BoxContainer subList)
private void UpdateSubGroupSelectedInfo(LoadoutContainer loadout, string itemName, BoxContainer subList)
{
var anyActive = subList.Children
var countSubSelected = subList.Children
.OfType<LoadoutContainer>()
.Any(c => c.Select.Pressed);
.Count(c => c.Select.Pressed);
toggle.Modulate = anyActive
? Color.Green
: Color.White;
if (countSubSelected > 0)
{
loadout.Text = Loc.GetString("loadouts-count-items-in-group", ("item", itemName), ("count", countSubSelected));
}
}
/// <summary>

View File

@@ -793,7 +793,7 @@ public sealed class MappingState : GameplayStateBase
if (_mapMan.TryFindGridAt(mapPos, out var gridUid, out var grid) &&
_entityManager.System<SharedMapSystem>().TryGetTileRef(gridUid, grid, coords, out var tileRef) &&
_allPrototypesDict.TryGetValue(tileRef.GetContentTileDefinition(), out button))
_allPrototypesDict.TryGetValue(_entityManager.System<TurfSystem>().GetContentTileDefinition(tileRef), out button))
{
OnSelected(button);
return true;

View File

@@ -33,7 +33,7 @@ public sealed class MarkerSystem : EntitySystem
private void UpdateVisibility(EntityUid uid)
{
if (EntityManager.TryGetComponent(uid, out SpriteComponent? sprite))
if (TryComp(uid, out SpriteComponent? sprite))
{
_sprite.SetVisible((uid, sprite), MarkersVisible);
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceNetwork;
@@ -14,6 +15,8 @@ namespace Content.Client.NetworkConfigurator;
[GenerateTypedNameReferences]
public sealed partial class NetworkConfiguratorLinkMenu : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private const string PanelBgColor = "#202023";
private readonly LinksRender _links;
@@ -33,6 +36,7 @@ public sealed partial class NetworkConfiguratorLinkMenu : FancyWindow
public NetworkConfiguratorLinkMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var footerStyleBox = new StyleBoxFlat()
{
@@ -61,7 +65,7 @@ public sealed partial class NetworkConfiguratorLinkMenu : FancyWindow
ButtonContainerRight.RemoveAllChildren();
_sources.Clear();
_sources.AddRange(linkState.Sources);
_sources.AddRange(linkState.Sources.Select(s => _prototypeManager.Index(s)));
_links.SourceButtons.Clear();
var i = 0;
foreach (var source in _sources)
@@ -73,7 +77,7 @@ public sealed partial class NetworkConfiguratorLinkMenu : FancyWindow
}
_sinks.Clear();
_sinks.AddRange(linkState.Sinks);
_sinks.AddRange(linkState.Sinks.Select(s => _prototypeManager.Index(s)));
_links.SinkButtons.Clear();
i = 0;
foreach (var sink in _sinks)

View File

@@ -14,6 +14,7 @@
<ui:OptionDropDown Name="DropDownLightingQuality" Title="{Loc 'ui-options-lighting-label'}" />
<CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
<CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
<CheckBox Name="AmbientOcclusionCheckBox" Text="{Loc 'ui-options-ambient-occlusion'}" />
<!-- Interface -->
<Label Text="{Loc 'ui-options-interface-label'}" StyleClasses="LabelKeyText"/>

View File

@@ -20,6 +20,7 @@ public sealed partial class GraphicsTab : Control
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CVars.DisplayVSync, VSyncCheckBox);
Control.AddOptionCheckBox(CCVars.AmbientOcclusion, AmbientOcclusionCheckBox);
Control.AddOption(new OptionFullscreen(Control, _cfg, FullscreenCheckBox));
Control.AddOption(new OptionLightingQuality(Control, _cfg, DropDownLightingQuality));

View File

@@ -64,7 +64,7 @@ public sealed class OrbitVisualsSystem : EntitySystem
{
base.FrameUpdate(frameTime);
var query = EntityManager.EntityQueryEnumerator<OrbitVisualsComponent, SpriteComponent>();
var query = EntityQueryEnumerator<OrbitVisualsComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out var orbit, out var sprite))
{
var progress = (float)(_timing.CurTime.TotalSeconds / orbit.OrbitLength) % 1;

View File

@@ -48,17 +48,14 @@ public sealed class PowerCellSystem : SharedPowerCellSystem
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
if (_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
{
if (level == 0)
{
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, false);
return;
}
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
level = 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, false);
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
if (positiveCharge)
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
}
}
private enum PowerCellVisualLayers : byte

View File

@@ -1,5 +1,6 @@
using System.Numerics;
using Content.Client.Gameplay;
using Content.Client.Hands.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.RCD.Components;
@@ -17,6 +18,7 @@ public sealed class AlignRCDConstruction : PlacementMode
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly SharedMapSystem _mapSystem;
private readonly HandsSystem _handsSystem;
private readonly RCDSystem _rcdSystem;
private readonly SharedTransformSystem _transformSystem;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -34,6 +36,7 @@ public sealed class AlignRCDConstruction : PlacementMode
{
IoCManager.InjectDependencies(this);
_mapSystem = _entityManager.System<SharedMapSystem>();
_handsSystem = _entityManager.System<HandsSystem>();
_rcdSystem = _entityManager.System<RCDSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
@@ -88,11 +91,9 @@ public sealed class AlignRCDConstruction : PlacementMode
}
// Determine if player is carrying an RCD in their active hand
if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
if (!_handsSystem.TryGetActiveItem(player.Value, out var heldEntity))
return false;
var heldEntity = hands.ActiveHand?.HeldEntity;
if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
return false;

View File

@@ -1,8 +1,7 @@
using Content.Shared.Hands.Components;
using Content.Client.Hands.Systems;
using Content.Shared.Interaction;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Content.Shared.RCD.Systems;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Shared.Enums;
@@ -10,13 +9,18 @@ using Robust.Shared.Prototypes;
namespace Content.Client.RCD;
/// <summary>
/// System for handling structure ghost placement in places where RCD can create objects.
/// </summary>
public sealed class RCDConstructionGhostSystem : EntitySystem
{
private const string PlacementMode = nameof(AlignRCDConstruction);
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private string _placementMode = typeof(AlignRCDConstruction).Name;
[Dependency] private readonly HandsSystem _hands = default!;
private Direction _placementDirection = default;
public override void Update(float frameTime)
@@ -33,12 +37,10 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
return;
// Determine if player is carrying an RCD in their active hand
var player = _playerManager.LocalSession?.AttachedEntity;
if (!TryComp<HandsComponent>(player, out var hands))
if (_playerManager.LocalSession?.AttachedEntity is not { } player)
return;
var heldEntity = hands.ActiveHand?.HeldEntity;
var heldEntity = _hands.GetActiveItem(player);
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
{
@@ -65,7 +67,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
var newObjInfo = new PlacementInformation
{
MobUid = heldEntity.Value,
PlacementOption = _placementMode,
PlacementOption = PlacementMode,
EntityType = prototype.Prototype,
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (prototype.Mode == RcdMode.ConstructTile),

View File

@@ -1,4 +1,6 @@
using Content.Client.Alerts;
using Content.Shared.Alert;
using Content.Shared.Alert.Components;
using Content.Shared.Revenant;
using Content.Shared.Revenant.Components;
using Robust.Client.GameObjects;
@@ -15,7 +17,7 @@ public sealed class RevenantSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<RevenantComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<RevenantComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
SubscribeLocalEvent<RevenantComponent, GetGenericAlertCounterAmountEvent>(OnGetCounterAmount);
}
private void OnAppearanceChange(EntityUid uid, RevenantComponent component, ref AppearanceChangeEvent args)
@@ -40,14 +42,14 @@ public sealed class RevenantSystem : EntitySystem
}
}
private void OnUpdateAlert(Entity<RevenantComponent> ent, ref UpdateAlertSpriteEvent args)
private void OnGetCounterAmount(Entity<RevenantComponent> ent, ref GetGenericAlertCounterAmountEvent args)
{
if (args.Alert.ID != ent.Comp.EssenceAlert)
if (args.Handled)
return;
var essence = Math.Clamp(ent.Comp.Essence.Int(), 0, 999);
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit1, $"{(essence / 100) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit2, $"{(essence / 10) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit3, $"{essence % 10}");
if (ent.Comp.EssenceAlert != args.Alert)
return;
args.Amount = ent.Comp.Essence.Int();
}
}

View File

@@ -90,7 +90,7 @@ namespace Content.Client.Sandbox
// Try copy entity.
if (uid.IsValid()
&& EntityManager.TryGetComponent(uid, out MetaDataComponent? comp)
&& TryComp(uid, out MetaDataComponent? comp)
&& !comp.EntityDeleted)
{
if (comp.EntityPrototype == null || comp.EntityPrototype.HideSpawnMenu || comp.EntityPrototype.Abstract)

View File

@@ -25,6 +25,29 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
private TimeSpan _retentionTime;
private readonly Dictionary<int, SensorData> _sensorData = new();
/// <summary>
/// <para>A shared array used to store vertices for drawing graphs in <see cref="GraphView"/>.
/// Prevents excessive allocations by reusing the same array across multiple graph views.</para>
/// <para>This effectively makes it so that each <see cref="SensorMonitoringWindow"/> has its own pooled array.</para>
/// </summary>
private Vector2[] _sharedVertices = [];
/// <summary>
/// Retrieves a shared array of vertices, ensuring that it has at least the requested size.
/// Assigns a new array of the requested size if the current shared array is smaller than the requested size.
/// </summary>
/// <param name="requestedSize">The minimum number of vertices required in the shared array.</param>
/// <returns>An array of <see cref="System.Numerics.Vector2"/> representing the shared vertices.</returns>
/// <remarks>This does not prevent other threads from accessing the same shared pool.</remarks>
public Vector2[] GetSharedVertices(int requestedSize)
{
if (_sharedVertices.Length < requestedSize)
{
_sharedVertices = new Vector2[requestedSize];
}
return _sharedVertices;
}
public SensorMonitoringWindow()
{
RobustXamlLoader.Load(this);
@@ -145,7 +168,7 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
}
});
Asdf.AddChild(new GraphView(stream.Samples, startTime, curTime, maxValue * 1.1f) { MinHeight = 150 });
Asdf.AddChild(new GraphView(stream.Samples, startTime, curTime, maxValue * 1.1f, this) { MinHeight = 150 });
Asdf.AddChild(new PanelContainer { StyleClasses = { StyleBase.ClassLowDivider } });
}
}
@@ -197,31 +220,37 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
private readonly TimeSpan _startTime;
private readonly TimeSpan _curTime;
private readonly float _maxY;
private readonly SensorMonitoringWindow _parentWindow;
public GraphView(Queue<SensorSample> samples, TimeSpan startTime, TimeSpan curTime, float maxY)
public GraphView(Queue<SensorSample> samples,
TimeSpan startTime,
TimeSpan curTime,
float maxY,
SensorMonitoringWindow parentWindow)
{
_samples = samples;
_startTime = startTime;
_curTime = curTime;
_maxY = maxY;
RectClipContent = true;
_parentWindow = parentWindow;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
var window = (float) (_curTime - _startTime).TotalSeconds;
// TODO: omg this is terrible don't fucking hardcode this size to something uncached huge omfg.
var vertices = new Vector2[25000];
var window = (float)(_curTime - _startTime).TotalSeconds;
var countVtx = 0;
var lastPoint = new Vector2(float.NaN, float.NaN);
var requiredVertices = 6 * (_samples.Count - 1);
var vertices = _parentWindow.GetSharedVertices(requiredVertices);
foreach (var (time, sample) in _samples)
{
var relTime = (float) (time - _startTime).TotalSeconds;
var relTime = (float)(time - _startTime).TotalSeconds;
var posY = PixelHeight - (sample / _maxY) * PixelHeight;
var posX = (relTime / window) * PixelWidth;
@@ -242,8 +271,9 @@ public sealed partial class SensorMonitoringWindow : FancyWindow, IComputerWindo
lastPoint = newPoint;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, vertices.AsSpan(0, countVtx), Color.White.WithAlpha(0.1f));
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList,
vertices.AsSpan(0, countVtx),
Color.White.WithAlpha(0.1f));
}
}
}

View File

@@ -0,0 +1,50 @@
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
namespace Content.Client.StatusEffectNew;
/// <inheritdoc/>
public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(Entity<StatusEffectContainerComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not StatusEffectContainerComponentState state)
return;
var toRemove = new ValueList<EntityUid>();
foreach (var effect in ent.Comp.ActiveStatusEffects)
{
if (state.ActiveStatusEffects.Contains(GetNetEntity(effect)))
continue;
toRemove.Add(effect);
}
foreach (var effect in toRemove)
{
ent.Comp.ActiveStatusEffects.Remove(effect);
var ev = new StatusEffectRemovedEvent(ent);
RaiseLocalEvent(effect, ref ev);
}
foreach (var effect in state.ActiveStatusEffects)
{
var effectUid = GetEntity(effect);
if (ent.Comp.ActiveStatusEffects.Contains(effectUid))
continue;
ent.Comp.ActiveStatusEffects.Add(effectUid);
var ev = new StatusEffectAppliedEvent(ent);
RaiseLocalEvent(effectUid, ref ev);
}
}
}

View File

@@ -68,11 +68,15 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
foreach (var hand in _hands.EnumerateHands(player.Value))
{
if (!scannerQuery.TryGetComponent(hand.HeldEntity, out var heldScanner) || !heldScanner.Enabled)
if (!_hands.TryGetHeldItem(player.Value, hand, out var heldEntity))
continue;
if (!scannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled)
continue;
range = MathF.Max(heldScanner.Range, range);
canSee = true;
break;
}
inRange = new HashSet<Entity<SubFloorHideComponent>>();

View File

@@ -131,7 +131,7 @@ namespace Content.Client.Tabletop
// Get the camera entity that the server has created for us
var camera = GetEntity(msg.CameraUid);
if (!EntityManager.TryGetComponent<EyeComponent>(camera, out var eyeComponent))
if (!TryComp<EyeComponent>(camera, out var eyeComponent))
{
// If there is no eye, print error and do not open any window
Log.Error("Camera entity does not have eye component!");
@@ -258,7 +258,7 @@ namespace Content.Client.Tabletop
private void StopDragging(bool broadcast = true)
{
// Set the dragging player on the component to noone
if (broadcast && _draggedEntity != null && EntityManager.HasComponent<TabletopDraggableComponent>(_draggedEntity.Value))
if (broadcast && _draggedEntity != null && HasComp<TabletopDraggableComponent>(_draggedEntity.Value))
{
RaisePredictiveEvent(new TabletopMoveEvent(GetNetEntity(_draggedEntity.Value), Transforms.GetMapCoordinates(_draggedEntity.Value), GetNetEntity(_table!.Value)));
RaisePredictiveEvent(new TabletopDraggingPlayerChangedEvent(GetNetEntity(_draggedEntity.Value), false));

View File

@@ -98,7 +98,8 @@ public sealed class AlertsUIController : UIController, IOnStateEntered<GameplayS
if (!EntityManager.TryGetComponent<SpriteComponent>(spriteViewEnt, out var sprite))
return;
var ev = new UpdateAlertSpriteEvent((spriteViewEnt, sprite), alert);
var ev = new UpdateAlertSpriteEvent((spriteViewEnt, sprite), player, alert);
EntityManager.EventBus.RaiseLocalEvent(player, ref ev);
EntityManager.EventBus.RaiseLocalEvent(spriteViewEnt, ref ev);
}
}

View File

@@ -57,10 +57,15 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
_sprite = _entityManager.System<SpriteSystem>();
TooltipSupplier = SupplyTooltip;
Alert = alert;
HorizontalAlignment = HAlignment.Left;
_severity = severity;
_icon = new SpriteView
{
Scale = new Vector2(2, 2)
Scale = new Vector2(2, 2),
MaxSize = new Vector2(64, 64),
Stretch = SpriteView.StretchMode.None,
HorizontalAlignment = HAlignment.Left
};
SetupIcon();

View File

@@ -207,7 +207,9 @@ public sealed class GasTankWindow
_btnInternals.Disabled = !canConnectInternals;
_lblInternals.SetMarkup(Loc.GetString("gas-tank-window-internal-text",
("status", Loc.GetString(internalsConnected ? "gas-tank-window-internal-connected" : "gas-tank-window-internal-disconnected"))));
_spbPressure.Value = outputPressure;
if (!_spbPressure.HasKeyboardFocus())
// Don't update release pressure if we're currently editing it
_spbPressure.Value = outputPressure;
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -7,6 +7,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Input;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Timing;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -28,7 +29,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private readonly Dictionary<string, int> _handContainerIndices = new();
private readonly Dictionary<string, HandButton> _handLookup = new();
private HandsComponent? _playerHandsComponent;
private HandButton? _activeHand = null;
private HandButton? _activeHand;
// We only have two item status controls (left and right hand),
// but we may have more than two hands.
@@ -38,7 +39,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
private HandButton? _statusHandLeft;
private HandButton? _statusHandRight;
private int _backupSuffix = 0; //this is used when autogenerating container names if they don't have names
private int _backupSuffix; //this is used when autogenerating container names if they don't have names
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
@@ -48,7 +49,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handsSystem.OnPlayerItemAdded += OnItemAdded;
_handsSystem.OnPlayerItemRemoved += OnItemRemoved;
_handsSystem.OnPlayerSetActiveHand += SetActiveHand;
_handsSystem.OnPlayerRemoveHand += RemoveHand;
_handsSystem.OnPlayerRemoveHand += OnRemoveHand;
_handsSystem.OnPlayerHandsAdded += LoadPlayerHands;
_handsSystem.OnPlayerHandsRemoved += UnloadPlayerHands;
_handsSystem.OnPlayerHandBlocked += HandBlocked;
@@ -61,28 +62,35 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handsSystem.OnPlayerItemAdded -= OnItemAdded;
_handsSystem.OnPlayerItemRemoved -= OnItemRemoved;
_handsSystem.OnPlayerSetActiveHand -= SetActiveHand;
_handsSystem.OnPlayerRemoveHand -= RemoveHand;
_handsSystem.OnPlayerRemoveHand -= OnRemoveHand;
_handsSystem.OnPlayerHandsAdded -= LoadPlayerHands;
_handsSystem.OnPlayerHandsRemoved -= UnloadPlayerHands;
_handsSystem.OnPlayerHandBlocked -= HandBlocked;
_handsSystem.OnPlayerHandUnblocked -= HandUnblocked;
}
private void OnAddHand(string name, HandLocation location)
private void OnAddHand(Entity<HandsComponent> entity, string name, HandLocation location)
{
if (entity.Owner != _player.LocalEntity)
return;
AddHand(name, location);
}
private void OnRemoveHand(Entity<HandsComponent> entity, string name)
{
if (entity.Owner != _player.LocalEntity)
return;
RemoveHand(name);
}
private void HandPressed(GUIBoundKeyEventArgs args, SlotControl hand)
{
if (_playerHandsComponent == null)
{
if (!_handsSystem.TryGetPlayerHands(out var hands))
return;
}
if (args.Function == EngineKeyFunctions.UIClick)
{
_handsSystem.UIHandClick(_playerHandsComponent, hand.SlotName);
_handsSystem.UIHandClick(hands.Value, hand.SlotName);
args.Handle();
}
else if (args.Function == EngineKeyFunctions.UseSecondary)
@@ -122,33 +130,33 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
}
}
private void LoadPlayerHands(HandsComponent handsComp)
private void LoadPlayerHands(Entity<HandsComponent> handsComp)
{
DebugTools.Assert(_playerHandsComponent == null);
if (HandsGui != null)
HandsGui.Visible = true;
_playerHandsComponent = handsComp;
foreach (var (name, hand) in handsComp.Hands)
foreach (var (name, hand) in handsComp.Comp.Hands)
{
var handButton = AddHand(name, hand.Location);
if (_entities.TryGetComponent(hand.HeldEntity, out VirtualItemComponent? virt))
if (_handsSystem.TryGetHeldItem(handsComp.AsNullable(), name, out var held) &&
_entities.TryGetComponent(held, out VirtualItemComponent? virt))
{
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
else
{
handButton.SetEntity(hand.HeldEntity);
handButton.SetEntity(held);
handButton.Blocked = false;
}
}
var activeHand = handsComp.ActiveHand;
if (activeHand == null)
if (handsComp.Comp.ActiveHandId == null)
return;
SetActiveHand(activeHand.Name);
SetActiveHand(handsComp.Comp.ActiveHandId);
}
private void HandBlocked(string handName)
@@ -260,19 +268,21 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
if (HandsGui != null &&
_playerHandsComponent != null &&
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), handName, out var hand))
{
var foldedLocation = hand.Location.GetUILocation();
var heldEnt = _handsSystem.GetHeldItem((playerEntity, _playerHandsComponent), handName);
var foldedLocation = hand.Value.Location.GetUILocation();
if (foldedLocation == HandUILocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
HandsGui.UpdatePanelEntityLeft(heldEnt);
}
else
{
// Middle or right
_statusHandRight = handControl;
HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
HandsGui.UpdatePanelEntityRight(heldEnt);
}
HandsGui.SetHighlightHand(foldedLocation);
@@ -292,7 +302,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
button.Pressed += HandPressed;
if (!_handLookup.TryAdd(handName, button))
throw new Exception("Tried to add hand with duplicate name to UI. Name:" + handName);
return _handLookup[handName];
if (HandsGui != null)
{
@@ -362,6 +372,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
RemoveHand(handName, out _);
}
[PublicAPI]
private bool RemoveHand(string handName, out HandButton? handButton)
{
if (!_handLookup.TryGetValue(handName, out handButton))
@@ -377,7 +388,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_statusHandRight = null;
_handLookup.Remove(handName);
handButton.Dispose();
handButton.Orphan();
UpdateVisibleStatusPanels();
return true;
}

View File

@@ -329,9 +329,8 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
var player = _playerUid;
if (!control.MouseIsHovering ||
_playerInventory == null ||
!_entities.TryGetComponent<HandsComponent>(player, out var hands) ||
hands.ActiveHandEntity is not { } held ||
player == null ||
!_handsSystem.TryGetActiveItem(player.Value, out var held) ||
!_entities.TryGetComponent(held, out SpriteComponent? sprite) ||
!_inventorySystem.TryGetSlotContainer(player.Value, control.SlotName, out var container, out var slotDef))
{
@@ -342,12 +341,12 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
// Set green / red overlay at 50% transparency
var hoverEntity = _entities.SpawnEntity("hoverentity", MapCoordinates.Nullspace);
var hoverSprite = _entities.GetComponent<SpriteComponent>(hoverEntity);
var fits = _inventorySystem.CanEquip(player.Value, held, control.SlotName, out _, slotDef) &&
_container.CanInsert(held, container);
var fits = _inventorySystem.CanEquip(player.Value, held.Value, control.SlotName, out _, slotDef) &&
_container.CanInsert(held.Value, container);
if (!fits && _entities.TryGetComponent<StorageComponent>(container.ContainedEntity, out var storage))
{
fits = _entities.System<StorageSystem>().CanInsert(container.ContainedEntity.Value, held, out _, storage);
fits = _entities.System<StorageSystem>().CanInsert(container.ContainedEntity.Value, held.Value, out _, storage);
}
else if (!fits && _entities.TryGetComponent<ItemSlotsComponent>(container.ContainedEntity, out var itemSlots))
{
@@ -357,14 +356,14 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
if (!slot.InsertOnInteract)
continue;
if (!itemSlotsSys.CanInsert(container.ContainedEntity.Value, held, null, slot))
if (!itemSlotsSys.CanInsert(container.ContainedEntity.Value, held.Value, null, slot))
continue;
fits = true;
break;
}
}
_sprite.CopySprite((held, sprite), (hoverEntity, hoverSprite));
_sprite.CopySprite((held.Value, sprite), (hoverEntity, hoverSprite));
_sprite.SetColor((hoverEntity, hoverSprite), fits ? new Color(0, 255, 0, 127) : new Color(255, 0, 0, 127));
control.HoverSpriteView.SetEntity(hoverEntity);

View File

@@ -225,7 +225,7 @@ namespace Content.Client.Verbs
// is this a client exclusive (gui) verb?
ExecuteVerb(verb, user, GetEntity(target));
else
EntityManager.RaisePredictiveEvent(new ExecuteVerbEvent(target, verb));
RaisePredictiveEvent(new ExecuteVerbEvent(target, verb));
}
private void HandleVerbResponse(VerbsResponseEvent msg)

View File

@@ -178,7 +178,7 @@ public sealed partial class GunSystem : SharedGunSystem
if (_inputSystem.CmdStates.GetState(useKey) != BoundKeyState.Down && !gun.BurstActivated)
{
if (gun.ShotCounter != 0)
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
return;
}
@@ -190,7 +190,7 @@ public sealed partial class GunSystem : SharedGunSystem
if (mousePos.MapId == MapId.Nullspace)
{
if (gun.ShotCounter != 0)
EntityManager.RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
RaisePredictiveEvent(new RequestStopShootEvent { Gun = GetNetEntity(gunUid) });
return;
}
@@ -204,7 +204,7 @@ public sealed partial class GunSystem : SharedGunSystem
Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
EntityManager.RaisePredictiveEvent(new RequestShootEvent
RaisePredictiveEvent(new RequestShootEvent
{
Target = target,
Coordinates = GetNetCoordinates(coordinates),

View File

@@ -0,0 +1,12 @@
using System.IO;
namespace Content.IntegrationTests;
/// <summary>
/// Generic implementation of <see cref="ITestContextLike"/> for usage outside of actual tests.
/// </summary>
public sealed class ExternalTestContext(string name, TextWriter writer) : ITestContextLike
{
public string FullName => name;
public TextWriter Out => writer;
}

View File

@@ -0,0 +1,13 @@
using System.IO;
namespace Content.IntegrationTests;
/// <summary>
/// Something that looks like a <see cref="TestContext"/>, for passing to integration tests.
/// </summary>
public interface ITestContextLike
{
string FullName { get; }
TextWriter Out { get; }
}

View File

@@ -0,0 +1,12 @@
using System.IO;
namespace Content.IntegrationTests;
/// <summary>
/// Canonical implementation of <see cref="ITestContextLike"/> for usage in actual NUnit tests.
/// </summary>
public sealed class NUnitTestContextWrap(TestContext context, TextWriter writer) : ITestContextLike
{
public string FullName => context.Test.FullName;
public TextWriter Out => writer;
}

View File

@@ -13,6 +13,7 @@ using Robust.Server.Player;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
@@ -84,6 +85,7 @@ public sealed partial class TestPair : IAsyncDisposable
var returnTime = Watch.Elapsed;
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
State = PairState.Ready;
}
private async Task ResetModifiedPreferences()
@@ -104,7 +106,7 @@ public sealed partial class TestPair : IAsyncDisposable
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
State = PairState.CleanDisposed;
await OnCleanDispose();
State = PairState.Ready;
DebugTools.Assert(State is PairState.Dead or PairState.Ready);
PoolManager.NoCheckReturn(this);
ClearContext();
}

View File

@@ -182,24 +182,29 @@ public static partial class PoolManager
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
public static async Task<TestPair> GetServerClient(PoolSettings? poolSettings = null)
public static async Task<TestPair> GetServerClient(
PoolSettings? poolSettings = null,
ITestContextLike? testContext = null)
{
return await GetServerClientPair(poolSettings ?? new PoolSettings());
return await GetServerClientPair(
poolSettings ?? new PoolSettings(),
testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out));
}
private static string GetDefaultTestName(TestContext testContext)
private static string GetDefaultTestName(ITestContextLike testContext)
{
return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", "");
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
private static async Task<TestPair> GetServerClientPair(PoolSettings poolSettings)
private static async Task<TestPair> GetServerClientPair(
PoolSettings poolSettings,
ITestContextLike testContext)
{
if (!_initialized)
throw new InvalidOperationException($"Pool manager has not been initialized");
// Trust issues with the AsyncLocal that backs this.
var testContext = TestContext.CurrentContext;
var testOut = TestContext.Out;
var testOut = testContext.Out;
DieIfPoolFailure();
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);

View File

@@ -0,0 +1,65 @@
#nullable enable
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Actions;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.RetractableItemAction;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Actions;
public sealed class RetractableItemActionTest : InteractionTest
{
private static readonly EntProtoId ArmBladeActionProtoId = "ActionRetractableItemArmBlade";
/// <summary>
/// Gives the player the arm blade action, then activates it and makes sure they are given the blade.
/// Afterwards, uses the action again to retract the blade and makes sure their hand is empty.
/// </summary>
[Test]
public async Task ArmBladeActivateDeactivateTest()
{
var actionsSystem = Server.System<SharedActionsSystem>();
var handsSystem = Server.System<SharedHandsSystem>();
var playerUid = SEntMan.GetEntity(Player);
await Server.WaitAssertion(() =>
{
// Make sure the player's hand starts empty
var heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) at start of test.");
// Inspect the action prototype to find the item it spawns
var armBladeActionProto = ProtoMan.Index(ArmBladeActionProtoId);
// Find the component
Assert.That(armBladeActionProto.TryGetComponent<RetractableItemActionComponent>(out var actionComp, SEntMan.ComponentFactory));
// Get the item protoId from the component
var spawnedProtoId = actionComp!.SpawnedPrototype;
// Add the action to the player
var actionUid = actionsSystem.AddAction(playerUid, ArmBladeActionProtoId);
// Make sure the player has the action now
Assert.That(actionUid, Is.Not.Null, "Failed to add action to player.");
var actionEnt = actionsSystem.GetAction(actionUid);
// Make sure the player's hand is still empty
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is holding an item ({SEntMan.ToPrettyString(heldItem)}) after adding action.");
// Activate the arm blade
actionsSystem.PerformAction(ToServer(Player), actionEnt!.Value);
// Make sure the player is now holding the expected item
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Not.Null, $"Expected player to be holding {spawnedProtoId} but was holding nothing.");
AssertPrototype(spawnedProtoId, SEntMan.GetNetEntity(heldItem));
// Use the action again to retract the arm blade
actionsSystem.PerformAction(ToServer(Player), actionEnt.Value);
// Make sure the player's hand is empty again
heldItem = handsSystem.GetActiveItem((playerUid, Hands));
Assert.That(heldItem, Is.Null, $"Player is still holding an item ({SEntMan.ToPrettyString(heldItem)}) after second use.");
});
}
}

View File

@@ -293,9 +293,9 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(buckle.Buckled);
// With items in all hands
foreach (var hand in hands.Hands.Values)
foreach (var hand in hands.Hands.Keys)
{
Assert.That(hand.HeldEntity, Is.Not.Null);
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null);
}
var bodySystem = entityManager.System<BodySystem>();
@@ -316,9 +316,9 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(buckle.Buckled);
// Now with no item in any hand
foreach (var hand in hands.Hands.Values)
foreach (var hand in hands.Hands.Keys)
{
Assert.That(hand.HeldEntity, Is.Null);
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null);
}
buckleSystem.Unbuckle(human, human);

View File

@@ -1,7 +1,6 @@
using Content.Client.Chemistry.UI;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Chemistry;
using Content.Server.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
namespace Content.IntegrationTests.Tests.Chemistry;
@@ -19,7 +18,7 @@ public sealed class DispenserTest : InteractionTest
// Insert beaker
await InteractUsing("Beaker");
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
// Open BUI
await Interact();
@@ -29,18 +28,18 @@ public sealed class DispenserTest : InteractionTest
await SendBui(ReagentDispenserUiKey.Key, ev);
// Beaker is back in the player's hands
Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
// Re-insert the beaker
await Interact();
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
// Re-eject using the button directly instead of sending a BUI event. This test is really just a test of the
// bui/window helper methods.
await ClickControl<ReagentDispenserWindow>(nameof(ReagentDispenserWindow.EjectButton));
await RunTicks(5);
Assert.That(Hands.ActiveHandEntity, Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(Hands.ActiveHandEntity));
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Not.Null);
AssertPrototype("Beaker", SEntMan.GetNetEntity(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands))));
}
}

View File

@@ -0,0 +1,46 @@
using Content.Shared.Cloning;
namespace Content.IntegrationTests.Tests.Cloning;
public sealed class CloningSettingsPrototypeTest
{
/// <summary>
/// Checks that the components named in every <see cref="CloningSettingsPrototype"/> are valid components known to the server.
/// This is used instead of <see cref="ComponentNameSerializer"/> because we only care if the components are registered with the server,
/// and instead of a <see cref="ComponentRegistry"/> because we only need component names.
/// </summary>
[Test]
public async Task ValidatePrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ProtoMan;
var compFactory = server.EntMan.ComponentFactory;
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
var protos = protoMan.EnumeratePrototypes<CloningSettingsPrototype>();
foreach (var proto in protos)
{
foreach (var compName in proto.Components)
{
Assert.That(compFactory.TryGetRegistration(compName, out _),
$"Failed to find a component named {compName} for {nameof(CloningSettingsPrototype)} \"{proto.ID}\""
);
}
foreach (var eventCompName in proto.EventComponents)
{
Assert.That(compFactory.TryGetRegistration(eventCompName, out _),
$"Failed to find a component named {eventCompName} for {nameof(CloningSettingsPrototype)} \"{proto.ID}\""
);
}
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,72 @@
#nullable enable
using System.Linq;
using Content.Server.Objectives;
using Content.Shared.Mind;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
namespace Content.IntegrationTests.Tests.Commands;
public sealed class ObjectiveCommandsTest
{
private const string ObjectiveProtoId = "MindCommandsTestObjective";
private const string DummyUsername = "MindCommandsTestUser";
[TestPrototypes]
private const string Prototypes = $"""
- type: entity
id: {ObjectiveProtoId}
components:
- type: Objective
difficulty: 1
issuer: objective-issuer-syndicate
icon:
sprite: error.rsi
state: error
- type: DieCondition
""";
/// <summary>
/// Creates a dummy session, and assigns it a mind, then
/// tests using <c>addobjective</c>, <c>lsobjectives</c>,
/// and <c>rmobjective</c> on it.
/// </summary>
[Test]
public async Task AddListRemoveObjectiveTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var playerMan = server.ResolveDependency<ISharedPlayerManager>();
var mindSys = server.System<SharedMindSystem>();
var objectivesSystem = server.System<ObjectivesSystem>();
await server.AddDummySession(DummyUsername);
await server.WaitRunTicks(5);
var playerSession = playerMan.Sessions.Single();
Entity<MindComponent>? mindEnt = null;
await server.WaitPost(() =>
{
mindEnt = mindSys.CreateMind(playerSession.UserId);
});
Assert.That(mindEnt, Is.Not.Null);
var mindComp = mindEnt.Value.Comp;
Assert.That(mindComp.Objectives, Is.Empty, "Dummy player started with objectives.");
await pair.WaitCommand($"addobjective {playerSession.Name} {ObjectiveProtoId}");
Assert.That(mindComp.Objectives, Has.Count.EqualTo(1), "addobjective failed to increase Objectives count.");
await pair.WaitCommand($"lsobjectives {playerSession.Name}");
await pair.WaitCommand($"rmobjective {playerSession.Name} 0");
Assert.That(mindComp.Objectives, Is.Empty, "rmobjective failed to remove objective");
await pair.CleanReturnAsync();
}
}

View File

@@ -266,7 +266,7 @@ public sealed class SuicideCommandTests
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});
@@ -338,7 +338,7 @@ public sealed class SuicideCommandTests
await server.WaitPost(() =>
{
var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHandId!));
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
Assert.That(executionComponent, Is.Not.EqualTo(null));
});

View File

@@ -13,10 +13,10 @@ public sealed class WallConstruction : InteractionTest
{
await StartConstruction(Wall);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
ClientAssertPrototype(Girder, Target);
await InteractUsing(Steel, 2);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
AssertPrototype(WallSolid);
}

View File

@@ -28,7 +28,7 @@ namespace Content.IntegrationTests.Tests.Disposal
SubscribeLocalEvent<DoInsertDisposalUnitEvent>(ev =>
{
var (_, toInsert, unit) = ev;
var insertTransform = EntityManager.GetComponent<TransformComponent>(toInsert);
var insertTransform = Comp<TransformComponent>(toInsert);
// Not in a tube yet
Assert.That(insertTransform.ParentUid, Is.EqualTo(unit));
}, after: new[] { typeof(SharedDisposalUnitSystem) });

View File

@@ -53,20 +53,20 @@ public sealed class HandTests
var xform = entMan.GetComponent<TransformComponent>(player);
item = entMan.SpawnEntity("Crowbar", tSys.GetMapCoordinates(player, xform: xform));
hands = entMan.GetComponent<HandsComponent>(player);
sys.TryPickup(player, item, hands.ActiveHand!);
sys.TryPickup(player, item, hands.ActiveHandId!);
});
// run ticks here is important, as errors may happen within the container system's frame update methods.
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
await server.WaitPost(() =>
{
sys.TryDrop(player, item, null!);
sys.TryDrop(player, item);
});
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.Null);
Assert.That(sys.GetActiveItem((player, hands)), Is.Null);
await server.WaitPost(() => mapSystem.DeleteMap(data.MapId));
await pair.CleanReturnAsync();
@@ -105,10 +105,10 @@ public sealed class HandTests
player = playerMan.Sessions.First().AttachedEntity!.Value;
tSys.PlaceNextTo(player, item);
hands = entMan.GetComponent<HandsComponent>(player);
sys.TryPickup(player, item, hands.ActiveHand!);
sys.TryPickup(player, item, hands.ActiveHandId!);
});
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.EqualTo(item));
// Open then close the box to place the player, who is holding the crowbar, inside of it
var storage = server.System<EntityStorageSystem>();
@@ -125,12 +125,12 @@ public sealed class HandTests
// with the item not being in the player's hands
await server.WaitPost(() =>
{
sys.TryDrop(player, item, null!);
sys.TryDrop(player, item);
});
await pair.RunTicksSync(5);
var xform = entMan.GetComponent<TransformComponent>(player);
var itemXform = entMan.GetComponent<TransformComponent>(item);
Assert.That(hands.ActiveHandEntity, Is.Not.EqualTo(item));
Assert.That(sys.GetActiveItem((player, hands)), Is.Not.EqualTo(item));
Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform)));
await server.WaitPost(() => mapSystem.DeleteMap(map.MapId));

View File

@@ -120,18 +120,18 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task DeleteHeldEntity()
{
if (Hands.ActiveHandEntity is { } held)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is { } held)
{
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), null, false, true, Hands));
Assert.That(HandSys.TryDrop((SEntMan.GetEntity(Player), Hands), null, false, true));
SEntMan.DeleteEntity(held);
SLogger.Debug($"Deleting held entity");
});
}
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
}
/// <summary>
@@ -152,7 +152,7 @@ public abstract partial class InteractionTest
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
{
if (Hands.ActiveHand == null)
if (Hands.ActiveHandId == null)
{
Assert.Fail("No active hand");
return default;
@@ -169,7 +169,7 @@ public abstract partial class InteractionTest
{
var playerEnt = SEntMan.GetEntity(Player);
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHandId, false, false, false, Hands));
// turn on welders
if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
@@ -179,7 +179,7 @@ public abstract partial class InteractionTest
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(item));
if (enableToggleable && itemToggle != null)
Assert.That(itemToggle.Activated);
@@ -193,7 +193,7 @@ public abstract partial class InteractionTest
{
entity ??= Target;
if (Hands.ActiveHand == null)
if (Hands.ActiveHandId == null)
{
Assert.Fail("No active hand");
return;
@@ -212,11 +212,11 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryPickup(SEntMan.GetEntity(Player), uid.Value, Hands.ActiveHand, false, false, Hands, item));
Assert.That(HandSys.TryPickup(ToServer(Player), uid.Value, Hands.ActiveHandId, false, false, false, Hands, item));
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(uid));
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.EqualTo(uid));
}
/// <summary>
@@ -224,7 +224,7 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task Drop()
{
if (Hands.ActiveHandEntity == null)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) == null)
{
Assert.Fail("Not holding any entity to drop");
return;
@@ -232,11 +232,11 @@ public abstract partial class InteractionTest
await Server.WaitPost(() =>
{
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), handsComp: Hands));
Assert.That(HandSys.TryDrop((ToServer(Player), Hands)));
});
await RunTicks(1);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((ToServer(Player), Hands)), Is.Null);
}
#region Interact
@@ -246,7 +246,7 @@ public abstract partial class InteractionTest
/// </summary>
protected async Task UseInHand()
{
if (Hands.ActiveHandEntity is not { } target)
if (HandSys.GetActiveItem((ToServer(Player), Hands)) is not { } target)
{
Assert.Fail("Not holding any entity");
return;

View File

@@ -1,15 +1,12 @@
#nullable enable
using System.Linq;
using System.Numerics;
using Content.Client.Construction;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.IntegrationTests.Pair;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.Body.Part;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
@@ -135,10 +132,13 @@ public abstract partial class InteractionTest
- type: entity
id: InteractionTestMob
components:
- type: Body
prototype: Aghost
- type: DoAfter
- type: Hands
hands:
hand_right: # only one hand, so that they do not accidentally pick up deconstruction products
location: Right
sortedHands:
- hand_right
- type: ComplexInteraction
- type: MindContainer
- type: Stripping
@@ -230,20 +230,6 @@ public abstract partial class InteractionTest
SEntMan.DeleteEntity(old.Value);
});
// Ensure that the player only has one hand, so that they do not accidentally pick up deconstruction products
await Server.WaitPost(() =>
{
// I lost an hour of my life trying to track down how the hell interaction tests were breaking
// so greatz to this. Just make your own body prototype!
var bodySystem = SEntMan.System<BodySystem>();
var hands = bodySystem.GetBodyChildrenOfType(SEntMan.GetEntity(Player), BodyPartType.Hand).ToArray();
for (var i = 1; i < hands.Length; i++)
{
SEntMan.DeleteEntity(hands[i].Id);
}
});
// Change UI state to in-game.
var state = Client.ResolveDependency<IStateManager>();
await Client.WaitPost(() => state.RequestStateChange<GameplayState>());

View File

@@ -410,7 +410,7 @@ namespace Content.IntegrationTests.Tests.Networking
{
var uid = GetEntity(message.Uid);
var component = EntityManager.GetComponent<PredictionTestComponent>(uid);
var component = Comp<PredictionTestComponent>(uid);
var old = component.Foo;
if (Allow)
{

View File

@@ -17,7 +17,7 @@ public sealed class TileConstructionTests : InteractionTest
await SetTile(null);
await InteractUsing(Rod);
await AssertTile(Lattice);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await InteractUsing(Cut);
await AssertTile(null);
await AssertEntityLookup((Rod, 1));
@@ -49,7 +49,7 @@ public sealed class TileConstructionTests : InteractionTest
AssertGridCount(1);
// Cut lattice
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await InteractUsing(Cut);
await AssertTile(null);
AssertGridCount(0);
@@ -83,13 +83,13 @@ public sealed class TileConstructionTests : InteractionTest
// Lattice -> Plating
await InteractUsing(FloorItem);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await AssertTile(Plating);
AssertGridCount(1);
// Plating -> Tile
await InteractUsing(FloorItem);
Assert.That(Hands.ActiveHandEntity, Is.Null);
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
await AssertTile(Floor);
AssertGridCount(1);

View File

@@ -13,6 +13,7 @@ public sealed class CommandLineArguments
public string OutputPath { get; set; } = DirectoryExtensions.MapImages().FullName;
public bool ArgumentsAreFileNames { get; set; } = false;
public bool ShowMarkers { get; set; } = false;
public bool OutputParallax { get; set; } = false;
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArguments? parsed)
{
@@ -70,7 +71,17 @@ public sealed class CommandLineArguments
PrintHelp();
return false;
case "--parallax":
parsed.OutputParallax = true;
break;
default:
if (argument.StartsWith('-'))
{
Console.WriteLine($"Unknown argument: {argument}");
return false;
}
parsed.Maps.Add(argument);
break;
}
@@ -95,7 +106,6 @@ Options:
Defaults to: png
--viewer
Causes the map renderer to create the map.json files required for use with the map viewer.
Also puts the maps in the required directory structure.
-o / --output <output path>
Changes the path the rendered maps will get saved to.
Defaults to Resources/MapImages
@@ -104,6 +114,8 @@ Options:
Example: Content.MapRenderer -f /Maps/box.yml /Maps/bagel.yml
-m / --markers
Show hidden markers on map render. Defaults to false.
--parallax
Output images and data used for map viewer parallax.
-h / --help
Displays this help text");
}

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Maths;
using System.Text.Json.Serialization;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.MapRenderer;
@@ -43,31 +45,31 @@ public sealed class LayerGroup
public GroupSource Source { get; set; } = new();
public List<Layer> Layers { get; set; } = new();
public static LayerGroup DefaultParallax()
public static LayerGroup DefaultParallax(IResourceManager resourceManager, ParallaxOutput output)
{
return new LayerGroup
{
Scale = new Position(0.1f, 0.1f),
Source = new GroupSource
{
Url = "https://i.imgur.com/3YO8KRd.png",
Extent = new Extent(6000, 4000)
Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer1.png")),
Extent = new Extent(6000, 4000),
},
Layers = new List<Layer>
{
new()
{
Url = "https://i.imgur.com/IannmmK.png"
Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer1.png")),
},
new()
{
Url = "https://i.imgur.com/T3W6JsE.png",
Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer2.png")),
Composition = "lighter",
ParallaxScale = new Position(0.2f, 0.2f)
},
new()
{
Url = "https://i.imgur.com/T3W6JsE.png",
Url = output.ReferenceResourceFile(resourceManager, new ResPath("/Textures/Parallaxes/layer3.png")),
Composition = "lighter",
ParallaxScale = new Position(0.3f, 0.3f)
}
@@ -91,9 +93,13 @@ public sealed class Layer
public readonly struct Extent
{
[JsonInclude]
public readonly float X1;
[JsonInclude]
public readonly float Y1;
[JsonInclude]
public readonly float X2;
[JsonInclude]
public readonly float Y2;
public Extent()
@@ -123,7 +129,9 @@ public readonly struct Extent
public readonly struct Position
{
[JsonInclude]
public readonly float X;
[JsonInclude]
public readonly float Y;
public Position(float x, float y)

View File

@@ -1,149 +1,158 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.IO;
using System.Threading.Tasks;
using Content.Client.Markers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.GameTicking;
using Robust.Client.GameObjects;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Content.MapRenderer.Painters
{
public sealed class MapPainter
public sealed class MapPainter : IAsyncDisposable
{
public static async IAsyncEnumerable<RenderedGridImage<Rgba32>> Paint(string map,
bool mapIsFilename = false,
bool showMarkers = false)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
private readonly RenderMap _map;
private readonly ITestContextLike _testContextLike;
await using var pair = await PoolManager.GetServerClient(new PoolSettings
private TestPair? _pair;
private Entity<MapGridComponent>[] _grids = [];
public MapPainter(RenderMap map, ITestContextLike testContextLike)
{
_map = map;
_testContextLike = testContextLike;
}
public async Task Initialize()
{
var stopwatch = RStopwatch.StartNew();
var poolSettings = new PoolSettings
{
DummyTicker = false,
Connected = true,
Destructive = true,
Fresh = true,
// Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time.
Map = mapIsFilename ? "Empty" : map,
});
var server = pair.Server;
var client = pair.Client;
Map = _map is RenderMapPrototype prototype ? prototype.Prototype : PoolManager.TestMap,
};
_pair = await PoolManager.GetServerClient(poolSettings, _testContextLike);
Console.WriteLine($"Loaded client and server in {(int)stopwatch.Elapsed.TotalMilliseconds} ms");
stopwatch.Restart();
var cEntityManager = client.ResolveDependency<IClientEntityManager>();
var cPlayerManager = client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
await client.WaitPost(() =>
if (_map is RenderMapFile mapFile)
{
if (cEntityManager.TryGetComponent(cPlayerManager.LocalEntity, out SpriteComponent? sprite))
using var stream = File.OpenRead(mapFile.FileName);
await _pair.Server.WaitPost(() =>
{
cEntityManager.System<SpriteSystem>().SetVisible((cPlayerManager.LocalEntity.Value, sprite), false);
var loadOptions = new MapLoadOptions
{
// Accept loading both maps and grids without caring about what the input file truly is.
DeserializationOptions =
{
LogOrphanedGrids = false,
},
};
if (!_pair.Server.System<MapLoaderSystem>().TryLoadGeneric(stream, mapFile.FileName, out var loadResult, loadOptions))
throw new IOException($"File {mapFile.FileName} could not be read");
_grids = loadResult.Grids.ToArray();
});
}
}
public async Task SetupView(bool showMarkers)
{
if (_pair == null)
throw new InvalidOperationException("Instance not initialized!");
await _pair.Client.WaitPost(() =>
{
if (_pair.Client.EntMan.TryGetComponent(_pair.Client.PlayerMan.LocalEntity, out SpriteComponent? sprite))
{
_pair.Client.System<SpriteSystem>()
.SetVisible((_pair.Client.PlayerMan.LocalEntity.Value, sprite), false);
}
});
if (showMarkers)
await pair.WaitClientCommand("showmarkers");
{
await _pair.Client.WaitPost(() =>
{
_pair.Client.System<MarkerSystem>().MarkersVisible = true;
});
}
}
public async Task<MapViewerData> GenerateMapViewerData(ParallaxOutput? parallaxOutput)
{
if (_pair == null)
throw new InvalidOperationException("Instance not initialized!");
var mapShort = _map.ShortName;
string fullName;
if (_map is RenderMapPrototype prototype)
{
fullName = _pair.Server.ProtoMan.Index(prototype.Prototype).MapName;
}
else
{
fullName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(mapShort);
}
var mapViewerData = new MapViewerData
{
Id = mapShort,
Name = fullName,
};
if (parallaxOutput != null)
{
await _pair.Client.WaitPost(() =>
{
var res = _pair.Client.InstanceDependencyCollection.Resolve<IResourceManager>();
mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax(res, parallaxOutput));
});
}
return mapViewerData;
}
public async IAsyncEnumerable<RenderedGridImage<Rgba32>> Paint()
{
if (_pair == null)
throw new InvalidOperationException("Instance not initialized!");
var client = _pair.Client;
var server = _pair.Server;
var sEntityManager = server.ResolveDependency<IServerEntityManager>();
var sPlayerManager = server.ResolveDependency<IPlayerManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entityManager.System<MapLoaderSystem>();
var mapSys = entityManager.System<SharedMapSystem>();
var deps = server.ResolveDependency<IEntitySystemManager>().DependencyCollection;
Entity<MapGridComponent>[] grids = [];
if (mapIsFilename)
{
var resPath = new ResPath(map);
if (!mapLoader.TryReadFile(resPath, out var data))
throw new IOException($"File {map} could not be read");
var ev = new BeforeEntityReadEvent();
server.EntMan.EventBus.RaiseEvent(EventSource.Local, ev);
var deserializer = new EntityDeserializer(deps,
data,
DeserializationOptions.Default,
ev.RenamedPrototypes,
ev.DeletedPrototypes);
if (!deserializer.TryProcessData())
{
throw new IOException($"Failed to process entity data in {map}");
}
if (deserializer.Result.Category == FileCategory.Unknown && deserializer.Result.Version < 7)
{
var mapCount = 0;
var gridCount = 0;
foreach (var (entId, ent) in deserializer.YamlEntities)
{
if (ent.Components != null && ent.Components.ContainsKey("MapGrid"))
{
gridCount++;
}
if (ent.Components != null && ent.Components.ContainsKey("Map"))
{
mapCount++;
}
}
if (mapCount == 1)
deserializer.Result.Category = FileCategory.Map;
else if (mapCount == 0 && gridCount == 1)
deserializer.Result.Category = FileCategory.Grid;
}
switch (deserializer.Result.Category)
{
case FileCategory.Map:
await server.WaitPost(() =>
{
if (mapLoader.TryLoadMap(resPath, out _, out var loadedGrids))
{
grids = loadedGrids.ToArray();
}
});
break;
case FileCategory.Grid:
await server.WaitPost(() =>
{
if (mapLoader.TryLoadGrid(resPath, out _, out var loadedGrids))
{
grids = [(Entity<MapGridComponent>)loadedGrids];
}
});
break;
default:
throw new IOException($"Unknown category {deserializer.Result.Category}");
}
}
await pair.RunTicksSync(10);
await _pair.RunTicksSync(10);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
var sMapManager = server.ResolveDependency<IMapManager>();
@@ -162,23 +171,23 @@ namespace Content.MapRenderer.Painters
sEntityManager.DeleteEntity(playerEntity.Value);
}
if (!mapIsFilename)
if (_map is RenderMapPrototype)
{
var mapId = sEntityManager.System<GameTicker>().DefaultMap;
grids = sMapManager.GetAllGrids(mapId).ToArray();
_grids = sMapManager.GetAllGrids(mapId).ToArray();
}
foreach (var (uid, _) in grids)
foreach (var (uid, _) in _grids)
{
var gridXform = xformQuery.GetComponent(uid);
xformSystem.SetWorldRotation(gridXform, Angle.Zero);
}
});
await pair.RunTicksSync(10);
await _pair.RunTicksSync(10);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
foreach (var (uid, grid) in grids)
foreach (var (uid, grid) in _grids)
{
var tiles = mapSys.GetAllTiles(uid, grid).ToList();
if (tiles.Count == 0)
@@ -219,16 +228,20 @@ namespace Content.MapRenderer.Painters
yield return renderedImage;
}
}
// We don't care if it fails as we have already saved the images.
try
{
await pair.CleanReturnAsync();
}
catch
{
// ignored
}
public async Task CleanReturnAsync()
{
if (_pair == null)
throw new InvalidOperationException("Instance not initialized!");
await _pair.CleanReturnAsync();
}
public async ValueTask DisposeAsync()
{
if (_pair != null)
await _pair.DisposeAsync();
}
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.IO;
using Robust.Shared.ContentPack;
using Robust.Shared.Utility;
namespace Content.MapRenderer;
/// <summary>
/// Helper class for collecting the files used for parallax output
/// </summary>
public sealed class ParallaxOutput
{
public const string OutputDirectory = "_parallax";
public readonly HashSet<ResPath> FilesToCopy = [];
private readonly string _outputPath;
/// <summary>
/// Helper class for collecting the files used for parallax output
/// </summary>
public ParallaxOutput(string outputPath)
{
_outputPath = outputPath;
Directory.CreateDirectory(Path.Combine(_outputPath, OutputDirectory));
}
public string ReferenceResourceFile(IResourceManager resourceManager, ResPath path)
{
var fileName = Path.Combine(OutputDirectory, path.Filename);
if (FilesToCopy.Add(path))
{
using var file = resourceManager.ContentFileRead(path);
using var target = File.Create(Path.Combine(_outputPath, fileName));
file.CopyTo(target);
}
return fileName;
}
}

View File

@@ -3,12 +3,11 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Text.Json;
using System.Threading.Tasks;
using Content.IntegrationTests;
using Content.MapRenderer.Painters;
using Content.Server.Maps;
using Newtonsoft.Json;
using Robust.Shared.Prototypes;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
@@ -21,20 +20,19 @@ namespace Content.MapRenderer
private static readonly Func<string, string> ChosenMapIdNotIntMessage = id => $"The chosen id is not a valid integer: {id}";
private static readonly Func<int, string> NoMapFoundWithIdMessage = id => $"No map found with chosen id: {id}";
private static readonly MapPainter MapPainter = new();
internal static async Task Main(string[] args)
{
if (!CommandLineArguments.TryParse(args, out var arguments))
return;
var testContext = new ExternalTestContext("Content.MapRenderer", Console.Out);
PoolManager.Startup();
if (arguments.Maps.Count == 0)
{
Console.WriteLine("Didn't specify any maps to paint! Loading the map list...");
await using var pair = await PoolManager.GetServerClient();
await using var pair = await PoolManager.GetServerClient(testContext: testContext);
var mapIds = pair.Server
.ResolveDependency<IPrototypeManager>()
.EnumeratePrototypes<GameMapPrototype>()
@@ -104,45 +102,118 @@ namespace Content.MapRenderer
Console.WriteLine($"Selected maps: {string.Join(", ", selectedMapPrototypes)}");
}
var maps = new List<RenderMap>();
if (arguments.ArgumentsAreFileNames)
{
Console.WriteLine("Retrieving maps by file names...");
//
// Handle legacy command line processing:
// Ideally, people pass file names that are relative to the process working directory.
// i.e. regular command-line behavior.
//
// However, the map renderer was originally written to only handle gameMap prototypes,
// so it would actually go through the list of prototypes and match file name arguments
// via a *very* coarse check.
//
// So if we have any input filenames that don't exist... we run the old behavior.
// Yes by the way this means a typo means spinning up an entire integration pool pair
// before the map renderer can report a proper failure.
//
// Note that this legacy processing is very important! The map server currently relies on it,
// because it wants to work with file names, but we *need* to resolve the input to a prototype
// to properly export viewer JSON data.
//
var lookupPrototypeFiles = new List<string>();
foreach (var map in arguments.Maps)
{
if (File.Exists(map))
{
maps.Add(new RenderMapFile { FileName = map });
}
else
{
lookupPrototypeFiles.Add(map);
}
}
if (lookupPrototypeFiles.Count > 0)
{
Console.Write($"Following map files did not exist on disk directly, searching through prototypes: {string.Join(", ", lookupPrototypeFiles)}");
await using var pair = await PoolManager.GetServerClient();
var mapPrototypes = pair.Server
.ResolveDependency<IPrototypeManager>()
.EnumeratePrototypes<GameMapPrototype>()
.ToArray();
foreach (var toFind in lookupPrototypeFiles)
{
foreach (var mapPrototype in mapPrototypes)
{
if (mapPrototype.MapPath.Filename == toFind)
{
maps.Add(new RenderMapPrototype { Prototype = mapPrototype, });
Console.WriteLine($"Found matching map prototype: {mapPrototype.MapName}");
goto found;
}
}
await Console.Error.WriteLineAsync($"Found no map prototype for file '{toFind}'!");
found: ;
}
}
}
else
{
foreach (var map in arguments.Maps)
{
maps.Add(new RenderMapPrototype { Prototype = map });
}
}
await Run(arguments);
await Run(arguments, maps, testContext);
PoolManager.Shutdown();
}
private static async Task Run(CommandLineArguments arguments)
private static async Task Run(
CommandLineArguments arguments,
List<RenderMap> toRender,
ExternalTestContext testContext)
{
Console.WriteLine($"Creating images for {arguments.Maps.Count} maps");
Console.WriteLine($"Creating images for {toRender.Count} maps");
var parallaxOutput = arguments.OutputParallax ? new ParallaxOutput(arguments.OutputPath) : null;
var mapNames = new List<string>();
foreach (var map in arguments.Maps)
foreach (var map in toRender)
{
Console.WriteLine($"Painting map {map}");
var mapViewerData = new MapViewerData
{
Id = map,
Name = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(map)
};
await using var painter = new MapPainter(map, testContext);
await painter.Initialize();
await painter.SetupView(showMarkers: arguments.ShowMarkers);
mapViewerData.ParallaxLayers.Add(LayerGroup.DefaultParallax());
var directory = Path.Combine(arguments.OutputPath, Path.GetFileNameWithoutExtension(map));
var mapViewerData = await painter.GenerateMapViewerData(parallaxOutput);
var mapShort = map.ShortName;
var directory = Path.Combine(arguments.OutputPath, mapShort);
mapNames.Add(mapShort);
var i = 0;
try
{
await foreach (var renderedGrid in MapPainter.Paint(map,
arguments.ArgumentsAreFileNames,
arguments.ShowMarkers))
await foreach (var renderedGrid in painter.Paint())
{
var grid = renderedGrid.Image;
Directory.CreateDirectory(directory);
var fileName = Path.GetFileNameWithoutExtension(map);
var savePath = $"{directory}{Path.DirectorySeparatorChar}{fileName}-{i}.{arguments.Format}";
var savePath = $"{directory}{Path.DirectorySeparatorChar}{mapShort}-{i}.{arguments.Format}";
Console.WriteLine($"Writing grid of size {grid.Width}x{grid.Height} to {savePath}");
@@ -167,9 +238,7 @@ namespace Content.MapRenderer
grid.Dispose();
mapViewerData.Grids.Add(new GridLayer(renderedGrid, Path.Combine(map, Path.GetFileName(savePath))));
mapNames.Add(fileName);
mapViewerData.Grids.Add(new GridLayer(renderedGrid, Path.Combine(mapShort, Path.GetFileName(savePath))));
i++;
}
}
@@ -182,8 +251,17 @@ namespace Content.MapRenderer
if (arguments.ExportViewerJson)
{
var json = JsonConvert.SerializeObject(mapViewerData);
await File.WriteAllTextAsync(Path.Combine(arguments.OutputPath, map, "map.json"), json);
var json = JsonSerializer.Serialize(mapViewerData);
await File.WriteAllTextAsync(Path.Combine(directory, "map.json"), json);
}
try
{
await painter.CleanReturnAsync();
}
catch (Exception e)
{
Console.WriteLine($"Exception while shutting down painter: {e}");
}
}

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