Upstream sync (#1779)

* Half a commit

* requested changes

* variable alias

* Combat box now huge. Reorganize order of abstract, parent, id, and name. Replaced "type: Food" with "type: Edible".

* Removed redundant edible components.

* Removed crazy burger from happyhonk fill table.

* Updated Aseprite Tools (#39358)

Modified aesprite tools

* Amber Station - Added Late Join and Pressure Update (#39943)

Added relief valve to amber

* Marathon - Pressure Update (#39955)

* Pressure update for marathon

* Fixed an issue

* Automatic changelog update

* Automatic changelog update

* Box Station - Pressure Update (#39954)

* Modified the burn chamber

* Fixed a couple things

* Automatic changelog update

* Bagel Station - Pressure Update (#39945)

redid the whole atmos

* Automatic changelog update

* Remove a default Cyborg name (#39948)

* 1984 clanker

* Fix prototype

* Stop Sentience Event targeting Zombified Creatures (#39950)

* Strip Target From Zed

* Update RandomSentienceRule.cs

* Update RandomSentienceRule.cs

* Update RandomSentienceRule.cs

* Update RandomSentienceRule.cs

* Update RandomSentienceRule.cs

* Automatic changelog update

* Sentry turrets - Part 7: Electronics and construction graphs (#35236)

* Initial commit

* Fixing merge conflict

* Updated for deployment

* Whitespace fixes

* Linter fixes

* Test fail fix

* Fixed test failure

* Add separate command circuitboards

* Addressed review comments

* Small Status Effect Cleanup (#39944)

Heroic

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

* Add SnoutCover appearance layer (#39949)

* init

* cover

* Fixed changelog error (#39971)

* Don't network ZombifyOnDeathComponent and ZombieImmuneComponent (#39963)

no networking

* [STAGING] 1984 Derelict Syndicate Borgs (#39978)

1984

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

* Fix forensic scanner leaking fingerprints onto the scanning object if you use the verb (#39964)

Update ForensicScannerSystem.cs

* Automatic changelog update

* Scurrets - can wear pet bags, mail bags and spears (#38774)

* Scurrets - can wear pet bags, mail bags and spears

* a

* Resolve PR comments

* Automatic changelog update

* Messy drinker immunity and cleanup (#39989)

init

* SharedKitchenSpikeSystem bugfixes (#39959)

* Fixes

* Update

* Update

* Stop derelict borgs from duplicating their ghost roles. (#39992)

Add reregister to derelict borgs

* Automatic changelog update

* Clown bags squeak when inserting items (#39931)

squeak!

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

* Automatic changelog update

* Bagel AI Turrets + Camera Coverage (#39968)

* Automatic changelog update

* Marathon AI Turrets + Camera Coverage (#39969)

* Automatic changelog update

* Nullable messydrinker tag (#40002)

init

* Update Credits (#40005)

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

* Removed unused asset from devmap (#39974)

* Migrate all mechs to PartAssembly and remove legacy MechAssemblySystem (#39027)

* Removed the old MechAssembly system and component.
Converted all mechs to use the unified PartAssembly system.
Removed dismantling mechs during assembly logic to simplify the code.

* Delete Chassis via migration

* Automatic changelog update

* improve spawnpoint error logging (#40021)

* TriggerOnMeleeHit and more (#39826)

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

* Fix docstring typo starts -> stops (#40031)

* set slot priority to 4

* Automatic changelog update

* Predict InjectorSystem (#39976)

* predict injectors

* hide verbs if no options

* Automatic changelog update

* Fix incorrect bullet & cartridge names (#39993)

* Fix conflicting names

* zero zeroes

* Texture Scaling for clothing (#39714)

scale

* Lizard Tails Can Be Hidden By Clothing (#40026)

Taken from @TiniestShark's PR.

* Fixed Corpsman Name (finally) (#40055)

Fixed Corpsman name (finally)

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

* no utensil

* Event Shuttle Fixes (#40059)

* Automatic changelog update

* Update 4 visitor shuttles & nanomed inventories (#39718)

* Automatic changelog update

* Fix radiation vomit for dead mobs (#40020)

* Fix Radiation Vomit for dead mobs

* Update Content.Server/Destructible/Thresholds/Behaviors/VomitBehavior.cs

Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>

* Fix Radiation Vomit for dead mobs

* Fix Radiation Vomit system for dead mobs

* refactors

* Adding mobStateSystem for validation

* refactor

* Unrelated cleanup

---------

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

* Automatic changelog update

* Remove empty `drink-component.ftl` file (#40064)

Probably had to be removed in #39031

* Add myself to Codeowners for Stunnable and Nutrition (#40061)

* I probably should do this

* Can't cut it up that easy cause of the events. Guess I'm just subscribed to all nutrition stuff ;_;

---------

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

* prevent double-mapping lights (#39939)

works?

* Adds a secHUD to the noir-tech glasses (#39859)

* Adds a SecHUD to the noir-tech glasses

* Adds a Security HUD to the noir-tech glasses

* Revert "Adds a Security HUD to the noir-tech glasses"

This reverts commit 68d7b9b6e190618e44a94df71cf311e056892392.

* Adds a security HUD to the noir-tech glasses

* Automatic changelog update

* Fix benchmark (#40039)

* Make git hooks work in git worktrees (#40038)

Make hooks work in worktrees and cleanup hooks

* Localize, cleanup, and LEC round control commands. (#38812)

* commit-progress

* commit

* Add CVar for disabling loadout item role timers (#36775)

* Don't show item dropping popup when wielding. (#40032)

silence

* Trimmed Sentience Targets from Corgis Smile and Cockroaches (#39810)

* Update animals.yml

* Update pets.yml

* Removed Sentience Target from corgi

* Update random-sentience.ftl

* Obliterated Scurret from .ftl

* Automatic changelog update

* Updated inspector description to reflect functionality (#40072)

update inspector desc.

Co-authored-by: Quasr <~182430031+quasr-9@users.noreply.github.com>

* Helm + Mask Displacements for Reptilians (and some unique helmets) (#39351)

* Liz displacements + unique helms

* small fix

* Couple of small fixes

* Reptilian tail sprites for hard/softsuits (#35842)

* Reptilian helmets and tail sprites for hard/softsuits (and a few others)!

* Removed the new fins from helmets.

* don't know how the caustic resistances got doubled? oops

* Update pirate helm sprites for lizards

* fixin errors

* ugh

* softsuit updates

* okay maybe this works now

* Corgi fixes. Boy it sure would be nice to not have to do this again.

* please work

* Helmets removed, will be done in another PR.

* missed a few.

* evil punctuation killed

* scream

* Re-parented clothingvisuals.

* Suits modified with tails

* oops chaplain tail was misnamed

* a

* one more time

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

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

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

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

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

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

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

* oops chaplain lost their suit

* Small fix

* roll back unrelated fix

---------

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

* Automatic changelog update

* Some more vox customization (#39083)

* Added docked and spiked tails, added talisman for vox

* Tail Talisman ftl

* adjusts talisman to be its own tail to avoid having to deal with other tails at all

* vox tail stuff

* Added amputated tail (courtesy of flareguy) and split tail

* adds TODO regarding marking conditionals

* rsi run check pls stop screaming at me

* Shelving the talisman for later, keeping the sprites around

* Add RSI credits

* Remove unnecessary files

* renamed amputated to vestigial

* Automatic changelog update

* Add inhand sprites to Cartons and Cups, give new inhands to Cans. (#39814)

* Added inhand sprites to cans, cartons and cups.

* small tweak

* Burger Inhands (#39894)

burger is in your hands

* Adjust bureaucratic error to prevent only passenger being available (#40001)

Fire all the interns

* Cleanup AddPolymorphActionCommand and LEC conversion. (#38853)

commit

* Automatic changelog update

* Automatic changelog update

* Use a fixed amount of decimal points in gas analyzer window (#40081)

This prevents the volume and temperature labels from changing width when the value lands on integer values, making the text easier to read.

Co-authored-by: opl <4833621+opl@users.noreply.github.com>

* Berry Delight recipe edit (#40085)

berry delight change

* Automatic changelog update

* Don't enqueue construction events without validation (#39869)

* Remove unused combat-equipped-helmet (#40095)

Remove unused combat-equipped-helmet and combat-equipped-helmet-dog

* Atmos Delta-Pressure Window Shattering (#39238)

This PR adds delta-pressure damage. In short, airtight structures can now take damage proportional to the difference in pressures between the sides of the structure.

* Automatic changelog update

* Give inflatable walls the DeltaPressure component (#40098)

give inflatable walls the deltapressure component

* Automatic changelog update

* Add heat distortion shader for hot gases (#39107)

* Automatic changelog update

* Fix QM Golden Knuckledusters not being a objective (#40096)

test

* Automatic changelog update

* Telepad Label Fix (#39975)

Fixed telapad order labels showing wrong account

* Automatic changelog update

* Fool players with decoy presets (#40053)

* added secret gamepresets

* cut down on alias

* remove all secret presets

* change the command to allow for a secret argument

* update test

* moved the secret argument after the number of rounds argument

* added completions

* localization and use of CompletionHelper.Booleans

* command now has a option for a decoy preset

* fixed decoy message in the end

* ops

* clean up

* hint 2

* improve localization

---------

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

* Automatic changelog update

* Fix: Ability to open AHelp in the lobby by pressing the hotkey (#39525)

* Fix

* Update

* Add 2.25 second delay to scurret petting (#40097)

Add 2.25 second delay to scurret petting rate

* Automatic changelog update

* Stop packaging `Resources/ServerInfo` and `Resources/Changelog` on the server (#39897)

* Stop packaging `Resources/ServerInfo`

This is only used by the client, it is unneccery to pack into the server. Plus it keeps getting people to think that just editing the server resources will modify the guidebook even though that needs a custom dev enviroment.

* Add credits too

* Package win-arm64 and osx-arm64 servers (#40113)

* Improve Gas Yaml Serialization (#40070)

* Make yaml gas serialization cleaner

* fix exception

* fix validation code

* rudimentary test & permissive loading

* change it a bit

* Test fixes and adjustments

* Organize JobIconPrototype yml (#39774)

grouped

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

* Dynamic anomaly scanner texture (#37585)

* Automatic changelog update

* Reuse lathe queue instead of redrawing (#39886)

* init

* init

* PUSH!!!

* //

* Me when the when the me when the

* review

---------

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

* Automatic changelog update

* Skip MapImages folder in packaging (#38928)

* Can't crawl over counters (#40099)

Counters and Tables separation

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

* Automatic changelog update

* Fixes Theobromine missing from Iced Coffee (#40063)

adding ice to coffee no longer removes its theobromine

* Automatic changelog update

* Fix xenoborg action icons (#40118)

commit

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

* Fixed a error in the "Adventures of Ian and Renault" books (#39932)

Fixed a publication error in the "Adventures of Ian and Renault" book series.

The editor of these books have been taken out back and shot.

* Fix DeltaPressure damage not capping beyond a certain pressure (#40125)

* Automatic changelog update

* Give shutters the DeltaPressure component (#40126)

* Automatic changelog update

* Alerts Cleanup and API (#39544)

* alert cleanup and API

* I expect update loops to be at the top.

* Address review

* Address review x 2

* Merg my PR

* Fix

* Update Content.Shared/Alert/AlertsSystem.cs

webedit

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

* FIX THAT TEST FAIL!!!!

* Me when I forget to actually give you alerts

* Hammedborgar

---------

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

* fix a typo in the comments for game.ipintel_exempt_playtime (#40129)

* Vulpkanin Species (#37539)

* [April Fools] Elf species  (#35353)

elf

* [April Fools] Juice that makes me go insane (#35370)

* kill me

* MAKE IT STOP

* is it finally over?

* web edit

* webedit

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

* webedit

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

* webedit

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

* webedit

* WEBEDIT PLEASE

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

---------

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

* vulpkanin species

the ultimate april fools joke

Co-Authored-By: Kr8art <188977876+kr8art@users.noreply.github.com>

* cant be bothered

* make build and test debug run on the april fools branch (#35396)

welp

* Revert "[April Fools] Juice that makes me go insane (#35370)"

This reverts commit fcbdcf8452cb1247733242aa44ea5b9f38dfa3d2.

* Revert "[April Fools] Elf species  (#35353)"

This reverts commit 704293a051033026bfca0c9e161d97796bc0cb81.

* nuh uh

* localized names

* fix survival box

* abilities (Mostly don't work yet, pending PRs)

* wagging loc

* slight heat tweaks

* stuff

* Update Vulpkanin.xml

* vulp screams + quieter howl

* MessyDrinker by Orks

* MessyDrinker guidebook

* animal bones

* fix wagging

* missed

* no loc

* Lower damage, remove butchering

* Revert "animal bones"

This reverts commit 3457cde13e57336678f62a20bf57fb315844c240.

* leap

* collision

* leap desc update

* leap guidebook

* fix leap

* microbalance

* comment

* microbalancing pt 2

* microbalancing pt 3

* test fail fix (i hope)

* fix organs

* no leaping on the ground

* remove SolutionScanner + ling work

* fix clone

* add stuff back

* microbalancing pt yes

* male vulp names

mainly slavic names with some similiar sounding ones mixed in

* Revert "male vulp names"

This reverts commit 1666f81821bfea241262627160a594dd1b0ce050.

* male vulps names pr 2 (slavic, dog names, few puns)

Slavic names, with mixed in typical dog names and some puns

* female names (mainly slavic, other stuff mixed in)

* oops

* Update vulpkanin.yml

* Vulps WIP Work (#1)

* Modified aesprite tools

* WIP

* wipperoni

* We are done here, wawa

* Revert "Merge branch 'aseprite-tools' into hannah/vulps-wip"

This reverts commit d258645df60a94d0217fecd85a38545f2b951cde, reversing
changes made to 0ae39e862152ef6548533eba0547709594c55e90.

* Resolve merge issue

* Fix Urist McVulp forgetting his fursuit

---------

Co-authored-by: Southbridge-fur <southbridgefur@gmail.com>

* re-add ears and husk overlay

* revert adding ears to vox

* Crest Markings

* Fade Markings

* Sharp Markings

* microbalance + comments

* censor character nudity option compliance

* Ear Markings

* 1984 wagging animations

* Tail Markings

* tail wag icon

* annoyance

* guidebook

* slight temperature tweak

* seperate out snouts, port over 2 markings for testing

* fix

* seperate snout file

* Snout/Head Markings

* cleanup + locale for ears

* Head and Snout loc

* gray eyes

* crest and husk cleanup

* Tail and Chest loc

* Legs and Arms loc

* most important commit

* mime vulps like rations too

* missing loc

* microbalance

* not needed

* goatee

* white eyes

* harmony feedback tail fixes

* personal changes and fixes after playing

* fix fluffy tail clipping

* Sprite fixes, displacement fixes (#2)

* Tune head displacement

* Vulps Sprite Tuning

* helmets

* detail

* Urist

* wag

* comban't

* fix attributions

* remove vulp biosuits

* fix vulp beards

* fixed up ears

* comment note

* further ear tweaks

* engi helmets

---------

Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kr8art <188977876+kr8art@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: Southbridge-fur <southbridgefur@gmail.com>

* Automatic changelog update

* Rejig LogStringHandler (#30706)

* Rejig LogStringHandler

* Fix session logs

* Fix properly

* comments

* IAsType support

* Fix mind logs

* Fix mind logging AGAIN

---------

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

* Expedite gender reassignment (#36894)

* Automatic changelog update

* Adjusted minimumPlayers for Wizard midround events. (#38424)

* Adjusted minimumPlayers for Ninja and Wizard midround events

Wizard from 10->20
Ninja from 30->20
20 players matches Dragon and Loneop.

* Wizard midround minimum players from 20 to 30

* Update Resources/Prototypes/GameRules/events.yml

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

---------

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

* Automatic changelog update

* Add water cooler interaction test (#39612)

* Add altInteract option to interaction test helper methods

* Add water cooler interaction test

* Oops, that's not a NetEntity

* Is.Not.Empty

* SPlayer

* Assert.Multiple

* Document parameters

* Add a space in osx-arm64 to fix arm64 osx builds (#40137)

* Fix admin logs going to admin chat (#40141)

Oops

* Drink outta da toiler (#40133)

* totally unrelated to any other recent additions

* dispare

* enum

* Automatic changelog update

* update for glue/lube tube inhand

* Disable vulpkanin human hair (#40144)

init

* Fix bad loop in LogStringHandler.AddFormat (#40147)

* Fix exo burn chamber (#40152)

* Automatic changelog update

* Fix resin windows inheriting wrong dP values (#40151)

Fix resin windows inheriting from regular windows

* Automatic changelog update

* [HOTFIX] Fix Burgers (#39773)

* Borgar

* Review

* Predicted queuedel

* Predict

---------

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

* [Hotfix] Wizard Rod doesn't gib the wizard. (#40041)

* Title

* Tired

* That shit did nothing goddamn

* Fix for real

* Use og code

* Hmmm borgaer

---------

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

* Revert antique laser and appraisal tool sizes (#40158)

* init

* appraisal antique pistol sounds kinda cool tho ngl

* actually fuck items

* Automatic changelog update

* Fixed disconnected grid on box station (#40161)

Fixed tesla area on box

* Automatic changelog update

* Add some alternate jumpsuit designs which can be toggled (#31213)

* inital

* testfa

* New Sec sprite

* Update atmos

* Update meta.json

* Update meta.json

* 0

* Update meta.json

* Automatic changelog update

* Update Credits (#40187)

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

* Atmospherics Delta-Pressure YAML refactor (#40174)

* fix: Atmos dP Window Inheritance (#40192)

* Restore transfer amounts on regular syringes to 5, 10, 15 (#40197)

add transferamounts to regular syringes

* Automatic changelog update

* Allow Vulps With Human Hair To Be Shaved Without Clyde Joining The Circus (#40171)

* Revert "Disable vulpkanin human hair (#40144)"

This reverts commit d02aa1a4e2.

* You can once again shave your pet Vulp

* I can see the ass, I'm safe

* Rectified docstring as I am a good person

* I am doing this instead of playing Silksong please help

* Fix forgetting to re-add shader overriding

* Automatic changelog update

* Bug fix for APCPowerReceiverBattery (#40188)

Initial commit

* Atmos dP Guidebook Entry (#40194)

* Add Atmos dP guidebook

* Update Resources/ServerInfo/Guidebook/Engineering/DeltaPressure.xml

---------

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

* Automatic changelog update

* Being grappled with a grapple gun allows you to cross chasms (#39983)

* Being grappled with a grapple gun allows you to cross chasms

Closes #31698

* Update Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs

* AAAAAAAAAAAAAAAAA

---------

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

* Automatic changelog update

* Lets diona sap trigger artifact blood nodes

* DoAfter support for Actions (#38253)

* Adds Action DoAfter Events

* Adds DoAfterArgs fields to DoAfterComp

* Adds a base doafter action

* Adds Attempt action doafter logic

* Adds doafter logic to actions

* Changes Action Attempt Doafter and action doafter to take in Performer and the original use delay. Use delay now triggers when a repeated action  is cancelled.

* Readds the TryPerformAction method and readds request perform action into the action doafter events

* Adds a force skip to DoAfter Cancel so we can skip the complete check

* Adds a Delay Reduction field to the comp and to the comp state

* Fixes doafter mispredict, changes doafter comp check to a guard clause, sets delay reduction if it exists.

* Cancels ActionDoAfter if charges is 0

* Serializes Attempt Frequency

* Comment for rework

* Changes todo into a comment

* Moves doafterargs to doafterargscomp

* Adds DoAfterArgs comp to BaseDoAfterAction

* Removes unused trycomp with actionDoAfter

* Replaces DoAfterRepateUseDelay const with timespan.zero

* Removes unused usings

* Makes SharedActionsSystem partial, adds DoAfter partial class to ActionSystem, moves ActionDoAfter logic to the SharedActionsSystem.DoAfter class

* Cleanup and prediction

* Renames OnActionDoAfterAttempt to OnActionDoAfter, moves both to Shared Action DoAfter

* Removes ActionAttemptDoAfterEvent and moves its summaries to ActionDoAfterEvent. Converts OnActionDoAfterAttempt into TryStartActionDoAfter

* Removes Extra check for charges and actiondoafters

* Sloptimization

* Cleanup

* Cleanup

* Adds param descs

---------

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

* Automatic changelog update

* Derelict Mediborgs can Scan Solutions and see Mob Health (#40206)

Add mediborg unique components to derelict mediborg

* Automatic changelog update

* Reworded the Galoshes description to be more clear about what they actually do. (#40200)

Reworded the Galoshes description to be more clear about what they do

* Automatic changelog update

* Fix usages of TryIndex() (#39124)

* Fix usages of TryIndex()

Most usages of TryIndex() were using it incorrectly. Checking whether prototype IDs specified in prototypes actually existed before using them. This is not appropriate as it's just hiding bugs that should be getting caught by the YAML linter and other tools. (#39115)

This then resulted in TryIndex() getting modified to log errors (94f98073b0), which is incorrect as it causes false-positive errors in proper uses of the API: external data validation. (#39098)

This commit goes through and checks every call site of TryIndex() to see whether they were correct. Most call sites were replaced with the new Resolve(), which is suitable for these "defensive programming" use cases.

Fixes #39115

Breaking change: while doing this I noticed IdCardComponent and related systems were erroneously using ProtoId<AccessLevelPrototype> for job prototypes. This has been corrected.

* fix tests

---------

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

* Cleanup: Remove unnecessary ``IEntityManager`` reference from the ``EmotesUIController`` (#40243)

Cleanup

* Make location in crew monitoring console localizable (#40247)

* Make Foldable Clothing Hidden Layers "reset" Hidden Layers when un/Folding (#40251)

foldable clothing hidden layers fix

* feat: SimpleRadial menu support for sprite-view and more extensibility (#39223)

* Decal spawners spawn on a higher layer (#39956)

changed decal spawn layer

* Fix RGB staff not working (#40258)

Add missing `TargetAction` to `ActionRgbLight`

This fixes the RGB staff not working.

* Automatic changelog update

* Make "Confirm" in VerbMenuUIController localizable (#40248)

* Add support for contraband text to the reagent guidebook (#37113)

* Add contraband text to reagent guidebook

* Add reagent for examining

* Update Content.Client/Guidebook/Controls/GuideReagentEmbed.xaml.cs

---------

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

* Fix emergency evac shuttle console early launch mispredict (#39751)

* Fix

* Yes

* Mess

* Update

* Like that?

* SpawnEntityTableOnTrigger (#39909)

* commit

* comment

* empty

* better xform

---------

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

* PopupOnTrigger (#39913)

* commit

* comment

* changes

* Update Content.Shared/Trigger/Systems/PopupOnTriggerSystem.cs

---------

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

* [HOTFIX] Fix Loadout Validations (#40189)

p0 bugfix

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

* Do after checks for being inside container (#39880)

fix

* Automatic changelog update

* Ignore non-content commands in AllCommandsHavePermissions (#39336)

Causing a test failure every time a Toolshed command gets added to engine is ridiculous.

* Allow to run `mappingclientsidesetup` and `showsubfloor` with +MAPPING permissions (#34455)

Allow +MAPPING admins to call mappingclientsidesetup and showsubfloor

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

* "idk" no longer shrugs, instead sanitizing to "I don't know" (#39024)

* idk no longer shrugs

* accidentally had this in here!

* this was accidentally in here too!

* fucked up the removal

* fucked it up again this should HOPEFULLY be the last one

turns out it's hard to revert the edit of a file that's just numbers and spaces who woulda thunk?

* Restore SpaceStation14.sln to master

* Restore development.toml

---------

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

* Automatic changelog update

* Improve Do Not Map test to whitelist specific prototypes per map and whitelist entire directories (#36117)

* Enable whitelisting specific DNM prototypes per map

* Enable whitelisting directories

* Rename fields

* Use a HashSet instead of an array

* Add check for unused whitelist entries

* Remove whitelisting for meta (warden's rubber stamp was removed)

* Add glob support courtesy of @IProduceWidgets

* Update xmldoc

* Make Butterflies zombie immune (#40265)

butterfly zombie no longer

* Automatic changelog update

* Cardboard Box Weightless Fix (#40260)

Box is no longer space faring

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

* Automatic changelog update

* Add admin shuttles (#32139)

* dis da shuttles tho

* power checking

* rerun tests

* purge invalids

* attributions

* Update shuttle save files for new serialization.

* get regexed nerd

* fix shuttle yml guh

* Kill actions

* Automatic changelog update

* Exo - Lighting update & more (#40199)

* Automatic changelog update

* Laser rifle is contraband again (#40253)

* Fixed contraband tag

* practice isnt contraband

* Added base

* Removal

* Automatic changelog update

* Fix darts inhand sprites (#40207)

fix

Co-authored-by: GeneralGaws <limonmessi@mail.ru>

* Fix APC breaker toggle button prediction by setting ToggleMode True (#40273)

* Use ToggleMode for toggle button

* Actually this bit doesn't require changing apparently

* fix chasmsystem resolve error (#40281)

* No take; Only throw. (#40143)

* commit atrocities

* hail satan

* Channel Pavlovian horror

* pet kitty cat

* Taint universe forever

* Assault sensibilities

* sully existence

* tarnish morality

* The degunnening

* encratening

* Update toys.yml

* add few pixels to fix vulp inhands

---------

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

* Automatic changelog update

* Clip the WindowTitle of FancyWindows, so close buttons don't get hidden (#40272)

ClipText of FancyWindow title, so close buttons don't get hidden

* Automatic changelog update

* They're milk jugs, not milk cartons!

* Fix Linter errors (#40283)

Fix yoml

* Remove unused BulletTennis (#40285)

init

* Automatic changelog update

* Make vending machine restocks predicted (and its sound not spammable) (#38609)

* feat: make vending machine restocks predicted

* refactor: VendingMachineRestockComponent cleanup

* refactor: minor simplification

* revert: refactor: minor simplification; load bearing IsFirstTimePredicted

lol second guessed myself

* chore: unneeded VendingMachineSystem dep

* Update Content.Shared/VendingMachines/VendingMachineComponent.cs

---------

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

* Added SmartFridge circuitboards (#39879)

* Automatic changelog update

* storage and inventory toolshed commands (#39046)

* First commit

* CommandImplementation on singletons is die, a la moony

* Fix duplicated thingy because yes

* Prototypes, bugfixes, refactoring oh my

* Remember to actually stage your ftl changes next time, leaf

* Automatic changelog update

* move all the radio components and system to Shared (#40293)

* move all the radio components and system to Shared.

* duh split impl

* address reviews

* cleanup

---------

Co-authored-by: walksanatora <walkerffo22@gmail.com>

* Clake frag round fix (#40294)

make the frag work

* Fix for can't stop pulling when cuffed (#40233)

* fix

* fix

* spaces added

* Stop microwaving! (#40132)

* Create KillMicrowaveTest.cs

* Update KillMicrowaveTest.cs

* Update Content.IntegrationTests/Tests/Microwave/KillMicrowaveTest.cs

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

* Update Content.IntegrationTests/Tests/Microwave/KillMicrowaveTest.cs

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

* Update Content.IntegrationTests/Tests/Microwave/KillMicrowaveTest.cs

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

* documentation

* Apply suggestions from code review

---------

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

* De-enumify humanoid species skin colours (#39175)

* De-enumify humanoid species skin colours

* Change index to resolve

---------

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

* Revert "Can't crawl over counters (#40099)"

This reverts commit 01a7fc66f0.

@Princess-Cheeseballs

This is being reverted per maintainer vote for the release cycle, check out the meeting video here https://youtu.be/N5-UYCLha2I?t=872

* Revert "Add heat distortion shader for hot gases" (#40352)

* Vulpkanin Color Clamping (#40348)

* [STAGING/HOTFIX] Butcher entities in containers. (#40299)

* Kitchen spike and sharp system

* Use transform and parent

* A

* Works

* A

---------

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

* adapt codebase

* fix injectors

* Update tools.yml

* Update tools.yml

* Update tools.yml

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: iaada <iaada@users.noreply.github.com>
Co-authored-by: 5tickman <5tick@comcast.net>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: Minemoder5000 <minemoder50000@gmail.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: ToastEnjoyer <masondoesgamingyes@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com>
Co-authored-by: Kowlin <10947836+Kowlin@users.noreply.github.com>
Co-authored-by: āda <ss.adasts@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: AndrewFenriz <78079974+AndrewFenriz@users.noreply.github.com>
Co-authored-by: Travis Reid <86178026+Travis-G-Reid@users.noreply.github.com>
Co-authored-by: Quasr <~182430031+quasr-9@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: InsoPL <lukasz.lindert@protonmail.com>
Co-authored-by: Nox <nebulousnox38@gmail.com>
Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: M4rchy-S <89603088+M4rchy-S@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: SolidSyn <220547106+SolidSyn@users.noreply.github.com>
Co-authored-by: Partmedia <kevinz5000@gmail.com>
Co-authored-by: War Pigeon <54217755+minus1over12@users.noreply.github.com>
Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
Co-authored-by: Centronias <me@centronias.com>
Co-authored-by: Quasr <182430031+quasr-9@users.noreply.github.com>
Co-authored-by: Tiniest Shark <head.rebel@yahoo.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Mora <46364955+TrixxedHeart@users.noreply.github.com>
Co-authored-by: MissKay1994 <15877268+MissKay1994@users.noreply.github.com>
Co-authored-by: opl- <opl-@users.noreply.github.com>
Co-authored-by: opl <4833621+opl@users.noreply.github.com>
Co-authored-by: breeplayx3 <breeplayx5@gmail.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
Co-authored-by: VoidMeticulous <voidmeticulous@gmail.com>
Co-authored-by: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com>
Co-authored-by: jkwookee <157201244+jkwookee@users.noreply.github.com>
Co-authored-by: DDDragoni <38265528+DDeegan@users.noreply.github.com>
Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com>
Co-authored-by: Myra <vasilis@pikachu.systems>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: CawsForConcern <kopczynski.zesty@gmail.com>
Co-authored-by: SpaceLizard <the.justice.league.of.canada@gmail.com>
Co-authored-by: Perry Fraser <perryprog@users.noreply.github.com>
Co-authored-by: Skye <me@skye.vg>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: Kr8art <188977876+kr8art@users.noreply.github.com>
Co-authored-by: Southbridge-fur <southbridgefur@gmail.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Matt Idzik <matt.idzik1@gmail.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Crude Oil <124208219+CroilBird@users.noreply.github.com>
Co-authored-by: Hayden <banditoz@protonmail.com>
Co-authored-by: OrbitSystem07 <emilymurray717@gmail.com>
Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com>
Co-authored-by: Kittygyat <202250949+Kittygyat@users.noreply.github.com>
Co-authored-by: Ser11y <160628372+Ser1-1y@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: Deerstop <edainturner@gmail.com>
Co-authored-by: Stefano Pigozzi <me@steffo.eu>
Co-authored-by: c4llv07e <igor@c4llv07e.xyz>
Co-authored-by: SweetAplle <151391001+SweetAplle@users.noreply.github.com>
Co-authored-by: imatsoup <93290208+imatsoup@users.noreply.github.com>
Co-authored-by: GeneralGaws <122978178+GeneralGaws@users.noreply.github.com>
Co-authored-by: GeneralGaws <limonmessi@mail.ru>
Co-authored-by: Absotively <jen@jenpollock.ca>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: Lillian Industries <47704287+luegamer@users.noreply.github.com>
Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com>
Co-authored-by: walksanatora <walkerffo22@gmail.com>
Co-authored-by: ApolloVector <149586366+ApolloVector@users.noreply.github.com>
This commit is contained in:
Red
2025-09-15 17:42:36 +03:00
committed by GitHub
1452 changed files with 85664 additions and 17884 deletions

View File

@@ -34,7 +34,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -41,7 +41,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -60,7 +60,7 @@ jobs:
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release

View File

@@ -1,17 +1,19 @@
#!/usr/bin/env python3
# Installs git hooks, updates them, updates submodules, that kind of thing.
"""
Installs git hooks, updates them, updates submodules, that kind of thing.
"""
import subprocess
import sys
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "2"
CURRENT_HOOKS_VERSION = "3"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
@@ -25,12 +27,10 @@ def run_command(command: List[str], capture: bool = False) -> subprocess.Complet
sys.stdout.flush()
completed = None
if capture:
completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
completed = subprocess.run(command, stdout=subprocess.PIPE, text=True)
else:
completed = subprocess.run(command, cwd="..")
completed = subprocess.run(command)
if completed.returncode != 0:
print("Error: command exited with code {}!".format(completed.returncode))
@@ -43,7 +43,7 @@ def update_submodules():
Updates all submodules.
"""
if ('GITHUB_ACTIONS' in os.environ):
if 'GITHUB_ACTIONS' in os.environ:
return
if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
@@ -76,22 +76,21 @@ def install_hooks():
print("No hooks change detected.")
return
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
print("Hooks need updating.")
hooks_target_dir = Path("..")/".git"/"hooks"
hooks_target_dir = Path(run_command(["git", "rev-parse", "--git-path", "hooks"], True).stdout.strip())
hooks_source_dir = Path("hooks")
# Clear entire tree since we need to kill deleted files too.
for filename in os.listdir(str(hooks_target_dir)):
os.remove(str(hooks_target_dir/filename))
for filename in os.listdir(hooks_target_dir):
os.remove(hooks_target_dir / filename)
for filename in os.listdir(str(hooks_source_dir)):
for filename in os.listdir(hooks_source_dir):
print("Copying hook {}".format(filename))
shutil.copy2(str(hooks_source_dir/filename),
str(hooks_target_dir/filename))
shutil.copy2(hooks_source_dir / filename, hooks_target_dir / filename)
with open("INSTALLED_HOOKS_VERSION", "w") as f:
f.write(CURRENT_HOOKS_VERSION)
def reset_solution():
@@ -107,8 +106,7 @@ def reset_solution():
def check_for_zip_download():
# Check if .git exists,
cur_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if not os.path.isdir(os.path.join(cur_dir, ".git")):
if run_command(["git", "rev-parse"]).returncode != 0:
print("It appears that you downloaded this repository directly from GitHub. (Using the .zip download option) \n"
"When downloading straight from GitHub, it leaves out important information that git needs to function. "
"Such as information to download the engine or even the ability to even be able to create contributions. \n"

View File

@@ -1,10 +1,10 @@
#!/bin/bash
gitroot=`git rev-parse --show-toplevel`
gitroot=$(git rev-parse --show-toplevel)
cd "$gitroot/BuildChecker"
cd "$gitroot/BuildChecker" || exit
if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
if [[ $(uname) == MINGW* || $(uname) == CYGWIN* ]]; then
# Windows
py -3 git_helper.py --quiet
else

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# Just call post-checkout since it does the same thing.
gitroot=`git rev-parse --show-toplevel`
bash "$gitroot/.git/hooks/post-checkout"
gitroot=$(git rev-parse --git-path hooks)
bash "$gitroot/post-checkout"

View File

@@ -0,0 +1,174 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
/// <summary>
/// Spawns N number of entities with a <see cref="DeltaPressureComponent"/> and
/// simulates them for a number of ticks M.
/// </summary>
[Virtual]
[GcServer(true)]
//[MemoryDiagnoser]
//[ThreadingDiagnoser]
public class DeltaPressureBenchmark
{
/// <summary>
/// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
public int EntityCount;
/// <summary>
/// Number of entities that each parallel processing job will handle.
/// </summary>
// [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
[Params(10)]
public int BatchSize;
/// <summary>
/// Number of entities to process per iteration in the DeltaPressure
/// processing loop.
/// </summary>
// [Params(100, 1000, 5000, 10000, 50000)]
[Params(1000)]
public int EntitiesPerIteration;
private readonly EntProtoId _windowProtoId = "Window";
private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private SharedMapSystem _map = default!;
private IRobustRandom _random = default!;
private IConfigurationManager _cvar = default!;
private ITileDefinitionManager _tileDefMan = default!;
private AtmosphereSystem _atmospereSystem = default!;
private Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>
_testEnt;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_map = _entMan.System<SharedMapSystem>();
_random = server.ResolveDependency<IRobustRandom>();
_cvar = server.ResolveDependency<IConfigurationManager>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_atmospereSystem = _entMan.System<AtmosphereSystem>();
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
_cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
_cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
var plating = _tileDefMan["Plating"].TileId;
/*
Basically, we want to have a 5-wide grid of tiles.
Edges are walled, and the length of the grid is determined by N + 2.
Windows should only touch the top and bottom walls, and each other.
*/
var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
const int height = 5;
await server.WaitPost(() =>
{
// Fill required tiles (extend grid) with plating
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
// Spawn perimeter walls and windows row in the middle (y = 2)
const int midY = height / 2;
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
if (isPerimeter)
{
_entMan.SpawnEntity(_wallProtoId, coords);
continue;
}
// Spawn windows only on the middle row, spanning interior (excluding side walls)
if (y == midY)
{
_entMan.SpawnEntity(_windowProtoId, coords);
}
}
}
});
// Next we run the fixgridatmos command to ensure that we have some air on our grid.
// Wait a little bit as well.
// TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
// a stamp-on in AtmosphereSystem.
await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
var uid = mapdata.Grid.Owner;
_testEnt = new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
uid,
_entMan.GetComponent<GridAtmosphereComponent>(uid),
_entMan.GetComponent<GasTileOverlayComponent>(uid),
_entMan.GetComponent<MapGridComponent>(uid),
_entMan.GetComponent<TransformComponent>(uid));
}
[Benchmark]
public async Task PerformFullProcess()
{
await _pair.Server.WaitPost(() =>
{
while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
});
}
[Benchmark]
public async Task PerformSingleRunProcess()
{
await _pair.Server.WaitPost(() =>
{
_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

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

View File

@@ -29,7 +29,7 @@ namespace Content.Client.Access.UI
foreach (var access in accessLevels)
{
if (!protoManager.TryIndex(access, out var accessLevel))
if (!protoManager.Resolve(access, out var accessLevel))
{
continue;
}

View File

@@ -57,7 +57,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
foreach (var accessGroup in _accessGroups)
{
if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
if (!_protoManager.Resolve(accessGroup, out var accessGroupProto))
continue;
_groupedAccessLevels.Add(accessGroupProto, new());
@@ -65,13 +65,13 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
// Ensure that the 'general' access group is added to handle
// misc. access levels that aren't associated with any group
if (_protoManager.TryIndex(GeneralAccessGroup, out var generalAccessProto))
if (_protoManager.Resolve(GeneralAccessGroup, out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in _accessLevels)
{
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
if (!_protoManager.Resolve(accessLevel, out var accessLevelProto))
continue;
var assigned = false;

View File

@@ -4,6 +4,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using static Content.Shared.Access.Components.IdCardConsoleComponent;
@@ -74,7 +75,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
{
if (newFullName.Length > _maxNameLength)
newFullName = newFullName[.._maxNameLength];

View File

@@ -123,7 +123,7 @@ namespace Content.Client.Access.UI
foreach (var group in job.AccessGroups)
{
if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
if (!_prototypeManager.Resolve(group, out AccessGroupPrototype? groupPrototype))
{
continue;
}

View File

@@ -316,8 +316,9 @@ public sealed partial class BanPanel : DefaultWindow
};
// 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))
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
// I know the ban manager is doing the same thing, but that should not leak into UI code.
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{

View File

@@ -0,0 +1,40 @@
using Robust.Client.Graphics;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.Anomaly;
/// <summary>
/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner
/// for an indicator of Anomaly Severity.
/// </summary>
/// <remarks>
/// In the future I would like to make this a more generic "DynamicTextureComponent" that can contain a dictionary
/// of texture components like "Bar(offset, size, minimumValue, maximumValue, AppearanceKey, LayerMapKey)" that can
/// just draw a bar or other basic drawn element that will show up on a texture layer.
/// </remarks>
[RegisterComponent]
[Access(typeof(AnomalyScannerSystem))]
public sealed partial class AnomalyScannerScreenComponent : Component
{
/// <summary>
/// This is the texture drawn as a layer on the Anomaly Scanner device.
/// </summary>
public OwnedTexture? ScreenTexture;
/// <summary>
/// A small buffer that we can reuse to draw the severity bar.
/// </summary>
public Rgba32[]? BarBuf;
/// <summary>
/// The position of the top-left of the severity bar in pixels.
/// </summary>
[DataField(readOnly: true)]
public Vector2i Offset = new Vector2i(12, 17);
/// <summary>
/// The width and height of the severity bar in pixels.
/// </summary>
[DataField(readOnly: true)]
public Vector2i Size = new Vector2i(10, 3);
}

View File

@@ -0,0 +1,110 @@
using System.Numerics;
using Content.Shared.Anomaly;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
namespace Content.Client.Anomaly;
/// <inheritdoc cref="SharedAnomalyScannerSystem"/>
public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private const float MaxHueDegrees = 360f;
private const float GreenHueDegrees = 110f;
private const float RedHueDegrees = 0f;
private const float GreenHue = GreenHueDegrees / MaxHueDegrees;
private const float RedHue = RedHueDegrees / MaxHueDegrees;
// Just an array to initialize the pixels of a new OwnedTexture
private static readonly Rgba32[] EmptyTexture = new Rgba32[32*32];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<AnomalyScannerScreenComponent, AppearanceChangeEvent>(OnScannerAppearanceChanged);
}
private void OnComponentInit(Entity<AnomalyScannerScreenComponent> ent, ref ComponentInit args)
{
if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true))
return;
// Allocate the OwnedTexture
ent.Comp.ScreenTexture = _clyde.CreateBlankTexture<Rgba32>(layer.PixelSize);
if (layer.PixelSize.X < ent.Comp.Offset.X + ent.Comp.Size.X ||
layer.PixelSize.Y < ent.Comp.Offset.Y + ent.Comp.Size.Y)
{
// If the bar doesn't fit, just bail here, ScreenTexture and BarBuf will remain null, and appearance updates
// will do nothing.
DebugTools.Assert(false, "AnomalyScannerScreenComponent: Bar does not fit within sprite");
return;
}
// Initialize the texture
ent.Comp.ScreenTexture.SetSubImage((0, 0), layer.PixelSize, new ReadOnlySpan<Rgba32>(EmptyTexture));
// Initialize bar drawing buffer
ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y];
}
private void OnComponentStartup(Entity<AnomalyScannerScreenComponent> ent, ref ComponentStartup args)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
_sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture);
}
private void OnScannerAppearanceChanged(Entity<AnomalyScannerScreenComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite is null || ent.Comp.ScreenTexture is null || ent.Comp.BarBuf is null)
return;
args.AppearanceData.TryGetValue(AnomalyScannerVisuals.AnomalySeverity, out var severityObj);
if (severityObj is not float severity)
severity = 0;
// Get the bar length
var barLength = (int)(severity * ent.Comp.Size.X);
// Calculate the bar color
// Hue "angle" of two colors to interpolate between depending on severity
// Just a lerp from Green hue at severity = 0.5 to Red hue at 1.0
var hue = Math.Clamp(2*GreenHue * (1 - severity), RedHue, GreenHue);
var color = new Rgba32(Color.FromHsv(new Vector4(hue, 1f, 1f, 1f)).RGBA);
var transparent = new Rgba32(0, 0, 0, 255);
for(var y = 0; y < ent.Comp.Size.Y; y++)
{
for (var x = 0; x < ent.Comp.Size.X; x++)
{
ent.Comp.BarBuf[y*ent.Comp.Size.X + x] = x < barLength ? color : transparent;
}
}
// Copy the buffer to the texture
try
{
ent.Comp.ScreenTexture.SetSubImage(
ent.Comp.Offset,
ent.Comp.Size,
new ReadOnlySpan<Rgba32>(ent.Comp.BarBuf)
);
}
catch (IndexOutOfRangeException)
{
Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}");
}
}
}

View File

@@ -7,7 +7,7 @@ using Robust.Shared.Timing;
namespace Content.Client.Anomaly;
public sealed class AnomalySystem : SharedAnomalySystem
public sealed partial class AnomalySystem : SharedAnomalySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
@@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);

View File

@@ -134,7 +134,7 @@ public sealed class AlignAtmosPipeLayers : SnapgridCenter
var newProtoId = altPrototypes[(int)layer];
if (!_protoManager.TryIndex(newProtoId, out var newProto))
if (!_protoManager.Resolve(newProtoId, out var newProto))
return;
if (newProto.Type != ConstructionType.Structure)

View File

@@ -208,7 +208,7 @@ namespace Content.Client.Atmos.UI
});
presBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.##}")),
Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.00}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
@@ -232,8 +232,8 @@ namespace Content.Client.Atmos.UI
tempBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
("tempK", $"{gasMix.Temperature:0.#}"),
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.#}")),
("tempK", $"{gasMix.Temperature:0.0}"),
("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.0}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});

View File

@@ -58,7 +58,7 @@ public sealed class JukeboxBoundUserInterface : BoundUserInterface
_menu.SetAudioStream(jukebox.AudioStream);
if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
if (_protoManager.Resolve(jukebox.SelectedSongId, out var songProto))
{
var length = EntMan.System<AudioSystem>().GetAudioLength(songProto.Path.Path.ToString());
_menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);

View File

@@ -39,7 +39,7 @@ public sealed class BarSignSystem : VisualizerSystem<BarSignComponent>
if (powered
&& sign.Current != null
&& _prototypeManager.TryIndex(sign.Current, out var proto))
&& _prototypeManager.Resolve(sign.Current, out var proto))
{
SpriteSystem.LayerSetSprite((id, sprite), 0, proto.Icon);
sprite.LayerSetShader(0, "unshaded");

View File

@@ -35,7 +35,7 @@ public sealed class BarSignBoundUserInterface(EntityUid owner, Enum uiKey) : Bou
public void Update(ProtoId<BarSignPrototype>? sign)
{
if (_prototype.TryIndex(sign, out var signPrototype))
if (_prototype.Resolve(sign, out var signPrototype))
_menu?.UpdateState(signPrototype);
}

View File

@@ -29,7 +29,7 @@ public sealed partial class BountyEntry : BoxContainer
UntilNextSkip = untilNextSkip;
if (!_prototype.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var bountyPrototype))
if (!_prototype.Resolve<CargoBountyPrototype>(bounty.Bounty, out var bountyPrototype))
return;
var items = new List<string>();

View File

@@ -19,7 +19,7 @@ public sealed partial class BountyHistoryEntry : BoxContainer
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
if (!_prototype.Resolve(bounty.Bounty, out var bountyPrototype))
return;
var items = new List<string>();

View File

@@ -1,4 +1,7 @@
using Content.Shared.Changeling.Systems;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Changeling.Components;
using Content.Shared.Changeling.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@@ -7,28 +10,58 @@ namespace Content.Client.Changeling.UI;
[UsedImplicitly]
public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private ChangelingTransformMenu? _window;
private SimpleRadialMenu? _menu;
private static readonly Color SelectedOptionBackground = StyleNano.ButtonColorGoodDefault.WithAlpha(128);
private static readonly Color SelectedOptionHoverBackground = StyleNano.ButtonColorGoodHovered.WithAlpha(128);
protected override void Open()
{
base.Open();
_window = this.CreateWindow<ChangelingTransformMenu>();
_window.OnIdentitySelect += SendIdentitySelect;
_window.Update(Owner);
_menu = this.CreateWindow<SimpleRadialMenu>();
Update();
_menu.OpenOverMouseScreenPosition();
}
public override void Update()
{
if (_window == null)
if (_menu == null)
return;
_window.Update(Owner);
if (!EntMan.TryGetComponent<ChangelingIdentityComponent>(Owner, out var lingIdentity))
return;
var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity);
_menu.SetButtons(models);
}
public void SendIdentitySelect(NetEntity identityId)
private IEnumerable<RadialMenuOptionBase> ConvertToButtons(
IEnumerable<EntityUid> identities,
EntityUid? currentIdentity
)
{
var buttons = new List<RadialMenuOptionBase>();
foreach (var identity in identities)
{
if (!EntMan.TryGetComponent<MetaDataComponent>(identity, out var metadata))
continue;
var option = new RadialMenuActionOption<NetEntity>(SendIdentitySelect, EntMan.GetNetEntity(identity))
{
IconSpecifier = RadialMenuIconSpecifier.With(identity),
ToolTip = metadata.EntityName,
BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null,
HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null
};
buttons.Add(option);
}
return buttons;
}
private void SendIdentitySelect(NetEntity identityId)
{
SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
}

View File

@@ -1,8 +0,0 @@
<ui:RadialMenu
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True">
<ui:RadialContainer Name="Main">
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -1,62 +0,0 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Changeling.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Changeling.UI;
[GenerateTypedNameReferences]
public sealed partial class ChangelingTransformMenu : RadialMenu
{
[Dependency] private readonly IEntityManager _entity = default!;
public event Action<NetEntity>? OnIdentitySelect;
public ChangelingTransformMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void Update(EntityUid uid)
{
Main.DisposeAllChildren();
if (!_entity.TryGetComponent<ChangelingIdentityComponent>(uid, out var identityComp))
return;
foreach (var identityUid in identityComp.ConsumedIdentities)
{
if (!_entity.TryGetComponent<MetaDataComponent>(identityUid, out var metadata))
continue;
var identityName = metadata.EntityName;
var button = new ChangelingTransformMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64, 64),
ToolTip = identityName,
};
var entView = new SpriteView()
{
SetSize = new Vector2(48, 48),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Stretch = SpriteView.StretchMode.Fill,
};
entView.SetEntity(identityUid);
button.OnButtonUp += _ =>
{
OnIdentitySelect?.Invoke(_entity.GetNetEntity(identityUid));
Close();
};
button.AddChild(entView);
Main.AddChild(button);
}
}
}
public sealed class ChangelingTransformMenuButton : RadialMenuTextureButtonWithSector;

View File

@@ -27,7 +27,7 @@ public sealed class TypingIndicatorVisualizerSystem : VisualizerSystem<TypingInd
if (overrideIndicator != null)
currentTypingIndicator = overrideIndicator.Value;
if (!_prototypeManager.TryIndex(currentTypingIndicator, out var proto))
if (!_prototypeManager.Resolve(currentTypingIndicator, out var proto))
{
Log.Error($"Unknown typing indicator id: {component.TypingIndicatorPrototype}");
return;

View File

@@ -2,7 +2,6 @@ using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.GameStates;
namespace Content.Client.Chemistry.EntitySystems;
@@ -11,6 +10,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}

View File

@@ -38,13 +38,13 @@ public sealed class InjectorStatusControl : Control
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.TransferAmount
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.TransferAmount;
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state
@@ -59,6 +59,6 @@ public sealed class InjectorStatusControl : Control
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
("transferVolume", _parent.Comp.TransferAmount)));
("transferVolume", _parent.Comp.CurrentTransferAmount)));
}
}

View File

@@ -1,12 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Client.DisplacementMap;
using Content.Client.Inventory;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.DisplacementMap;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@@ -14,7 +12,6 @@ using Content.Shared.Item;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -182,6 +179,7 @@ public sealed class ClientClothingSystem : ClothingSystem
var layer = new PrototypeLayerData();
layer.RsiPath = rsi.Path.ToString();
layer.State = state;
layer.Scale = clothing.Scale;
layers = new() { layer };
return true;

View File

@@ -45,7 +45,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
var newTargets = new List<EntProtoId>();
foreach (var target in targets)
{
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
if (string.IsNullOrEmpty(target) || !_proto.Resolve(target, out EntityPrototype? proto))
continue;
if (!proto.TryGetComponent(out TagComponent? tag, EntMan.ComponentFactory) || !_tag.HasTag(tag, st.RequiredTag))

View File

@@ -54,7 +54,7 @@ public sealed partial class ChameleonMenu : DefaultWindow
foreach (var id in _possibleIds)
{
if (!_prototypeManager.TryIndex(id, out EntityPrototype? proto))
if (!_prototypeManager.Resolve(id, out EntityPrototype? proto))
continue;
var lowId = id.Id.ToLowerInvariant();

View File

@@ -80,7 +80,7 @@ namespace Content.Client.Construction
{
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (!PrototypeManager.TryIndex(constructionProto.Graph, out var graphProto))
if (!PrototypeManager.Resolve(constructionProto.Graph, out var graphProto))
continue;
if (constructionProto.TargetNode is not { } targetNodeId)
@@ -121,17 +121,14 @@ namespace Content.Client.Construction
// If we got the id of the prototype, we exit the “recursion” by clearing the stack.
stack.Clear();
if (!PrototypeManager.TryIndex(constructionProto.ID, out ConstructionPrototype? recipe))
if (!PrototypeManager.Resolve(entityId, out var proto))
continue;
if (!PrototypeManager.TryIndex(entityId, out var proto))
continue;
var name = constructionProto.SetName.HasValue ? Loc.GetString(constructionProto.SetName) : proto.Name;
var desc = constructionProto.SetDescription.HasValue ? Loc.GetString(constructionProto.SetDescription) : proto.Description;
var name = recipe.SetName.HasValue ? Loc.GetString(recipe.SetName) : proto.Name;
var desc = recipe.SetDescription.HasValue ? Loc.GetString(recipe.SetDescription) : proto.Description;
recipe.Name = name;
recipe.Description = desc;
constructionProto.Name = name;
constructionProto.Description = desc;
_recipesMetadataCache.Add(constructionProto.ID, entityId);
} while (stack.Count > 0);
@@ -172,7 +169,7 @@ namespace Content.Client.Construction
"construction-ghost-examine-message",
("name", component.Prototype.Name)));
if (!PrototypeManager.TryIndex(component.Prototype.Graph, out var graph))
if (!PrototypeManager.Resolve(component.Prototype.Graph, out var graph))
return;
var startNode = graph.Nodes[component.Prototype.StartNode];

View File

@@ -528,7 +528,7 @@ namespace Content.Client.Construction.UI
foreach (var id in favorites)
{
if (_prototypeManager.TryIndex(id, out ConstructionPrototype? recipe, logError: false))
if (_prototypeManager.TryIndex(id, out ConstructionPrototype? recipe))
_favoritedRecipes.Add(recipe);
}

View File

@@ -150,7 +150,7 @@ public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponen
// If the damage container on our entity's DamageableComponent
// is not null, we can try to check through its groups.
if (damageComponent.DamageContainerID != null
&& _prototypeManager.TryIndex<DamageContainerPrototype>(damageComponent.DamageContainerID, out var damageContainer))
&& _prototypeManager.Resolve<DamageContainerPrototype>(damageComponent.DamageContainerID, out var damageContainer))
{
// Are we using damage overlay sprites by group?
// Check if the container matches the supported groups,

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.DisplacementMap;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -10,6 +11,11 @@ public sealed class DisplacementMapSystem : EntitySystem
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private static string? BuildDisplacementLayerKey(object key)
{
return key.ToString() is null ? null : $"{key}-displacement";
}
/// <summary>
/// Attempting to apply a displacement map to a specific layer of SpriteComponent
/// </summary>
@@ -19,21 +25,22 @@ public sealed class DisplacementMapSystem : EntitySystem
/// <param name="key">Unique layer key, which will determine which layer to apply displacement map to</param>
/// <param name="displacementKey">The key of the new displacement map layer added by this function.</param>
/// <returns></returns>
public bool TryAddDisplacement(DisplacementData data,
public bool TryAddDisplacement(
DisplacementData data,
Entity<SpriteComponent> sprite,
int index,
object key,
out string displacementKey)
[NotNullWhen(true)] out string? displacementKey
)
{
displacementKey = $"{key}-displacement";
if (key.ToString() is null)
displacementKey = BuildDisplacementLayerKey(key);
if (displacementKey is null)
return false;
if (data.ShaderOverride != null)
sprite.Comp.LayerSetShader(index, data.ShaderOverride);
EnsureDisplacementIsNotOnSprite(sprite, key);
_sprite.RemoveLayer(sprite.AsNullable(), displacementKey, false);
if (data.ShaderOverride is not null)
sprite.Comp.LayerSetShader(index, data.ShaderOverride);
//allows you not to write it every time in the YML
foreach (var pair in data.SizeMaps)
@@ -70,7 +77,11 @@ public sealed class DisplacementMapSystem : EntitySystem
}
var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString() ?? "this is impossible";
// This previously assigned a string reading "this is impossible" if key.ToString eval'd to false.
// However, for the sake of sanity, we've changed this to assert non-null - !.
// If this throws an error, we're not sorry. Nanotrasen thanks you for your service fixing this bug.
displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString()!;
_sprite.AddLayer(sprite.AsNullable(), displacementLayer, index);
_sprite.LayerMapSet(sprite.AsNullable(), displacementKey, index);
@@ -78,14 +89,18 @@ public sealed class DisplacementMapSystem : EntitySystem
return true;
}
/// <inheritdoc cref="TryAddDisplacement"/>
[Obsolete("Use the Entity<SpriteComponent> overload")]
public bool TryAddDisplacement(DisplacementData data,
SpriteComponent sprite,
int index,
object key,
out string displacementKey)
/// <summary>
/// Ensures that the displacement map associated with the given layer key is not in the Sprite's LayerMap.
/// </summary>
/// <param name="sprite">The sprite to remove the displacement layer from.</param>
/// <param name="key">The key of the layer that is referenced by the displacement layer we want to remove.</param>
/// <param name="logMissing">Whether to report an error if the displacement map isn't on the sprite.</param>
public void EnsureDisplacementIsNotOnSprite(Entity<SpriteComponent> sprite, object key)
{
return TryAddDisplacement(data, (sprite.Owner, sprite), index, key, out displacementKey);
var displacementLayerKey = BuildDisplacementLayerKey(key);
if (displacementLayerKey is null)
return;
_sprite.RemoveLayer(sprite.AsNullable(), displacementLayerKey, false);
}
}

View File

@@ -142,7 +142,7 @@ public sealed class DoorSystem : SharedDoorSystem
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
{
if (!_prototypeManager.TryIndex(targetProto, out var target))
if (!_prototypeManager.Resolve(targetProto, out var target))
return;
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))

View File

@@ -1,25 +1,58 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Components;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Ghost;
public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface
public sealed class GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private GhostRoleRadioMenu? _ghostRoleRadioMenu;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
private SimpleRadialMenu? _ghostRoleRadioMenu;
protected override void Open()
{
base.Open();
_ghostRoleRadioMenu = this.CreateWindow<GhostRoleRadioMenu>();
_ghostRoleRadioMenu.SetEntity(Owner);
_ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage;
_ghostRoleRadioMenu = this.CreateWindow<SimpleRadialMenu>();
// The purpose of this radial UI is for ghost role radios that allow you to select
// more than one potential option, such as with kobolds/lizards.
// This means that it won't show anything if SelectablePrototypes is empty.
if (!EntMan.TryGetComponent<GhostRoleMobSpawnerComponent>(Owner, out var comp))
return;
var list = ConvertToButtons(comp.SelectablePrototypes);
_ghostRoleRadioMenu.SetButtons(list);
}
private IEnumerable<RadialMenuOptionBase> ConvertToButtons(List<ProtoId<GhostRolePrototype>> protoIds)
{
var list = new List<RadialMenuOptionBase>();
foreach (var ghostRoleProtoId in protoIds)
{
// For each prototype we find we want to create a button that uses the name of the ghost role
// as the hover tooltip, and the icon is taken from either the ghost role entityprototype
// or the indicated icon entityprototype.
if (!_prototypeManager.Resolve(ghostRoleProtoId, out var ghostRoleProto))
continue;
var option = new RadialMenuActionOption<ProtoId<GhostRolePrototype>>(SendGhostRoleRadioMessage, ghostRoleProtoId)
{
ToolTip = Loc.GetString(ghostRoleProto.Name),
// pick the icon if it exists, otherwise fallback to the ghost role's entity
IconSpecifier = ghostRoleProto.IconPrototype != null
&& _prototypeManager.Resolve(ghostRoleProto.IconPrototype, out var iconProto)
? RadialMenuIconSpecifier.With(iconProto)
: RadialMenuIconSpecifier.With(ghostRoleProto.EntityPrototype)
};
list.Add(option);
}
return list;
}
private void SendGhostRoleRadioMessage(ProtoId<GhostRolePrototype> protoId)

View File

@@ -1,8 +0,0 @@
<ui:RadialMenu
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True">
<ui:RadialContainer Name="Main">
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -1,105 +0,0 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client.Ghost;
public sealed partial class GhostRoleRadioMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public event Action<ProtoId<GhostRolePrototype>>? SendGhostRoleRadioMessageAction;
public EntityUid Entity { get; set; }
public GhostRoleRadioMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
public void SetEntity(EntityUid uid)
{
Entity = uid;
RefreshUI();
}
private void RefreshUI()
{
// The main control that will contain all the clickable options
var main = FindControl<RadialContainer>("Main");
// The purpose of this radial UI is for ghost role radios that allow you to select
// more than one potential option, such as with kobolds/lizards.
// This means that it won't show anything if SelectablePrototypes is empty.
if (!_entityManager.TryGetComponent<GhostRoleMobSpawnerComponent>(Entity, out var comp))
return;
foreach (var ghostRoleProtoString in comp.SelectablePrototypes)
{
// For each prototype we find we want to create a button that uses the name of the ghost role
// as the hover tooltip, and the icon is taken from either the ghost role entityprototype
// or the indicated icon entityprototype.
if (!_prototypeManager.TryIndex<GhostRolePrototype>(ghostRoleProtoString, out var ghostRoleProto))
continue;
var button = new GhostRoleRadioMenuButton()
{
SetSize = new Vector2(64, 64),
ToolTip = Loc.GetString(ghostRoleProto.Name),
ProtoId = ghostRoleProto.ID,
};
var entProtoView = new EntityPrototypeView()
{
SetSize = new Vector2(48, 48),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Stretch = SpriteView.StretchMode.Fill
};
// pick the icon if it exists, otherwise fallback to the ghost role's entity
if (_prototypeManager.TryIndex(ghostRoleProto.IconPrototype, out var iconProto))
entProtoView.SetPrototype(iconProto);
else
entProtoView.SetPrototype(ghostRoleProto.EntityPrototype);
button.AddChild(entProtoView);
main.AddChild(button);
AddGhostRoleRadioMenuButtonOnClickActions(main);
}
}
private void AddGhostRoleRadioMenuButtonOnClickActions(Control control)
{
var mainControl = control as RadialContainer;
if (mainControl == null)
return;
foreach (var child in mainControl.Children)
{
var castChild = child as GhostRoleRadioMenuButton;
if (castChild == null)
continue;
castChild.OnButtonUp += _ =>
{
SendGhostRoleRadioMessageAction?.Invoke(castChild.ProtoId);
Close();
};
}
}
}
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<GhostRolePrototype> ProtoId { get; set; }
}

View File

@@ -5,14 +5,17 @@ using Content.Client.Guidebook.Richtext;
using Content.Client.Message;
using Content.Client.UserInterface.ControlExtensions;
using Content.Shared.Body.Prototypes;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Contraband;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -27,8 +30,10 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
private readonly ChemistryGuideDataSystem _chemistryGuideData;
private readonly ContrabandSystem _contraband;
private readonly ISawmill _sawmill;
public IPrototype? RepresentedPrototype { get; private set; }
@@ -39,6 +44,7 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("guidebook.reagent");
_chemistryGuideData = _systemManager.GetEntitySystem<ChemistryGuideDataSystem>();
_contraband = _systemManager.GetEntitySystem<ContrabandSystem>();
MouseFilter = MouseFilterMode.Stop;
}
@@ -204,6 +210,25 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
description.PushNewline();
description.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-physical-description",
("description", reagent.LocalizedPhysicalDescription)));
if (_config.GetCVar(CCVars.ContrabandExamine))
{
// Department-restricted text
if (reagent.AllowedJobs.Count > 0 || reagent.AllowedDepartments.Count > 0)
{
description.PushNewline();
description.AddMarkupPermissive(
_contraband.GenerateDepartmentExamineMessage(reagent.AllowedDepartments, reagent.AllowedJobs, ContrabandItemType.Reagent));
}
// Other contraband text
else if (reagent.ContrabandSeverity != null &&
_prototype.Resolve(reagent.ContrabandSeverity.Value, out var severity))
{
description.PushNewline();
description.AddMarkupPermissive(Loc.GetString(severity.ExamineText, ("type", ContrabandItemType.Reagent)));
}
}
ReagentDescription.SetMessage(description);
}

View File

@@ -53,7 +53,7 @@ public sealed partial class DocumentParsingManager
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
{
if (!_prototype.TryIndex(entryId, out var entry))
if (!_prototype.Resolve(entryId, out var entry))
return false;
using var file = _resourceManager.ContentFileReadText(entry.Text);

View File

@@ -300,25 +300,26 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
private void RemoveMarking(Marking marking, Entity<SpriteComponent> spriteEnt)
{
if (!_markingManager.TryGetMarking(marking, out var prototype))
{
return;
}
foreach (var sprite in prototype.Sprites)
{
if (sprite is not SpriteSpecifier.Rsi rsi)
{
continue;
}
var layerId = $"{marking.MarkingId}-{rsi.RsiState}";
if (!_sprite.LayerMapTryGet(spriteEnt.AsNullable(), layerId, out var index, false))
{
continue;
}
_sprite.LayerMapRemove(spriteEnt.AsNullable(), layerId);
_sprite.RemoveLayer(spriteEnt.AsNullable(), index);
// If this marking is one that can be displaced, we need to remove the displacement as well; otherwise
// altering a marking at runtime can lead to the renderer falling over.
// The Vulps must be shaved.
// (https://github.com/space-wizards/space-station-14/issues/40135).
if (prototype.CanBeDisplaced)
_displacement.EnsureDisplacementIsNotOnSprite(spriteEnt, layerId);
}
}
@@ -357,9 +358,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
var sprite = entity.Comp2;
if (!_sprite.LayerMapTryGet((entity.Owner, sprite), markingPrototype.BodyPart, out var targetLayer, false))
{
return;
}
visible &= !IsHidden(humanoid, markingPrototype.BodyPart);
visible &= humanoid.BaseLayers.TryGetValue(markingPrototype.BodyPart, out var setting)
@@ -370,9 +369,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
var markingSprite = markingPrototype.Sprites[j];
if (markingSprite is not SpriteSpecifier.Rsi rsi)
{
continue;
}
return;
var layerId = $"{markingPrototype.ID}-{rsi.RsiState}";
@@ -386,26 +383,18 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
_sprite.LayerSetVisible((entity.Owner, sprite), layerId, visible);
if (!visible || setting == null) // this is kinda implied
{
continue;
}
// Okay so if the marking prototype is modified but we load old marking data this may no longer be valid
// and we need to check the index is correct.
// So if that happens just default to white?
if (colors != null && j < colors.Count)
{
_sprite.LayerSetColor((entity.Owner, sprite), layerId, colors[j]);
}
else
{
_sprite.LayerSetColor((entity.Owner, sprite), layerId, Color.White);
}
if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
{
_displacement.TryAddDisplacement(displacementData, (entity.Owner, sprite), targetLayer + j + 1, layerId, out _);
}
}
}

View File

@@ -28,7 +28,7 @@ public sealed class ImplanterSystem : SharedImplanterSystem
Dictionary<string, string> implants = new();
foreach (var implant in component.DeimplantWhitelist)
{
if (_proto.TryIndex(implant, out var proto))
if (_proto.Resolve(implant, out var proto))
implants.Add(proto.ID, proto.Name);
}

View File

@@ -62,7 +62,7 @@ public sealed partial class ChameleonControllerMenu : FancyWindow
// Go through every outfit and add them to the correct department.
foreach (var outfit in _outfits)
{
_prototypeManager.TryIndex(outfit.Job, out var jobProto);
_prototypeManager.Resolve(outfit.Job, out var jobProto);
var name = outfit.LoadoutName ?? outfit.Name ?? jobProto?.Name ?? "Prototype has no name or job.";

View File

@@ -49,7 +49,7 @@ public sealed class ImplanterStatusControl : Control
if (_parent.CurrentMode == ImplanterToggleMode.Draw)
{
string implantName = _parent.DeimplantChosen != null
? (_prototype.TryIndex(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text"))
? (_prototype.Resolve(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text"))
: Loc.GetString("implanter-empty-text");
_label.SetMarkup(Loc.GetString("implanter-label-draw",

View File

@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
@@ -96,7 +97,7 @@ public sealed partial class LatheMenu : DefaultWindow
var recipesToShow = new List<LatheRecipePrototype>();
foreach (var recipe in Recipes)
{
if (!_prototypeManager.TryIndex(recipe, out var proto))
if (!_prototypeManager.Resolve(recipe, out var proto))
continue;
// Category filtering
@@ -128,21 +129,50 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
RecipeList.Children.Clear();
// Get the existing list of queue controls
var oldChildCount = RecipeList.ChildCount;
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
int idx = 0;
foreach (var prototype in sortedRecipesToShow)
{
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
var tooltipFunction = () => GenerateTooltipText(prototype);
var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
if (idx >= oldChildCount)
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
amount = 1;
RecipeQueueAction?.Invoke(s, amount);
};
RecipeList.AddChild(control);
var control = new RecipeControl(_lathe, prototype, tooltipFunction, canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
amount = 1;
RecipeQueueAction?.Invoke(s, amount);
};
RecipeList.AddChild(control);
}
else
{
var child = RecipeList.GetChild(idx) as RecipeControl;
if (child == null)
{
DebugTools.Assert($"Lathe menu recipe control at {idx} is not of type RecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetRecipe(prototype);
child.SetTooltipSupplier(tooltipFunction);
child.SetCanProduce(canProduce);
child.SetDisplayControl(GetRecipeDisplayControl(prototype));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
RecipeList.RemoveChild(childIdx);
}
}
@@ -153,7 +183,7 @@ public sealed partial class LatheMenu : DefaultWindow
foreach (var (id, amount) in prototype.Materials)
{
if (!_prototypeManager.TryIndex(id, out var proto))
if (!_prototypeManager.Resolve(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier);
@@ -238,9 +268,10 @@ public sealed partial class LatheMenu : DefaultWindow
/// <param name="queue"></param>
public void PopulateQueueList(IReadOnlyCollection<LatheRecipeBatch> queue)
{
QueueList.DisposeAllChildren();
// Get the existing list of queue controls
var oldChildCount = QueueList.ChildCount;
var idx = 1;
var idx = 0;
foreach (var batch in queue)
{
var recipe = _prototypeManager.Index(batch.Recipe);
@@ -248,18 +279,40 @@ public sealed partial class LatheMenu : DefaultWindow
var itemName = _lathe.GetRecipeName(batch.Recipe);
string displayText;
if (batch.ItemsRequested > 1)
displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx + 1), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
else
displayText = Loc.GetString("lathe-menu-item-single", ("index", idx), ("name", itemName));
displayText = Loc.GetString("lathe-menu-item-single", ("index", idx + 1), ("name", itemName));
var queuedRecipeBox = new QueuedRecipeControl(displayText, idx - 1, GetRecipeDisplayControl(recipe));
queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
if (idx >= oldChildCount)
{
var queuedRecipeBox = new QueuedRecipeControl(displayText, idx, GetRecipeDisplayControl(recipe));
queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
QueueList.AddChild(queuedRecipeBox);
}
else
{
var child = QueueList.GetChild(idx) as QueuedRecipeControl;
QueueList.AddChild(queuedRecipeBox);
if (child == null)
{
DebugTools.Assert($"Lathe menu queued recipe control at {idx} is not of type QueuedRecipeControl"); // Something's gone terribly wrong.
continue;
}
child.SetDisplayText(displayText);
child.SetIndex(idx);
child.SetDisplayControl(GetRecipeDisplayControl(recipe));
}
idx++;
}
// Shrink list if new list is shorter than old list.
for (var childIdx = oldChildCount - 1; idx <= childIdx; childIdx--)
{
QueueList.RemoveChild(childIdx);
}
}
public void SetQueueInfo(ProtoId<LatheRecipePrototype>? recipeProto)

View File

@@ -11,26 +11,46 @@ public sealed partial class QueuedRecipeControl : Control
public Action<int>? OnMoveUpPressed;
public Action<int>? OnMoveDownPressed;
private int _index;
public QueuedRecipeControl(string displayText, int index, Control displayControl)
{
RobustXamlLoader.Load(this);
RecipeName.Text = displayText;
RecipeDisplayContainer.AddChild(displayControl);
SetDisplayText(displayText);
SetDisplayControl(displayControl);
SetIndex(index);
_index = index;
MoveUp.OnPressed += (_) =>
{
OnMoveUpPressed?.Invoke(index);
OnMoveUpPressed?.Invoke(_index);
};
MoveDown.OnPressed += (_) =>
{
OnMoveDownPressed?.Invoke(index);
OnMoveDownPressed?.Invoke(_index);
};
Delete.OnPressed += (_) =>
{
OnDeletePressed?.Invoke(index);
OnDeletePressed?.Invoke(_index);
};
}
public void SetDisplayText(string displayText)
{
RecipeName.Text = displayText;
}
public void SetDisplayControl(Control displayControl)
{
RecipeDisplayContainer.Children.Clear();
RecipeDisplayContainer.AddChild(displayControl);
}
public void SetIndex(int index)
{
_index = index;
}
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -11,20 +12,47 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
private ProtoId<LatheRecipePrototype> _recipeId;
private LatheSystem _latheSystem;
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl)
{
RobustXamlLoader.Load(this);
RecipeName.Text = latheSystem.GetRecipeName(recipe);
RecipeDisplayContainer.AddChild(displayControl);
Button.Disabled = !canProduce;
_latheSystem = latheSystem;
_recipeId = recipe.ID;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;
SetRecipe(recipe);
SetCanProduce(canProduce);
SetDisplayControl(displayControl);
Button.OnPressed += (_) =>
{
OnButtonPressed?.Invoke(recipe.ID);
OnButtonPressed?.Invoke(_recipeId);
};
Button.TooltipSupplier = SupplyTooltip;
}
public void SetRecipe(LatheRecipePrototype recipe)
{
RecipeName.Text = _latheSystem.GetRecipeName(recipe);
_recipeId = recipe.ID;
}
public void SetTooltipSupplier(Func<string> tooltipTextSupplier)
{
TooltipTextSupplier = tooltipTextSupplier;
}
public void SetCanProduce(bool canProduce)
{
Button.Disabled = !canProduce;
}
public void SetDisplayControl(Control displayControl)
{
RecipeDisplayContainer.Children.Clear();
RecipeDisplayContainer.AddChild(displayControl);
}
private Control? SupplyTooltip(Control sender)

View File

@@ -72,6 +72,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
});
_configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleLoadoutTimers, _ => RefreshProfileEditor());
_configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
}
@@ -361,7 +362,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
{
foreach (var loadout in group)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
if (!_prototypeManager.Resolve(loadout.Prototype, out var loadoutProto))
continue;
_spawn.EquipStartingGear(uid, loadoutProto);
@@ -384,14 +385,14 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
{
foreach (var loadout in loadouts)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
if (!_prototypeManager.Resolve(loadout.Prototype, out var loadoutProto))
continue;
// TODO: Need some way to apply starting gear to an entity and replace existing stuff coz holy fucking shit dude.
foreach (var slot in slots)
{
// Try startinggear first
if (_prototypeManager.TryIndex(loadoutProto.StartingGear, out var loadoutGear))
if (_prototypeManager.Resolve(loadoutProto.StartingGear, out var loadoutGear))
{
var itemType = ((IEquipmentLoadout) loadoutGear).GetGear(slot.Name);
@@ -426,7 +427,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
}
}
if (!_prototypeManager.TryIndex(job.StartingGear, out var gear))
if (!_prototypeManager.Resolve(job.StartingGear, out var gear))
return;
foreach (var slot in slots)

View File

@@ -9,7 +9,6 @@ using Content.Client.Players.PlayTimeTracking;
using Content.Client.Sprite;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._CP14.Humanoid;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
@@ -811,7 +810,7 @@ namespace Content.Client.Lobby.UI
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = new ProtoId<GuideEntryPrototype>(species.Id); // Gross. See above todo comment.
if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
if (_prototypeManager.Resolve(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
@@ -1089,10 +1088,11 @@ namespace Content.Client.Lobby.UI
if (Profile is null) return;
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
var strategy = _prototypeManager.Index(skin).Strategy;
switch (skin)
switch (strategy.InputType)
{
case HumanoidSkinColor.HumanToned:
case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
@@ -1100,39 +1100,14 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = false;
}
var color = SkinColor.HumanSkinTone((int) Skin.Value);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
break;
}
case HumanoidSkinColor.Hues:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
break;
}
case HumanoidSkinColor.TintedHues:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
var color = strategy.FromUnary(Skin.Value);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
case HumanoidSkinColor.VoxFeathers:
case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
@@ -1140,28 +1115,13 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = true;
}
var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
// CP14 - Custom HumanoidSkinColor - Start
case HumanoidSkinColor.TieflingHues:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
var color = CP14SkinColor.MakeTieflingHueValid(_rgbSkinColorSelector.Color);
Markings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
// CP14 - Custom HumanoidSkinColor - End
}
ReloadProfilePreview();
@@ -1308,7 +1268,7 @@ namespace Content.Client.Lobby.UI
var sexes = new List<Sex>();
// add species sex options, default to just none if we are in bizzaro world and have no species
if (_prototypeManager.TryIndex<SpeciesPrototype>(Profile.Species, out var speciesProto))
if (_prototypeManager.Resolve<SpeciesPrototype>(Profile.Species, out var speciesProto))
{
foreach (var sex in speciesProto.Sexes)
{
@@ -1338,10 +1298,11 @@ namespace Content.Client.Lobby.UI
return;
var skin = _prototypeManager.Index<SpeciesPrototype>(Profile.Species).SkinColoration;
var strategy = _prototypeManager.Index(skin).Strategy;
switch (skin)
switch (strategy.InputType)
{
case HumanoidSkinColor.HumanToned:
case SkinColorationStrategyInput.Unary:
{
if (!Skin.Visible)
{
@@ -1349,11 +1310,11 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = false;
}
Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
Skin.Value = strategy.ToUnary(Profile.Appearance.SkinColor);
break;
}
case HumanoidSkinColor.Hues:
case SkinColorationStrategyInput.Color:
{
if (!RgbSkinColorContainer.Visible)
{
@@ -1361,49 +1322,11 @@ namespace Content.Client.Lobby.UI
RgbSkinColorContainer.Visible = true;
}
// set the RGB values to the direct values otherwise
_rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
break;
}
case HumanoidSkinColor.TintedHues:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
// set the RGB values to the direct values otherwise
_rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
break;
}
case HumanoidSkinColor.VoxFeathers:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
_rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
_rgbSkinColorSelector.Color = strategy.ClosestSkinColor(Profile.Appearance.SkinColor);
break;
}
// CP14 - Custom HumanoidSkinColor - Start
case HumanoidSkinColor.TieflingHues:
{
if (!RgbSkinColorContainer.Visible)
{
Skin.Visible = false;
RgbSkinColorContainer.Visible = true;
}
_rgbSkinColorSelector.Color = CP14SkinColor.MakeTieflingHueValid(Profile.Appearance.SkinColor);
break;
}
// CP14 - Custom HumanoidSkinColor - End
}
}
public void UpdateSpeciesGuidebookIcon()
@@ -1414,7 +1337,7 @@ namespace Content.Client.Lobby.UI
if (species is null)
return;
if (!_prototypeManager.TryIndex<SpeciesPrototype>(species, out var speciesProto))
if (!_prototypeManager.Resolve<SpeciesPrototype>(species, out var speciesProto))
return;
// Don't display the info button if no guide entry is found

View File

@@ -40,7 +40,7 @@ public sealed partial class LoadoutContainer : BoxContainer
SelectButton.TooltipSupplier = _ => tooltip;
}
if (_protoManager.TryIndex(proto, out var loadProto))
if (_protoManager.Resolve(proto, out var loadProto))
{
var ent = loadProto.DummyEntity ?? _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);

View File

@@ -62,7 +62,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
});
}
if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
if (protoMan.Resolve(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
@@ -112,14 +112,14 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
})
.ToList();
/*
* Determine which element should be displayed first:
* - If any element is currently selected (its button is pressed), use it.
* - Otherwise, fallback to the first element in the list.
*
* This moves the selected item outside of the sublist for better usability,
* making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
* without having to search inside expanded subgroups.
/*
* Determine which element should be displayed first:
* - If any element is currently selected (its button is pressed), use it.
* - Otherwise, fallback to the first element in the list.
*
* This moves the selected item outside of the sublist for better usability,
* making it easier for players to quickly toggle loadout options (e.g. clothing, accessories)
* without having to search inside expanded subgroups.
*/
var firstElement = uiElements.FirstOrDefault(e => e.Select.Pressed) ?? uiElements[0];
@@ -195,8 +195,8 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
/// <summary>
/// Creates a UI container for a single Loadout item.
///
/// This method was extracted from RefreshLoadouts because the logic for creating
/// individual loadout items is used multiple times inside that method, and duplicating
/// This method was extracted from RefreshLoadouts because the logic for creating
/// individual loadout items is used multiple times inside that method, and duplicating
/// the code made it harder to maintain.
///
/// Logic:

View File

@@ -68,7 +68,7 @@ public sealed partial class LoadoutWindow : FancyWindow
{
foreach (var group in proto.Groups)
{
if (!protoManager.TryIndex(group, out var groupProto))
if (!protoManager.Resolve(group, out var groupProto))
continue;
if (groupProto.Hidden)

View File

@@ -64,7 +64,9 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
if (!LocalizedNames.TryGetValue(netEntity, out var name))
name = "Unknown";
var message = name + "\nLocation: [x = " + MathF.Round(blip.Coordinates.X) + ", y = " + MathF.Round(blip.Coordinates.Y) + "]";
var message = name + "\n" + Loc.GetString("navmap-location",
("x", MathF.Round(blip.Coordinates.X)),
("y", MathF.Round(blip.Coordinates.Y)));
_trackedEntityLabel.Text = message;
_trackedEntityPanel.Visible = true;

View File

@@ -57,7 +57,7 @@ public sealed class EntityHealthBarOverlay : Overlay
const float scale = 1f;
var scaleMatrix = Matrix3Helpers.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3Helpers.CreateRotation(-rotation);
_prototype.TryIndex(StatusIcon, out var statusIcon);
_prototype.Resolve(StatusIcon, out var statusIcon);
var query = _entManager.AllEntityQueryEnumerator<MobThresholdsComponent, MobStateComponent, DamageableComponent, SpriteComponent>();
while (query.MoveNext(out var uid,

View File

@@ -22,7 +22,7 @@ public sealed class ShowCriminalRecordIconsSystem : EquipmentHudSystem<ShowCrimi
if (!IsActive)
return;
if (_prototype.TryIndex(component.StatusIcon, out var iconPrototype))
if (_prototype.Resolve(component.StatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -78,9 +78,9 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
if (TryComp<MobStateComponent>(entity, out var state))
{
// Since there is no MobState for a rotting mob, we have to deal with this case first.
if (HasComp<RottingComponent>(entity) && _prototypeMan.TryIndex(damageableComponent.RottingIcon, out var rottingIcon))
if (HasComp<RottingComponent>(entity) && _prototypeMan.Resolve(damageableComponent.RottingIcon, out var rottingIcon))
result.Add(rottingIcon);
else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.TryIndex(value, out var icon))
else if (damageableComponent.HealthIcons.TryGetValue(state.CurrentState, out var value) && _prototypeMan.Resolve(value, out var icon))
result.Add(icon);
}
}

View File

@@ -51,7 +51,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
}
}
if (_prototype.TryIndex(iconId, out var iconPrototype))
if (_prototype.Resolve(iconId, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
else
Log.Error($"Invalid job icon prototype: {iconPrototype}");

View File

@@ -23,7 +23,7 @@ public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShiel
{
if(!IsActive)
return;
if (component.IsEnabled && _prototype.TryIndex(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
if (component.IsEnabled && _prototype.Resolve(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
ev.StatusIcons.Add(fakeStatusIconPrototype);
}
@@ -32,7 +32,7 @@ public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShiel
if (!IsActive)
return;
if (_prototype.TryIndex(component.MindShieldStatusIcon, out var iconPrototype))
if (_prototype.Resolve(component.MindShieldStatusIcon, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -66,7 +66,7 @@ public sealed partial class StencilOverlay : Overlay
{
foreach (var (proto, weather) in comp.Weather)
{
if (!_protoManager.TryIndex(proto, out var weatherProto))
if (!_protoManager.Resolve<WeatherPrototype>(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);

View File

@@ -120,8 +120,8 @@ public sealed class MoverController : SharedMoverController
base.SetSprinting(entity, subTick, walking);
if (walking && _cfg.GetCVar(CCVars.ToggleWalk))
_alerts.ShowAlert(entity, WalkingAlert, showCooldown: false, autoRemove: false);
_alerts.ShowAlert(entity.Owner, WalkingAlert, showCooldown: false, autoRemove: false);
else
_alerts.ClearAlert(entity, WalkingAlert);
_alerts.ClearAlert(entity.Owner, WalkingAlert);
}
}

View File

@@ -20,7 +20,7 @@
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
StyleClasses="StatusFieldTitle" MinWidth="120"/>
<BoxContainer Orientation="Horizontal" MinWidth="90">
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True"/>
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
</BoxContainer>
<!--Charging Status-->
<Label Text="{Loc 'apc-menu-external-label'}" StyleClasses="StatusFieldTitle" MinWidth="120" />

View File

@@ -51,10 +51,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
_menu.OpenOverMouseScreenPosition();
}
private IEnumerable<RadialMenuOption> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
private IEnumerable<RadialMenuOptionBase> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
{
Dictionary<string, List<RadialMenuActionOption>> buttonsByCategory = new();
ValueList<RadialMenuActionOption> topLevelActions = new();
Dictionary<string, List<RadialMenuActionOptionBase>> buttonsByCategory = new();
ValueList<RadialMenuActionOptionBase> topLevelActions = new();
foreach (var protoId in prototypes)
{
var prototype = _prototypeManager.Index(protoId);
@@ -62,7 +62,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
var topLevelActionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
{
Sprite = prototype.Sprite,
IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
topLevelActions.Add(topLevelActionOption);
@@ -74,26 +74,26 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
{
list = new List<RadialMenuActionOption>();
list = new List<RadialMenuActionOptionBase>();
buttonsByCategory.Add(prototype.Category, list);
}
var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
{
Sprite = prototype.Sprite,
IconSpecifier = RadialMenuIconSpecifier.With(prototype.Sprite),
ToolTip = GetTooltip(prototype)
};
list.Add(actionOption);
}
var models = new RadialMenuOption[buttonsByCategory.Count + topLevelActions.Count];
var models = new RadialMenuOptionBase[buttonsByCategory.Count + topLevelActions.Count];
var i = 0;
foreach (var (key, list) in buttonsByCategory)
{
var groupInfo = PrototypesGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list)
{
Sprite = groupInfo.Sprite,
IconSpecifier = RadialMenuIconSpecifier.With(groupInfo.Sprite),
ToolTip = Loc.GetString(groupInfo.Tooltip)
};
i++;
@@ -125,8 +125,10 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
_prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
_prototypeManager.Resolve(proto.Prototype, out var entProto))
{
name = entProto.Name;
}
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
@@ -142,7 +144,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
&& proto.Prototype != null
&& _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
&& _prototypeManager.Resolve(proto.Prototype, out var entProto))
{
tooltip = Loc.GetString(entProto.Name);
}

View File

@@ -1,11 +1,12 @@
using Content.Client.Radio.Ui;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
using Content.Shared.Radio.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Radio.EntitySystems;
public sealed class RadioDeviceSystem : EntitySystem
public sealed class RadioDeviceSystem : SharedRadioDeviceSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;

View File

@@ -42,7 +42,7 @@ public sealed partial class IntercomMenu : FancyWindow
for (var i = 0; i < entity.Comp.SupportedChannels.Count; i++)
{
var channel = entity.Comp.SupportedChannels[i];
if (!_prototype.TryIndex(channel, out var prototype))
if (!_prototype.Resolve(channel, out var prototype))
continue;
_channels.Add(channel);

View File

@@ -25,13 +25,13 @@ public sealed class RevolutionarySystem : SharedRevolutionarySystem
if (HasComp<HeadRevolutionaryComponent>(ent))
return;
if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
private void GetHeadRevIcon(Entity<HeadRevolutionaryComponent> ent, ref GetStatusIconsEvent args)
{
if (_prototype.TryIndex(ent.Comp.StatusIcon, out var iconPrototype))
if (_prototype.Resolve(ent.Comp.StatusIcon, out var iconPrototype))
args.StatusIcons.Add(iconPrototype);
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Shuttles.Systems;
namespace Content.Client.Shuttles.Systems;
public sealed partial class EmergencyShuttleSystem : SharedEmergencyShuttleSystem;

View File

@@ -26,7 +26,7 @@ public sealed partial class LawDisplay : Control
private readonly Dictionary<Button, TimeSpan> _nextAllowedPress = new();
public LawDisplay(EntityUid uid, SiliconLaw law, HashSet<string>? radioChannels)
public LawDisplay(EntityUid uid, SiliconLaw law, HashSet<ProtoId<RadioChannelPrototype>>? radioChannels)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

View File

@@ -23,15 +23,15 @@ public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : B
_menu.Open();
}
private IEnumerable<RadialMenuActionOption> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
private IEnumerable<RadialMenuActionOptionBase> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
{
var models = new RadialMenuActionOption[actions.Count];
var models = new RadialMenuActionOptionBase[actions.Count];
for (int i = 0; i < actions.Count; i++)
{
var action = actions[i];
models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event)
{
Sprite = action.Sprite,
IconSpecifier = RadialMenuIconSpecifier.With(action.Sprite),
ToolTip = action.Tooltip
};
}

View File

@@ -44,7 +44,7 @@ public sealed partial class StationAiCustomizationMenu : FancyWindow
StationAiCustomizationPrototype? selectedPrototype = null;
if (stationAiCustomization?.ProtoIds.TryGetValue(groupPrototype, out var selectedProtoId) == true)
_protoManager.TryIndex(selectedProtoId, out selectedPrototype);
_protoManager.Resolve(selectedProtoId, out selectedPrototype);
_buttonGroups[groupPrototype] = new ButtonGroup();
_groupContainers[groupPrototype] = new StationAiCustomizationGroupContainer(groupPrototype, selectedPrototype, _buttonGroups[groupPrototype], this, _protoManager);
@@ -76,7 +76,7 @@ public sealed partial class StationAiCustomizationMenu : FancyWindow
// Create UI entries for all customization in the group
foreach (var protoId in groupPrototype.ProtoIds)
{
if (!protoManager.TryIndex(protoId, out var prototype))
if (!protoManager.Resolve(protoId, out var prototype))
continue;
var entry = new StationAiCustomizationEntryContainer(groupPrototype, prototype, buttonGroup, menu);

View File

@@ -65,7 +65,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
var groupList = new List<string>();
foreach (var groupId in category.Groups)
{
if (!Proto.TryIndex(groupId, out var group))
if (!Proto.Resolve(groupId, out var group))
continue;
groupList.Add(groupId);

View File

@@ -42,7 +42,7 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
var forceRedrawBase = false;
if (AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
if (_prototypeManager.TryIndex(prototype, out var proto))
if (_prototypeManager.Resolve(prototype, out var proto))
{
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
{

View File

@@ -33,7 +33,7 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
_validCurrencies.Clear();
foreach (var currency in balance)
{
if (!_prototypeManager.TryIndex(currency.Key, out var proto))
if (!_prototypeManager.Resolve(currency.Key, out var proto))
continue;
_validCurrencies.Add(proto, currency.Value);

View File

@@ -10,7 +10,7 @@
<PanelContainer StyleClasses="WindowHeadingBackground" />
<BoxContainer Margin="4 2 8 0" Orientation="Horizontal">
<Label Name="WindowTitle"
HorizontalExpand="True" VAlign="Center" StyleClasses="FancyWindowTitle" />
HorizontalExpand="True" VAlign="Center" StyleClasses="FancyWindowTitle" ClipText="true" />
<TextureButton Name="HelpButton" StyleClasses="windowHelpButton" VerticalAlignment="Center" Disabled="True" Visible="False" Access="Public" />
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
VerticalAlignment="Center" />

View File

@@ -229,10 +229,10 @@ public class RadialMenu : BaseWindow
/// from interactions.
/// </summary>
[Virtual]
public class RadialMenuTextureButtonBase : TextureButton
public abstract class RadialMenuButtonBase : BaseButton
{
/// <inheritdoc />
protected RadialMenuTextureButtonBase()
protected RadialMenuButtonBase()
{
EnableAllKeybinds = true;
}
@@ -242,7 +242,9 @@ public class RadialMenuTextureButtonBase : TextureButton
{
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
base.KeyBindUp(args);
}
}
}
@@ -253,8 +255,14 @@ public class RadialMenuTextureButtonBase : TextureButton
/// works only if control have parent, and ActiveContainer property is set.
/// Also considers all space outside of radial menu buttons as itself for clicking.
/// </summary>
public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
public sealed class RadialMenuContextualCentralTextureButton : TextureButton
{
/// <inheritdoc />
public RadialMenuContextualCentralTextureButton()
{
EnableAllKeybinds = true;
}
public float InnerRadius { get; set; }
public Vector2? ParentCenter { get; set; }
@@ -271,15 +279,25 @@ public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTexture
var innerRadiusSquared = InnerRadius * InnerRadius;
// comparing to squared values is faster then making sqrt
// comparing to squared values is faster, then making sqrt
return distSquared < innerRadiusSquared;
}
/// <inheritdoc />
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.UIClick
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
{
base.KeyBindUp(args);
}
}
}
/// <summary>
/// Menu button for outer area of radial menu (covers everything 'outside').
/// </summary>
public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
public sealed class RadialMenuOuterAreaButton : RadialMenuButtonBase
{
public float OuterRadius { get; set; }
@@ -303,7 +321,7 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
}
[Virtual]
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
public class RadialMenuButton : RadialMenuButtonBase
{
/// <summary>
/// Upon clicking this button the radial menu will be moved to the layer of this control.
@@ -319,9 +337,8 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuTextureButton()
public RadialMenuButton()
{
EnableAllKeybinds = true;
OnButtonUp += OnClicked;
}
@@ -391,7 +408,7 @@ public interface IRadialMenuItemWithSector
}
[Virtual]
public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
public class RadialMenuButtonWithSector : RadialMenuButton, IRadialMenuItemWithSector
{
private Vector2[]? _sectorPointsForDrawing;
@@ -500,7 +517,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuTextureButtonWithSector()
public RadialMenuButtonWithSector()
{
}

View File

@@ -7,6 +7,8 @@ using Robust.Client.GameObjects;
using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Input;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls;
@@ -30,7 +32,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
_attachMenuToEntity = owner;
}
public void SetButtons(IEnumerable<RadialMenuOption> models, SimpleRadialMenuSettings? settings = null)
public void SetButtons(IEnumerable<RadialMenuOptionBase> models, SimpleRadialMenuSettings? settings = null)
{
ClearExistingChildrenRadialButtons();
@@ -45,7 +47,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
private void Fill(
IEnumerable<RadialMenuOption> models,
IEnumerable<RadialMenuOptionBase> models,
SpriteSystem sprites,
ICollection<Control> rootControlChildren,
SimpleRadialMenuSettings settings
@@ -77,7 +79,7 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
}
private RadialMenuTextureButton RecursiveContainerExtraction(
private RadialMenuButton RecursiveContainerExtraction(
SpriteSystem sprites,
ICollection<Control> rootControlChildren,
RadialMenuNestedLayerOption model,
@@ -112,8 +114,8 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return thisLayerLinkButton;
}
private RadialMenuTextureButton ConvertToButton(
RadialMenuOption model,
private RadialMenuButton ConvertToButton(
RadialMenuOptionBase model,
SpriteSystem sprites,
SimpleRadialMenuSettings settings,
bool haveNested
@@ -121,29 +123,26 @@ public sealed partial class SimpleRadialMenu : RadialMenu
{
var button = settings.UseSectors
? ConvertToButtonWithSector(model, settings)
: new RadialMenuTextureButton();
: new RadialMenuButton();
button.SetSize = new Vector2(64f, 64f);
button.ToolTip = model.ToolTip;
if (model.Sprite != null)
var imageControl = model.IconSpecifier switch
{
var scale = Vector2.One;
RadialMenuTextureIconSpecifier textureSpecifier => CreateTexture(textureSpecifier.Sprite, sprites),
RadialMenuEntityIconSpecifier entitySpecifier => CreateSpriteView(entitySpecifier.Entity),
RadialMenuEntityPrototypeIconSpecifier entProtoSpecifier => CreateEntityPrototypeView(entProtoSpecifier.ProtoId),
_ => null
};
var texture = sprites.Frame0(model.Sprite);
if (texture.Width <= 32)
{
scale *= 2;
}
if(imageControl != null)
button.AddChild(imageControl);
button.TextureNormal = texture;
button.Scale = scale;
}
if (model is RadialMenuActionOption actionOption)
if (model is RadialMenuActionOptionBase actionOption)
{
button.OnPressed += _ =>
{
actionOption.OnPressed?.Invoke();
if(!haveNested)
if (!haveNested)
Close();
};
}
@@ -151,9 +150,53 @@ public sealed partial class SimpleRadialMenu : RadialMenu
return button;
}
private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
private Control CreateEntityPrototypeView(EntProtoId protoId)
{
var button = new RadialMenuTextureButtonWithSector
var entProtoView = new EntityPrototypeView
{
SetSize = new Vector2(48, 48),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Stretch = SpriteView.StretchMode.Fill,
};
entProtoView.SetPrototype(protoId);
return entProtoView;
}
private static Control CreateSpriteView(EntityUid entityForSpriteView)
{
var entView = new SpriteView
{
SetSize = new Vector2(48, 48),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Stretch = SpriteView.StretchMode.Fill,
};
entView.SetEntity(entityForSpriteView);
return entView;
}
private static Control CreateTexture(SpriteSpecifier spriteSpecifier, SpriteSystem sprites)
{
var scale = Vector2.One;
var texture = sprites.Frame0(spriteSpecifier);
if (texture.Width <= 32)
{
scale *= 2;
}
var imageControl = new TextureRect()
{
Texture = texture,
TextureScale = scale
};
return imageControl;
}
private static RadialMenuButtonWithSector ConvertToButtonWithSector(RadialMenuOptionBase model, SimpleRadialMenuSettings settings)
{
var button = new RadialMenuButtonWithSector
{
DrawBorder = settings.DisplayBorders,
DrawBackground = !settings.NoBackground
@@ -228,32 +271,99 @@ public sealed partial class SimpleRadialMenu : RadialMenu
}
public abstract class RadialMenuOption
/// <summary>
/// Abstract representation of a way to specify icon in radial menu.
/// </summary>
public abstract record RadialMenuIconSpecifier
{
public string? ToolTip { get; init; }
/// <summary> Use entity prototype viewer. </summary>
public static RadialMenuIconSpecifier? With(EntProtoId? protoId)
{
if (protoId is null)
return null;
public SpriteSpecifier? Sprite { get; init; }
public Color? BackgroundColor { get; set; }
public Color? HoverBackgroundColor { get; set; }
return new RadialMenuEntityPrototypeIconSpecifier(protoId.Value);
}
/// <summary> Use simple texture icon. </summary>
public static RadialMenuIconSpecifier? With(SpriteSpecifier? sprite)
{
if (sprite == null)
return null;
return new RadialMenuTextureIconSpecifier(sprite);
}
/// <summary> Use entity sprite viewer. </summary>
public static RadialMenuIconSpecifier? With(EntityUid? entity)
{
if (entity == null)
return null;
return new RadialMenuEntityIconSpecifier(entity.Value);
}
}
public abstract class RadialMenuActionOption(Action onPressed) : RadialMenuOption
/// <summary> Marker that <see cref="SpriteView"/> should be used to display radial menu icon. </summary>
public sealed record RadialMenuEntityIconSpecifier(EntityUid Entity) : RadialMenuIconSpecifier;
/// <summary> Marker that <see cref="TextureRect"/> should be used to display radial menu icon. </summary>
public sealed record RadialMenuTextureIconSpecifier(SpriteSpecifier Sprite) : RadialMenuIconSpecifier;
/// <summary> Marker that <see cref="EntityPrototypeView"/> should be used to display radial menu icon. </summary>
public sealed record RadialMenuEntityPrototypeIconSpecifier(EntProtoId ProtoId) : RadialMenuIconSpecifier;
/// <summary> Container for common options for radial menu button. </summary>
public abstract class RadialMenuOptionBase
{
/// <summary> Tooltip to be displayed when button is hovered. </summary>
public string? ToolTip { get; init; }
/// <summary>
/// Color for button background.
/// Is used only with sector radial (<see cref="SimpleRadialMenuSettings.UseSectors"/>).
/// </summary>
public Color? BackgroundColor { get; set; }
/// <summary>
/// Color for button background when it is hovered.
/// Is used only with sector radial (<see cref="SimpleRadialMenuSettings.UseSectors"/>).
/// </summary>
public Color? HoverBackgroundColor { get; set; }
/// <summary>
/// Specifier that describes icon to be used for radial menu button.
/// </summary>
public RadialMenuIconSpecifier? IconSpecifier { get; set; }
}
/// <summary> Base type for model of radial menu button with some action on button pressed. </summary>
/// <param name="onPressed"></param>
public abstract class RadialMenuActionOptionBase(Action onPressed) : RadialMenuOptionBase
{
/// <summary> Action to be executed on button press. </summary>
public Action OnPressed { get; } = onPressed;
}
public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data)
: RadialMenuActionOption(onPressed: () => onPressed(data));
/// <summary> Strong-typed model for radial menu button with action, stores provided data to be used upon button press. </summary>
public sealed class RadialMenuActionOption<T>(Action<T> onPressed, T data) : RadialMenuActionOptionBase(onPressed: () => onPressed(data));
public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOption> nested, float containerRadius = 100)
: RadialMenuOption
/// <summary>
/// Model for radial menu button that represents reference for next layer of radial buttons.
/// </summary>
/// <param name="nested">List of button models for next layer of menu.</param>
/// <param name="containerRadius">Radius for radial menu buttons of next layer.</param>
public sealed class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOptionBase> nested, float containerRadius = 100) : RadialMenuOptionBase
{
/// <summary> Radius for radial menu buttons of next layer. </summary>
public float? ContainerRadius { get; } = containerRadius;
public IReadOnlyCollection<RadialMenuOption> Nested { get; } = nested;
/// <summary> List of button models for next layer of menu. </summary>
public IReadOnlyCollection<RadialMenuOptionBase> Nested { get; } = nested;
}
/// <summary>
/// Additional settings for radial menu render.
/// </summary>
public sealed class SimpleRadialMenuSettings
{
/// <summary>

View File

@@ -16,6 +16,7 @@ using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Audio;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
@@ -37,6 +38,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IInputManager _input = default!;
[UISystemDependency] private readonly AudioSystem _audio = default!;
private BwoinkSystem? _bwoinkSystem;
@@ -98,15 +100,13 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
_bwoinkSystem = system;
_bwoinkSystem.OnBwoinkTextMessageRecieved += ReceivedBwoink;
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenAHelp,
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
.Register<AHelpUIController>();
_input.SetInputCommand(ContentKeyFunctions.OpenAHelp,
InputCmdHandler.FromDelegate(_ => ToggleWindow()));
}
public void OnSystemUnloaded(BwoinkSystem system)
{
CommandBinds.Unregister<AHelpUIController>();
_input.SetInputCommand(ContentKeyFunctions.OpenAHelp, null);
DebugTools.Assert(_bwoinkSystem != null);
_bwoinkSystem!.OnBwoinkTextMessageRecieved -= ReceivedBwoink;

View File

@@ -116,8 +116,9 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
}
// Make sure any name tagged as ours gets highlighted only when others say it.
keyword = StartAtSign.Replace(keyword, "(?<=(?<=/name.*)|(?<=,.*\"\".*))");
// Make sure the character's name is highlighted only when mentioned directly (eg. it's said by someone),
// for example in 'Name Surname says, "..."' 'Name Surname' won't be highlighted.
keyword = StartAtSign.Replace(keyword, @"(?<=(?<=^.?OOC:.*:.*)|(?<=,.*"".*)|(?<=\n.*))");
_highlights.Add(keyword);
}

View File

@@ -18,7 +18,6 @@ namespace Content.Client.UserInterface.Systems.Emotes;
[UsedImplicitly]
public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayState>
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -133,12 +132,12 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
_menu = null;
}
private IEnumerable<RadialMenuOption> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
private IEnumerable<RadialMenuOptionBase> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
{
var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
var player = _playerManager.LocalSession?.AttachedEntity;
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
Dictionary<EmoteCategory, List<RadialMenuOptionBase>> emotesByCategory = new();
foreach (var emote in emotePrototypes)
{
if(emote.Category == EmoteCategory.Invalid)
@@ -158,19 +157,19 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
if (!emotesByCategory.TryGetValue(emote.Category, out var list))
{
list = new List<RadialMenuOption>();
list = new List<RadialMenuOptionBase>();
emotesByCategory.Add(emote.Category, list);
}
var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote)
{
Sprite = emote.Icon,
IconSpecifier = RadialMenuIconSpecifier.With(emote.Icon),
ToolTip = Loc.GetString(emote.Name)
};
list.Add(actionOption);
}
var models = new RadialMenuOption[emotesByCategory.Count];
var models = new RadialMenuOptionBase[emotesByCategory.Count];
var i = 0;
foreach (var (key, list) in emotesByCategory)
{
@@ -178,7 +177,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
models[i] = new RadialMenuNestedLayerOption(list)
{
Sprite = tuple.Sprite,
IconSpecifier = RadialMenuIconSpecifier.With(tuple.Sprite),
ToolTip = Loc.GetString(tuple.Tooltip)
};
i++;
@@ -189,6 +188,6 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
private void HandleRadialButtonClick(EmotePrototype prototype)
{
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(prototype.ID));
EntityManager.RaisePredictiveEvent(new PlayEmoteMessage(prototype.ID));
}
}

View File

@@ -120,7 +120,7 @@ namespace Content.Client.VendingMachines.UI
{
var entry = inventory[i];
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
if (!_prototypeManager.Resolve(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue;

View File

@@ -273,7 +273,7 @@ namespace Content.Client.Verbs.UI
if (verbElement.SubMenu == null)
{
var popupElement = new ConfirmationMenuElement(verb, "Confirm");
var popupElement = new ConfirmationMenuElement(verb, Loc.GetString("generic-confirm"));
verbElement.SubMenu = new ContextMenuPopup(_context, verbElement);
_context.AddElement(verbElement.SubMenu, popupElement);
}

View File

@@ -21,6 +21,7 @@ public static partial class PoolManager
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),

View File

@@ -0,0 +1,417 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
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.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
/// <summary>
/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
/// handling the DeltaPressureComponent.
/// </summary>
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
public sealed class DeltaPressureTest
{
#region Prototypes
[TestPrototypes]
private const string Prototypes = @"
- type: entity
parent: BaseStructure
id: DeltaPressureSolidTest
placement:
mode: SnapgridCenter
snap:
- Wall
components:
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: ""-0.5,-0.5,0.5,0.5""
mask:
- FullTileMask
layer:
- WallLayer
density: 1000
- type: Airtight
- type: DeltaPressure
minPressure: 15000
minPressureDelta: 10000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
- type: Damageable
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 300
behaviors:
- !type:SpawnEntitiesBehavior
spawn:
Girder:
min: 1
max: 1
- !type:DoActsBehavior
acts: [ ""Destruction"" ]
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestNoAutoJoin
components:
- type: DeltaPressure
autoJoinProcessingList: false
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestAbsolute
components:
- type: DeltaPressure
minPressure: 10000
minPressureDelta: 15000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
";
#endregion
private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
/// <summary>
/// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to true is automatically added to the DeltaPressure processing list
/// on the grid's GridAtmosphereComponent.
///
/// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to false is not automatically added to the DeltaPressure processing list.
/// </summary>
[Test]
public async Task ProcessingListAutoJoinTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
await server.WaitAssertion(() =>
{
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
entMan.DeleteEntity(uid);
Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure.
/// </summary>
[Test]
public async Task ProcessingDeltaStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the threshold.
/// </summary>
[Test]
public async Task ProcessingDeltaDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure when using absolute pressure thresholds.
/// </summary>
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the absolute threshold.
/// </summary>
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
// Above absolute threshold but below delta threshold to ensure absolute alone causes damage
var toPressurize = dpEnt.Comp!.MinPressure + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,85 @@
using System.Linq;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Atmos;
[TestFixture]
[TestOf(typeof(Atmospherics))]
public sealed class GasArrayTest
{
private const string GasTankTestDummyId = "GasTankTestDummy";
private const string GasTankLegacyTestDummyId = "GasTankLegacyTestDummy";
[TestPrototypes]
private const string Prototypes = $@"
- type: entity
id: {GasTankTestDummyId}
components:
- type: GasTank
air:
volume: 5
moles:
Frezon: 20
Oxygen: 10
- type: entity
id: {GasTankLegacyTestDummyId}
components:
- type: GasTank
air:
volume: 5
moles:
- 0
- 0
- 0
- 10
";
[Test]
public async Task TestGasArrayDeserialization()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var compFactory = server.ResolveDependency<IComponentFactory>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
var gasTank = prototypeManager.Index(GasTankTestDummyId);
Assert.Multiple(() =>
{
Assert.That(gasTank.TryGetComponent<GasTankComponent>(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Oxygen), Is.EqualTo(10));
Assert.That(gasTankComponent!.Air.GetMoles(Gas.Frezon), Is.EqualTo(20));
foreach (var gas in Enum.GetValues<Gas>().Where(p => p != Gas.Oxygen && p != Gas.Frezon))
{
Assert.That(gasTankComponent!.Air.GetMoles(gas), Is.EqualTo(0));
}
});
var legacyGasTank = prototypeManager.Index(GasTankLegacyTestDummyId);
Assert.Multiple(() =>
{
Assert.That(legacyGasTank.TryGetComponent<GasTankComponent>(out var gasTankComponent, compFactory));
Assert.That(gasTankComponent!.Air.GetMoles(3), Is.EqualTo(10));
// Iterate through all other gases: check for 0 values
for (var i = 0; i < Atmospherics.AdjustedNumberOfGases; i++)
{
if (i == 3) // our case with a value.
continue;
Assert.That(gasTankComponent!.Air.GetMoles(i), Is.EqualTo(0));
}
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -146,8 +146,8 @@ public sealed class SuicideCommandTests
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
damageableComp = entManager.GetComponent<DamageableComponent>(player);
if (protoMan.TryIndex(DamageType, out var slashProto))
damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
var slashProto = protoMan.Index(DamageType);
damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
});
// Check that running the suicide command kills the player

View File

@@ -0,0 +1,49 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Construction.Components;
using Content.Shared.Temperature;
namespace Content.IntegrationTests.Tests.Construction.Interaction;
public sealed class EdgeClobbering : InteractionTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: constructionGraph
id: ExampleGraph
start: A
graph:
- node: A
edges:
- to: B
steps:
- tool: Anchoring
doAfter: 1
- to: C
steps:
- tool: Screwing
doAfter: 1
- node: B
- node: C
- type: entity
id: ExampleEntity
components:
- type: Construction
graph: ExampleGraph
node: A
";
[Test]
public async Task EnsureNoEdgeClobbering()
{
await SpawnTarget("ExampleEntity");
var sTarget = SEntMan.GetEntity(Target!.Value);
await InteractUsing(Screw, false);
SEntMan.EventBus.RaiseLocalEvent(sTarget, new OnTemperatureChangeEvent(0f, 0f, 0f));
await AwaitDoAfters();
Assert.That(SEntMan.GetComponent<ConstructionComponent>(sTarget).Node, Is.EqualTo("C"));
}
}

View File

@@ -27,8 +27,11 @@ public sealed class ContrabandTest
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
continue;
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
if (!protoMan.TryIndex(contraband.Severity, out var severity))
{
Assert.Fail($"{proto.ID} has a ContrabandComponent with a unknown severity.");
continue;
}
if (!severity.ShowDepartmentsAndJobs)
continue;

View File

@@ -264,9 +264,10 @@ public abstract partial class InteractionTest
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true, bool altInteract = false)
{
await InteractUsing((id, quantity), awaitDoAfters);
await InteractUsing((id, quantity), awaitDoAfters, altInteract);
}
/// <summary>
@@ -274,7 +275,8 @@ public abstract partial class InteractionTest
/// </summary>
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
/// <param name="altInteract">If true, perform an alternate interaction instead of a standard one.
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true, bool altInteract = false)
{
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
// (e.g., servers attempt to assemble construction examine hints).
@@ -284,18 +286,19 @@ public abstract partial class InteractionTest
}
await PlaceInHands(entity);
await Interact(awaitDoAfters);
await Interact(awaitDoAfters, altInteract);
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
protected async Task Interact(bool awaitDoAfters = true)
/// <param name="altInteract">If true, performs an alternate interaction instead of a standard one.
protected async Task Interact(bool awaitDoAfters = true, bool altInteract = false)
{
if (Target == null || !Target.Value.IsClientSide())
{
await Interact(Target, TargetCoords, awaitDoAfters);
await Interact(Target, TargetCoords, awaitDoAfters, altInteract);
return;
}
@@ -311,23 +314,23 @@ public abstract partial class InteractionTest
await CheckTargetChange();
}
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool,bool)"/>
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
var coords = SEntMan.GetCoordinates(coordinates);
Assert.That(coords.IsValid(SEntMan));
await Interact(sTarget, coords, awaitDoAfters);
await Interact(sTarget, coords, awaitDoAfters, altInteract);
}
/// <summary>
/// Interact with an entity using the currently held entity.
/// </summary>
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true, bool altInteract = false)
{
Assert.That(SEntMan.TryGetEntity(Player, out var player));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target, altInteract: altInteract));
await RunTicks(1);
if (awaitDoAfters)

View File

@@ -88,14 +88,18 @@ public sealed class LatheTest
// Check each recipe assigned to this lathe
foreach (var recipeId in recipes)
{
Assert.That(protoMan.TryIndex(recipeId, out var recipeProto));
if (!protoMan.TryIndex(recipeId, out var recipeProto))
{
Assert.Fail($"Lathe recipe '{recipeId}' does not exist");
continue;
}
// Track the total material volume of the recipe
var totalQuantity = 0;
// Check each material called for by the recipe
foreach (var (materialId, quantity) in recipeProto.Materials)
{
Assert.That(protoMan.TryIndex(materialId, out var materialProto));
Assert.That(protoMan.HasIndex(materialId), $"Material '{materialId}' does not exist");
// Make sure the material is accepted by the lathe
Assert.That(acceptedMaterials, Does.Contain(materialId), $"Lathe {latheProto.ID} has recipe {recipeId} but does not accept any materials containing {materialId}");
totalQuantity += quantity;

View File

@@ -145,10 +145,7 @@ public sealed partial class MindTests
await server.WaitAssertion(() =>
{
var damageable = entMan.GetComponent<DamageableComponent>(entity);
if (!protoMan.TryIndex(BluntDamageType, out var prototype))
{
return;
}
var prototype = protoMan.Index(BluntDamageType);
damageableSystem.SetDamage(entity, damageable, new DamageSpecifier(prototype, FixedPoint2.New(401)));
Assert.That(mindSystem.GetMind(entity, mindContainerComp), Is.EqualTo(mindId));

View File

@@ -0,0 +1,99 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Content.Shared.Storage.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Nutrition;
public sealed class WaterCoolerInteractionTest : InteractionTest
{
/// <summary>
/// ProtoId of the water cooler entity.
/// </summary>
private static readonly EntProtoId WaterCooler = "WaterCooler";
/// <summary>
/// ProtoId of the paper cup entity dispensed by the water cooler.
/// </summary>
private static readonly EntProtoId PaperCup = "DrinkWaterCup";
/// <summary>
/// ProtoId of the water reagent that is stored in the water cooler.
/// </summary>
private static readonly ProtoId<ReagentPrototype> Water = "Water";
/// <summary>
/// Spawns a water cooler and tests that the player can retrieve a paper cup
/// by interacting with it, and can return the paper cup by alt-interacting with it.
/// </summary>
[Test]
public async Task GetAndReturnCup()
{
// Spawn the water cooler
var cooler = await SpawnTarget(WaterCooler);
// Record how many paper cups are in the cooler
var binComp = Comp<BinComponent>(cooler);
var initialCount = binComp.Items.Count;
Assert.That(binComp.Items, Is.Not.Empty, "Water cooler didn't start with any cups");
// Interact with the water cooler using an empty hand to grab a paper cup
await Interact();
var cup = HandSys.GetActiveItem((SPlayer, Hands));
Assert.Multiple(() =>
{
// Make sure the player is now holding a cup
Assert.That(cup, Is.Not.Null, "Player's hand is empty");
AssertPrototype(PaperCup, SEntMan.GetNetEntity(cup));
// Make sure the number of cups in the cooler has decreased by one
Assert.That(binComp.Items, Has.Count.EqualTo(initialCount - 1), "Number of cups in cooler bin did not decrease by one");
// Make sure the cup isn't somehow still in the cooler too
Assert.That(binComp.Items, Does.Not.Contain(cup));
});
// Alt-interact with the water cooler while holding the cup to put it back
await Interact(altInteract: true);
Assert.Multiple(() =>
{
// Make sure the player's hand is empty
Assert.That(HandSys.ActiveHandIsEmpty((SPlayer, Hands)), "Player's hand is not empty");
// Make sure the count has gone back up by one
Assert.That(binComp.Items, Has.Count.EqualTo(initialCount), "Number of cups in cooler bin did not return to initial count");
// Make sure the cup is in the cooler
Assert.That(binComp.Items, Contains.Item(cup), "Cup was not returned to cooler");
});
}
/// <summary>
/// Spawns a water cooler and gives the player an empty paper cup.
/// Tests that the player can put water into the cup by interacting
/// with the water cooler while holding the cup.
/// </summary>
[Test]
public async Task FillCup()
{
var solutionSys = Server.System<SharedSolutionContainerSystem>();
// Spawn the water cooler
await SpawnTarget(WaterCooler);
// Give the player a cup
var cup = await PlaceInHands(PaperCup);
// Make the player interact with the water cooler using the held cup
await Interact();
// Make sure the cup now contains water
Assert.That(solutionSys.GetTotalPrototypeQuantity(ToServer(cup), Water), Is.GreaterThan(FixedPoint2.Zero),
"Cup does not contain any water");
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
@@ -47,6 +48,31 @@ namespace Content.IntegrationTests.Tests
AdminTestArenaSystem.ArenaMapPath
};
/// <summary>
/// A dictionary linking maps to collections of entity prototype ids that should be exempt from "DoNotMap" restrictions.
/// </summary>
/// <remarks>
/// This declares that the listed entity prototypes are allowed to be present on the map
/// despite being categorized as "DoNotMap", while any unlisted prototypes will still
/// cause the test to fail.
/// </remarks>
private static readonly Dictionary<string, HashSet<EntProtoId>> DoNotMapWhitelistSpecific = new()
{
{"/Maps/bagel.yml", ["RubberStampMime"]},
{"/Maps/reach.yml", ["HandheldCrewMonitor"]},
{"/Maps/Shuttles/ShuttleEvent/honki.yml", ["GoldenBikeHorn", "RubberStampClown"]},
{"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", ["RubberStampSyndicate"]},
{"/Maps/Shuttles/ShuttleEvent/cruiser.yml", ["ShuttleGunPerforator"]},
{"/Maps/Shuttles/ShuttleEvent/instigator.yml", ["ShuttleGunFriendship"]},
};
/// <summary>
/// Maps listed here are given blanket freedom to contain "DoNotMap" entities. Use sparingly.
/// </summary>
/// <remarks>
/// It is also possible to whitelist entire directories here. For example, adding
/// "/Maps/Shuttles/**" will whitelist all shuttle maps.
/// </remarks>
private static readonly string[] DoNotMapWhitelist =
{
//CrystallEdge Maps
@@ -54,14 +80,16 @@ namespace Content.IntegrationTests.Tests
"/Maps/_CP14/Dungeon/artifact_room.yml",
//CrystallEdge Maps end
"/Maps/centcomm.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
"/Maps/Shuttles/ShuttleEvent/instigator.yml", // Contains EXP-320g "Friendship"
"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", // Contains syndicate rubber stamp
"/Maps/Shuttles/AdminSpawn/**" // admin gaming
};
/// <summary>
/// Converts the above globs into regex so your eyes dont bleed trying to add filepaths.
/// </summary>
private static readonly Regex[] DoNotMapWhiteListRegexes = DoNotMapWhitelist
.Select(glob => new Regex(GlobToRegex(glob), RegexOptions.IgnoreCase | RegexOptions.Compiled))
.ToArray();
private static readonly string[] GameMaps =
{
//CrystallEdge Map replacement
@@ -243,18 +271,30 @@ namespace Content.IntegrationTests.Tests
await pair.CleanReturnAsync();
}
private bool IsWhitelistedForMap(EntProtoId protoId, ResPath map)
{
if (!DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var allowedProtos))
return false;
return allowedProtos.Contains(protoId);
}
/// <summary>
/// Check that maps do not have any entities that belong to the DoNotMap entity category
/// </summary>
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
if (DoNotMapWhitelist.Contains(map.ToString()))
return;
foreach (var regex in DoNotMapWhiteListRegexes)
{
if (regex.IsMatch(map.ToString()))
return;
}
var yamlEntities = node["entities"];
if (!protoManager.TryIndex(DoNotMapCategory, out var dnmCategory))
return;
var dnmCategory = protoManager.Index(DoNotMapCategory);
// Make a set containing all the specific whitelisted proto ids for this map
HashSet<EntProtoId> unusedExemptions = DoNotMapWhitelistSpecific.TryGetValue(map.ToString(), out var exemptions) ? new(exemptions) : [];
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
@@ -262,13 +302,20 @@ namespace Content.IntegrationTests.Tests
var protoId = yamlEntity["proto"].AsString();
// This doesn't properly handle prototype migrations, but thats not a significant issue.
if (!protoManager.TryIndex(protoId, out var proto, false))
if (!protoManager.TryIndex(protoId, out var proto))
continue;
Assert.That(!proto.Categories.Contains(dnmCategory),
Assert.That(!proto.Categories.Contains(dnmCategory) || IsWhitelistedForMap(protoId, map),
$"\nMap {map} contains entities in the DO NOT MAP category ({proto.Name})");
// The proto id is used on this map, so remove it from the set
unusedExemptions.Remove(protoId);
}
});
// If there are any proto ids left, they must not have been used in the map!
Assert.That(unusedExemptions, Is.Empty,
$"Map {map} has DO NOT MAP entities whitelisted that are not present in the map: {string.Join(", ", unusedExemptions)}");
}
private bool IsPreInit(ResPath map,
@@ -329,7 +376,7 @@ namespace Content.IntegrationTests.Tests
MapId mapId;
try
{
var opts = DeserializationOptions.Default with {InitializeMaps = true};
var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), out mapId, opts);
}
catch (Exception ex)
@@ -439,7 +486,7 @@ namespace Content.IntegrationTests.Tests
#nullable enable
while (queryPoint.MoveNext(out T? comp, out var xform))
{
var spawner = (ISpawnPoint) comp;
var spawner = (ISpawnPoint)comp;
if (spawner.SpawnType is not SpawnPointType.LateJoin
|| xform.GridUid == null
@@ -553,5 +600,20 @@ namespace Content.IntegrationTests.Tests
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Lets us the convert the filepaths to regex without eyeglaze trying to add new paths.
/// </summary>
private static string GlobToRegex(string glob)
{
var regex = Regex.Escape(glob)
.Replace(@"\*\*", "**") // replace **
.Replace(@"\*", "*") // replace *
.Replace("**", ".*") // ** → match across folders
.Replace("*", @"[^/]*") // * → match within a single folder
.Replace(@"\?", "."); // ? → any single character
return $"^{regex}$";
}
}
}

View File

@@ -25,6 +25,11 @@ public sealed class AdminTest : ToolshedTest
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
// Only care about content commands.
var assemblyName = cmd.Cmd.GetType().Assembly.FullName;
if (assemblyName == null || !assemblyName.StartsWith("Content."))
continue;
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
}
});

View File

@@ -0,0 +1,41 @@
using Content.Shared.Kitchen;
namespace Content.IntegrationTests.Tests.WizdenContentFreeze;
/// <summary>
/// These tests are limited to adding a specific type of content, essentially freezing it. If you are a fork developer, you may want to disable these tests.
/// </summary>
public sealed class WizdenContentFreeze
{
/// <summary>
/// This freeze prohibits the addition of new microwave recipes.
/// The maintainers decided that the mechanics of cooking food in the microwave should be removed,
/// and all recipes should be ported to other cooking methods.
/// All added recipes essentially increase the technical debt of future cooking refactoring.
///
/// https://github.com/space-wizards/space-station-14/issues/8524
/// </summary>
[Test]
public async Task MicrowaveRecipesFreezeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoMan = server.ProtoMan;
var recipesCount = protoMan.Count<FoodRecipePrototype>();
var recipesLimit = 218;
if (recipesCount > recipesLimit)
{
Assert.Fail($"PLEASE STOP ADDING NEW MICROWAVE RECIPES. MICROWAVE RECIPES ARE FROZEN AND NEED TO BE REPLACED WITH PROPER COOKING MECHANICS! See https://github.com/space-wizards/space-station-14/issues/8524. Keep it under {recipesLimit}. Current count: {recipesCount}");
}
if (recipesCount < recipesLimit)
{
Assert.Fail($"Oh, you deleted the microwave recipes? YOU ARE SO COOL! Please lower the number of recipes in MicrowaveRecipesFreezeTest from {recipesLimit} to {recipesCount} so that future contributors cannot add new recipes back.");
}
await pair.CleanReturnAsync();
}
}

View File

@@ -78,7 +78,11 @@ public static class ClientPackaging
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
cancel: cancel);
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
await RobustClientPackaging.WriteClientResources(
contentDir,
inputPass,
SharedPackaging.AdditionalIgnoredResources,
cancel);
inputPass.InjectFinished();
}

View File

@@ -25,6 +25,12 @@ public static class ServerPackaging
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static IReadOnlySet<string> ServerContentIgnoresResources { get; } = new HashSet<string>
{
"ServerInfo",
"Changelog",
};
private static List<string> PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -211,7 +217,11 @@ public static class ServerPackaging
contentAssemblies,
cancel: cancel);
await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
await RobustServerPackaging.WriteServerResources(
contentDir,
inputPassResources,
ServerContentIgnoresResources.Concat(SharedPackaging.AdditionalIgnoredResources).ToHashSet(),
cancel);
if (hybridAcz)
{

View File

@@ -0,0 +1,10 @@
namespace Content.Packaging;
public sealed class SharedPackaging
{
public static readonly IReadOnlySet<string> AdditionalIgnoredResources = new HashSet<string>
{
// MapRenderer outputs into Resources. Avoid these getting included in packaging.
"MapImages",
};
}

View File

@@ -45,7 +45,7 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(ent, out var idCardComp))
return;
_prototypeManager.TryIndex(args.Args.ChameleonOutfit.Job, out var jobProto);
_prototypeManager.Resolve(args.Args.ChameleonOutfit.Job, out var jobProto);
var jobIcon = args.Args.ChameleonOutfit.Icon ?? jobProto?.Icon;
var jobName = args.Args.ChameleonOutfit.Name ?? jobProto?.Name ?? "";
@@ -130,7 +130,7 @@ namespace Content.Server.Access.Systems
if (!TryComp<IdCardComponent>(uid, out var idCard))
return;
if (!_prototypeManager.TryIndex(args.JobIconId, out var jobIcon))
if (!_prototypeManager.Resolve(args.JobIconId, out var jobIcon))
return;
_cardSystem.TryChangeJobIcon(uid, jobIcon, idCard);

View File

@@ -98,7 +98,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
var targetIdComponent = Comp<IdCardComponent>(targetId);
var targetAccessComponent = Comp<AccessComponent>(targetId);
var jobProto = targetIdComponent.JobPrototype ?? new ProtoId<AccessLevelPrototype>(string.Empty);
var jobProto = targetIdComponent.JobPrototype ?? new ProtoId<JobPrototype>(string.Empty);
if (TryComp<StationRecordKeyStorageComponent>(targetId, out var keyStorage)
&& keyStorage.Key is { } key
&& _record.TryGetRecord<GeneralStationRecord>(key, out var record))
@@ -130,7 +130,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
string newFullName,
string newJobTitle,
List<ProtoId<AccessLevelPrototype>> newAccessList,
ProtoId<AccessLevelPrototype> newJobProto,
ProtoId<JobPrototype> newJobProto,
EntityUid player,
IdCardConsoleComponent? component = null)
{
@@ -144,7 +144,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
_idCard.TryChangeJobTitle(targetId, newJobTitle, player: player);
if (_prototype.TryIndex<JobPrototype>(newJobProto, out var job)
&& _prototype.TryIndex(job.Icon, out var jobIcon))
&& _prototype.Resolve(job.Icon, out var jobIcon))
{
_idCard.TryChangeJobIcon(targetId, jobIcon, player: player);
_idCard.TryChangeJobDepartment(targetId, job);

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