Compare commits

...
303 Commits
Author SHA1 Message Date
Tobias Berger 66f23833c6 Fix tests 2026-01-30 04:08:49 +01:00
Tobias Berger 333948591f More trait stuff
HasTraitCondition instead of Conflicts and Requirements
RemoveBodyPartEffect instead of AmputeeComponent/System
Remove redundant BorgChassis checks
Remove failed condition check and just show the full one
Improve vision trait conditions
2026-01-30 03:12:03 +01:00
Milon 51d605f67b Formatting and some other fixes 2026-01-30 00:35:09 +01:00
Delta-V bot f7106c611a Automatic changelog update 2026-01-29 18:47:58 +01:00
Field Commandandpre-commit-ci[bot] 00615a24ed CentComm stuff (#5315)
* CentComm stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* hmm...

---------

Signed-off-by: Field Command <159087063+FieldCommand@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-29 18:47:38 +01:00
Theo ab308d2832 Halve Space Law Sentencing Times (#5249)
* Reduce times in space law and SOP, in guidebook and wiki

* cat c 12 minutes instead of 10

* update crime assist
2026-01-29 19:39:19 +02:00
Delta-V bot fca6d1e23a Automatic changelog update 2026-01-29 18:25:25 +01:00
Pharaz4 430dabf980 Remove Syndie comms from disaster victims and guards (#5280)
* remove syndicomms from disaster victims

* removed from syndicate guard as well. (dropship ruin has a guard)

* fix harpy wing showing on cc hardsuit
2026-01-29 17:25:05 +00:00
Delta-V bot 44276029d8 Automatic changelog update 2026-01-29 17:56:04 +01:00
Pharaz4 75fad779a8 Merge pull request #5293 from Pharaz4/cherrypickattempt
LIZARDS GET TAILS ON HARDSUITS!!!
2026-01-29 17:55:43 +01:00
Tobias Berger 29eaf0a10f Merge pull request #5087 from BarryNorfolk/merge_november
Upstream Merge (November 2025)
2026-01-29 15:49:13 +00:00
Tobias Berger 8a2be49722 Changelog stuff! 2026-01-29 16:26:10 +01:00
Tobias Berger 80690ed56f Tiny things not worth requesting changes for 2026-01-29 15:07:33 +01:00
ConstantlyConfused f71bf01e3c Make unholy water actually have a color (#5312)
make unholy water actually have a color
2026-01-29 00:48:47 -06:00
Delta-V bot 1a59f9d47b Automatic changelog update 2026-01-27 19:58:44 +01:00
Tobias Berger 3bcaf85b04 hotfix-plasteel (#5298) 2026-01-27 18:58:24 +00:00
c58b2a30f7 Tank Dispenser Recolor (#5267)
* Add files via upload

Tank Dispenser Recolor

Signed-off-by: Thingy461 <darmentici@gmail.com>

* Add files via upload

Minor tweak to vending machine YAML for tank dispenser recolor

Signed-off-by: Thingy461 <darmentici@gmail.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix line endings

* Apply suggestions from code review

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* Apply suggestion from @ShepardToTheStars

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Thingy461 <darmentici@gmail.com>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-27 12:50:37 -06:00
BarryNorfolk d486fedb48 Apply #41461 to _DV security borg modules 2026-01-27 19:13:59 +01:00
Delta-V bot f1eef43203 Automatic changelog update 2026-01-27 18:55:31 +01:00
Pharaz4 8f8f5042ab Disable Wizard until Rebalanced (#5228)
* Remove wizard from midround antags until rebalanced

* Removed wizard from roundstart antags

---------

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>
2026-01-27 11:55:11 -06:00
Delta-V bot a5c9af9759 Automatic changelog update 2026-01-27 17:05:40 +01:00
AstraandTobias Berger 7cf3873b4c Printable plasteel (#5284)
* Printable plasteel

* Actually make it a bit faster

* Move to that other recipe pack

* Recipes are named after their result

---------

Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-27 10:05:20 -06:00
Delta-V bot 31b8acfdd9 Automatic changelog update 2026-01-27 06:51:00 +01:00
Halo3moth bd6a1b461c Security longcoat resprite (#5288)
* My coats are so long

* making the hos longcoat more hoslike
2026-01-26 23:50:41 -06:00
BarryNorfolk 08b9954cf4 Also apply #41149 to all magazines and ammo boxes 2026-01-26 20:09:09 +01:00
BarryNorfolk 837028c356 Apply #41583 to all thief stash-like entities 2026-01-26 19:51:43 +01:00
BarryNorfolk 96e9b4faee Move xenoborg lawsets above DeltaV 2026-01-26 19:29:45 +01:00
BarryNorfolk b13941f317 Use existing hardsuit extra table 2026-01-26 19:25:33 +01:00
BarryNorfolk bfa08f7f69 Upa APC max load to 40 kW 2026-01-26 19:25:33 +01:00
BarryNorfolk 22fff89a01 Fix issue with weaponUids not being passed correctly for weapons used by entities 2026-01-26 19:25:32 +01:00
BarryNorfolk 6a6ab152f8 Disable xenoborgs at roundstart 2026-01-26 19:25:32 +01:00
MilonandPrincess Cheeseballs 857ddff7cb fix solution contents duplication on spill behavior (#33231)
* I’M SCREAMING INTO THE VOID AND IT’S NOT LISTENING

* review

* explodes pancakes with mind

* graaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

* Meteors RAAAAAAAAAAH

* I'm so tired of solutions

* whhop

* revert

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
Noah BeverlyandJustin Pfeifler 3a96154bca Mail Cart (#41482)
* Add mailcart crate type

* Add recent progress, move location of mailcart.rsi

* Allow letters and packages to be inserted

* Add updated sprites for mailcart

* Add storage for letters

* Bugfix for sprite

* Add fix for mincount mailcart sprite updates

* Add dynamic names to packages in mailcart

* Add additional whitelist items

* Update tag comments

* Remove max item size

* Update fixture for mailcart, remove unused code from crates.yml

* Add mail cart to cargo ordering

* Update meta.json

* Remove unused comment

* Update copyright

* Add missing ContainerContainer

* Update ContainerContainer

* Revert "Update ContainerContainer"

This reverts commit 1805dcd58d8c0f52baca9db0d3173940ee241159.

* Remove container slots and increase storage

* Remove unused tags

* Remove LetterDelivery tag

---------

Co-authored-by: Justin Pfeifler <jrpl101998@gmail.com>
2026-01-26 19:25:32 +01:00
BarryNorfolk 12c74b3da3 Fix power draw on Defib cabinet when not charging anything 2026-01-26 19:25:32 +01:00
BarryNorfolk ec6e4af68e Stop the laser gun bounty allowing LaserTag guns 2026-01-26 19:25:32 +01:00
ādaandiaada 039a377a51 Five bounty arbitrage fixes (#41846)
* commit

* tweak

* more bounties

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
BarryNorfolk bdc0d0120d Only run StationPowerTests over stations in DeltaV 2026-01-26 19:25:32 +01:00
Samukaandāda e248258369 Fix xenoborg modules (#41625)
* change name from heavy laser to laser cannon

* fixed some ids

* fix name and description

* change cost of upgrade modules

* remove unwield penalty

* commentary

* change name of scout modules

* same firerate as before

* typo

Co-authored-by: āda <ss.adasts@gmail.com>

* new line at end of file

* duplicated

---------

Co-authored-by: āda <ss.adasts@gmail.com>
2026-01-26 19:25:32 +01:00
BarryNorfolk 349337e1d5 Downstream fixes for battery unification 2026-01-26 19:25:32 +01:00
slarticodefastandPrincess Cheeseballs 17ff77b61f Unify BatteryComponent and PredictedBatteryComponent (#41867)
* unify

* cleanup and merge conflicts

* floating points

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
slarticodefast d039f6fdfd Fix rigged power cells exploding early (#41813)
* fix riggable

* fix
2026-01-26 19:25:32 +01:00
slarticodefast 055fffa5da fix AI battery alert (#41879)
* fix AI battery alert

* fix copy paste error
2026-01-26 19:25:32 +01:00
slarticodefast 530d9dda4d Add debug hitscan weapon (#41658)
* debug gun

* predict
2026-01-26 19:25:32 +01:00
Princess CheeseballsandPrincess Cheeseballs 2e6b2141ee Fix Damageable API (#41657)
Fix damageable api

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
alexalexmaxandslarticodefast 1bba368d22 Add Changeling DNA store (#41632)
* created changeling dna currency and locale currency name

* created changeling store category, created changeling-catalog.ftl

* added store and action to Urist McLing

* found the entity

* make armblade a purchase

* comment

* created ApplyToMob datafield and applied to armblade

* ApplyToMob check in storesystem

* Update Resources/Prototypes/Catalog/changeling_catalog.yml

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
Princess CheeseballsandPrincess Cheeseballs 5fbeecb011 [HOTFIX] FIX LIGHT MELEE LOGS (#41655)
God fugcking

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
BarryNorfolk aa86c808f9 Fork fixes for multi reagent bloodstreams 2026-01-26 19:25:32 +01:00
Ignaz "Ian" KraftandPrincess Cheeseballs c292094b77 multi reagent bloodstream (#41489)
* multi reagent bloodstream

* pluralize the comments

* fix TryModifyBloodLevel return logic

* now with quantity

* now with solution

* implement suggestions

* fix forensics

* minor thing

* Nevermind undo that caps matters.

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:32 +01:00
beck-thompson 38bb9232c0 Add water flower for clowns (#41469)
* Spray!

* Add to clown loadout

* Fix the easy things

* lot nicer

* spray update..

* Fix yaml

* fixes

* changed it to warning!

* review

* review

* sku
2026-01-26 19:25:32 +01:00
slarticodefast ac4fcd5dc6 fix rcd overlay getting stuck for borg modules (#41648)
fix rcd overlay getting stuck
2026-01-26 19:25:31 +01:00
Princess CheeseballsandPrincess Cheeseballs b4d96c43b5 Make Firespread logical (#41636)
* FIRE

* code comment fix

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:25:31 +01:00
slarticodefast 8e298dbbac predict name identifiers (#41605) 2026-01-26 19:25:31 +01:00
slarticodefast da88db8775 Predict borgs (#41600)
* predict borgs

* small fix

* fix MMI item slot serialization

* fix movement speed for mothership core

* review and minor improvement

* fix resolve

* review
2026-01-26 19:25:31 +01:00
SlamBamActionman 43832b5d5a Fix clientsided alerts being overwritten by server (#37033)
Initial commit
2026-01-26 19:25:31 +01:00
ādaandiaada c98d7cbb13 Fix EquipmentVerbs not showing up in strip menu (#41631)
commit

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:25:31 +01:00
Samuka 8445778bc1 Make xenoborg round end text better (#41623)
* make xenoborg win color easier to read

* display the max number of xenoborgs and the number of crew alive in the end

* make it a datafield

* add : (literally)

* add period

* make text better
2026-01-26 19:25:31 +01:00
Minemoder5000 ce42a2aafb Add StatusIcon component to MobBaseAncestor (#41624)
add StatusIcon to ancestor
2026-01-26 19:25:31 +01:00
JackspajfMain 97bbb53d7f Change to add shot glasses to the bartender guidebook entry (#41618)
adds shot glass to bartender guidebook
2026-01-26 19:25:31 +01:00
NoreUhhandNoreUhh 33a9c44e55 Felinase/Caninase Reagent Tweaks (#41527)
* Tweaked reagents required for recipes, increased output of recipe, lowered time of accent per unit, and tweaked chem thresholds

* tweaks

i was lazy

* product change

lazy again

* yuh

* pluh

---------

Co-authored-by: NoreUhh <noreuhh@gmail.com>
2026-01-26 19:25:31 +01:00
ArtisticRoomba fff5850a67 Properly document AtmosDeviceEnabled(Disabled)Event (#41613)
kill misnomers
2026-01-26 19:25:31 +01:00
slarticodefast 447b4c5109 Fix looking at verbs causing sounds or popups (#41609)
* fix verb popups

* spelling
2026-01-26 19:25:31 +01:00
alexalexmaxandslarticodefast d7da7cb7d0 Adds option to whitelist or blacklist store entries based on buyer objectives (#41493)
* BuyerObjectiveWhitelistCondition done

* fix to check every buyer objective for a blacklisted objective before checking whitelisted objectives

* resolved requested changes

* forgot a line

* Update Content.Server/Store/Conditions/BuyerObjectiveWhitelistCondition.cs

* Replaced IsBlacklistPass with IsWhitelistPass

* cleaning up

* placed whitelist == null check above objective loop

* Moved empty whitelist check below foreach loop

* final cleanup i pray

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:25:31 +01:00
Pok 53b5100f6a Helper method for get charge level (#41601)
* get-charge-percent

* review
2026-01-26 19:25:31 +01:00
imatsoup b2256e29a3 Change thief backpack ui name and description with Component fields (#41583)
* Add new name and description fields to the thiefbackpack component and ui

* Change fields to locids, remove Title from menu.xaml, add comments to  thiefbackpackui.cs
2026-01-26 19:25:31 +01:00
slarticodefast 525ffdc8f5 Fix helmet lights (#41599)
fix helmet lights
2026-01-26 19:25:31 +01:00
Samuka f2d31a3d31 Update mothership again (#41491)
* add xenoborg recharging station

* it was backwards

* make it a tad darker

* touch ups

* add xenoborg blast door sprites

* update sprites

* add xenoborg blast door

* improve blast door sprite

* typo

* make it thinner

* update mothership

* moved yml lines

* updated xenoborg charger sprite

* removed unused state

* fixes to the mothership
2026-01-26 19:25:31 +01:00
Alzore a8502bc4e6 Meatball Salvage wreck remake (#41589)
Remake-Meatball-Wreck
2026-01-26 19:25:31 +01:00
Errant b0b28467f5 Make gameticker spawn code more modular (#41588)
gameticker dospawn
2026-01-26 19:25:31 +01:00
CDWimmer 317ab5a006 Fewer War Crimes (#41541)
* Fewer War Crimes

Re-colour the syndicate medical bags to not use the legally restricted Red Cross symbol and instead use the same blue cross colours as used across the rest of the game's medical symbolism.
Because no-one wants to go to the Hauge. Also consistency is nice.

* Update meta.json

* Update another meta.json

* Update meta.json again

additional detail
2026-01-26 19:25:31 +01:00
YoungThug b0de59321a Tourniquets can now fit in medical belts (#41584) 2026-01-26 19:25:30 +01:00
Connor Huffine 1a911f708b Remove commented items (#41577) 2026-01-26 19:25:30 +01:00
Stefano Pigozzi 883244dc0e Fix typo in Ion Storm lawsets (#41574)
Pizzaria → Pizzeria
2026-01-26 19:25:30 +01:00
BarryNorfolk 0a27397ffb Fix several systems and protos after powercell prediction change 2026-01-26 19:25:30 +01:00
BarryNorfolk 831c73235d Fix for missing namespace in Nanochat 2026-01-26 19:25:30 +01:00
BarryNorfolk 7ebd8d9dc8 Fix and mark microwave time addition for BeingMicrowaved 2026-01-26 19:25:30 +01:00
slarticodefast 50b2986ef7 Predict powercells, chargers and PowerCellDraw (#41379)
* cleanup

* fix fixtures

* prediction

* fix test

* review

* fix svalinn visuals

* fix chargers

* fix portable recharger and its unlit visuals

* fix borgs

* oomba review

* fix examination prediction
2026-01-26 19:25:30 +01:00
Jessey van Hoeijen 92de25f0f3 Fix stunbaton component namespace (#34790)
* Fixed incorrect namespace

* .Components was weird so I removed it
2026-01-26 19:25:30 +01:00
slarticodefast 8158432428 Fix gas filters (#41567)
fix gas filters
2026-01-26 19:25:30 +01:00
Samuka e1609ce7ee No more sub wizards in xenoborg preset (#41565)
no more sub wizards in xenoborg preset
2026-01-26 19:25:30 +01:00
slarticodefast 0afecfea98 Add test checking for multiple container fills, mark StorageFillComponent as obsolete (#41562)
add test, obsolete storagefill
2026-01-26 19:25:30 +01:00
BarryNorfolk b1ebff0742 Remove blacklist methods from fork changes 2026-01-26 19:25:30 +01:00
slarticodefast 23c832b185 Remove blacklist methods in EntityWhitelistSystem (#40932)
* replace blacklist methods

* VS is trolling me
2026-01-26 19:25:30 +01:00
Leon Friedrichandslarticodefast 368f68376c Add Whitelist.Components yaml valiation (#40916)
* Add Whitelist.Components yaml valiation

* poke_tests

* fix linter

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:25:30 +01:00
HohenzolarenandReWAFFlution 0381e99b73 Balloons now float and slide smoothly. (#41102)
* The balloons became airy

* Attributions + Sound

* Add BaseBalloon

* Proto fixes

---------

Co-authored-by: ReWAFFlution <239567049+brokendot@users.noreply.github.com>
2026-01-26 19:25:30 +01:00
BarryNorfolk e68e6d07ae Replace all storage fills with entity table container fills 2026-01-26 19:25:30 +01:00
SnappingOpossum 73c686a7d3 Replace (almost) all existing storage fills with entity table container fills (#41360)
* Replace all existing storage fills with container fills, adjust some formatting to accomadate

* Forgot to remove that

* I really need to spell better

* Right, those are 3x3

* Change some NestedSelectors to GroupSelectors

* Actual container fills, test fix (maybe), and a few minor formatting changes

* Fix some more mistakes

* Forgot to add to that comment

* Some parenting fixes

* Test optimisation, maybe

* Roll holiday lights instead of individually listing it

* Move eletrical toolbox to NestedSelector

* Back to entity tables (except for vending)

* More nesting (and a TODO)

* Yet another fix

* Probably shoulda done it this way originally

* Entity tables for vending restocks

* I am good at formatting

* Make added table names consistent

* Make grand lottery weights a little more readable

* Use existing fills for bio suit crates

* Get rid of some more redundant amounts

* Nuke the StorageFill test section and update some comments

* Apply changes from code review

* Split off changes to vending restocks

* Split off changes to vending restocks

* Apply more changes from code review

* Attempt #2 of detangling this mess (oh shit I missed the pin)

* They look so similar

* I am great at formatting

* Missed a newline

* How did I manage to do that

* Move toy in FoodMealHappyHonkClown to top to prioritise

* Apply changes from review and move dank pizza to organise by weight
2026-01-26 19:22:40 +01:00
Samuka c8ad093fcf Add xenoborg laws to ion storm chance (#41532)
add xenoborg laws to ion storm chance
2026-01-26 19:22:40 +01:00
Justin Pfeifler fadcb72d80 Fix Paper Default Interaction (#41547)
* Fix paper default interaction

* Remove BeforeUIOpen

* Revert "Remove BeforeUIOpen"

This reverts commit c0db6c743f7cd07279b8a509317a54925a5f7d27.

* Revert "Fix paper default interaction"

This reverts commit 7efae70b96fbb5800a0e043b6deca176babe3399.

* Trigger UI before Ingestion

* Revert "Trigger UI before Ingestion"

This reverts commit a642ab40bf5b973d7e5f5b772d578dc2b9a436c1.

* Trigger ingestion after ui
2026-01-26 19:22:40 +01:00
ādaandiaada 87167d0267 Bugfix: Borg's can't die anymore (#41492)
* commit

* other borgs

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:40 +01:00
rwrv e8dcc60a8e Remove references to fingers in low tier cold damage (#41496)
* Update health-examinable-carbon.ftl

* Rework wording to be more similar to other descriptions
2026-01-26 19:22:40 +01:00
slarticodefast 21d9f97988 Revert "FTL Fixes (#39040)"
This reverts commit 52a9f9b576.
2026-01-26 19:22:40 +01:00
slarticodefast 4dc3688751 Make artifact analyzer power supply sane (#41543)
make artifact analyzer power supply sane
2026-01-26 19:22:40 +01:00
Ciarán Walsh 8d90d3f3ad Defibs will now also shock anyone still interacting with the target. (#35998)
* Defibs will now also shock anyone still interacting with the target.

* Improvements to test readability

* Apply fixes to other tests

* Refactor the interacting entities query to use an event.

* Include pullers as interacting with the entity they are pulling

* Broadcast event

* Use a constant

* Convert new test to InteractionTest

* Convert existing test

* Add behaviour note

* Revert "Convert existing test"

This reverts commit b8a8f2f68e3733bdb6ec254faf955a42096d47d7.

* Move new test into separate (InteractionTest) test file

* Use ToServer

* Use a constant for prototype id

* Use ToServer

* Add EntProtoId constructor

* Add assertion failure messages

* Manual cleanup of test entities

* Remove obsolete flag

* Add test summaries

* Remove tuple constructor

* Wrap entity deletion in WaitPost

* Extend DoAfter interacting test with an extra mob
2026-01-26 19:22:40 +01:00
rlebell33andslarticodefast c153abae29 Improve hitboxes for knives and shivs to better match their sprites (#39100)
* update hitbox for kitchen knife and butcher's cleaver

* update hitbox for the 4 glass type shivs

* update hitbox for the medical shiv

* Update the hitbox for the plastic knife

* review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:40 +01:00
korczoczek 2c2e05684b Let PopupOnTrigger display names of user and item (#41157)
* waved away

* waves at you with my [Prime Cut Corgi Meat]

* pls dont litter the repository with unnecessary components

* forgot to remove a variable from testing

* guh

* the more things change, the more things stay the same

* and then she ternaried everywhere

* owie
2026-01-26 19:22:40 +01:00
Errant 2a9fa18625 HOTFIX Add logging to gamepreset fallback (#41522)
* Add logging to gamepreset fallback

* cleanup
2026-01-26 19:22:40 +01:00
K-DynamicandCadunkus 59b6e85654 Add vox hairstyle (catfish hair) (#40689)
* add vox catfish

Co-authored-by: Cadunkus <140308611+Cadunkus@users.noreply.github.com>

* change indents from 2 to 4 in meta.json

* update sprite

Co-authored-by: Cadunkus <140308611+Cadunkus@users.noreply.github.com>

---------

Co-authored-by: Cadunkus <140308611+Cadunkus@users.noreply.github.com>
2026-01-26 19:22:40 +01:00
Errant a7bdabe06d Admin improvements for Looc and Deadchat (#41273)
* looc and deadchat speak interrupt events

* comment

* admin log tweak

* clean up log entity targets; switch admin deadchat logged entity from player to mob

* one event only

* rename event, broadcast player session with the event
2026-01-26 19:22:40 +01:00
SnappingOpossum ebc5a5b2d7 Move vending restocks to EntityTableContainerFills (#41442)
* Move vending restocks to EntityTableContainerFills

* Glad I can actually see that fail now

* I could of just gone back to checking all the containers but nooo had to be stubborn

* Apply changes from code review
2026-01-26 19:22:40 +01:00
Nemanjaandslarticodefast 6cecca668a Decouple GasPrototype IDs from Gas Enum (#41266)
* Remove final enum coupling from gas YAML

* Fix comment

* Fix test

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:40 +01:00
MrPersivalandSnappingOpossum fc52f91396 Fix scalefriends food problem #39982. Hivelord Remains now meat and edible by anyone who can digest it (#41317)
* fix(hivelord-remains): now reptilians can also eat hivelord remains

* fix(hivelord-remains): added meat and raw tags

* moved(hivelord-remains): moved from meat.yml back to asteroid.yml

* fixed(meat): added the new line

Co-authored-by: SnappingOpossum <snappingopossum@outlook.com>

---------

Co-authored-by: SnappingOpossum <snappingopossum@outlook.com>
2026-01-26 19:22:40 +01:00
Partmedia 6dcf6b07d5 Trip APCs when they exceed a power limit (#41377)
* Implement APC overloading

* Add power test

* Review

* Some more reviews

* Show map coordinates for test failures

* Widen column 2

* Reduce singularity beacon power consumption

* Try to get grid coordinates
2026-01-26 19:22:40 +01:00
Princess CheeseballsandPrincess Cheeseballs 15b6134646 [1984] Beach balls no longer collide with players (#41450)
* 1984

* me when I'm dumb

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:22:40 +01:00
Nuggets219 cde5756823 Give Rust Removal a shorter Doafter (#770) (#41518)
* Give Rust Removal a shorter Doafter (#770)

* Yeah, yeah, mircobalance. Every janitor player will fight you though. Welding and brushing walls free of rust now takes just three seconds.

* Now reinforced walls take a bit longer to de-rust. Times are as follows:

Solid wall: 1s weld, 3s brush
Reinforced wall: 2s weld, 3s brush

* Actually, just make both take 4s to brush and 1s to weld. Problem solved.

* changed the welding timer from 1 to 2 seconds for lag compensation.
2026-01-26 19:22:40 +01:00
ThatGuyUSA a6e117acf2 Add admin antag control command for making people into Wizards (#41494)
* innit bruv, but also done!

* added comment to signify why it's like that
2026-01-26 19:22:40 +01:00
ThatGuyUSA 4138a87959 Munitions visibility tweaking and cleanup (#41137)
* I didn't make an init commit, 1: because i forgot, 2: because the PR is so tiny, the whole thing is done already

* beans and shards no glow, clean up

* commented out PointLight, does too little for how straining it would be

* Improvised shells now use shards as their bullet sprite, they still don't embed though
2026-01-26 19:22:39 +01:00
lzk c100ca0e01 cleanup evil beach ball (#41499) 2026-01-26 19:22:39 +01:00
korczoczek fec6410ff5 Fix blinking animation of disembodied head hologram (#41498)
start blinking normal
2026-01-26 19:22:39 +01:00
Samuka e426c4e523 Fix the round end trigger for xenoborgs (#41467)
math
2026-01-26 19:22:39 +01:00
Tiniest Sharkandslarticodefast 53116a1f57 Remove Basic Hardsuit Helmet Sprites (#41490)
* Deletes basic helmet

* you forgot one file

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
Hitlinemoss 6c5c264ec8 Removed redundant clothing from CMO locker; moved neck gaiter from HoS locker to HoS dresser (#41487) 2026-01-26 19:22:39 +01:00
371d1082c0 AdminLogOnTrigger (#41474)
* commit

* requested

* also requested

* Update Content.Shared/Trigger/Components/Effects/AdminLogOnTriggerComponent.cs

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
ādaandiaada 68497caf9a 5 new triggers: EmptyContainers, Knockdown, Stun, TriggerOnThrowDoHit (#41472)
* ideas

* finish components

* systems

* one more

* requested

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
slarticodefastandiaada cfccd56886 AddTagOnTrigger, RemoveTagOnTrigger, SwapPositionOnTrigger, JitterOnTrigger (#41476)
* more triggers

* comment

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
slarticodefast 7c05afc362 Cleanup TipsSystem, add TippyOnTrigger (#41477)
* tippy on trigger and refactor

* optional parameter for command
2026-01-26 19:22:39 +01:00
Samuka 8c8800abe8 add 2 empty lines to xenoborg round end text (#41436)
* add 2 empty lines

* add one line on the top

* swap from the start to the end
2026-01-26 19:22:39 +01:00
cammusubi 7308866f1a Per issue #41143, 'ClusterBang' was changed to 'clusterbang' (#41470)
Per issue #41143, 'ClusterBang' was changed to display as 'clusterbang' in-game.
2026-01-26 19:22:39 +01:00
Samukaandslarticodefast 7c1c298c8c Examine which borg that module fits into (#41461)
* modules tip

* add color

* solved a edge case

* use ContentLocalizationManager instead of hardcoded grammar

* improve summary

* improve improved summary

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

* not my first language

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

* keep names consistent

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

* keep consistent part 2

* fixed the yml error

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
Samuka a9f7ccb3a7 Make mothership core able to pull (#41460)
* make mothership core able to pull again

* make it work like borgs
2026-01-26 19:22:39 +01:00
SamukaandScarKy0 9a70cf8470 Update mothership (#41453)
* add body crusher sprites

* body crusher yml

* add xenoborg thruster sprites

* add xenoborg thruster yml

* update mothership

* only one IFF computer

* tabs

* fuck

* fixed indentation

* replace tabs with spaces

* change id MachineBodyCrusher to MachineArtifactCrusherXenoborg

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

* change id XenoborgThruster to ThrusterXenoborg

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

* update Ids in mothership

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
slarticodefast b6b9421075 Make crawl speed depend on your free hand count (#41458)
* crawl speed

* I can't spell
2026-01-26 19:22:39 +01:00
slarticodefast 8673745f13 fix DamageAbleSystem.SetDamage (#41456)
fix SetDamage
2026-01-26 19:22:39 +01:00
Princess CheeseballsandPrincess Cheeseballs 2e6996eb3b Fix Gas Reagent Thresholds. (#41438)
* Fix reagent gas scales

* add comment

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:22:39 +01:00
ScarKy0 9df5861552 Remove Aghosts from "Warp to Most Followed" (#41448)
* init

* review
2026-01-26 19:22:39 +01:00
Fildranceandpa.pecherskij 9b6fd85222 Minor Knock spell refactor + fix its obstruction check (#41447)
* refactor: remove isFirstTimepredicted for knock XAE

* refactor: cleanups

* refactor: do not count laser obstructions toward stuff that blocks Knock spell, project spell from artifact and not node

* refactor: no method-events for knock-spells

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
2026-01-26 19:22:39 +01:00
Connor Huffine 7f2efcf6cc Fix typo in roundstartvariation (#41444)
Fix typo
2026-01-26 19:22:38 +01:00
lzk 46428ce2de yaml cleanups (id, hypopen, inflatable wall) (#41432)
* cleanups

* yes i can
2026-01-26 19:22:38 +01:00
April ee533cb3f2 chore: made regalrat go the speed he should go (#41420)
that's a 5, not the defaulted back 4.5 from `UpdateMovementSpeed` in
Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
2026-01-26 19:22:38 +01:00
lzk 8f79ae88be rotate more items in inventory part 3 + cleanup for good (#41149)
* rotate more items in inventory part 3 + cleanup for good

* i forgor
2026-01-26 19:22:38 +01:00
lzk 39850bafd5 improve APE desc, battery weapon desc, and locked desc (#41431)
* improve APE desc and locked desc

* 2
2026-01-26 19:22:38 +01:00
6c89c5df3d Mini Energy Crossbow (#39796)
* draft 1

* mono audio

* missed something

* end the torment

* release me

* let me free

* Submodule fix?

* submodule fix?

* thousand years of suffering

* fix conflicts mehopes

* fuuuhuck

* balance update

* small numbers change

* re-organises the stun values yada yada

* newline

---------

Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
2026-01-26 19:22:38 +01:00
2738eb5f5f Xenoborgs part 7 (#40042)
* add mothership module

* option for mothership to open self UI

* fix mothership module

* remove mothership body

* swapp real hands for modules

* action sprite for mothership module

* removed hands from mothership core sprite

* xenoborgs now drop a pinpointer to the mothership core once destroyed

* add pinpointer to space movment module

* add base for XenoborgRule

* add xenoborg antag option

* something was needed

* something else was needed

* add ghost role spawn points

* change name in antag selection to Xenoborg Core

* add random spawnPoint markers that don't do anything

* add spawn points to mothership

* update spawn points

* add xenoborgs rule

* add xenoborgs rule to rotation

* add xenoborgs preset

* update preset with secret version
also added xenoborg mode description

* modify Antag Selection system to allow for custom entities via the AntagSelectionDefinition

* fix ghostroles spawners

* fix rule with new entityPrototype

* add spawnpoints to the mothership

* whitelist system to spawnpoints

* updated xenoborg components

* added xenoborg component to xenoborgs

* updated spawnpoints of xenoborgs in the mothership

* add new tags for xenoborg and mothership core

* add new tags for xenoborgs and mothership core

* update ghostrole spawners for xenoborgs

* message for when you get the xenoborg role

* explode all xenoborgs when mothership core dies

* for real now. explode all xenoborgs when mothership core is destroyed

* round end summary for xenoborgs

* temporary

* add guidebook entry for xenoborgs

* instructions on how to borg players

* removed lock from xenoborg control computer

* announcement when all xenoborgs die

* announcement when mothership core is destroyed

* typos

* fix error

* improve xenoborg mind role

* move sounds to xenoborg and mothership component

* play sounds when turned into xenoborg

* change sprites of mothership core actions

* minor fix

* add custom xenoborg start sound

* carps now attack xenoborgs

* added guide link to xenoborgs

* add guidebook link to xenoborgs

* added guidebook link to mothership core

* add link to source of the sound

* fixed minor issue

* has to be 1

* typo

* add light layer to mothership core sprite

* fixed antag selection system

* update guideboook

* update the guidebook again

* alphabet

* documentation

* simplify documentation

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

* make the briefing yml instead of code

* fix bug with sprites

* fix formating

* remove stuff from AntagSelection

* add stuff to AntagSpawner

* fix game rule

* removed secret xenoborg preset

* support for more than one entity for each antag role

* fix gamerule

* fixes

* no xeonborgs

* add xenoborgs to all at once

* engi xenoborg included

* more comments

* more recognizable

* more xenoborgs when more players

* removed unused stuff

* correct access

* removed unnecessary stuff

* use GetAliveHumans and make comments better

* Make the system more robust

* use a constant

* remove overload from the Destroy method

* has to be public to be used in the xenoborg system

* fix the mindrole methods

* not sure, but I was asked to do it

* use a constant for the color

* forgot to make it static

* removed param comments

* removed useless parameters

* fixed stuff

* added event listeneers to xenoborg mind change

* only 4

* unfuck the antagSpawner

* comment

* unfix stuff

* commentary

* removed xenoborg stuff from siliconLawSystem

* move some stuff to the component

* removed space

* removed uncessary stuff

* no need to crate a var

* move stuff from mothership comp to xenoborg comp

* removed XenoborgCoreRoleComponent

* comment on the AntagSelectLocationEvent

* added back empty line

* comment

* make the summary better

* make AntagRoleToPrototypes summary better

* adding useless stuff back cause it was there before

* hascomp instead of trycomp

* again

* LocId instead of string

* make a new logic for the whitelist of the spawnpoint

* added ghostrole tags back

* use hascomp instead of trycomp

* removed whitelist from SpawnPointComponent

* not needed anymore

* no longer subverted

* fixed names

* make it better

* add not

* i'm dumb

* briefing is now handled by the xenoborg system

* call evac if there is too many xenoborgs

* update submodule

* fix division

* Add AutoGenerateComponentPause and AutoPausedField to XenoborgsRuleComponent

* add lines between stuff

* Make the Blocking system more robust

* Make mothership inherit from BaseMob

* remove this stuff cause is bad

* Revert "Make the Blocking system more robust"

This reverts commit 099babfe1daef00e6073e04108920327416e4ca4.

* Mothership core snaps to grid

* stop mothership core from moving

* mothership core is static again

* make guidebook entry on how to xenoborg crew better

* Make mothership core damageable

* If xenoborgs need it, so do zombies i guess

* Start the NextRoundEndCheck

* follow private static readonly naming rule

* Samething

* Fix announcments

* Make it a datafield, no?

* Revert "Make it a datafield, no?"

This reverts commit 62f6255ccccdd583d7f833ae4dbcd09a670f721a.

* remove stuff

* doesn't need to move

* is kinda of a structure

* so it doesn't pry floors as soon as it spawns

* powercell hand to mothership core module

* label for new hand

* core_e -> core-e

* mothership core can pilot the shuttle again

* fix duplicated tag + description to xenoborg tags

* scout xenoborg can now move in space without the jetpack so it can better use the sword module

* improve basic xenoborg module

* remove changes from zombie rule comp

* swap AllEntityQuery for EntityQueryEnumerator

* new line at the end

* change to 15 seconds

* make MothershipCoreDeathAnnouncmentSent into a datafield

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: beck-thompson <beck314159@hotmail.com>
2026-01-26 19:22:38 +01:00
Hitlinemoss 47d3d41b7f Security IDs no longer have Service access by default (#41411) 2026-01-26 19:22:38 +01:00
ādaandiaada 8ec79f47fe RandomTriggerOnTriggerComponent (#41422)
* commit

* rename

* prevent recusion

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:38 +01:00
MissKay1994andSlamBamActionman 849948fa12 Briefcase gun for lawyers (#40210)
* definitely not a gun

* You only imagined this tag

* Brace with your gut

* Definitely did not procrastinate for eternity

* Suing javascript formatting

* Tear down the system

* Yay for hardcoding

* EVIL COURT

* Attributions for skreee

* Move new sprites into own folder

---------

Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
2026-01-26 19:22:38 +01:00
BeBrightandBeBright 0855551989 MingGetBriefing -> MindGetBriefing (#41413)
Co-authored-by: BeBright <98597725+be1bright@users.noreply.github.com>
2026-01-26 19:22:38 +01:00
metalgearsloth 483a0e714b Update engine submodule to 268.0.0 (#41408) 2026-01-26 19:22:38 +01:00
Leon Friedrich d8fd83a18f Update RaiseEventBenchmark for engine changes (#37349)
* Update RaiseEventBenchmark

* fix test

* poke_tests
2026-01-26 19:22:38 +01:00
slarticodefastandKyle Tyo e06942410d Add keybinds for openening a specified component in VV (#41348)
* quick inspect

* Update Content.Client/Commands/QuickInspectCommand.cs

Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>

* documentation!!!

---------

Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com>
2026-01-26 19:22:38 +01:00
Leon Friedrich 2fba7e49f6 Fix admin log exception (#41242)
Fix admin logs
2026-01-26 19:22:38 +01:00
metalgearsloth 05b8cb4d9e Bandaid solution name setter events (#41400)
Ideally PDAs + IDs wouldn't be doing this but this is slamming it out.
2026-01-26 19:22:38 +01:00
ādaandiaada 9c1552a14e Add missing plushies to AllPlushiesTable (#41398)
* weh!

* rare lamp

* among

* buff

* nerf

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:38 +01:00
Nox 6c7d4f68a9 More Resilient Grenades (#34499)
* Inital commit

* Balanced damage to explode instantly

* Rebalanced based on suggestions

* matched all grenade instant thresholds to 45 damage.

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

* Redid files and values

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

* Minor spelling mistake

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

* Moved abstracts and fixed typo in comments

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

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
2026-01-26 19:22:38 +01:00
April 750d4cd809 chore: added a full stop to the tooltip of the accentless speech trait (#41392)
chore: added a full stop to the accentless tooltip in en-us

this makes it consistent with the other tooltips
2026-01-26 19:22:37 +01:00
SolidSyn 34b698ddf2 Adds the clown skirt and black lawyer suitskirt to HoPs uniform lathe (#39846) 2026-01-26 19:22:37 +01:00
Pieter-Jan Briers a9283034ad Update GDPR dump script to latest migration (#41387) 2026-01-26 19:22:37 +01:00
Princess CheeseballsandPrincess Cheeseballs 420f0bc836 [HOTFIX] Prevent Payload Enumeration Failure. (#41376)
* Prevent payload enumeration failure when spawning entities

* hate it here

* boop

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:22:37 +01:00
ArtisticRoombaandslarticodefast 75ba38b45c Document Atmospherics Utils (#41385)
* Utils docs

* Update Content.Server/Atmos/EntitySystems/AtmosphereSystem.Utils.cs

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-01-26 19:22:37 +01:00
ArtisticRoomba df38a61127 Document Atmospherics API (#41382)
* api docs

* rem using dir. linq in atmospherics not allowed.

* address review
2026-01-26 19:22:37 +01:00
Aiden b41b453e31 Prototype-ize Mind (#41384)
* Use MindBase prototype for mind creation

* Create mind.yml

* Add ContainerContainer
2026-01-26 19:22:37 +01:00
beck-thompson 59ca21e28a Remove wizards healing wand for the random magic item spell (#41365)
Remove wizards healing wand for the random spell
2026-01-26 19:22:37 +01:00
kin98 a9b49de934 Fix: obsolete warnings in SharedAbsorbentSystem (#40885)
* fix: formating warnings

* split GetAbsorbentReagents into string and protoid

* fix: changed SplitSolutionWhitout [obsolete] to use new method

* fix: missing using directive after merge

* Replaced methods with ProtoId variant
2026-01-26 19:22:37 +01:00
metalgearsloth 6319a96441 Fix invalid AABBs (#41381)
LBRT LBRT LBRT LBRT not RTLB
2026-01-26 19:22:37 +01:00
Errant ef6dd83899 HOTFIX: automatically reset fallback gamemode to default (#41367)
automatically reset fallback gamemode to default after the round
2026-01-26 19:22:37 +01:00
SlamBamActionman 88ab510a05 HOTFIX: Reword Wizard objective text to be more hostile (#41371)
Initial commit
2026-01-26 19:22:37 +01:00
ArtisticRoombaandCrude Oil 27e51c7228 Yet another revitalization of engine lighting changes (#41361)
* cherrypick lighting changes

* increase computer light

---------

Co-authored-by: Crude Oil <crude@croil.net>
2026-01-26 19:22:37 +01:00
metalgearsloth 4a3d74b74d Update submodule to 267.4.0 (#41364) 2026-01-26 19:22:37 +01:00
Sir Warock cc26f5cb0f Fix Ammo Visuals Bug (#41362)
Fix Ammo Count Updating
2026-01-26 19:22:37 +01:00
Princess CheeseballsandPrincess Cheeseballs c97cdd847f Fix Paradox Clone Forensics (#41358)
i forgor to push

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-01-26 19:22:37 +01:00
slarticodefastandArtisticRoomba 05f08d7221 Cleanup BatterySystem (#41298)
* cleanup

* fix fixtures

* this belongs into the next PR

* review

* misc

---------

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2026-01-26 19:22:37 +01:00
BarryNorfolk ac49b73132 Add removed Magnum tag to _dv 2026-01-26 19:22:37 +01:00
ādaandiaada a2888e322a Remove unused tags (#41265)
* the letter R

* the letter S

* extras

* the letter T

* the letter U

* the letter V

* the letter W

* the final letter ( X )

* the easy ones

* Unused b and c

* unused d

* unused G

* unused I

* unused K

* un used m and p

* the letter R

* unused T

* un used W

* re-add heavy rifle, document magazines

* final alphabet

* Merge remote-tracking branch 'upstream/master' into work/tag-nuke

* delete haunted

* Revert "delete haunted"

This reverts commit 870d782f7d97a1d7cb34cd14a288cc0cd0299cce.

* mistake

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:37 +01:00
ādaandiaada 3eb627023e Document tags.yml: R-Z (#41257)
* the letter R

* the letter S

* extras

* the letter T

* the letter U

* the letter V

* the letter W

* the final letter ( X )

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2026-01-26 19:22:36 +01:00
Delta-V bot 1f15e3c4b9 Automatic changelog update 2026-01-26 11:46:42 +01:00
keekee38andVanessa e43c61c7c0 A new security SMG, maybe (#5212)
* still very wip sprite wise but functions

* idunno if it worked

* testing suitstorage

* ok getting there,

* ok maybe ready now :3

* oopsie daisy,

* Revert "oopsie daisy,"

This reverts commit d9bfdff6c2.

* okay please play nice github,

* im just a dog smh

* changes done ^^

* figured should at least make it consistent with other safe ids

* added descriptions to em all :3

* Partial migration of removed content from upstream #34293. Moved files to DV namespace.

---------

Co-authored-by: Vanessa <vanessalouwagie@gmail.com>
2026-01-26 10:46:22 +00:00
Delta-V bot 0f5940d463 Automatic changelog update 2026-01-25 17:46:31 +01:00
MD360andVanessa c8b5038ccb Chem Dispenser Auto-Labeling (Frontier) (#5261)
* Auto label port

* Make it so it works/comments

* Post review changes.

* Post review change part two.

* review pt 3

* Fixed client UI from not getting updated.

---------

Co-authored-by: Vanessa <vanessalouwagie@gmail.com>
2026-01-25 10:46:12 -06:00
Delta-V bot 6bb4151430 Automatic changelog update 2026-01-25 17:32:14 +01:00
Vanessaandpre-commit-ci[bot] ec041a0f22 Of Hats and Hardsuit Helmets (#5263)
* Now you can toggle your helm with a hat on

* Added container to toggleable clothing in YAML.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed YAML fail.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-25 10:31:55 -06:00
Delta-V bot c9faffdfa5 Automatic changelog update 2026-01-25 17:31:42 +01:00
Pharaz4andVanessa 08f668496e Fixing some issues (#5278)
* cleaned effect spam in entity panel

* fix drinking whole keg (fatty)

* fix smart fridge sprite

* added utensil fork to bowl base thing

* butlerton can no longer wreak havoc

* added comment

* Fixes #1451

* Apply suggestions from code review

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-25 10:31:38 -06:00
AeraAulinandVanessa 0e107c0bc7 shadow damage tweak (#5268)
* one line yaml change

* forgot the comment

* Update Resources/Prototypes/Damage/modifier_sets.yml

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-25 10:31:19 -06:00
Delta-V bot 16c3948503 Automatic changelog update 2026-01-25 17:30:37 +01:00
Milonandpre-commit-ci[bot] d16a306326 marked as protected trait (#5274)
* protected trait

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* right

* direction

* REFLECTION

* whoopsie

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-25 10:30:18 -06:00
Milonandpre-commit-ci[bot] 884cf115c6 more traits improvements (#5285)
* wowie

* whoopsie

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* WHY DID I DO THAT

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-25 09:12:38 -06:00
Delta-V bot df364a806a Automatic changelog update 2026-01-25 15:42:28 +01:00
Tobias Berger 78e0a0943e Add more info to escape menu (#5260) 2026-01-25 08:42:09 -06:00
Delta-V bot 2dc9dace20 Automatic changelog update 2026-01-25 14:49:13 +01:00
Halo3moth 9f46103e59 Energy magnum fix (#5270)
* Fixes it?

* truth be told i have no idea what im doing
2026-01-25 07:48:53 -06:00
github-actions[bot]andDeltaV-Bot de8953d6fe Update Credits (#5281)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2026-01-25 02:23:08 +01:00
Delta-V bot 4011a81ba0 Automatic changelog update 2026-01-25 01:13:30 +01:00
Field Command 2bd6751034 Pebble map standard update (#5164)
* Pebble standard update

* Added red phone to bridge.
2026-01-24 16:13:10 -08:00
Delta-V bot c5a556d2fb Automatic changelog update 2026-01-24 22:30:17 +01:00
Milon 96304b787b add uncloneable back (#5273)
* why

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
2026-01-24 21:29:58 +00:00
Delta-V bot 599994118e Automatic changelog update 2026-01-24 21:55:51 +01:00
Milon 1b464c265d fix trait points bar rendering on ultrawide montiors (#5275)
* whoopsie
2026-01-24 20:55:31 +00:00
Delta-V bot 6a45579bd5 Automatic changelog update 2026-01-24 21:16:41 +01:00
Colin-Tel 44a7f48861 Update division.yml (#5271)
* Update division.yml

* Update division.yml
2026-01-24 12:16:21 -08:00
Delta-V bot db9b9faf33 Automatic changelog update 2026-01-23 20:36:18 +01:00
pootslap d6b4992638 LPO cannot speak on binary comms (#5243)
* minus one encryption key

* Update events.yml
2026-01-23 19:35:57 +00:00
Delta-V bot a86c4d6ee6 Automatic changelog update 2026-01-23 20:32:08 +01:00
d39d9caae4 Captain's armored winter coat and suit buff (#5227)
* caps carapa

* caps hardsuit

* caps winter coat

* caps hardsuit part2

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Revert "caps carapa"

This reverts commit dc007a12e5.

* Revert "caps hardsuit part2"

This reverts commit e210aa5792.

* Revert "caps hardsuit"

This reverts commit ffe1055df2.

* makes caps winter coat the same

* fix

* Update Resources/Prototypes/_DV/Entities/Clothing/OuterClothing/wintercoats.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* fix some things

---------

Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-23 13:31:48 -06:00
Delta-V bot 15ec1b94fe Automatic changelog update 2026-01-23 11:24:52 +01:00
8f130cb0bb Migrate Chronic Pain to the New Status Effect System (#5180)
* Renamed InPain to ChronicPain. Changed references.

* Updated meds to use the name status effect system for chronic pain suppression. Standardized popup chances for all meds.

* Migrated ChronicPain to the new Status effect system. Basically re-wrote it and re-organized it.

* Cleanup and tweaks.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed cloning traits for pain.

* Addressing Milon-san's feedback uwu

* Apply suggestions from code review

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* Turned floats into TimeStamps.

* Derp

* Forgot the trait lol

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-23 10:24:32 +00:00
github-actions[bot]andDeltaV-Bot 989b978712 Update Credits (#5259)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2026-01-21 23:23:13 +01:00
Tobias Berger 522556a628 Automate credits workflow (#5258)
* Change GITHUB_TOKEN to BOT_TOKEN in credits workflow

Signed-off-by: Tobias Berger <toby@tobot.dev>

* If at first you don't succeed

Try try again

* Maybe this one?

* Surely this time it'll work

* Chest compressions, chest compressions, chest compressions

* squasmgsh

* Undo that

* Hm yes newline

---------

Signed-off-by: Tobias Berger <toby@tobot.dev>
2026-01-21 23:20:18 +01:00
Delta-V bot 178f913afc Automatic changelog update 2026-01-21 17:29:14 +01:00
Apachito cb1619933e Fix crafting graph on Tide breaker (#5239)
* Fix crafting graph

* Fix real

* Update tidebreaker.yml

* Revert "Update tidebreaker.yml"

This reverts commit 0c1abdce31.
2026-01-21 10:28:54 -06:00
Delta-V bot d745475364 Automatic changelog update 2026-01-21 17:23:34 +01:00
Vanessaandpre-commit-ci[bot] 260cd9f83d Looooooooooooongcoats (#5235)
* Fixed some inconsistencies in DV-specific winter coats

* Added longcoat sprites. Changed like everything in the YAML.

* Fixed captain's armored longcoat having pockets.

* Added crew and sec longcoats to their respective drobes.

* Added longcoats to dressers and captain's locker.

* Added non-armored coats to lathe packs.

* Sprite namespace

* Added longcoats to loadouts.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* typo

* Fixed sec off's coat armor values.

* Removed duplicate of WardenLongcoat

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Arbitrage tests once again

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-21 16:23:13 +00:00
Delta-V bot b4f9c28e48 Automatic changelog update 2026-01-21 15:51:09 +01:00
Tobias Berger c608211019 Change FTL times (#5257)
This partially reverts #3309 because the changed startup_time made the
FTL sound end way before FTL actually happens.
2026-01-21 08:50:49 -06:00
Delta-V bot 61a236698a Automatic changelog update 2026-01-21 15:19:16 +01:00
VanessaandTobias Berger ea2f0a21aa Ninja Stealth Fix?/Nerf? (#5217)
* Changed up how ninja's stealth is removed after they get hit.

* Logic fix

* Update Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-21 15:18:56 +01:00
Delta-V bot ab5541668a Automatic changelog update 2026-01-21 10:37:27 +01:00
Colin-TelandTobias Berger cd2a9368de Anglish accent (#4092)
* Adds Anglish accent

* I'm done

* really??

* add traits and add/subtract

* Update anglish.ftl

lol

* Did the stuff

* changed eth to thorn and added species

* forgot about a file & misc change

* Apply suggestions from code review

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Colin-Tel <113523727+Colin-Tel@users.noreply.github.com>

* rip Regex

* misspelling

* ILOVEWORDSILOVEWORDS

* vitally important replacement

* even more words

* updated it

* update

---------

Signed-off-by: Colin-Tel <113523727+Colin-Tel@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-21 10:37:06 +01:00
Vanessa db61bff22f Turned MIT licensing into opt-out rather than opt-in (#5253)
* Turned MIT licensing into opt-out rather than opt-in

* Clarity
2026-01-21 09:20:43 +01:00
Delta-V bot 390d594ec8 Automatic changelog update 2026-01-21 06:52:29 +01:00
Cepelinas1 0ef3ceec7d Gives Logistics Officer access to shipyard (#5251)
first thing
2026-01-20 23:52:09 -06:00
Delta-V bot 469603ca32 Automatic changelog update 2026-01-21 06:17:25 +01:00
turtlemutt ebf2bd7246 More Random Names (#5254)
more names
2026-01-20 23:17:05 -06:00
Delta-V bot 4b6f56785f Automatic changelog update 2026-01-21 00:30:27 +01:00
Colin-Tel fdc3ce6284 Division & Chibi updates (#5241)
* updated maps

* adjust surgeon role amount
2026-01-20 17:30:07 -06:00
Halo3moth 6b16212f18 Buff and fixes for energy shotgun (#5204)
* Buffs and fixes

* Direction changes
2026-01-20 15:29:18 -06:00
Delta-V bot 55bacdaa07 Automatic changelog update 2026-01-20 22:15:41 +01:00
ConstantlyConfused 5169b5324e Make space pens stop killing avali (#5181)
* make avali no longer spawn with space pens

* blows up test fail with mind

* un-modify awawi survial kits

* give space pens "stabilizine" (a modified lepo
2026-01-20 15:15:21 -06:00
Delta-V bot 81c323df20 Automatic changelog update 2026-01-20 12:50:19 +01:00
4364eaa388 real traits system (#5208)
* holy shit?

* final touches

* this is dumb

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* whatever go my integration tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* whoopsie forgot blacklists

* load bearing loc

* minor stuff

* whoopsie

* cool

* I LOVE REFLECTION

* got a call from the stink department

* i love fluent yes

* direction changes

* waiter more migrations please

* typo ops

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-20 12:49:59 +01:00
pootslapandVanessa f05e2613df Long range examining (#5219)
* no more up close examines

* Update Content.Shared/Examine/ExamineSystemShared.cs

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* Update Content.Shared/Examine/ExamineSystemShared.cs

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-20 12:49:48 +01:00
Tobias Berger 0c62185537 idk why I didn't do this in the first place (#5244) 2026-01-19 13:24:59 +01:00
Vanessa ed1a3d6af1 Medical Cyborg Tweaks (#5126)
* Allow health analyzers to work if you switch hands

* Added consistency to empty hand borg locales

* Added pills and jet injectors to chemical module. Added soap to advanced surgery. Fixed borg locale strings.

* DeltaV comment

* Also added the ability for surgery modules to hold organs. Fixes #4479.
2026-01-19 08:56:45 +00:00
Delta-V bot 3470957717 Automatic changelog update 2026-01-19 03:21:51 +01:00
turtlemutt afb9184f2c Lighthouse Tweaks (again) (#5154)
* Update lighthouse.yml

* Update lighthouse.yml

* Update lighthouse.yml

* Update lighthouse.yml
2026-01-19 03:21:31 +01:00
Delta-V bot 0cf93249c5 Automatic changelog update 2026-01-18 19:47:27 +01:00
Dorragon 0f3eb79aa7 Port Ready Manifest from Impstation (#5187)
Meow meow moew meow emweo
2026-01-18 12:47:07 -06:00
ConstantlyConfused 428ca45cdb Remove the duplicate syndicate radio uplink entry (#5240)
remove the duplicate syndicate radio uplink entry
2026-01-18 11:29:16 -06:00
Delta-V bot f25f84cc07 Automatic changelog update 2026-01-18 18:19:04 +01:00
96b1623016 Reinforced cameras / ai cameras and router (#5027)
* AI cam router

* reinforced camera try one

* reinforced camera try two

* reinforced camera try 3

* reinforced camera try 4

* reinforced camera try 5

* reinforced camera try 6

* reinforced camera try 7

* reinforced camera try 8

* AI router fix

* test balance

* Test construction rework

* name fix

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* New reinforced camera code

Recommend by ShepardToTheStars thanks a lot for the recommendation and code start

* fixed the one missing lv wire

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Name fix agian

* removed the extra frequency

* AHHHHHHHHH

* fuck

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* 1

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Structures/Wallmounts/WallmountMachines/reinforced_surveillance_camera.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* last part

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

* now the last batch

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>

---------

Signed-off-by: snowywinters <134970424+snowywinters@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-18 11:18:45 -06:00
Delta-V bot bd18a7f1f8 Automatic changelog update 2026-01-18 05:41:07 +01:00
DisposableCrewmember42 d022112deb fix: You can no longer drink the deep fryer (#5238)
Signed-off-by: DisposableCrewmember42 <disposablecrewmember42@proton.me>
2026-01-17 22:40:48 -06:00
Delta-V bot 5f7d5c29e9 Automatic changelog update 2026-01-18 02:55:15 +01:00
biddygelson 7e6e96ca99 Academy 1.5.0 (#5130)
* * replaced distro and waste pumps with enabled versions
* added vacuum tiles to unused atmos chambers, now trit recycling won't be mixed in chamber.
* split epi artifact APCs to better manage power, wasn't a problem at idle but one scrubber would go over 20k for an APC.
* added fishing spot in park with fishing vendor.
* Robotics Control Console added to robotics.
* Added arrivals signs to arrivals.
* Added AI restoration console to Mystagogue Office
* Added anti-meteor zones for Arrivals and Evac
* Added anti-anomaly zone to PA exterior, no anoms in the Tesla/Singularity!
* Made botany/hydro larger for more trays and grinder.
* Named air alarm in armory.

* removed bombing targets from waypoints to keep up with upstream.

* Added basic materials and plasteel to cargo.
Moved all department holopad fronts behind the counter rather than public access.

* tweaks; replaced toilets with filled versions, named APC in AI core, added valve in atmos exterior, changed most* bedroom lights to have warm lights.
2026-01-18 01:54:55 +00:00
DeltaV Bot e68610febd Merge pull request #5237 from DeltaV-Station/automated/credits-2026-01-17T22-06-15
Update Credits
2026-01-17 23:06:22 +01:00
DeltaV-Bot f9853b5d8d Update Credits 2026-01-17 22:06:17 +00:00
Delta-V bot 8ddd156e23 Automatic changelog update 2026-01-17 22:37:04 +01:00
Halo3moth 344481771e Readding the slim version of T1 vest to sec (#5220)
* God i hope i didnt fuck up where to put this

* Forgot lockers existed for a moment
2026-01-17 15:36:45 -06:00
Tobias Berger 5b25fcb2c5 Fix indentation in workflow
Bypass rules yolo

Signed-off-by: Tobias Berger <toby@tobot.dev>
2026-01-17 22:22:40 +01:00
Delta-V bot c4ffead268 Automatic changelog update 2026-01-17 22:14:18 +01:00
VelcroboyandVelcroboy 718dcabb6f Clean up perma chefvend and add dull bread knife (#5234)
* Clean up perma chefvend and add dull bread knife

* add more s&P

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-01-17 15:13:59 -06:00
Tobias Berger 645d8b6b26 Maintaining (#5214)
* Try and touch up workflows, god help me if this breaks something

* Remove unused email

* Remove Breaking changes, we don't do those here

* Better PR template

* Change header in new pr template

* Use different token

* No approval workflow
2026-01-17 15:12:52 -06:00
Vanessa b9202e5955 Fixed RubberStampGreytide Heisentest Fail (#5233)
Fixed a failing DV test because RubberStampGreytide inherited a component that said it shouldn't be mapped, but it now appears in maints lockers
2026-01-17 22:11:07 +01:00
Delta-V bot f4837ba87e Automatic changelog update 2026-01-17 17:52:49 +01:00
This_Is_Gray 2f549d5522 Minor Elegance Fix (#5198)
* Update elegance.yml

* many fish dancing OUTSIDE the pot, wait get back in there . GUYS WHERE ARE YOU GOING PLEASE!!
2026-01-17 10:52:29 -06:00
Delta-V bot 922c4a5e50 Automatic changelog update 2026-01-16 20:08:05 +01:00
Tobias BergerandVanessa ce5817ebd4 No more footprint prediction (#5224)
* Revert "footprints prediction (#5111)"

This reverts commit 0d0d5a17de.

* No footprint slowdown

---------

Co-authored-by: Vanessa <vanessalouwagie@gmail.com>
2026-01-16 13:07:45 -06:00
Delta-V bot a79352cbb7 Automatic changelog update 2026-01-16 19:59:50 +01:00
Proxy also works :3 134b2d59b8 remove prison knife (#4934)
* remove prison knife

* as per suggestion

* topy i cant accept changes to lines that no longer exist, its not letting me this is your fault i had to redo all of this cmon
2026-01-16 12:59:31 -06:00
Delta-V bot 1a47a607e8 Automatic changelog update 2026-01-16 19:59:11 +01:00
Charlie MorleyandTobias Berger f94e1ce0eb oracle no longer requests items that are far away in the research tree (#4430)
* oracle no longer requests items blocked by lots of research

* review comments: improve Oracle GetAllProtos efficiency, remove "dv change" comments from nyano code

* review comments: remove more "dv change" comments in nyano code

* review comment: oracle, ditch the "1 tech remaining in tier" check

* Update Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Charlie Morley <cmorley191@gmail.com>

* minor review comment

---------

Signed-off-by: Charlie Morley <cmorley191@gmail.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-16 12:58:50 -06:00
Delta-V bot 4ae44109e4 Automatic changelog update 2026-01-16 19:41:30 +01:00
Astra dc44f4dd7c Engi Void Suit printable (#5222)
Engi Void Suit Real
2026-01-16 12:41:11 -06:00
Delta-V bot 48185ad218 Automatic changelog update 2026-01-16 19:37:52 +01:00
Vanessaandpre-commit-ci[bot] 58e500d254 CentComm Shipyard (#5172)
* Added CentComm shipyard console. Brought some things in Shipyard console up to modern standards.

* More shipyard stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* PR Feedback

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-16 19:37:33 +01:00
VelcroboyandVelcroboy fce57f5120 Fix Asakim Shuttle warp point (#5230)
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-01-16 19:37:24 +01:00
Delta-V bot 47a196e896 Automatic changelog update 2026-01-16 19:32:40 +01:00
Charlie Morley 6117b7b5e1 harpy gasp when pollutant gases are present (#5158)
* harpy suffocate in the presence of trace pollutant gases

(cherry picked from commit 1a618f3f12716d22c3879ba0760c83d6419a2fd6)

# Conflicts:
#	Resources/Prototypes/_DV/Body/Organs/harpy.yml

* harpy asphyxiation finally accurate in the guidebook
2026-01-16 12:32:20 -06:00
Delta-V bot aaf12704af Automatic changelog update 2026-01-16 19:29:34 +01:00
pootslap f89bc2b1be New toys for Carpy (#5169)
* carpWithHands prototype

* adds stamp and pen

* Update carp.yml

* Update pets.yml

* Update pets.yml

* Update pets.yml

* how many things can I take out until it breaks

* adds stamp for LPO and silly name for carpy

* removed double chaplain stamp too

* made pen do damage

* added the lpo stamp to the right locker

why are there two identical ones
2026-01-16 12:29:14 -06:00
Delta-V bot cef1a4f4ac Automatic changelog update 2026-01-16 19:27:41 +01:00
BungleBoss e7b97149a1 Make AI Nanites good (#5210)
* Make AI Nanites NOT horrid

Makes AI nanites a actual competitor to metal foam

* Increase useDelay from 60 to 75

Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Changed to even heal 50. hopefully

Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

---------

Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>
2026-01-16 12:27:22 -06:00
Delta-V bot e36b636cef Automatic changelog update 2026-01-16 19:26:26 +01:00
DisposableCrewmember42 92cda3be15 feat: nudge singularity towards station periodically [mald PR] (#5209)
feat: nudge singularity towards station periodically
2026-01-16 12:26:06 -06:00
Delta-V bot cd228f717d Automatic changelog update 2026-01-16 19:19:24 +01:00
Astra 6405b2284e Microwave Emitter (#5205)
Add microwave emitter
2026-01-16 12:19:04 -06:00
ConstantlyConfusedandpre-commit-ci[bot] 1a319993eb Fix a handful of chemistry guidebook issues (#5206)
* fix some chem locale issues

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-16 12:17:00 -06:00
Delta-V bot 532dee9dd6 Automatic changelog update 2026-01-16 19:13:20 +01:00
ecad26f937 Increase devil chance to match wizard (#5225)
* yippeee yay yippee

* Apply suggestions from code review

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-16 18:12:59 +00:00
Delta-V bot c09395f432 Automatic changelog update 2026-01-16 19:10:13 +01:00
BarryNorfolk a022aae6fe [Mapping] Add new command for showing network links (#5188)
* Add new command for showing network links

* Use AllEntityQueryEnumerator to bypass EntityPaused check

* Localise description
2026-01-16 12:09:53 -06:00
Delta-V bot 7465b2c5d0 Automatic changelog update 2026-01-16 18:59:31 +01:00
Pharaz4 3765f61952 Fix upstream hardsuit spawning in Sec suit storage (#5201)
* Commented out Extra hardsuit given to Sec from upstream.

* Fixed my laziness

* Un-Commented upstreams suit storage

Also got rid of dv tweak, i thought i fixed this in my last commit? guess not

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

* Forgot to uncomment, again

I should stop being lazy and rushing the commit...

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

* made the cahnged files look cleaner

im silly

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

* Removed Archivist PDA from pTech

* Fixes #4714 (Parent of library airlocks being service based).

* Remove LawyerPDA from ptech

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

---------

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>
2026-01-16 11:59:12 -06:00
Delta-V bot c92f2b7a40 Automatic changelog update 2026-01-16 18:58:20 +01:00
Halo3moth 7863d2e4fd Maints locker changes (#5221)
gamer loot changes
2026-01-16 11:58:01 -06:00
Delta-V bot 8429054fa2 Automatic changelog update 2026-01-16 18:55:41 +01:00
Minerva da47e5fdf2 Fixes Ovinia mimes not getting a survival box (#5207) 2026-01-16 11:55:21 -06:00
Delta-V bot 0e2ac6c10c Automatic changelog update 2026-01-16 18:46:36 +01:00
Halo3mothandTobias Berger 5962801f41 Head of Security briefcase (#5203)
* vaguely functional

* Well it didnt crash my game

* what If i capitized Things properly

* I forgor

* poke 1 at fixing the fail?

* Update Resources/Locale/en-US/_DV/objectives/conditions/steal-target-groups.ftl

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Prototypes/_DV/Objectives/stealTargetGroups.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Locale/en-US/_DV/tools/Hos-pistol.ftl

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Prototypes/_DV/Entities/Objects/Specific/HosBriefcase.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Prototypes/_DV/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Prototypes/_DV/Catalog/HosSidarms.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* Update Resources/Prototypes/_DV/Objectives/traitor.yml

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

* what if i didnt shitcode?

* Should fix the fail again....

* That should be the last of it?

* Hate visual studio

* Update steal-target-groups.ftl

* Everything but fr this time surely

* im out of joke commit titles

* Tada

---------

Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2026-01-16 11:46:16 -06:00
Delta-V bot 369fb5687e Automatic changelog update 2026-01-15 16:45:18 +01:00
Pharaz4 848978bf16 Hard hats tweaked to be protective (#5175)
* Made hardhats reduce incoming blunt damage by 15%

* lowered to 0.90

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

---------

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>
2026-01-15 09:44:56 -06:00
Delta-V bot 4fedd8f40a Automatic changelog update 2026-01-15 14:52:24 +01:00
Field Command 5a9d3115dc New ATS! (#4825)
* New ATS!

* Little bit of polish

* Update trading_outpost_dv.yml

* Small update, plus defib.

* Small fix
2026-01-15 14:52:04 +01:00
Delta-V bot 363580c8b9 Automatic changelog update 2026-01-15 08:30:05 +01:00
VelcroboyandVelcroboy aa98f49eab Update warp points on non-station grids to use ghost-only (#5213)
Update warp points on no-station grids to use ghost-only

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-01-15 08:29:44 +01:00
Delta-V bot 887ccf8c55 Automatic changelog update 2026-01-15 08:14:23 +01:00
Field Command c2654a68fb DV default evac shuttle! (#4970)
DV default evac shuttle
2026-01-15 01:14:03 -06:00
Delta-V bot 454ef8648d Automatic changelog update 2026-01-14 21:23:59 +01:00
Milonandpre-commit-ci[bot] e0fd0cb2cd tips / tutorial system (#5199)
* somewhat functional yes

* some cleanup

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* review

* missed one

* why not params…

* review 2

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-14 14:23:39 -06:00
Delta-V bot 885d6e8872 Automatic changelog update 2026-01-14 14:23:42 +01:00
Pharaz4 2f048b06de Syndiborg fixes (#5197)
* - Increased EngiBorg max modules to 7 to fit the EngiModule that has the RCD
- Gave Syndicate assault, medical, and saboteur borgs cool name generator
- Removed unintentional Extra RCD that came with the kit.

* forgot syndicate medical cyborg was in dv folder and not mainstream

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>

---------

Signed-off-by: Pharaz4 <143268074+Pharaz4@users.noreply.github.com>
2026-01-14 14:23:22 +01:00
Delta-V bot 4d412e71ca Automatic changelog update 2026-01-13 18:23:34 +01:00
VelcroboyandVelcroboy 103c0aed24 Map Edits: The Hive (#5143)
* Map Edits: The Hive

* null data

* The rest

* No mo boms

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-01-13 18:23:13 +01:00
Delta-V bot 5157b58798 Automatic changelog update 2026-01-13 06:01:16 +01:00
c2c8eaef13 Add ammonia tank that can be purchased at logi for mixing space cleaner (#5167)
* Add ammonia tank that can be purchased at logi for mixing space cleaner

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-12 23:00:57 -06:00
Delta-V bot ce428bde54 Automatic changelog update 2026-01-12 10:13:35 +01:00
SirrRequiem dfc28529a1 Terra: The Throwback Episode (#5089)
THE GRINCH STOLE ALL THE SNOW
2026-01-12 10:13:15 +01:00
Delta-V bot 9cb2ad63e1 Automatic changelog update 2026-01-12 05:42:26 +01:00
Unkn0wn_Gh0standVanessa aef4666593 Gulp Emote from Goob (#5033)
* Gulp...

On skibid ohio rizz

* Yaml Patching and Gulp fixes for IPC & Avali

* Missed one single line

Haha 67.... I'm such a fat fucking chud

* Update Resources/Audio/_Goobstation/Voice/attributions.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>

* Update Resources/Audio/_Goobstation/Voice/Human/attributions.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>

---------

Signed-off-by: Unkn0wn_Gh0st <shadowstalkermll@gmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-11 22:42:07 -06:00
Delta-V bot a1962115f7 Automatic changelog update 2026-01-11 23:08:32 +01:00
Apachito 37d184081f Update 1 to Starlight Makeshift Armory Port (#5129)
* Tide-Update

* Removed the construction graph
2026-01-11 16:08:13 -06:00
Delta-V bot c9bdfb0802 Automatic changelog update 2026-01-11 20:57:57 +01:00
DisposableCrewmember42andVanessa c8bc3a7108 fix: rev ectoplasm can no longer be used to spike drinks (#5125)
* fix: rev ectoplasm can no longer be used to spike drinks

* Revert "fix: rev ectoplasm can no longer be used to spike drinks"

This reverts commit 717d817d98.

* fix: simply adjust yaml instead

* Update Resources/Prototypes/_DV/Entities/Objects/Specific/Medical/morgue.yml

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-01-11 19:57:37 +00:00
Delta-V bot 68516a036d Automatic changelog update 2026-01-11 20:49:17 +01:00
DisposableCrewmember42 eef49a1573 fix: rev can haunt again (#5124) 2026-01-11 13:48:57 -06:00
1558 changed files with 116840 additions and 75879 deletions
+12 -6
View File
@@ -10,22 +10,28 @@
<!-- Summary of code changes for easier review. -->
## Media
<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc).
<!-- Attach media if the PR makes in-game changes (clothing, items, features, etc).
Small fixes/refactors are exempt. -->
## Requirements
<!-- Confirm the following by placing an X in the brackets: [X] -->
<!-- Confirm the following by placing an X in the brackets without spaces inside (for example: [X] ): -->
- [ ] I have tested all added content and changes.
- [ ] I have added media to this PR or it does not require an ingame showcase.
- [ ] I have added media to this PR or it does not require an in-game showcase.
<!-- You should understand that not following the above may get your PR closed at maintainer’s discretion -->
## Breaking changes
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them. -->
## Licensing
<!-- This is for the benefit of other forks wishing to port features from DeltaV
Our repository is AGPL, meaning projects using the MIT license would have to ask for permission from the PR author (ideally that's you, reading this) before being allowed to port it over
This just saves them the trouble. Note that AGPL or MIT licensing is only applicable to SOFTWARE, and that assets such as textures and audio have their own licensing scheme that is defined in the codebase itself. -->
- [ ] I confirm that I am the creator of the code in this PR, and allow licensing it under the following license(s), or that the original author(s) has given me permission to do so:
- [X] AGPL (https://github.com/DeltaV-Station/Delta-v/blob/master/LICENSE-AGPLv3.txt) <!-- This one is required to contribute to DeltaV -->
- [X] MIT (https://github.com/DeltaV-Station/Delta-v/blob/master/LICENSE-MIT.txt) <!-- Optional - A lot of other SS14 forks use MIT -->
<!-- Feel free to add more licenses as you see fit -->
**Changelog**
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
Make sure to read the guidelines and take this Changelog template out of the comment block in order for it to show up.
Changelogs must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
Changelog must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
<!--
:cl:
- add: Added fun!
+1 -1
View File
@@ -2,7 +2,7 @@
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+1 -1
View File
@@ -2,7 +2,7 @@ name: Build & Test Debug
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+1 -30
View File
@@ -2,19 +2,11 @@
on:
pull_request_target:
types: [review_requested]
pull_request_review:
types: [submitted]
permissions:
pull-requests: write
issues: write
contents: write
types: [review_requested, opened]
jobs:
add_label:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
steps:
- uses: actions-ecosystem/action-add-labels@v1
with:
@@ -22,24 +14,3 @@ jobs:
- uses: actions-ecosystem/action-remove-labels@v1
with:
labels: "S: Awaiting Changes"
# Doesn't work on prs from foreign repos currently
#handle_review:
# runs-on: ubuntu-latest
# if: github.event_name == 'pull_request_review'
# steps:
# - uses: actions-ecosystem/action-add-labels@v1
# if: |
# github.event.review.state == 'changes_requested' &&
# (github.event.review.author_association == 'OWNER' ||
# github.event.review.author_association == 'MEMBER')
# with:
# labels: "S: Awaiting Changes"
#
# - uses: actions-ecosystem/action-remove-labels@v1
# if: |
# github.event.review.state == 'changes_requested' &&
# (github.event.review.author_association == 'OWNER' ||
# github.event.review.author_association == 'MEMBER')
# with:
# labels: "S: Needs Review"
+24
View File
@@ -0,0 +1,24 @@
# The PAT does not have the correct permissions to make this work; Eventually we'll want to replace this workflow with a proper bot
# name: "Labels: Approved"
# on:
# pull_request_review:
# types: [submitted]
# jobs:
# add_label:
# # Change the repository name after you've made sure the team name is correct for your fork!
# if: ${{ (github.repository == 'DeltaV-Station/Delta-v') && (github.event.review.state == 'APPROVED') }}
# permissions:
# contents: read
# pull-requests: write
# runs-on: ubuntu-latest
# steps:
# - uses: tspascoal/get-user-teams-membership@v3
# id: checkUserMember
# with:
# username: ${{ github.actor }}
# team: "maintainers"
# GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
# - if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
# uses: actions-ecosystem/action-add-labels@v1
# with:
# labels: "S: Approved"
+1 -1
View File
@@ -2,7 +2,7 @@
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
paths:
- '**.cs'
- '**.csproj'
+18 -5
View File
@@ -34,17 +34,19 @@ jobs:
# Uncomment this and comment the other line if you do this.
# https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches
#- name: Commit new credit files
# uses: stefanzweifel/git-auto-commit-action@v4
# with:
# commit_message: Update Credits
# commit_author: PJBot <pieterjan.briers+bot@gmail.com>
# - name: Commit new credit files
# uses: stefanzweifel/git-auto-commit-action@v7
# with:
# commit_message: Update Credits
# commit_author: DeltaV-Bot <github@deltav.gay>
# token: ${{ secrets.GITHUB_TOKEN }}
# This will make a PR
- name: Set current date as env variable
run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v5
with:
commit-message: Update Credits
@@ -52,3 +54,14 @@ jobs:
body: This is an automated Pull Request. This PR updates the github contributors in the credits section.
author: DeltaV-Bot <github@deltav.gay>
branch: automated/credits-${{env.NOW}}
- name: Auto-merge PR with bypass
if: steps.cpr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{secrets.CREDITS_TOKEN}}
run: |
gh pr merge ${{ steps.cpr.outputs.pull-request-number }} \
--squash \
--admin \
--delete-branch \
--repo ${{ github.repository }}
+1 -1
View File
@@ -1,7 +1,7 @@
name: RGA schema validator
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+1 -1
View File
@@ -2,7 +2,7 @@ name: RSI Validator
on:
push:
branches: [ staging, trying ]
branches: [ master ]
merge_group:
pull_request:
paths:
+1 -1
View File
@@ -1,7 +1,7 @@
name: Map file schema validator
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+1 -1
View File
@@ -2,7 +2,7 @@ name: YAML Linter
on:
push:
branches: [ master, staging, trying ]
branches: [ master ]
merge_group:
pull_request:
types: [ opened, reopened, synchronize, ready_for_review ]
+21 -21
View File
@@ -204,49 +204,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style="border:1px solid black;" | 301
|style= "border:1px solid black;"| [[File:SL_Manslaughter.png]]
|style= "border:1px solid black;"| Manslaughter
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| 10 minutes
|style= "border:1px solid black;"| To act without malice in a manner which, directly or indirectly, leads to the death of an Employee.
|-
|style= "border:1px solid black;"| 302
|style= "border:1px solid black;"| [[File:SL_Kidnapping.png]]
|style= "border:1px solid black;"| Kidnapping
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| 10 minutes
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an Employee against their will.
|-
| style="border:1px solid black;" | 303
| style="border:1px solid black;" | WIP
| style="border:1px solid black;" | Grand Possession
| style="border:1px solid black;" | 15-25 minutes
| style="border:1px solid black;" | 12-20 minutes
| style="border:1px solid black;" | To be in the possession of a Category C, D, or E Controlled Armament.
|-
| style="border:1px solid black;" | 304
| style="border:1px solid black;" | [[File:SL_Mindbreaking.png]]
| style="border:1px solid black;" | Noöspheric Tampering
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To maliciously tamper with or create noöspheric anomalies, entities, aberrations, or other elements; Or to otherwise deliberately perform actions that would raise Glimmer or other anomalous reading levels in a way that threatens normative reality.
|-
| style="border:1px solid black;" | 305
| style="border:1px solid black;" | [[File:SL_Sabotage.png]]
| style="border:1px solid black;" | Sabotage
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To maliciously commit an act that, directly or indirectly, hinders the operation of a vessel of its part; or, to commit an act that modifies and/or damages technology or equipment one is not authorized to access.
|-
| style="border:1px solid black;" | 306
| style="border:1px solid black;" | [[File:SL_AbuseOfPower.png]]
| style="border:1px solid black;" | Abuse of Power
| style="border:1px solid black;" | 14 minutes
| style="border:1px solid black;" | 10 minutes
| style="border:1px solid black;" | To intentionally misuse or wrongfully exercise one’s own authority, influence, or control, resulting in harm, unjust treatment, or demonstratable loss to another Employee.
|-
| style="border:1px solid black;" | 307
| style="border:1px solid black;" | [[File:SL_GrandTheft.png]]
| style="border:1px solid black;" | Grand Larceny
| style="border:1px solid black;" | 12 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To deprive an Employee of any Controlled Item in their lawful possession.
|-
| style="border:1px solid black;" | 308
| style="border:1px solid black;" | [[File:SL_BlackMarketeering.png]]
| style="border:1px solid black;" | Black Marketeering
| style="border:1px solid black;" | 12 minutes
| style="border:1px solid black;" | 8 minutes
| style="border:1px solid black;" | To sell, distribute, or otherwise circulate Controlled Items to unauthorized entities.
|}
----
@@ -262,49 +262,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 201
| style="border: 1px solid black;" | [[File:SL_Assault.png]]
! style="border: 1px solid black;" | {{anchor|Assault}}Assault
| style="border: 1px solid black;" | 10 minutes
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To cause physical harm or to effect unwanted physical contact on an Employee, without the apparent intent to kill them, or to threaten such actions with both capability and intent to do so.
|-
| style="border: 1px solid black;" | 202
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
| style="border: 1px solid black;" | 10 minutes
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
|-
| style="border: 1px solid black;" | 203
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]
! style="border: 1px solid black;" | {{anchor|Rioting}}Rioting
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To partake in an unauthorised riotous, tumultuous, and disruptive public assembly that refuses to disperse after warning.
|-
| style="border: 1px solid black;" | 204
| style="border: 1px solid black;" | [[File:SL_Endangerment.png]]
! style="border: 1px solid black;" | {{anchor|Endangerment}}Endangerment
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | To recklessly abandon obligations involving the continued wellbeing and/or protection of life and property, through malpractice, action, or inaction.
|-
| style="border: 1px solid black;" | 205
| style="border: 1px solid black;" | [[File:SL_Possession.png]]
! style="border: 1px solid black;" | {{anchor|Possession}}Possession
| style="border: 1px solid black;" | 5-10 minutes
| style="border: 1px solid black;" | 3-8 minutes
| style="border: 1px solid black;" | To be in unauthorised possession of restricted items or items of particular danger. See [[Standard_Operating_Procedure#Controlled_Substances/Items|Controlled Substances/Items]], [[Standard_Operating_Procedure#Controlled_Armaments|Controlled Armaments]].
|-
| style="border: 1px solid black;" | 206
| style="border: 1px solid black;" | [[File:SL_ObstructionOfJustice.png]]
! style="border: 1px solid black;" | {{anchor|Obstruction of Justice}}Obstruction of Justice
| style="border: 1px solid black;" | 8 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To wilfully disobey, interfere with, or refuse a decree of the court, warrant, or arrest.
|-
| style="border: 1px solid black;" | 207
| style="border: 1px solid black;" | [[File:SL_PerjuryOrFalseReport.png]]
! style="border: 1px solid black;" | {{anchor|Perjury or False Report}}Perjury or False Report
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To wilfully and maliciously tell an untruth either in court or in the process of making an actionable report to law enforcement.
|-
| style="border: 1px solid black;" | 208
| style="border: 1px solid black;" | [[File:SL_ContemptOfCourt.png]]
! style="border: 1px solid black;" | {{anchor|Contempt of Court}}Contempt of Court
| style="border: 1px solid black;" | 6 minutes
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | To conduct oneself disruptively and disrespectfully before the court.
|-
|style= "border:1px solid black;"| 209
@@ -329,13 +329,13 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 101
| style="border: 1px solid black;" | [[File:SL_AnimalCruelty.png]]
! style="border: 1px solid black;" | {{anchor|Animal Cruelty}}Animal Cruelty
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To inflict unnecessary suffering on a Pet with malicious intent.
|-
|style= "border:1px solid black;"| 102
|style= "border:1px solid black;"| WIP
!style= "border:1px solid black;"| Harassment
|style= "border:1px solid black;"| 5 minutes
|style= "border:1px solid black;"| 3 minutes
|style= "border:1px solid black;"| To verbally demean, humiliate, or harass an Employee with malice; or maliciously spread false information amongst a population.
|-
|style= "border:1px solid black;"| 103
@@ -347,19 +347,19 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | 104
| style="border: 1px solid black;" | [[File:SL_Theft.png]]
! style="border: 1px solid black;" | {{anchor|Theft}}Petty Larceny
| style="border: 1px solid black;" | 4 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To deprive an Employee of an item in their lawful property without consent.
|-
| style="border: 1px solid black;" | 105
| style="border: 1px solid black;" | [[File:SL_Trespass.png]]
! style="border: 1px solid black;" | {{anchor|Trespass}}Trespass
| style="border: 1px solid black;" | 3 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To enter into an area where one is not authorised nor invited.
|-
| style="border: 1px solid black;" | 106
| style="border: 1px solid black;" | [[File:SL_Vandalism.png]]
! style="border: 1px solid black;" | {{anchor|Vandalism}}Vandalism
| style="border: 1px solid black;" | 3 minutes
| style="border: 1px solid black;" | 2 minutes
| style="border: 1px solid black;" | To deface or superficially damage public property, or property belonging to another Employee.
|-
| style="border: 1px solid black;" | 107
@@ -102,25 +102,25 @@ A list of such Substances follow:
Controlled Armaments are grouped into categories of increasing severity. If the offender possesses several categories of contraband, only the highest severity of said items should be charged.
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig and further legal action as the situation demands.''
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 3min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed, adapted, or used to cause physical harm in close combat
* Any object or equipment specifically designed or adapted to protect against physical harm caused by weapons, projectiles, or other forms of direct assault
* Any object or instrument specifically designed or adapted to incapacitate or disorient by using light or sound without causing permanent injury
* Any object or equipment specifically designed or adapted for explicitly training purposes that would otherwise fall under another category
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig and further legal action as the situation demands.''
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 8min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed or adapted to cause physical harm in close combat, which employs a powered system to enhance performance
* Any manually-operated ballistic arms that possess an internal magazine or feeding system, or lack an internal magazine or feeding system altogether
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 12min brig and further legal action as the situation demands.''
* Any manually-operated ballistic arms that use an external magazine or feeding system
* Any semi-automatic ballistic arms not easily concealable within a pocket or coat
* Any object or instrument specifically designed or adapted for explicitly less-lethal purposes that would otherwise fall under another category
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
* Any semi-automatic ballistic arms that are easily concealable
* Any fully-automatic ballistic arms
@@ -128,7 +128,7 @@ Controlled Armaments are grouped into categories of increasing severity. If the
* Any object or device specifically designed or adapted to cause physical harm through detonation of an explosive charge
* Any object or equipment specifically designed or adapted to protect against both a space environment and physical harm caused by weapons, projectiles, or other forms of direct assault
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig and further legal action as the situation demands.''
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
* Any crew-served or emplaced weapon
* Any object or instrument specifically designed or adapted to launch large-calibre explosives or projectiles for offensive or defensive purposes
+24 -2
View File
@@ -23,6 +23,8 @@ public class RaiseEventBenchmark
PoolManager.Startup(typeof(BenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
var entMan = _pair.Server.EntMan;
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
var bus = (EntityEventBus)entMan.EventBus;
_sys = entMan.System<BenchSystem>();
_pair.Server.WaitPost(() =>
@@ -30,6 +32,8 @@ public class RaiseEventBenchmark
var uid = entMan.Spawn();
_sys.Ent = new(uid, entMan.GetComponent<TransformComponent>(uid));
_sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp);
_sys.NetId = fact.GetRegistration<TransformComponent>().NetID!.Value;
_sys.EvSubs = bus.GetNetCompEventHandlers<BenchSystem.BenchEv>();
})
.GetAwaiter()
.GetResult();
@@ -60,6 +64,12 @@ public class RaiseEventBenchmark
return _sys.RaiseICompEvent();
}
[Benchmark]
public int RaiseNetEvent()
{
return _sys.RaiseNetIdEvent();
}
[Benchmark]
public int RaiseCSharpEvent()
{
@@ -74,6 +84,8 @@ public class RaiseEventBenchmark
public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev);
public event EntityEventHandler? OnCSharpEvent;
public ushort NetId;
internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!;
public override void Initialize()
{
@@ -92,7 +104,7 @@ public class RaiseEventBenchmark
public int RaiseCompEvent()
{
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
return ev.N;
}
@@ -100,7 +112,16 @@ public class RaiseEventBenchmark
{
// Raise with an IComponent instead of concrete type
var ev = new BenchEv();
EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
return ev.N;
}
public int RaiseNetIdEvent()
{
// Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events)
var ev = new BenchEv();
ref var unitEv = ref Unsafe.As<BenchEv, EntityEventBus.Unit>(ref ev);
EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv);
return ev.N;
}
@@ -118,6 +139,7 @@ public class RaiseEventBenchmark
}
[ByRefEvent]
[ComponentEvent(Exclusive = false)]
public struct BenchEv
{
public int N;
@@ -60,8 +60,24 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast)
return;
// Save all client-sided alerts to later put back in
var clientAlerts = new Dictionary<AlertKey, AlertState>();
foreach (var alert in alerts.Comp.Alerts)
{
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
{
if (alertProto.ClientHandled)
clientAlerts[alert.Key] = alert.Value;
}
}
alerts.Comp.Alerts = new(cast.Alerts);
foreach (var alert in clientAlerts)
{
alerts.Comp.Alerts[alert.Key] = alert.Value;
}
UpdateHud(alerts);
}
@@ -3,6 +3,7 @@ using Content.Client.Atmos.Components;
using Content.Client.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -23,6 +24,7 @@ namespace Content.Client.Atmos.Overlays
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedAtmosphereSystem _atmosphereSystem;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSys;
@@ -54,6 +56,7 @@ namespace Content.Client.Atmos.Overlays
{
_entManager = entManager;
_mapManager = IoCManager.Resolve<IMapManager>();
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSys = xformSys;
_shader = protoMan.Index(UnshadedShader).Instance();
@@ -67,7 +70,7 @@ namespace Content.Client.Atmos.Overlays
for (var i = 0; i < _gasCount; i++)
{
var gasPrototype = protoMan.Index<GasPrototype>(system.VisibleGasId[i].ToString());
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
SpriteSpecifier overlay;
@@ -52,14 +52,18 @@ namespace Content.Client.Atmos.UI
private void OnSelectGasPressed()
{
if (_window is null) return;
if (_window is null)
return;
if (_window.SelectedGas is null)
{
SendMessage(new GasFilterSelectGasMessage(null));
}
else
{
if (!int.TryParse(_window.SelectedGas, out var gas)) return;
if (!Enum.TryParse<Gas>(_window.SelectedGas, out var gas))
return;
SendMessage(new GasFilterSelectGasMessage(gas));
}
}
@@ -0,0 +1,52 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Robust.Client.Input;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Client.Commands;
/// <summary>
/// Sets the a <see cref="CCVars.DebugQuickInspect"/> CVar to the name of a component, which allows the client to quickly open a VV window for that component
/// by using the Alt+C or Alt+B hotkeys.
/// </summary>
public sealed class QuickInspectCommand : LocalizedEntityCommands
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
public override string Command => "quickinspect";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteLine(Loc.GetString("shell-wrong-arguments-number"));
return;
}
_configurationManager.SetCVar(CCVars.DebugQuickInspect, args[0]);
var serverKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectServerComponent);
var clientKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectClientComponent);
shell.WriteLine(Loc.GetString($"cmd-quickinspect-success", ("component", args[0]), ("serverKeybind", serverKey), ("clientKeybind", clientKey)));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
// Not ideal since it only shows client-side components, but you can still type in any name you want.
// If you know how to get server component names on the client then please fix this.
var options = EntityManager.ComponentFactory.AllRegisteredTypes
.Select(p => new CompletionOption(
EntityManager.ComponentFactory.GetComponentName(p)
));
return CompletionResult.FromOptions(options);
}
return CompletionResult.Empty;
}
}
+13 -1
View File
@@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
solution = new Solution();
return false;
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution spilled,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;
+7
View File
@@ -0,0 +1,7 @@
using Content.Shared.Fluids.Components;
using Content.Shared.Fluids.EntitySystems;
using Robust.Shared.Map;
namespace Content.Client.Fluids;
public sealed class SpraySystem : SharedSpraySystem;
@@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.Clickable;
using Content.Client.UserInterface;
using Content.Client.Viewport;
using Content.Shared.CCVar;
using Content.Shared.Input;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
@@ -13,6 +14,7 @@ using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
@@ -40,6 +42,7 @@ namespace Content.Client.Gameplay
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IViewVariablesManager _vvm = default!;
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
private ClickableEntityComparer _comparer = default!;
@@ -83,6 +86,8 @@ namespace Content.Client.Gameplay
_comparer = new ClickableEntityComparer();
CommandBinds.Builder
.Bind(ContentKeyFunctions.InspectEntity, new PointerInputCmdHandler(HandleInspect, outsidePrediction: true))
.Bind(ContentKeyFunctions.InspectServerComponent, new PointerInputCmdHandler(HandleInspectServerComponent, outsidePrediction: true))
.Bind(ContentKeyFunctions.InspectClientComponent, new PointerInputCmdHandler(HandleInspectClientComponent, outsidePrediction: true))
.Register<GameplayStateBase>();
}
@@ -99,6 +104,21 @@ namespace Content.Client.Gameplay
return true;
}
private bool HandleInspectServerComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
if (_entityManager.TryGetNetEntity(uid, out var net))
_conHost.ExecuteCommand($"vv /entity/{net.Value.Id}/{component}");
return true;
}
private bool HandleInspectClientComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
_conHost.ExecuteCommand($"vv /c/entity/{uid}/{component}");
return true;
}
public EntityUid? GetClickedEntity(MapCoordinates coordinates)
{
return GetClickedEntity(coordinates, _eyeManager.CurrentEye);
+2
View File
@@ -38,6 +38,8 @@ namespace Content.Client.Input
common.AddFunction(ContentKeyFunctions.ZoomIn);
common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity);
common.AddFunction(ContentKeyFunctions.InspectServerComponent);
common.AddFunction(ContentKeyFunctions.InspectClientComponent);
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
// Begin DeltaV Additions
common.AddFunction(ContentKeyFunctions.OpenCHelp);
+13
View File
@@ -14,6 +14,7 @@ using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Configuration;
using Robust.Shared.Timing;
using Content.Client._Impstation.ReadyManifest; // Impstation - Ready Manifest
namespace Content.Client.Lobby
{
@@ -31,6 +32,7 @@ namespace Content.Client.Lobby
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
private ReadyManifestSystem _readyManifest = default!; // Impstation - Ready Manifest
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
public LobbyGui? Lobby;
@@ -48,6 +50,7 @@ namespace Content.Client.Lobby
_gameTicker = _entityManager.System<ClientGameTicker>();
_contentAudioSystem = _entityManager.System<ContentAudioSystem>();
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
_readyManifest = _entityManager.EntitySysManager.GetEntitySystem<ReadyManifestSystem>(); // Impstation - Ready Manifest
chatController.SetMainChat(true);
@@ -69,6 +72,7 @@ namespace Content.Client.Lobby
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
Lobby.ReadyButton.OnPressed += OnReadyPressed;
Lobby.ReadyButton.OnToggled += OnReadyToggled;
Lobby.ManifestButton.OnPressed += OnManifestPressed; // Impstation - Ready Manifest
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
@@ -89,6 +93,7 @@ namespace Content.Client.Lobby
Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
Lobby!.ManifestButton.OnPressed -= OnManifestPressed; // Impstation - Ready Manifest
Lobby = null;
}
@@ -120,6 +125,12 @@ namespace Content.Client.Lobby
SetReady(args.Pressed);
}
// Impstation - Ready Manifest
private void OnManifestPressed(BaseButton.ButtonEventArgs args)
{
_readyManifest.RequestReadyManifest();
}
public override void FrameUpdate(FrameEventArgs e)
{
if (_gameTicker.IsGameStarted)
@@ -182,6 +193,7 @@ namespace Content.Client.Lobby
Lobby!.ReadyButton.ToggleMode = false;
Lobby!.ReadyButton.Pressed = false;
Lobby!.ObserveButton.Disabled = false;
Lobby!.ManifestButton.Disabled = true; //imp
}
else
{
@@ -191,6 +203,7 @@ namespace Content.Client.Lobby
Lobby!.ReadyButton.ToggleMode = true;
Lobby!.ReadyButton.Disabled = false;
Lobby!.ObserveButton.Disabled = true;
Lobby!.ManifestButton.Disabled = false; // imp
}
if (_gameTicker.ServerInfoBlob != null)
+4 -4
View File
@@ -123,10 +123,10 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_profileEditor.RefreshSpecies();
}
if (obj.WasModified<TraitPrototype>())
{
_profileEditor.RefreshTraits();
}
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
// {
// _profileEditor.RefreshTraits();
// }
}
}
@@ -3,6 +3,7 @@
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
xmlns:traits="clr-namespace:Content.Client._DV.Traits.UI"
HorizontalExpand="True">
<!-- Left side -->
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
@@ -133,12 +134,15 @@
<BoxContainer Name="AntagList" Orientation="Vertical" />
</ScrollContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Margin="10">
<!-- Traits -->
<ScrollContainer VerticalExpand="True">
<BoxContainer Name="TraitsList" Orientation="Vertical" />
</ScrollContainer>
</BoxContainer>
<!-- DV - Traits -->
<!-- <BoxContainer Orientation="Vertical" Margin="10"> -->
<!-- ~1~ Traits @1@ -->
<!-- <ScrollContainer VerticalExpand="True"> -->
<!-- <BoxContainer Name="TraitsList" Orientation="Vertical" /> -->
<!-- </ScrollContainer> -->
<!-- </BoxContainer> -->
<traits:TraitsTab Name="Traits"/>
<!-- End DV -->
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
<!-- Markings -->
<ScrollContainer VerticalExpand="True">
@@ -39,6 +39,7 @@ using System.Globalization;
using Content.Client._CD.Records.UI;
using Content.Shared._CD.Records;
// End CD - Character Records
using Content.Shared._DV.Traits; // DV - Traits
namespace Content.Client.Lobby.UI
{
@@ -180,6 +181,8 @@ namespace Content.Client.Lobby.UI
Save?.Invoke();
};
Traits.OnTraitsChanged += OnTraitsSelectionChanged; // DeltaV
#region Left
#region Name
@@ -239,7 +242,6 @@ namespace Content.Client.Lobby.UI
{
SpeciesButton.SelectId(args.Id);
SetSpecies(_species[args.Id].ID);
RefreshTraits(); // DeltaV - Allows for hiding traits
UpdateHairPickers();
OnSkinColorOnValueChanged();
};
@@ -462,7 +464,7 @@ namespace Content.Client.Lobby.UI
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
RefreshTraits();
// RefreshTraits(); // DeltaV
#region Markings
@@ -515,6 +517,58 @@ namespace Content.Client.Lobby.UI
IsDirty = false;
}
// Begin DeltaV - Traits Integration
/// <summary>
/// Called when trait selection changes in the TraitsTab.
/// Updates the profile with the new trait selection.
/// </summary>
private void OnTraitsSelectionChanged(HashSet<ProtoId<TraitPrototype>> traits)
{
if (Profile is null)
return;
// Remove all existing traits - iterate directly over readonly collection
foreach (var existingTrait in Profile.TraitPreferences)
{
Profile = Profile.WithoutTraitPreference(existingTrait, _prototypeManager);
}
// Add newly selected traits
foreach (var trait in traits)
{
Profile = Profile.WithTraitPreference(trait.Id, _prototypeManager);
}
SetDirty();
}
/// <summary>
/// Updates the traits tab with the current profile's selected traits.
/// </summary>
private void UpdateTraitsSelection()
{
if (Profile is null)
{
Traits.SetSelectedTraits(new HashSet<ProtoId<TraitPrototype>>());
return;
}
// Convert profile's trait preferences (strings) to ProtoId<TraitPrototype>
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>(Profile.TraitPreferences.Count);
foreach (var traitId in Profile.TraitPreferences)
{
// Validate that the trait still exists in prototypes
if (_prototypeManager.HasIndex(traitId))
{
selectedTraits.Add(new ProtoId<TraitPrototype>(traitId));
}
}
Traits.SetSelectedTraits(selectedTraits);
Traits.UpdateConditions(Profile);
}
// End DeltaV - Traits Integration
/// <summary>
/// Refreshes the flavor text editor status.
/// </summary>
@@ -549,122 +603,122 @@ namespace Content.Client.Lobby.UI
/// <summary>
/// Refreshes traits selector
/// </summary>
public void RefreshTraits()
{
TraitsList.RemoveAllChildren();
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
if (traits.Count < 1)
{
TraitsList.AddChild(new Label
{
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
FontColorOverride = Color.Gray,
});
return;
}
// Setup model
Dictionary<string, List<string>> traitGroups = new();
List<string> defaultTraits = new();
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
foreach (var trait in traits)
{
// Begin DeltaV Additions - Species trait exclusion
if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
{
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
continue;
}
// End DeltaV Additions
if (trait.Category == null)
{
defaultTraits.Add(trait.ID);
continue;
}
if (!_prototypeManager.HasIndex(trait.Category))
continue;
var group = traitGroups.GetOrNew(trait.Category);
group.Add(trait.ID);
}
// Create UI view from model
foreach (var (categoryId, categoryTraits) in traitGroups)
{
TraitCategoryPrototype? category = null;
if (categoryId != TraitCategoryPrototype.Default)
{
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
// Label
TraitsList.AddChild(new Label
{
Text = Loc.GetString(category.Name),
Margin = new Thickness(0, 10, 0, 0),
StyleClasses = { StyleClass.LabelHeading },
});
}
List<TraitPreferenceSelector?> selectors = new();
var selectionCount = 0;
foreach (var traitProto in categoryTraits)
{
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
var selector = new TraitPreferenceSelector(trait);
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
if (selector.Preference)
selectionCount += trait.Cost;
selector.PreferenceChanged += preference =>
{
if (preference)
{
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
}
else
{
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
}
SetDirty();
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
};
selectors.Add(selector);
}
// Selection counter
if (category is { MaxTraitPoints: >= 0 })
{
TraitsList.AddChild(new Label
{
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
FontColorOverride = Color.Gray
});
}
foreach (var selector in selectors)
{
if (selector == null)
continue;
if (category is { MaxTraitPoints: >= 0 } &&
selector.Cost + selectionCount > category.MaxTraitPoints)
{
selector.Checkbox.Label.FontColorOverride = Color.Red;
}
TraitsList.AddChild(selector);
}
}
}
// public void RefreshTraits()
// {
// TraitsList.RemoveAllChildren();
//
// var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
// TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
//
// if (traits.Count < 1)
// {
// TraitsList.AddChild(new Label
// {
// Text = Loc.GetString("humanoid-profile-editor-no-traits"),
// FontColorOverride = Color.Gray,
// });
// return;
// }
//
// // Setup model
// Dictionary<string, List<string>> traitGroups = new();
// List<string> defaultTraits = new();
// traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
//
// foreach (var trait in traits)
// {
// // Begin DeltaV Additions - Species trait exclusion
// if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
// {
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
// continue;
// }
// // End DeltaV Additions
//
// if (trait.Category == null)
// {
// defaultTraits.Add(trait.ID);
// continue;
// }
//
// if (!_prototypeManager.HasIndex(trait.Category))
// continue;
//
// var group = traitGroups.GetOrNew(trait.Category);
// group.Add(trait.ID);
// }
//
// // Create UI view from model
// foreach (var (categoryId, categoryTraits) in traitGroups)
// {
// TraitCategoryPrototype? category = null;
//
// if (categoryId != TraitCategoryPrototype.Default)
// {
// category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
// // Label
// TraitsList.AddChild(new Label
// {
// Text = Loc.GetString(category.Name),
// Margin = new Thickness(0, 10, 0, 0),
// StyleClasses = { StyleClass.LabelHeading },
// });
// }
//
// List<TraitPreferenceSelector?> selectors = new();
// var selectionCount = 0;
//
// foreach (var traitProto in categoryTraits)
// {
// var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
// var selector = new TraitPreferenceSelector(trait);
//
// selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
// if (selector.Preference)
// selectionCount += trait.Cost;
//
// selector.PreferenceChanged += preference =>
// {
// if (preference)
// {
// Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
// }
// else
// {
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
// }
//
// SetDirty();
// RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
// };
// selectors.Add(selector);
// }
//
// // Selection counter
// if (category is { MaxTraitPoints: >= 0 })
// {
// TraitsList.AddChild(new Label
// {
// Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
// FontColorOverride = Color.Gray
// });
// }
//
// foreach (var selector in selectors)
// {
// if (selector == null)
// continue;
//
// if (category is { MaxTraitPoints: >= 0 } &&
// selector.Cost + selectionCount > category.MaxTraitPoints)
// {
// selector.Checkbox.Label.FontColorOverride = Color.Red;
// }
//
// TraitsList.AddChild(selector);
// }
// }
// }
/// <summary>
/// Refreshes the species selector.
@@ -845,11 +899,13 @@ namespace Content.Client.Lobby.UI
_recordsTab.Update(profile);
// End CD - Character Records
UpdateTraitsSelection(); // DeltaV - Traits
RefreshAntags();
RefreshJobs();
RefreshLoadouts();
RefreshSpecies();
RefreshTraits();
// RefreshTraits(); // DeltaV
RefreshFlavorText();
ReloadPreview();
+4
View File
@@ -36,6 +36,10 @@
Align="Left"
FontColorOverride="{x:Static maths:Color.DarkGray}"
StyleClasses="LabelBig" HorizontalExpand="True" />
<!-- Imp manifest button -->
<Button Name="ManifestButton" Access="Public"
Text="Manifest"
StyleClasses="ButtonBig" MinWidth="137" />
<Button Name="ReadyButton" Access="Public" ToggleMode="True"
Text="{Loc 'ui-lobby-ready-up-button'}"
StyleClasses="ButtonBig" MinWidth="137" />
@@ -19,22 +19,24 @@ public sealed partial class TraitPreferenceSelector : Control
public event Action<bool>? PreferenceChanged;
public TraitPreferenceSelector(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
text += Loc.GetString(trait.Name);
Cost = trait.Cost;
Checkbox.Text = text;
Checkbox.OnToggled += OnCheckBoxToggled;
if (trait.Description is { } desc)
{
Checkbox.ToolTip = Loc.GetString(desc);
}
}
// DeltaV - This whole control is generally unused but the compiler wouldn't compile if I removed it
// So I'm just gonna comment this part out
// public TraitPreferenceSelector(TraitPrototype trait)
// {
// RobustXamlLoader.Load(this);
//
// var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
// text += Loc.GetString(trait.Name);
//
// Cost = trait.Cost;
// Checkbox.Text = text;
// Checkbox.OnToggled += OnCheckBoxToggled;
//
// if (trait.Description is { } desc)
// {
// Checkbox.ToolTip = Loc.GetString(desc);
// }
// }
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
{
+16
View File
@@ -1,4 +1,6 @@
using System.Text.RegularExpressions;
using Content.Client._DV.UserInterfaces.BuildInfo; // DeltaV - More info in Escape Menu
using Content.Client.Credits; // DeltaV - More info in Escape Menu
using Content.Client.MainMenu.UI;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client;
@@ -48,6 +50,8 @@ namespace Content.Client.MainMenu
_mainMenuControl.DirectConnectButton.OnPressed += DirectConnectButtonPressed;
_mainMenuControl.AddressBox.OnTextEntered += AddressBoxEntered;
_mainMenuControl.ChangelogButton.OnPressed += ChangelogButtonPressed;
_mainMenuControl.BuildInfoButton.OnPressed += BuildInfoButtonPressed; // DeltaV - More info in Escape Menu
_mainMenuControl.CreditsButton.OnPressed += CreditsButtonPressed; // DeltaV - More info in Escape Menu
_client.RunLevelChanged += RunLevelChanged;
}
@@ -66,6 +70,18 @@ namespace Content.Client.MainMenu
_userInterfaceManager.GetUIController<ChangelogUIController>().ToggleWindow();
}
// Begin DeltaV - More info in Escape Menu
private void BuildInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
_userInterfaceManager.GetUIController<BuildInfoUIController>().ToggleWindow();
}
private void CreditsButtonPressed(BaseButton.ButtonEventArgs args)
{
new CreditsWindow().Open();
}
// End DeltaV - More info in Escape Menu
private void OptionsButtonPressed(BaseButton.ButtonEventArgs args)
{
_userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
@@ -48,5 +48,18 @@
<clog:ChangelogButton
Name="ChangelogButton"
Access="Public"/>
<!-- Begin DeltaV - More info in Escape Menu -->
<Button
Name="CreditsButton"
Access="Public"
Text="{Loc server-info-credits-button}"
TextAlign="Center" />
<Button
Name="BuildInfoButton"
Access="Public"
Text="{Loc build-info-button}"
ToolTip="{Loc build-info-tooltip}"
TextAlign="Center" />
<!-- End DeltaV - More info in Escape Menu -->
</BoxContainer>
</Control>
@@ -0,0 +1,5 @@
using Content.Shared.NameIdentifier;
namespace Content.Client.NameIdentifier;
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem;
@@ -17,5 +17,10 @@
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
<Button Access="Public" Name="DisconnectButton" Text="{Loc 'ui-escape-disconnect'}" />
<Button Access="Public" Name="QuitButton" Text="{Loc 'ui-escape-quit'}" StyleClasses="ButtonColorRed" />
<!-- Begin DeltaV - More info in Escape Menu -->
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
<Button Access="Public" Name="CreditsButton" Text="{Loc server-info-credits-button}" />
<Button Access="Public" Name="BuildInfoButton" Text="{Loc build-info-button}" ToolTip="{Loc build-info-tooltip}" />
<!-- End DeltaV - More info in Escape Menu -->
</BoxContainer>
</ui1:EscapeMenu>
@@ -292,6 +292,8 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.ShowDebugMonitors);
AddButton(EngineKeyFunctions.HideUI);
AddButton(ContentKeyFunctions.InspectEntity);
AddButton(ContentKeyFunctions.InspectServerComponent);
AddButton(ContentKeyFunctions.InspectClientComponent);
AddHeader("ui-options-header-text-cursor");
AddButton(EngineKeyFunctions.TextCursorLeft);
+1 -1
View File
@@ -19,7 +19,7 @@
<!-- Power On/Off -->
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
StyleClasses="highlight" MinWidth="120"/>
<BoxContainer Orientation="Horizontal" MinWidth="90">
<BoxContainer Orientation="Horizontal" MinWidth="150">
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
</BoxContainer>
<!--Charging Status-->
+8 -1
View File
@@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
if (PowerLabel != null)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
if (castState.Tripped)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
}
else
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
}
}
if (ExternalPowerStateLabel != null)
@@ -18,7 +18,9 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
return;
}
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
if (!args.Silent)
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
}
@@ -1,5 +0,0 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class ChargerSystem : SharedChargerSystem;
@@ -1,67 +0,0 @@
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
[UsedImplicitly]
public sealed class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
}
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanUse;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanDraw;
}
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
return;
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
if (positiveCharge)
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
}
private enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}
}
@@ -0,0 +1,11 @@
namespace Content.Client.PowerCell;
/// <summary>
/// Sprite layers for power cells.
/// For use with the generic visualizer.
/// </summary>
public enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}
@@ -1,4 +0,0 @@
namespace Content.Client.PowerCell;
[RegisterComponent]
public sealed partial class PowerCellVisualsComponent : Component {}
@@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Client.PowerCell;
@@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <summary>
/// The base sprite state used if the power cell charger does not contain a power cell.
/// </summary>
[DataField("emptyState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string EmptyState = "empty";
/// <summary>
/// The base sprite state used if the power cell charger contains a power cell.
/// </summary>
[DataField("occupiedState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string OccupiedState = "full";
/// <summary>
@@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
/// </summary>
[DataField("lightStates")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public Dictionary<CellChargerStatus, string> LightStates = new()
{
[CellChargerStatus.Off] = "light-off",
@@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
@@ -20,7 +20,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
[Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly HandsSystem _hands = default!;
private Direction _placementDirection = default;
public override void Update(float frameTime)
@@ -42,6 +42,11 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
var heldEntity = _hands.GetActiveItem(player);
// Don't open the placement overlay for client-side RCDs.
// This may happen when predictively spawning one in your hands.
if (heldEntity != null && IsClientSide(heldEntity.Value))
return;
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
{
// If the player was holding an RCD, but is no longer, cancel placement
@@ -69,7 +74,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
MobUid = heldEntity.Value,
PlacementOption = PlacementMode,
EntityType = prototype.Prototype,
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (prototype.Mode == RcdMode.ConstructTile),
UseEditorContext = false,
};
@@ -1,7 +1,6 @@
using Content.Shared._DV.Silicons.Borgs; // DeltaV
using Content.Shared.Silicons.Borgs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.Borgs;
@@ -25,32 +24,31 @@ public sealed class BorgBoundUserInterface : BoundUserInterface
_menu.BrainButtonPressed += () =>
{
SendMessage(new BorgEjectBrainBuiMessage());
SendPredictedMessage(new BorgEjectBrainBuiMessage());
};
_menu.IdChipButtonPressed += () => SendMessage(new BorgEjectIdChipMessage()); // DeltaV
_menu.EjectBatteryButtonPressed += () =>
{
SendMessage(new BorgEjectBatteryBuiMessage());
SendPredictedMessage(new BorgEjectBatteryBuiMessage());
};
_menu.NameChanged += name =>
{
SendMessage(new BorgSetNameBuiMessage(name));
SendPredictedMessage(new BorgSetNameBuiMessage(name));
};
_menu.RemoveModuleButtonPressed += module =>
{
SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (state is not BorgBuiState msg)
return;
_menu?.UpdateState(msg);
_menu?.UpdateBatteryButton();
_menu?.UpdateBrainButton();
_menu?.UpdateIdChipButton(); // DeltaV
_menu?.UpdateModulePanel();
}
}
+26 -18
View File
@@ -5,8 +5,8 @@ using Content.Shared.Access.Components; // DeltaV
using Content.Shared.CCVar;
using Content.Shared.NameIdentifier;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Preferences;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
@@ -22,6 +22,8 @@ public sealed partial class BorgMenu : FancyWindow
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entity = default!;
private readonly NameModifierSystem _nameModifier;
private readonly PowerCellSystem _powerCell;
private readonly SharedBatterySystem _battery;
public Action? BrainButtonPressed;
public Action? IdChipButtonPressed; // DeltaV
@@ -44,6 +46,8 @@ public sealed partial class BorgMenu : FancyWindow
IoCManager.InjectDependencies(this);
_nameModifier = _entity.System<NameModifierSystem>();
_powerCell = _entity.System<PowerCellSystem>();
_battery = _entity.System<SharedBatterySystem>();
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
@@ -64,8 +68,6 @@ public sealed partial class BorgMenu : FancyWindow
NameLineEdit.OnTextChanged += OnNameChanged;
NameLineEdit.OnTextEntered += OnNameEntered;
NameLineEdit.OnFocusExit += OnNameFocusExit;
UpdateBrainButton();
}
public void SetEntity(EntityUid entity)
@@ -86,6 +88,10 @@ public sealed partial class BorgMenu : FancyWindow
NameIdentifierLabel.Visible = false;
NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName;
}
UpdateBatteryButton();
UpdateBrainButton();
UpdateModulePanel();
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -93,22 +99,24 @@ public sealed partial class BorgMenu : FancyWindow
base.FrameUpdate(args);
AccumulatedTime += args.DeltaSeconds;
BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2);
}
BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2);
public void UpdateState(BorgBuiState state)
{
EjectBatteryButton.Disabled = !state.HasBattery;
ChargeBar.Value = state.ChargePercent;
var chargeFraction = 0f;
if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery))
chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
ChargeBar.Value = chargeFraction;
ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
("charge", (int) MathF.Round(state.ChargePercent * 100)));
UpdateBrainButton();
UpdateIdChipButton(); // DeltaV
UpdateModulePanel();
("charge", (int)MathF.Round(chargeFraction * 100)));
}
private void UpdateBrainButton()
public void UpdateBatteryButton()
{
EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
}
public void UpdateBrainButton()
{
if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain)
{
@@ -130,7 +138,7 @@ public sealed partial class BorgMenu : FancyWindow
/// <summary>
/// DeltaV: Updates the Eject ID Chip button text and enabled status.
/// </summary>
private void UpdateIdChipButton()
public void UpdateIdChipButton()
{
if (!_entity.TryGetComponent<IdChipSlotComponent>(Entity, out var comp))
{
@@ -151,7 +159,7 @@ public sealed partial class BorgMenu : FancyWindow
}
}
private void UpdateModulePanel()
public void UpdateModulePanel()
{
if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis))
return;
@@ -0,0 +1,83 @@
using Content.Shared.PowerCell.Components;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Player;
namespace Content.Client.Silicons.Borgs;
public sealed partial class BorgSystem
{
// How often to update the battery alert.
// Also gets updated instantly when switching bodies or a battery is inserted or removed.
private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
// Don't put this on the component because we only need to track the time for a single entity
// and we don't want to TryComp it every single tick.
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
private EntityQuery<BorgChassisComponent> _chassisQuery;
private EntityQuery<PowerCellSlotComponent> _slotQuery;
public void InitializeBattery()
{
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
}
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
{
UpdateBatteryAlert((ent.Owner, ent.Comp, null));
}
private void OnPlayerDetached(Entity<BorgChassisComponent> ent, ref LocalPlayerDetachedEvent args)
{
// Remove all borg related alerts.
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
}
private void UpdateBatteryAlert(Entity<BorgChassisComponent, PowerCellSlotComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2, false))
return;
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery))
{
_alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert);
return;
}
// Alert levels from 0 to 10.
var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2)))
{
chargeLevel = 1;
}
_alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel);
}
// Periodically update the charge indicator.
// We do this with a client-side alert so that we don't have to network the charge level.
public void UpdateBattery(float frameTime)
{
if (_player.LocalEntity is not { } localPlayer)
return;
var curTime = _timing.CurTime;
if (curTime < _nextAlertUpdate)
return;
_nextAlertUpdate = curTime + AlertUpdateDelay;
if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot))
return;
UpdateBatteryAlert((localPlayer, chassis, slot));
}
}
+49 -22
View File
@@ -1,72 +1,93 @@
using Content.Shared.Mobs;
using Content.Shared.Alert;
using Content.Shared.Mobs;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Client.Silicons.Borgs;
/// <inheritdoc/>
public sealed class BorgSystem : SharedBorgSystem
public sealed partial class BorgSystem : SharedBorgSystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
InitializeBattery();
SubscribeLocalEvent<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
}
private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
{
if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
bui.Update();
}
private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> chassis, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateBorgAppearance(uid, component, args.Component, args.Sprite);
UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite));
}
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
protected override void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
if (!chassis.Comp.Initialized)
return;
base.OnInserted(uid, component, args);
UpdateBorgAppearance(uid, component);
base.OnInserted(chassis, ref args);
UpdateUI(chassis.AsNullable());
UpdateBorgAppearance((chassis, chassis.Comp));
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
}
protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
protected override void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
{
if (!component.Initialized)
if (!chassis.Comp.Initialized)
return;
base.OnRemoved(uid, component, args);
UpdateBorgAppearance(uid, component);
base.OnRemoved(chassis, ref args);
UpdateUI(chassis.AsNullable());
UpdateBorgAppearance((chassis, chassis.Comp));
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
}
private void UpdateBorgAppearance(EntityUid uid,
BorgChassisComponent? component = null,
AppearanceComponent? appearance = null,
SpriteComponent? sprite = null)
private void UpdateBorgAppearance(Entity<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> ent)
{
if (!Resolve(uid, ref component, ref appearance, ref sprite))
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3))
return;
if (_appearance.TryGetData<MobState>(uid, MobStateVisuals.State, out var state, appearance))
if (_appearance.TryGetData<MobState>(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2))
{
if (state != MobState.Alive)
{
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false);
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false);
return;
}
}
if (!_appearance.TryGetData<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
if (!_appearance.TryGetData<bool>(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2))
hasPlayer = false;
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer);
_sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState);
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer);
_sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState);
}
private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args)
@@ -107,4 +128,10 @@ public sealed class BorgSystem : SharedBorgSystem
borg.Comp.HasMindState = hasMindState;
borg.Comp.NoMindState = noMindState;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateBattery(frameTime);
}
}
@@ -1,7 +1,6 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'thief-backpack-window-title'}"
MinSize="700 700">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<!-- First Informational panel -->
@@ -25,7 +24,6 @@
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<!-- Third approve button panel -->
<PanelContainer Margin="10">
<Button Name="ApproveButton"
@@ -46,7 +46,8 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
selectedNumber++;
}
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
Title = Loc.GetString(state.ToolName);
Description.Text = Loc.GetString(state.ToolDesc, ("maxCount", state.MaxSelectedSets));
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
ApproveButton.Disabled = selectedNumber != state.MaxSelectedSets;
}
+5
View File
@@ -0,0 +1,5 @@
using Content.Shared.Tips;
namespace Content.Client.Tips;
public sealed class TipsSystem : SharedTipsSystem;
@@ -147,7 +147,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
if (emote.Category == EmoteCategory.Invalid
|| emote.ChatTriggers.Count == 0
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|| whitelistSystem.IsWhitelistPass(emote.Blacklist, player.Value))
continue;
if (!emote.Available
@@ -1,4 +1,6 @@
using Content.Client.Gameplay;
using Content.Client._DV.UserInterfaces.BuildInfo; // DeltaV - More info in Escape Menu
using Content.Client.Credits; // DeltaV - More info in Escape Menu
using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Client.UserInterface.Systems.Info;
@@ -25,6 +27,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
[Dependency] private readonly InfoUIController _info = default!;
[Dependency] private readonly OptionsUIController _options = default!;
[Dependency] private readonly GuidebookUIController _guidebook = default!;
[Dependency] private readonly BuildInfoUIController _buildInfo = default!; // DeltaV - More info in Escape Menu
private Options.UI.EscapeMenu? _escapeWindow;
@@ -103,6 +106,19 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
_guidebook.ToggleGuidebook();
};
// Begin DeltaV - More info in Escape Menu
_escapeWindow.BuildInfoButton.OnPressed += _ =>
{
CloseEscapeWindow();
_buildInfo.OpenWindow();
};
_escapeWindow.CreditsButton.OnPressed += _ =>
{
new CreditsWindow().Open();
};
// End DeltaV - More info in Escape Menu
// Hide wiki button if we don't have a link for it.
_escapeWindow.WikiButton.Visible = _cfg.GetCVar(CCVars.InfoLinksWiki) != "";
@@ -7,23 +7,20 @@ public sealed partial class GunSystem
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
}
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not BoxesStatusControl boxes) return;
if (args.Control is not BoxesStatusControl boxes)
return;
boxes.Update(component.Shots, component.Capacity);
boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
}
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new BoxesStatusControl();
}
@@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
@@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo();
}
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
{
UpdateAmmoCount(uid, ammoComp);
}
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
@@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return;
@@ -0,0 +1,73 @@
using Content.Client._DV.ChronicPain.Overlays;
using Content.Shared._DV.ChronicPain.Components;
using Content.Shared._DV.ChronicPain.EntitySystems;
using Robust.Client.Graphics;
using Robust.Shared.Player;
namespace Content.Client._DV.ChronicPain.EntitySystems;
public sealed class ChronicPainSystem : SharedChronicPainSystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
private ChronicPainOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
_overlay = new();
}
protected override void OnChronicPainInit(Entity<ChronicPainComponent> entity, ref ComponentInit args)
{
if (entity.Owner != _playerManager.LocalEntity)
return;
_overlayMan.AddOverlay(_overlay);
}
protected override void OnChronicPainShutdown(Entity<ChronicPainComponent> entity, ref ComponentShutdown args)
{
if (entity.Owner != _playerManager.LocalEntity)
return;
_overlayMan.RemoveOverlay(_overlay);
}
protected override void OnPlayerAttached(Entity<ChronicPainComponent> entity, ref LocalPlayerAttachedEvent args)
{
if (!IsChronicPainSuppressed((entity.Owner, entity.Comp)))
_overlayMan.AddOverlay(_overlay);
}
protected override void OnPlayerDetached(Entity<ChronicPainComponent> entity, ref LocalPlayerDetachedEvent args)
{
_overlayMan.RemoveOverlay(_overlay);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Handle showing/hiding overlay based on suppression status
if (_playerManager.LocalEntity is not { } player)
return;
if (!TryComp<ChronicPainComponent>(player, out var comp))
return;
var isSuppressed = IsChronicPainSuppressed((player, comp));
if (isSuppressed && _overlayMan.TryGetOverlay<ChronicPainOverlay>(out var overlay))
_overlayMan.RemoveOverlay(overlay);
if (isSuppressed) // If its suppressed and we don't have an overlay, just return
return;
if (!isSuppressed && !_overlayMan.HasOverlay<ChronicPainOverlay>())
_overlayMan.AddOverlay(_overlay);
}
}
@@ -1,36 +1,31 @@
using System.Numerics;
using Content.Shared._DV.Pain;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Overlays;
namespace Content.Client._DV.ChronicPain.Overlays;
public sealed partial class PainOverlay : Overlay
public sealed partial class ChronicPainOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IEntityManager _entity = default!;
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _painShader;
private readonly ProtoId<ShaderPrototype> _shaderProto = "ChromaticAberration";
public PainOverlay()
public ChronicPainOverlay()
{
IoCManager.InjectDependencies(this);
_painShader = _prototype.Index(_shaderProto).Instance().Duplicate();
_painShader = _prototype.Index(_shaderProto).InstanceUnique();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (_player.LocalEntity is not { Valid: true } player
|| !_entity.HasComponent<PainComponent>(player))
{
if (_player.LocalEntity is not { Valid: true })
return false;
}
return base.BeforeDraw(in args);
}
@@ -0,0 +1,77 @@
using Content.Client.NetworkConfigurator.Systems;
using Content.Shared.DeviceNetwork.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Client._DV.NetworkConfigurator;
/// <summary>
/// Mapping version of <see cref="NetworkConfiguratorLinkOverlay"> that shows the linkages
/// as we move around the map, rather than using component markers.
/// Using markers means it only applies to objects the user is currently in PVS range, but this
/// version continually searches for device networks to draw.
/// </summary>
public sealed class MappingNetworkConfiguratorLinkOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private readonly DeviceListSystem _deviceListSystem;
private readonly SharedTransformSystem _transformSystem;
public Dictionary<EntityUid, Color> Colors = [];
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public MappingNetworkConfiguratorLinkOverlay()
{
IoCManager.InjectDependencies(this);
_deviceListSystem = _entityManager.System<DeviceListSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
}
protected override void Draw(in OverlayDrawArgs args)
{
var query = _entityManager.AllEntityQueryEnumerator<DeviceNetworkComponent>();
while (query.MoveNext(out var uid, out _))
{
if (_entityManager.Deleted(uid) || !_entityManager.TryGetComponent(uid, out DeviceListComponent? deviceList))
continue; // Deleted or doesn't have any device linkages
var sourceTransform = _entityManager.GetComponent<TransformComponent>(uid);
if (sourceTransform.MapID == MapId.Nullspace)
{
// Can happen if the item is outside the client's view. In that case,
// we don't have a sensible transform to draw, so we need to skip it.
continue;
}
if (!Colors.TryGetValue(uid, out var color))
{
color = new Color(
_random.NextByte(0, 255),
_random.NextByte(0, 255),
_random.NextByte(0, 255));
Colors.Add(uid, color);
}
foreach (var device in _deviceListSystem.GetAllDevices(uid, deviceList))
{
if (_entityManager.Deleted(device))
{
continue;
}
var linkTransform = _entityManager.GetComponent<TransformComponent>(device);
if (linkTransform.MapID == MapId.Nullspace)
{
continue;
}
args.WorldHandle.DrawLine(_transformSystem.GetWorldPosition(sourceTransform), _transformSystem.GetWorldPosition(linkTransform), color);
}
}
}
}
@@ -0,0 +1,20 @@
using Robust.Client.Graphics;
using Robust.Shared.Console;
namespace Content.Client._DV.NetworkConfigurator;
public sealed class ToggleNetworkLinksCommand : LocalizedEntityCommands
{
[Dependency] private readonly IOverlayManager _overlay = default!;
public override string Command => "togglenetworklinks";
public override string Description => Loc.GetString("cmd-togglenetworklinks-desc");
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (_overlay.RemoveOverlay<MappingNetworkConfiguratorLinkOverlay>())
return;
_overlay.AddOverlay(new MappingNetworkConfiguratorLinkOverlay());
}
}
@@ -1,21 +1,18 @@
<tabs:DeltaTab xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:tabs="clr-namespace:Content.Client._DV.Options.UI.Tabs"
xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:optionsUI="clr-namespace:Content.Client.Options.UI"
xmlns:s="clr-namespace:Content.Client.Stylesheets"
xmlns:ui="clr-namespace:Content.Client.Options.UI"
xmlns:dvui="clr-namespace:Content.Client._DV.Options.UI">
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<Label Text="{Loc 'ui-options-general-forknotice'}"
FontColorOverride="{xNamespace:Static s:StyleNano.ConcerningOrangeFore}"
StyleClasses="LabelKeyText"/>
<Label Text="{Loc 'ui-options-general-accessibility'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="DisableFiltersCheckBox" Text="{Loc 'ui-options-no-filters'}" />
<CheckBox Name="DisableGlimmerEffectCheckBox" Text="{Loc 'ui-options-disable-glimmer-effect'}" />
<Label Text="{Loc 'ui-options-delta-tips'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="DisableTipsCheckBox" Text="{Loc 'ui-options-delta-disable-tips'}" />
<Button Name="ResetTipsButton" Text="{Loc 'ui-options-delta-reset-tips'}" MaxWidth="200" Margin="0 4 0 0" />
</BoxContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
@@ -1,5 +1,5 @@
using Content.Shared._DV.CCVars;
using Content.Client.Options.UI;
using Content.Shared._DV.Tips;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
@@ -9,6 +9,8 @@ namespace Content.Client._DV.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class DeltaTab : Control
{
[Dependency] private readonly IEntityNetworkManager _entNet = default!;
public DeltaTab()
{
RobustXamlLoader.Load(this);
@@ -16,9 +18,15 @@ public sealed partial class DeltaTab : Control
Control.AddOptionCheckBox(DCCVars.NoVisionFilters, DisableFiltersCheckBox);
Control.AddOptionCheckBox(DCCVars.DisableGlimmerShader, DisableGlimmerEffectCheckBox);
//Control.AddOptionCheckBox(DCCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox); // Migrating to the new system
//Control.AddOptionColorSlider(DCCVars.ChatHighlightsColor, HighlightsColorSlider); // Migrating to the new system
Control.AddOptionCheckBox(DCCVars.DisableTips, DisableTipsCheckBox);
ResetTipsButton.OnPressed += _ => OnResetTipsPressed();
Control.Initialize();
}
private void OnResetTipsPressed()
{
_entNet.SendSystemNetworkMessage(new ResetAllSeenTipsRequest());
}
}
-65
View File
@@ -1,65 +0,0 @@
using Content.Shared._DV.Pain;
using Robust.Client.Graphics;
using Robust.Shared.Player;
namespace Content.Client._DV.Overlays;
public sealed partial class PainSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly ISharedPlayerManager _playerMan = default!;
private PainOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PainComponent, ComponentInit>(OnPainInit);
SubscribeLocalEvent<PainComponent, ComponentShutdown>(OnPainShutdown);
SubscribeLocalEvent<PainComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<PainComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_overlay = new();
}
private void OnPainInit(Entity<PainComponent> ent, ref ComponentInit args)
{
if (ent.Owner == _playerMan.LocalEntity && !ent.Comp.Suppressed)
_overlayMan.AddOverlay(_overlay);
}
private void OnPainShutdown(Entity<PainComponent> ent, ref ComponentShutdown args)
{
if (ent.Owner == _playerMan.LocalEntity)
_overlayMan.RemoveOverlay(_overlay);
}
private void OnPlayerAttached(Entity<PainComponent> ent, ref LocalPlayerAttachedEvent args)
{
if (!ent.Comp.Suppressed)
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(Entity<PainComponent> ent, ref LocalPlayerDetachedEvent args)
{
_overlayMan.RemoveOverlay(_overlay);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Handle showing/hiding overlay based on suppression status
if (_playerMan.LocalEntity is not { } player)
return;
if (!TryComp<PainComponent>(player, out var comp))
return;
if (comp.Suppressed && _overlayMan.HasOverlay<PainOverlay>())
_overlayMan.RemoveOverlay(_overlay);
else if (!comp.Suppressed && !_overlayMan.HasOverlay<PainOverlay>())
_overlayMan.AddOverlay(_overlay);
}
}
-5
View File
@@ -1,5 +0,0 @@
using Content.Shared._DV.Pain;
namespace Content.Client._DV.Pain;
public sealed class PainSystem : SharedPainSystem;
@@ -19,10 +19,12 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
public event Action<string>? OnPurchased;
private readonly List<VesselPrototype> _vessels = [];
private readonly List<string> _categories = [];
private readonly List<VesselCategoryPrototype> _categories = [];
public Entity<ShipyardConsoleComponent> Console;
private string? _category;
// The currently selected category
private ProtoId<VesselCategoryPrototype>? _category;
public ShipyardConsoleMenu(EntityUid console, IPrototypeManager proto, IEntityManager entMan, IPlayerManager player, AccessReaderSystem access, EntityWhitelistSystem whitelist)
{
@@ -33,46 +35,44 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
_access = access;
_player = player;
_categories.Clear();
foreach (var vesselCategoryProto in Console.Comp.Categories)
{
if (proto.Resolve(vesselCategoryProto, out var vesselCategory))
_categories.Add(vesselCategory);
}
// don't include ships that aren't allowed by whitelist, server won't accept them anyway
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
if (whitelist.IsWhitelistPassOrNull(vessel.Whitelist, console))
_vessels.Add(vessel);
}
_vessels.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase));
// only list categories in said ships
foreach (var vessel in _vessels)
{
foreach (var category in vessel.Categories)
{
if (!_categories.Contains(category))
_categories.Add(category);
}
}
_categories.Sort();
// inserting here and not adding at the start so it doesn't get affected by sort
_categories.Insert(0, Loc.GetString("cargo-console-menu-populate-categories-all-text"));
PopulateCategories();
PopulateCategories(Console);
SearchBar.OnTextChanged += _ => PopulateProducts();
SearchBar.OnTextChanged += _ => PopulateProducts(Console);
Categories.OnItemSelected += args =>
{
_category = args.Id == 0 ? null : _categories[args.Id];
_category = _categories[args.Id];
Categories.SelectId(args.Id);
PopulateProducts();
PopulateProducts(Console);
};
PopulateProducts(Console);
}
/// <summary>
/// Populates the list of products that will actually be shown, using the current filters.
/// </summary>
private void PopulateProducts()
private void PopulateProducts(Entity<ShipyardConsoleComponent> entity)
{
Vessels.RemoveAllChildren();
var access = _player.LocalSession?.AttachedEntity is {} player
var access = _player.LocalSession?.AttachedEntity is { } player
&& _access.IsAllowed(player, Console);
var search = SearchBar.Text.Trim().ToLowerInvariant();
@@ -80,10 +80,11 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
{
if (search.Length != 0 && !vessel.Name.Contains(search, StringComparison.InvariantCultureIgnoreCase))
continue;
if (_category != null && !vessel.Categories.Contains(_category))
if (_category != null && !vessel.Categories.Contains(_category.Value.Id))
continue;
var vesselEntry = new VesselRow(vessel, access);
var vesselEntry = new VesselRow(vessel, access, isFree: !entity.Comp.UseStationFunds);
vesselEntry.OnPurchasePressed += () => OnPurchased?.Invoke(vessel.ID);
Vessels.AddChild(vesselEntry);
}
@@ -92,18 +93,29 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
/// <summary>
/// Populates the list categories that will actually be shown, using the current filters.
/// </summary>
private void PopulateCategories()
private void PopulateCategories(Entity<ShipyardConsoleComponent> entity)
{
Categories.Clear();
foreach (var category in _categories)
// Guh, there's gotta be an easier way to select a category by default...
var selectedId = 0; // Default to the first category. May get overridden later.
for (var i = 0; i < _categories.Count; i++)
{
Categories.AddItem(category);
Categories.AddItem(_categories[i].LocalizedName, i);
if (entity.Comp.DefaultCategory.HasValue && entity.Comp.DefaultCategory.Value.Id.Equals(_categories[i].ID, StringComparison.CurrentCultureIgnoreCase))
selectedId = i;
}
if (selectedId >= 0 && selectedId < _categories.Count)
{
_category = _categories[selectedId];
Categories.SelectId(selectedId);
}
}
public void UpdateState(ShipyardConsoleState state)
{
BankAccountLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", state.Balance.ToString()));
PopulateProducts();
PopulateProducts(Console);
}
}
@@ -12,7 +12,7 @@ public sealed partial class VesselRow : PanelContainer
{
public event Action? OnPurchasePressed;
public VesselRow(VesselPrototype vessel, bool access)
public VesselRow(VesselPrototype vessel, bool access = false, bool isFree = false)
{
RobustXamlLoader.Load(this);
@@ -24,6 +24,9 @@ public sealed partial class VesselRow : PanelContainer
Purchase.Disabled = !access;
Purchase.OnPressed += _ => OnPurchasePressed?.Invoke();
Price.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
if (isFree)
Purchase.Text = Loc.GetString("shipyard-console-price-free");
else
Purchase.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
}
}
+126
View File
@@ -0,0 +1,126 @@
using Content.Client._DV.Tips.UI;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.Tips;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Tips;
/// <summary>
/// Client-side system that receives tip events from the server and displays the tip popup UI.
/// </summary>
public sealed class TipSystem : SharedTipSystem
{
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
/// <summary>
/// Queue of tips waiting to be shown. Only one tip is displayed at a time.
/// </summary>
private readonly Queue<ShowTipEvent> _tipQueue = new();
/// <summary>
/// Currently displayed tip popup, if any.
/// </summary>
private TipPopup? _currentPopup;
/// <summary>
/// The tip ID of the currently displayed popup, used for dismiss events.
/// </summary>
private ProtoId<TipPrototype>? _currentTipId;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<ShowTipEvent>(OnShowTip);
}
private void OnShowTip(ShowTipEvent ev)
{
// Return if tips are disabled globally
if (_cfg.GetCVar(DCCVars.DisableTipsGlobal))
return;
// Return if tips are disabled locally and IgnoreCvar is the default value
if (_cfg.GetCVar(DCCVars.DisableTips) && !ev.IgnoreCvar)
return;
_tipQueue.Enqueue(ev);
TryShowNextTip();
}
private void TryShowNextTip()
{
while (true)
{
if (_currentPopup != null)
return;
if (_tipQueue.Count == 0)
return;
var tipEvent = _tipQueue.Dequeue();
if (!Prototype.TryIndex(tipEvent.TipId, out var tipProto))
{
Log.Warning($"Received tip event for unknown prototype: {tipEvent.TipId}");
continue;
}
ShowTipPopup(tipProto, tipEvent);
break;
}
}
private void ShowTipPopup(TipPrototype tip, ShowTipEvent ev)
{
_currentPopup = new TipPopup(tip);
_currentPopup.OnClose += OnPopupClosed;
_currentTipId = tip.ID;
_ui.RootControl.AddChild(_currentPopup);
// Play the sound
_audio.PlayGlobal(ev.Sound, Filter.Local(), false);
}
private void OnPopupClosed(bool dontShowAgain)
{
if (_currentPopup != null && _currentTipId != null)
{
// Send dismiss event to server
RaiseNetworkEvent(new TipDismissedEvent(_currentTipId.Value, dontShowAgain));
_currentPopup.OnClose -= OnPopupClosed;
_currentPopup?.Close();
_currentPopup = null;
_currentTipId = null;
}
TryShowNextTip();
}
/// <summary>
/// Closes the current tip popup if one is open.
/// </summary>
[PublicAPI]
public void CloseCurrentTip()
{
_currentPopup?.Close();
}
/// <summary>
/// Clears all queued tips without showing them.
/// </summary>
[PublicAPI]
public void ClearTipQueue()
{
_tipQueue.Clear();
}
}
+41
View File
@@ -0,0 +1,41 @@
<Control xmlns="https://spacestation14.io"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="80 100 0 0">
<PanelContainer Name="BackgroundPanel"
MinWidth="320"
MaxWidth="420">
<BoxContainer Orientation="Vertical"
Margin="15">
<!-- Title at the top, centered -->
<Label Name="TitleLabel"
StyleClasses="LabelBig"
HorizontalAlignment="Center"
Margin="0 0 0 10" />
<!-- Description in a flexible vertical container -->
<PanelContainer StyleClasses="BackgroundDark"
Margin="0 0 0 12">
<BoxContainer Orientation="Vertical"
Margin="10">
<RichTextLabel Name="DescriptionLabel"
HorizontalAlignment="Left"
VerticalExpand="True" />
</BoxContainer>
</PanelContainer>
<!-- Bottom controls -->
<BoxContainer Orientation="Vertical"
HorizontalAlignment="Center">
<CheckBox Name="DontShowAgainCheckBox"
Text="{Loc 'tip-popup-dont-show-again'}"
HorizontalAlignment="Center"
Margin="0 0 0 8" />
<Button Name="OkayButton"
Text="{Loc 'tip-popup-okay-button'}"
HorizontalAlignment="Center"
MinWidth="100" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</Control>
@@ -0,0 +1,98 @@
using Content.Client.Guidebook.Richtext;
using Content.Shared._DV.Tips;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client._DV.Tips.UI;
/// <summary>
/// Popup control that displays a tip to the player.
/// Appears in the top-left corner with a semi-transparent background.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class TipPopup : Control
{
/// <summary>
/// Event raised when the popup is closed. The bool parameter indicates
/// whether the "Don't show again" checkbox was checked.
/// </summary>
public event Action<bool>? OnClose;
private readonly TipPrototype _tip;
/// <summary>
/// Whether the "Don't show again" checkbox is checked.
/// </summary>
public bool DontShowAgain => DontShowAgainCheckBox.Pressed;
public TipPopup(TipPrototype tip)
{
RobustXamlLoader.Load(this);
_tip = tip;
InitializeUI();
InitializeEvents();
}
private void InitializeUI()
{
TitleLabel.Text = Loc.GetString(_tip.Title);
var description = Loc.GetString(_tip.Description);
DescriptionLabel.SetMessage(FormattedMessage.FromMarkupPermissive(description),
tagsAllowed:
[
typeof(ItalicTag),
typeof(ColorTag),
typeof(BoldTag),
typeof(BoldItalicTag),
typeof(KeyBindTag),
]);
ApplyTransparentStyle();
}
private void ApplyTransparentStyle()
{
var styleBox = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#1a1a1a").WithAlpha(0.85f),
BorderColor = Color.FromHex("#3a3a3a"),
BorderThickness = new Thickness(1),
ContentMarginLeftOverride = 0,
ContentMarginRightOverride = 0,
ContentMarginTopOverride = 0,
ContentMarginBottomOverride = 0,
};
BackgroundPanel.PanelOverride = styleBox;
}
private void InitializeEvents()
{
OkayButton.OnPressed += OnOkayPressed;
}
private void OnOkayPressed(BaseButton.ButtonEventArgs args)
{
Close();
}
public void Close()
{
if (Parent == null)
{
return;
}
var dontShowAgain = DontShowAgain;
Parent.RemoveChild(this);
OnClose?.Invoke(dontShowAgain);
}
}
@@ -0,0 +1,51 @@
using Content.Client._DV.Traits.UI;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.Traits;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Traits;
/// <summary>
/// Client system that shows a popup when traits are disabled due to unmet conditions.
/// </summary>
public sealed class DisabledTraitsPopupSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
private DisabledTraitsPopup? _window;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<DisabledTraitsEvent>(OnDisabledTraits);
}
private void OnDisabledTraits(DisabledTraitsEvent ev)
{
// Don't show if user has opted to skip this popup
if (_cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup))
return;
// Don't show if no traits were actually disabled
if (ev.DisabledTraits.Count == 0)
return;
OpenDisabledTraitsPopup(ev.DisabledTraits);
}
private void OpenDisabledTraitsPopup(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{
// Close existing window if one is open
if (_window != null)
{
_window.Close();
_window = null;
}
_window = new DisabledTraitsPopup(disabledTraits);
_window.OpenCentered();
_window.OnClose += () => _window = null;
}
}
@@ -0,0 +1,48 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc disabled-traits-popup-title}"
MinSize="400 200"
SetSize="500 350">
<BoxContainer Orientation="Vertical" Margin="10">
<!-- Header -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 10">
<TextureRect TexturePath="/Textures/Interface/info.svg.192dpi.png"
TextureScale="0.5 0.5"
VerticalAlignment="Center"
Margin="0 0 10 0" />
<Label Name="TitleLabel"
StyleClasses="LabelHeading"
VAlign="Center" />
</BoxContainer>
<!-- Message -->
<RichTextLabel Name="MessageLabel"
Margin="0 0 0 15" />
<!-- List of disabled traits -->
<Label Text="{Loc disabled-traits-popup-list-header}"
StyleClasses="LabelSubText"
Margin="0 0 0 5" />
<PanelContainer StyleClasses="BackgroundDark" VerticalExpand="True">
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
<BoxContainer Name="DisabledTraitsContainer"
Orientation="Vertical"
Margin="5" />
</ScrollContainer>
</PanelContainer>
<!-- Footer with checkbox and close button -->
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<CheckBox Name="SkipCheckBox"
Text="{Loc disabled-traits-popup-skip-checkbox}"
HorizontalAlignment="Center" />
<Button Name="ButtonClose"
Text="{Loc disabled-traits-popup-close-button}"
HorizontalAlignment="Center"
Margin="0 10 0 0"
MinSize="120 0" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,100 @@
using Content.Client.UserInterface.Controls;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._DV.Traits.UI;
[GenerateTypedNameReferences]
public sealed partial class DisabledTraitsPopup : FancyWindow
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
private bool _initialSkipState;
public DisabledTraitsPopup(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
InitializeUI(disabledTraits);
InitializeEvents();
}
private void InitializeUI(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{
TitleLabel.Text = Loc.GetString("disabled-traits-popup-label");
MessageLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(
Loc.GetString("disabled-traits-popup-message")));
_initialSkipState = _cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup);
SkipCheckBox.Pressed = _initialSkipState;
PopulateDisabledTraits(disabledTraits);
}
private void PopulateDisabledTraits(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
{
DisabledTraitsContainer.RemoveAllChildren();
foreach (var (traitId, reasons) in disabledTraits)
{
if (!_prototype.TryIndex(traitId, out var trait))
continue;
var container = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Margin = new Thickness(0, 0, 0, 10)
};
// Trait name
var nameLabel = new Label
{
Text = Loc.GetString(trait.Name),
FontColorOverride = Color.FromHex("#ff6b6b"),
StyleClasses = { "LabelSubText" }
};
container.AddChild(nameLabel);
// Reasons why it was disabled
foreach (var reason in reasons)
{
var reasonLabel = new RichTextLabel
{
Margin = new Thickness(10, 2, 0, 0)
};
reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(reason));
container.AddChild(reasonLabel);
}
DisabledTraitsContainer.AddChild(container);
}
}
private void InitializeEvents()
{
OnClose += SaveSkipState;
ButtonClose.OnPressed += OnClosePressed;
}
private void SaveSkipState()
{
if (SkipCheckBox.Pressed == _initialSkipState)
return;
_cfg.SetCVar(DCCVars.SkipDisabledTraitsPopup, SkipCheckBox.Pressed);
_cfg.SaveToFile();
}
private void OnClosePressed(BaseButton.ButtonEventArgs args)
{
Close();
}
}
@@ -0,0 +1,50 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0 0 0 8">
<!-- Category Header -->
<PanelContainer Name="HeaderPanel" StyleClasses="TraitsCategoryHeader">
<Button Name="HeaderButton"
StyleClasses="TraitsCategoryHeaderButton"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" Margin="8 6">
<!-- Expand/Collapse Icon -->
<Label Name="ExpandIcon"
Text="▼"
StyleClasses="TraitsCategoryExpandIcon"
Margin="0 0 8 0"/>
<!-- Category Name -->
<Label Name="CategoryNameLabel"
StyleClasses="TraitsCategoryNameLabel"/>
<Control HorizontalExpand="True"/>
<!-- Category Stats -->
<Label Name="CategoryStatsLabel"
StyleClasses="TraitsCategoryStatsLabel"
Margin="0 0 4 0"/>
<!-- Category Points (if applicable) -->
<Label Name="CategoryPointsLabel"
StyleClasses="TraitsCategoryPointsLabel"
Visible="False"/>
</BoxContainer>
</Button>
</PanelContainer>
<!-- Accent Bar -->
<PanelContainer Name="AccentBar"
StyleClasses="TraitsCategoryAccent"
SetHeight="3"
Margin="0 0 0 0"/>
<!-- Traits Container -->
<PanelContainer Name="ContentPanel" StyleClasses="TraitsCategoryContent">
<BoxContainer Name="TraitsContainer"
Orientation="Vertical"
HorizontalExpand="True"
Margin="8 8"/>
</PanelContainer>
</BoxContainer>
@@ -0,0 +1,192 @@
using System.Linq;
using Content.Shared._DV.Traits;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Traits.UI;
[GenerateTypedNameReferences]
public sealed partial class TraitCategory : BoxContainer
{
public event Action<ProtoId<TraitPrototype>, bool>? OnTraitToggled;
private readonly TraitCategoryPrototype _category;
private readonly List<TraitPrototype> _allTraits;
private readonly Dictionary<ProtoId<TraitPrototype>, TraitEntry> _traitEntries = new();
private bool _isExpanded;
public int SelectedCount;
public int PointsSpent;
public TraitCategory(TraitCategoryPrototype category, List<TraitPrototype> traits)
{
RobustXamlLoader.Load(this);
_category = category;
_allTraits = traits;
_isExpanded = category.DefaultExpanded;
CategoryNameLabel.Text = Loc.GetString(category.Name);
SetAccentColor(category.AccentColor);
HeaderButton.OnPressed += _ => ToggleExpanded();
PopulateTraits();
UpdateExpandedState();
UpdateStats();
}
private void SetAccentColor(Color color)
{
AccentBar.PanelOverride = new StyleBoxFlat { BackgroundColor = color }; // dumb stylesheet modulation workaround
}
private void PopulateTraits()
{
TraitsContainer.RemoveAllChildren();
_traitEntries.Clear();
foreach (var trait in _allTraits)
{
var entry = new TraitEntry(trait);
entry.OnToggled += selected => OnTraitEntryToggled(trait.ID, selected);
_traitEntries[trait.ID] = entry;
TraitsContainer.AddChild(entry);
}
}
private void OnTraitEntryToggled(ProtoId<TraitPrototype> traitId, bool selected)
{
OnTraitToggled?.Invoke(traitId, selected);
}
private void ToggleExpanded()
{
_isExpanded = !_isExpanded;
UpdateExpandedState();
}
private void UpdateExpandedState()
{
ContentPanel.Visible = _isExpanded;
ExpandIcon.Text = _isExpanded ? "▼" : "▶";
}
public void UpdateStats()
{
SelectedCount = _traitEntries.Values.Count(e => e.IsSelected);
PointsSpent = _traitEntries.Values
.Where(e => e.IsSelected)
.Sum(e => e.TraitCost);
if (_category.MaxTraits.HasValue)
{
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits",
("selected", SelectedCount),
("max", _category.MaxTraits.Value));
}
else
{
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits-unlimited",
("selected", SelectedCount));
}
if (_category.MaxPoints.HasValue)
{
CategoryPointsLabel.Visible = true;
CategoryPointsLabel.Text = Loc.GetString("trait-category-points",
("selected", PointsSpent),
("max", _category.MaxPoints.Value));
}
else
{
CategoryPointsLabel.Visible = false;
}
}
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
{
if (_traitEntries.TryGetValue(traitId, out var entry))
{
entry.SetSelected(selected);
}
}
public void ClearSelection()
{
foreach (var (_, entry) in _traitEntries)
{
entry.SetSelected(false);
}
SelectedCount = 0;
PointsSpent = 0;
UpdateStats();
}
/// <summary>
/// Gets the IDs of all currently selected traits in this category.
/// </summary>
public IEnumerable<ProtoId<TraitPrototype>> GetSelectedTraitIds()
{
return _traitEntries
.Where(kvp => kvp.Value.IsSelected)
.Select(kvp => kvp.Key);
}
/// <summary>
/// Filters traits based on search text only
/// </summary>
public void FilterTraits(string searchText)
{
var hasVisibleTraits = false;
foreach (var (traitId, entry) in _traitEntries)
{
var trait = _allTraits.First(t => t.ID == traitId);
var name = Loc.GetString(trait.Name);
var description = Loc.GetString(trait.Description);
var matchesSearch = string.IsNullOrEmpty(searchText) ||
name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
description.Contains(searchText, StringComparison.OrdinalIgnoreCase);
entry.Visible = matchesSearch;
if (entry.Visible)
hasVisibleTraits = true;
}
// Hide entire category if no traits match search
Visible = hasVisibleTraits;
}
/// <summary>
/// Updates condition states for all trait entries based on current job/species.
/// Traits that don't meet conditions are disabled but still visible.
/// </summary>
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{
foreach (var (_, entry) in _traitEntries)
{
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, traits);
}
// Update stats since some traits may have been deselected
UpdateStats();
}
/// <summary>
/// Checks if a trait in this category meets its conditions.
/// </summary>
public bool TraitMeetsConditions(ProtoId<TraitPrototype> traitId)
{
return _traitEntries.TryGetValue(traitId, out var entry) && entry.MeetsConditions;
}
}
@@ -0,0 +1,53 @@
<PanelContainer xmlns="https://spacestation14.io"
StyleClasses="TraitsEntryPanel"
HorizontalExpand="True"
Margin="0 0 0 4">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
Margin="10 8">
<!-- Toggle Container -->
<BoxContainer Name="ToggleContainer"
Orientation="Horizontal"
VerticalAlignment="Top"
Margin="0 2 10 0"
SetWidth="24">
<!-- Checkbox -->
<CheckBox Name="TraitCheckbox"
StyleClasses="TraitsEntryCheckbox"
VerticalAlignment="Center"/>
<!-- Lock Icon -->
<TextureRect Name="LockIcon"
VerticalAlignment="Center"
HorizontalAlignment="Center"
SetSize="20 20"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png"
Visible="False"/>
</BoxContainer>
<!-- Trait Info -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalAlignment="Center"
Name="TraitInfo">
<!-- Name and Cost Row -->
<BoxContainer Orientation="Horizontal">
<Label Name="TraitNameLabel"
StyleClasses="TraitsEntryNameLabel"/>
<Control HorizontalExpand="True"/>
<Label Name="TraitCostLabel"
StyleClasses="TraitsEntryCostLabel"/>
</BoxContainer>
<!-- Description -->
<RichTextLabel Name="TraitDescriptionLabel"
StyleClasses="TraitsEntryDescriptionLabel"
Margin="0 4 0 0"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
@@ -0,0 +1,266 @@
using System.Text;
using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
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._DV.Traits.UI;
[GenerateTypedNameReferences]
public sealed partial class TraitEntry : PanelContainer
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
public event Action<bool>? OnToggled;
public bool IsSelected => TraitCheckbox.Pressed;
public readonly int TraitCost;
public bool MeetsConditions { get; private set; } = true;
private readonly TraitPrototype _trait;
private bool _isUpdating;
public TraitEntry(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_trait = trait;
TraitCost = trait.Cost;
// Enable mouse events so tooltips work
MouseFilter = MouseFilterMode.Pass;
TraitNameLabel.Text = Loc.GetString(trait.Name);
TraitDescriptionLabel.SetMessage(Loc.GetString(trait.Description));
// Format cost display
var costPrefix = trait.Cost > 0 ? "+" : "";
var costColor = trait.Cost > 0 ? "#ff6b6b" : trait.Cost < 0 ? "#6bff6b" : "#888888";
TraitCostLabel.Text = $"{costPrefix}{trait.Cost}";
TraitCostLabel.ModulateSelfOverride = Color.FromHex(costColor);
TraitCheckbox.OnToggled += OnCheckboxToggled;
// Build condition tooltips
UpdateConditionTooltips();
}
private void UpdateConditionTooltips()
{
var conditionsTooltip = new StringBuilder();
foreach (var condition in _trait.Conditions)
{
conditionsTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
}
if (conditionsTooltip.Length > 0)
{
var tooltip = Loc.GetString("trait-conditions-tooltip", ("conditions", conditionsTooltip.ToString())).ToString().Trim();
var markupTooltip = CreateMarkupTooltip(tooltip);
TooltipSupplier = _ => markupTooltip;
}
else
{
TooltipSupplier = null;
}
}
/// <summary>
/// Creates a tooltip control that properly parses markup.
/// </summary>
private static Tooltip CreateMarkupTooltip(string markupText)
{
var tooltip = new Tooltip();
// Parse the markup into a FormattedMessage
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(markupText));
return tooltip;
}
/// <summary>
/// Updates whether conditions are met based on current job/species.
/// </summary>
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
{
var result = condition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, traits),
_ => true,
};
// Apply inversion
result ^= condition.Invert;
if (!result)
{
MeetsConditions = false;
}
}
UpdateDisabledState();
}
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
{
if (!_prototype.TryIndex(speciesId, out var species))
return false;
return species == condition.Species;
}
private bool CheckJobCondition(HasJobCondition condition, ProtoId<JobPrototype>? jobId)
{
if (!_prototype.TryIndex(jobId, out var job))
return false;
return job == condition.Job;
}
private bool CheckDepartmentCondition(InDepartmentCondition condition, ProtoId<JobPrototype>? jobId)
{
if (!_prototype.TryIndex(jobId, out var job))
return false;
if (!_prototype.TryIndex(condition.Department, out var department))
return false;
return department.Roles.Contains(job);
}
private bool CheckAntagEligibleCondition(IsAntagEligibleCondition condition, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
{
if (antagPreferences is null)
return false;
return antagPreferences.Contains(condition.Antag);
}
private bool CheckHasTraitCondition(HasTraitCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{
if (traits is null)
return false;
return traits.Contains(condition.Trait);
}
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
{
if (condition.Conditions.Count == 0)
return false;
// Return true if ANY child condition evaluates to true
foreach (var childCondition in condition.Conditions)
{
var result = childCondition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, traits), // Recursive!
_ => true,
};
// Apply child's inversion
result ^= childCondition.Invert;
// If any child passes, the AnyOf passes
if (result)
return true;
}
// None of the children passed
return false;
}
private void UpdateDisabledState()
{
if (!MeetsConditions)
{
// Hide checkbox, show lock icon
TraitCheckbox.Visible = false;
LockIcon.Visible = true;
// Deselect if conditions no longer met
if (TraitCheckbox.Pressed)
{
_isUpdating = true;
TraitCheckbox.Pressed = false;
UpdateSelectedStyle();
_isUpdating = false;
OnToggled?.Invoke(false);
}
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
}
else
{
// Show checkbox, hide lock icon
TraitCheckbox.Visible = true;
LockIcon.Visible = false;
// Remove disabled styling - stylesheet restores normal colors
RemoveStyleClass("TraitsEntryDisabled");
// Reset to normal tooltips
UpdateConditionTooltips();
}
}
private void OnCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
{
if (_isUpdating)
return;
if (!MeetsConditions)
{
// This shouldn't happen since checkbox is hidden, but just in case
_isUpdating = true;
TraitCheckbox.Pressed = false;
_isUpdating = false;
return;
}
UpdateSelectedStyle();
OnToggled?.Invoke(args.Pressed);
}
public void SetSelected(bool selected)
{
_isUpdating = true;
TraitCheckbox.Pressed = selected && MeetsConditions;
UpdateSelectedStyle();
_isUpdating = false;
}
private void UpdateSelectedStyle()
{
if (TraitCheckbox.Pressed)
AddStyleClass("TraitsEntrySelected");
else
RemoveStyleClass("TraitsEntrySelected");
}
}
@@ -0,0 +1,237 @@
using Content.Client.Stylesheets;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Client.Stylesheets.StylesheetHelpers;
namespace Content.Client._DV.Traits.UI;
[CommonSheetlet]
public sealed class TraitsSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
{
public override StyleRule[] GetRules(T sheet, object config)
{
// Color palette
// sorry but the default ColorPalette just sucks in terms of ligher/darker colors
var bgDark = Color.FromHex("#1a1a22");
var bgMedium = Color.FromHex("#22222a");
var bgLight = Color.FromHex("#2a2a35");
var bgLighter = Color.FromHex("#32323e");
var textPrimary = Color.FromHex("#e0e0e0");
var textSecondary = Color.FromHex("#a0a0a0");
var textMuted = Color.FromHex("#707070");
var accentGreen = Color.FromHex("#4ade80");
var accentYellow = Color.FromHex("#fbbf24");
var accentRed = Color.FromHex("#f87171");
var accentBlue = Color.FromHex("#60a5fa");
// StyleBoxes
var headerPanelBox = new StyleBoxFlat
{
BackgroundColor = bgLight,
BorderColor = bgLighter,
BorderThickness = new Thickness(0, 0, 0, 1)
};
headerPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var searchBarBox = new StyleBoxFlat { BackgroundColor = bgMedium };
searchBarBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var searchInputBox = new StyleBoxFlat
{
BackgroundColor = bgDark,
ContentMarginLeftOverride = 8,
ContentMarginRightOverride = 8
};
var footerPanelBox = new StyleBoxFlat
{
BackgroundColor = bgMedium,
BorderColor = bgLighter,
BorderThickness = new Thickness(0, 1, 0, 0)
};
var categoryHeaderBox = new StyleBoxFlat { BackgroundColor = bgLight };
categoryHeaderBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var categoryHeaderButtonBox = new StyleBoxFlat { BackgroundColor = Color.Transparent };
categoryHeaderButtonBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var categoryContentBox = new StyleBoxFlat { BackgroundColor = bgMedium };
var categoryAccentBox = new StyleBoxFlat { BackgroundColor = accentBlue };
var entryPanelBox = new StyleBoxFlat
{
BackgroundColor = bgLight,
BorderColor = bgLighter,
BorderThickness = new Thickness(1)
};
entryPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var entrySelectedBox = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#2a3a4a"),
BorderColor = accentBlue,
BorderThickness = new Thickness(1, 1, 1, 1)
};
entrySelectedBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var entryDisabledBox = new StyleBoxFlat
{
BackgroundColor = Color.FromHex("#1a1a22"),
BorderColor = Color.FromHex("#2a2a2a"),
BorderThickness = new Thickness(1)
};
entryDisabledBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
var progressBarBgBox = new StyleBoxFlat
{
BackgroundColor = bgDark,
BorderColor = bgLighter,
BorderThickness = new Thickness(1)
};
var progressBarFillFull = new StyleBoxFlat { BackgroundColor = accentGreen };
var progressBarFillPartial = new StyleBoxFlat { BackgroundColor = accentYellow };
var progressBarFillLow = new StyleBoxFlat { BackgroundColor = accentRed };
var progressBarFillEmpty = new StyleBoxFlat { BackgroundColor = bgDark };
var rules = new List<StyleRule>
{
// ===== HEADER PANEL =====
E<PanelContainer>()
.Class("TraitsHeaderPanel")
.Panel(headerPanelBox),
E<Label>()
.Class("TraitsTitleLabel")
.Font(sheet.BaseFont.GetFont(14))
.FontColor(textPrimary),
E<Label>()
.Class("TraitsSubtitleLabel")
.Font(sheet.BaseFont.GetFont(11))
.FontColor(textSecondary),
E<Label>()
.Class("TraitsStatLabel")
.Font(sheet.BaseFont.GetFont(12))
.FontColor(accentBlue),
// ===== PROGRESS BAR =====
E<PanelContainer>()
.Class("TraitsProgressBarBg")
.Panel(progressBarBgBox),
E<PanelContainer>()
.Class("TraitsProgressBarFill")
.Panel(progressBarFillFull),
E<PanelContainer>()
.Class("TraitsProgressBarFull")
.Panel(progressBarFillFull),
E<PanelContainer>()
.Class("TraitsProgressBarPartial")
.Panel(progressBarFillPartial),
E<PanelContainer>()
.Class("TraitsProgressBarLow")
.Panel(progressBarFillLow),
E<PanelContainer>()
.Class("TraitsProgressBarEmpty")
.Panel(progressBarFillEmpty),
// ===== SEARCH BAR =====
E<PanelContainer>()
.Class("TraitsSearchBar")
.Panel(searchBarBox),
E<LineEdit>()
.Class("TraitsSearchInput")
.Prop(LineEdit.StylePropertyStyleBox, searchInputBox),
// ===== FOOTER =====
E<PanelContainer>()
.Class("TraitsFooterPanel")
.Panel(footerPanelBox),
E<Label>()
.Class("TraitsFooterText")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(textMuted),
// ===== CATEGORY HEADER =====
E<PanelContainer>()
.Class("TraitsCategoryHeader")
.Panel(categoryHeaderBox),
E<Button>()
.Class("TraitsCategoryHeaderButton")
.Prop(ContainerButton.StylePropertyStyleBox, categoryHeaderButtonBox),
E<Label>()
.Class("TraitsCategoryExpandIcon")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(textSecondary),
E<Label>()
.Class("TraitsCategoryNameLabel")
.Font(sheet.BaseFont.GetFont(12))
.FontColor(textPrimary),
E<Label>()
.Class("TraitsCategoryStatsLabel")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(textSecondary),
E<Label>()
.Class("TraitsCategoryPointsLabel")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(textMuted),
// ===== CATEGORY ACCENT =====
E<PanelContainer>()
.Class("TraitsCategoryAccent")
.Panel(categoryAccentBox),
// ===== CATEGORY CONTENT =====
E<PanelContainer>()
.Class("TraitsCategoryContent")
.Panel(categoryContentBox),
// ===== TRAIT ENTRY =====
E<PanelContainer>()
.Class("TraitsEntryPanel")
.Panel(entryPanelBox),
E<PanelContainer>()
.Class("TraitsEntryPanel", "TraitsEntrySelected")
.Panel(entrySelectedBox),
// Disabled entry styling
E<PanelContainer>()
.Class("TraitsEntryPanel", "TraitsEntryDisabled")
.Panel(entryDisabledBox)
.Modulate(new Color(1f, 1f, 1f, 0.5f)),
E<Label>()
.Class("TraitsEntryNameLabel")
.Font(sheet.BaseFont.GetFont(11))
.FontColor(textPrimary),
E<Label>()
.Class("TraitsEntryCostLabel")
.Font(sheet.BaseFont.GetFont(11)),
E<RichTextLabel>()
.Class("TraitsEntryDescriptionLabel")
.Font(sheet.BaseFont.GetFont(10))
.FontColor(textSecondary),
};
return rules.ToArray();
}
}
@@ -0,0 +1,68 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Header Panel with Global Stats -->
<PanelContainer StyleClasses="TraitsHeaderPanel" Margin="0 0 0 8">
<BoxContainer Orientation="Vertical" Margin="12 10">
<!-- Title Row -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 8">
<Label Text="{Loc 'trait-editor-title'}"
StyleClasses="TraitsTitleLabel"/>
<Control HorizontalExpand="True"/>
<Label Name="GlobalTraitCountLabel"
StyleClasses="TraitsStatLabel"/>
</BoxContainer>
<!-- Points Progress Bar -->
<BoxContainer Orientation="Vertical" Margin="0 4 0 0">
<BoxContainer Orientation="Horizontal" Margin="0 0 0 4">
<Label Text="{Loc 'trait-editor-points-label'}"
StyleClasses="TraitsSubtitleLabel"/>
<Control HorizontalExpand="True"/>
<Label Name="GlobalPointsLabel"
StyleClasses="TraitsStatLabel"/>
</BoxContainer>
<!-- Progress Bar Container -->
<PanelContainer StyleClasses="TraitsProgressBarBg" SetHeight="12">
<PanelContainer Name="GlobalPointsBar"
StyleClasses="TraitsProgressBarFill"
HorizontalAlignment="Left"/>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</PanelContainer>
<!-- Search Bar -->
<PanelContainer StyleClasses="TraitsSearchBar" Margin="0 0 0 8">
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'trait-editor-search-placeholder'}"
HorizontalExpand="True"
StyleClasses="TraitsSearchInput"
Margin="10 8"/>
</PanelContainer>
<!-- Categories Container (Scrollable) -->
<ScrollContainer VerticalExpand="True"
HScrollEnabled="False">
<BoxContainer Name="CategoriesContainer"
Orientation="Vertical"
HorizontalExpand="True"
Margin="0 0 4 0"/>
</ScrollContainer>
<!-- Footer Info -->
<PanelContainer StyleClasses="TraitsFooterPanel" Margin="0 8 0 0">
<BoxContainer Orientation="Horizontal" Margin="10 6">
<Label Name="FooterHintLabel"
Text="{Loc 'trait-editor-footer-hint'}"
StyleClasses="TraitsFooterText"/>
<Control HorizontalExpand="True"/>
<Label Name="FooterInfoLabel"
Text="{Loc 'trait-editor-footer-info'}"
StyleClasses="TraitsFooterText"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
@@ -0,0 +1,355 @@
using System.Linq;
using Content.Shared._DV.CCVars;
using Content.Shared._DV.Traits;
using Content.Shared.Preferences;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Traits.UI;
[GenerateTypedNameReferences]
public sealed partial class TraitsTab : BoxContainer
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
/// <summary>
/// Event fired when trait selection changes.
/// </summary>
public event Action<HashSet<ProtoId<TraitPrototype>>>? OnTraitsChanged;
private readonly Dictionary<ProtoId<TraitCategoryPrototype>, TraitCategory> _categoryUis = new();
private readonly HashSet<ProtoId<TraitPrototype>> _selectedTraits = new();
private int _maxGlobalTraits;
private int _maxGlobalPoints;
private int _currentTraitCount;
private int _currentPointsSpent;
private string _currentSearchText = string.Empty;
private bool _awaitingLayoutUpdate;
private HumanoidCharacterProfile? _profile;
public TraitsTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SearchBar.OnTextChanged += OnSearchTextChanged;
_prototype.PrototypesReloaded += OnProtoReload;
// Subscribe to CVars
_cfg.OnValueChanged(DCCVars.MaxTraitCount, OnMaxTraitCountChanged, true);
_cfg.OnValueChanged(DCCVars.MaxTraitPoints, OnMaxTraitPointsChanged, true);
PopulateCategories();
UpdateGlobalStats();
}
private void OnMaxTraitCountChanged(int value)
{
_maxGlobalTraits = value;
UpdateGlobalStats();
}
private void OnMaxTraitPointsChanged(int value)
{
_maxGlobalPoints = value;
UpdateGlobalStats();
}
private void OnProtoReload(PrototypesReloadedEventArgs args)
{
// Don't refresh if control has been disposed
if (Disposed)
return;
if (args.WasModified<TraitPrototype>() || args.WasModified<TraitCategoryPrototype>())
RefreshTraits();
}
public void RefreshTraits()
{
PopulateCategories();
UpdateGlobalStats();
}
private void PopulateCategories()
{
CategoriesContainer.RemoveAllChildren();
_categoryUis.Clear();
var categories = _prototype.EnumeratePrototypes<TraitCategoryPrototype>()
.OrderBy(c => c.Priority)
.ThenBy(c => Loc.GetString(c.Name))
.ToList();
var traitsByCategory = _prototype.EnumeratePrototypes<TraitPrototype>()
.GroupBy(t => t.Category)
.ToDictionary(g => g.Key, g => g.OrderBy(t => Loc.GetString(t.Name)).ToList());
foreach (var category in categories)
{
if (!traitsByCategory.TryGetValue(category.ID, out var traits) || traits.Count == 0)
continue;
var categoryUi = new TraitCategory(category, traits);
categoryUi.OnTraitToggled += OnTraitToggled;
_categoryUis[category.ID] = categoryUi;
CategoriesContainer.AddChild(categoryUi);
}
// Apply current filters and conditions
ApplySearchFilter();
UpdateAllConditions();
}
private void OnTraitToggled(ProtoId<TraitPrototype> traitId, bool selected)
{
var trait = _prototype.Index(traitId);
if (selected)
{
// Check global limits
if (_currentTraitCount >= _maxGlobalTraits)
{
RevertTraitToggle(traitId);
return;
}
if (_currentPointsSpent + trait.Cost > _maxGlobalPoints)
{
RevertTraitToggle(traitId);
return;
}
// Check category limits
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
{
var categoryProto = _prototype.Index(trait.Category);
if (categoryProto.MaxTraits.HasValue &&
categoryUi.SelectedCount >= categoryProto.MaxTraits.Value)
{
RevertTraitToggle(traitId);
return;
}
if (categoryProto.MaxPoints.HasValue &&
categoryUi.PointsSpent + trait.Cost > categoryProto.MaxPoints.Value)
{
RevertTraitToggle(traitId);
return;
}
// Check if trait meets conditions
if (!categoryUi.TraitMeetsConditions(traitId))
{
RevertTraitToggle(traitId);
return;
}
}
_selectedTraits.Add(traitId);
_currentTraitCount++;
_currentPointsSpent += trait.Cost;
}
else
{
_selectedTraits.Remove(traitId);
_currentTraitCount--;
_currentPointsSpent -= trait.Cost;
}
UpdateGlobalStats();
UpdateCategoryStats(trait.Category);
UpdateAllConditions();
OnTraitsChanged?.Invoke(_selectedTraits);
}
private void RevertTraitToggle(ProtoId<TraitPrototype> traitId)
{
var trait = _prototype.Index(traitId);
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
{
categoryUi.SetTraitSelected(traitId, _selectedTraits.Contains(traitId));
}
}
private void UpdateGlobalStats()
{
GlobalTraitCountLabel.Text = $"{_currentTraitCount} / {_maxGlobalTraits}";
GlobalPointsLabel.Text = $"{_maxGlobalPoints - _currentPointsSpent} / {_maxGlobalPoints}";
// Calculate remaining points (clamped to not go below 0 in display)
var remainingPoints = _maxGlobalPoints - _currentPointsSpent;
GlobalPointsLabel.Text = $"{remainingPoints} / {_maxGlobalPoints}";
// Calculate progress bar percentage - clamp between 0 and 1
var percentage = _maxGlobalPoints > 0
? Math.Clamp((float)remainingPoints / _maxGlobalPoints, 0f, 1f)
: 0f;
// Update progress bar using percentage-based sizing
var parent = GlobalPointsBar.Parent;
if (parent != null)
{
var parentWidth = parent.Width;
// If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
{
// Schedule update after parent layout (only once)
_awaitingLayoutUpdate = true;
parent.OnResized += OnProgressBarParentResized;
}
}
// Update progress bar color class
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarFull");
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarPartial");
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarLow");
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarEmpty");
GlobalPointsBar.AddStyleClass(percentage switch
{
>= 0.99f => "TraitsProgressBarFull",
>= 0.5f => "TraitsProgressBarPartial",
> 0f => "TraitsProgressBarLow",
_ => "TraitsProgressBarEmpty"
});
}
private void OnProgressBarParentResized()
{
_awaitingLayoutUpdate = false;
UpdateGlobalStats();
}
private void UpdateCategoryStats(ProtoId<TraitCategoryPrototype> categoryId)
{
if (_categoryUis.TryGetValue(categoryId, out var categoryUi))
{
categoryUi.UpdateStats();
}
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
{
_currentSearchText = args.Text.Trim();
ApplySearchFilter();
}
private void ApplySearchFilter()
{
foreach (var (_, categoryUi) in _categoryUis)
{
categoryUi.FilterTraits(_currentSearchText);
}
}
/// <summary>
/// Updates all trait conditions based on current job and species.
/// Call this when job or species changes in the character creator.
/// </summary>
/// <param name="profile">Current profile</param>
public void UpdateConditions(HumanoidCharacterProfile profile)
{
_profile = profile;
UpdateAllConditions();
}
private void UpdateAllConditions()
{
foreach (var (_, categoryUi) in _categoryUis)
{
// If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
}
RecalculateStats();
}
private void RecalculateStats()
{
_currentTraitCount = 0;
_currentPointsSpent = 0;
// Rebuild selected traits based on what's actually selected in the UI
var previouslySelected = new HashSet<ProtoId<TraitPrototype>>(_selectedTraits);
_selectedTraits.Clear();
foreach (var (_, categoryUi) in _categoryUis)
{
// Get the selected traits from this category
var selectedInCategory = categoryUi.GetSelectedTraitIds();
foreach (var traitId in selectedInCategory)
{
if (!_prototype.TryIndex(traitId, out var trait))
continue;
_selectedTraits.Add(traitId);
_currentTraitCount++;
_currentPointsSpent += trait.Cost;
}
}
UpdateGlobalStats();
foreach (var (categoryId, _) in _categoryUis)
{
UpdateCategoryStats(categoryId);
}
// Fire event if selection changed
if (!_selectedTraits.SetEquals(previouslySelected))
{
OnTraitsChanged?.Invoke(_selectedTraits);
}
}
/// <summary>
/// Sets the currently selected traits (e.g., when loading a profile).
/// </summary>
public void SetSelectedTraits(IEnumerable<ProtoId<TraitPrototype>> traits)
{
// Clear current selection
foreach (var (_, categoryUi) in _categoryUis)
{
categoryUi.ClearSelection();
}
_selectedTraits.Clear();
_currentTraitCount = 0;
_currentPointsSpent = 0;
// Apply new selection
foreach (var traitId in traits)
{
if (!_prototype.TryIndex(traitId, out var trait))
continue;
_selectedTraits.Add(traitId);
_currentTraitCount++;
_currentPointsSpent += trait.Cost;
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
{
categoryUi.SetTraitSelected(traitId, true);
}
}
UpdateGlobalStats();
foreach (var (categoryId, _) in _categoryUis)
{
UpdateCategoryStats(categoryId);
}
}
}
@@ -0,0 +1,41 @@
using Content.Client._DV.UserInterfaces.BuildInfo.Controls;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controllers;
namespace Content.Client._DV.UserInterfaces.BuildInfo;
[UsedImplicitly]
public sealed class BuildInfoUIController : UIController
{
private BuildInfoWindow _changeLogWindow = default!;
public void OpenWindow()
{
EnsureWindow();
_changeLogWindow.OpenCentered();
_changeLogWindow.MoveToFront();
}
private void EnsureWindow()
{
if (_changeLogWindow is { Disposed: false })
{
// return; ADD THIS BACK WHEN I DON'T NEED HOT RELOAD
}
_changeLogWindow = UIManager.CreateWindow<BuildInfoWindow>();
}
public void ToggleWindow()
{
if (_changeLogWindow is { IsOpen: true })
{
_changeLogWindow.Close();
}
else
{
OpenWindow();
}
}
}
@@ -0,0 +1,7 @@
<ui:BuildInfoRow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client._DV.UserInterfaces.BuildInfo.Controls"
Orientation="Horizontal">
<Label Name="Label" HorizontalAlignment="Left" />
<Label Name="Value" HorizontalAlignment="Right" HorizontalExpand="True" Margin="4" />
<Button Name="CopyButton" HorizontalAlignment="Right" Text="{Loc ui-options-function-text-copy}" />
</ui:BuildInfoRow>
@@ -0,0 +1,38 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.UserInterfaces.BuildInfo.Controls;
[GenerateTypedNameReferences]
public sealed partial class BuildInfoRow : BoxContainer
{
[Dependency] private readonly IClipboardManager _clipboard = default!;
public string? LabelText
{
get => Label.Text;
set => Label.Text = value;
}
public string? ValueText
{
get => Value.Text;
set => Value.Text = value;
}
private void CopyValue(BaseButton.ButtonEventArgs args)
{
if (ValueText is { } value)
_clipboard.SetText(value);
}
public BuildInfoRow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
CopyButton.OnPressed += CopyValue;
}
}
@@ -0,0 +1,14 @@
<buildInfo:BuildInfoWindow xmlns="https://spacestation14.io"
xmlns:buildInfo="clr-namespace:Content.Client._DV.UserInterfaces.BuildInfo.Controls"
Title="{Loc build-info-window-title}"
Resizable="True">
<BoxContainer Orientation="Vertical" Margin="4">
<buildInfo:BuildInfoRow Name="EngineVersion" LabelText="{Loc build-info-engine-version-label}" />
<buildInfo:BuildInfoRow Name="Fork" LabelText="{Loc build-info-fork-label}" />
<buildInfo:BuildInfoRow Name="Version" LabelText="{Loc build-info-version-label}" />
<buildInfo:BuildInfoRow Name="DownloadUrl" LabelText="{Loc build-info-download-url-label}" />
<buildInfo:BuildInfoRow Name="BuildHash" LabelText="{Loc build-info-build-hash-label}" />
<buildInfo:BuildInfoRow Name="BuildManifestUrl" LabelText="{Loc build-info-build-manifest-url-label}" />
<buildInfo:BuildInfoRow Name="BuildManifestHash" LabelText="{Loc build-info-build-manifest-hash-label}" />
</BoxContainer>
</buildInfo:BuildInfoWindow>
@@ -0,0 +1,27 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
namespace Content.Client._DV.UserInterfaces.BuildInfo.Controls;
[GenerateTypedNameReferences]
public sealed partial class BuildInfoWindow : DefaultWindow
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public BuildInfoWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
Version.ValueText = _cfg.GetCVar(CVars.BuildVersion);
EngineVersion.ValueText = _cfg.GetCVar(CVars.BuildEngineVersion);
Fork.ValueText = _cfg.GetCVar(CVars.BuildForkId);
DownloadUrl.ValueText = _cfg.GetCVar(CVars.BuildDownloadUrl);
BuildHash.ValueText = _cfg.GetCVar(CVars.BuildHash);
BuildManifestUrl.ValueText = _cfg.GetCVar(CVars.BuildManifestUrl);
BuildManifestHash.ValueText = _cfg.GetCVar(CVars.BuildManifestHash);
}
}
@@ -0,0 +1,47 @@
using Content.Client.Eui;
using Content.Shared.Eui;
using Content.Shared.ReadyManifest;
using JetBrains.Annotations;
namespace Content.Client._Impstation.ReadyManifest;
[UsedImplicitly]
public sealed class ReadyManifestEui : BaseEui
{
private readonly ReadyManifestUi _window;
public ReadyManifestEui()
{
_window = new();
_window.OnClose += () =>
{
SendMessage(new CloseEuiMessage());
};
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not ReadyManifestEuiState cast)
{
return;
}
_window.RebuildUI(cast.JobCounts);
}
}
@@ -0,0 +1,20 @@
using Content.Shared.ReadyManifest;
namespace Content.Client._Impstation.ReadyManifest;
public sealed class ReadyManifestSystem : EntitySystem
{
private HashSet<string> _departments = new();
public IReadOnlySet<string> Departments => _departments;
public override void Initialize()
{
base.Initialize();
}
public void RequestReadyManifest()
{
RaiseNetworkEvent(new RequestReadyManifestMessage());
}
}
@@ -0,0 +1,10 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="Ready Manifest"
SetSize="450 750">
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="ReadyManifestListing" Orientation="Vertical"/>
</ScrollContainer>
</BoxContainer>
</DefaultWindow>
@@ -0,0 +1,137 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client._Impstation.ReadyManifest;
[GenerateTypedNameReferences]
public sealed partial class ReadyManifestUi : DefaultWindow
{
private readonly IEntitySystemManager _entitySystem;
private readonly IPrototypeManager _prototypeManager;
private readonly ISharedPlayerManager _playerManager;
private readonly Dictionary<string, BoxContainer> _jobCategories;
private readonly SpriteSystem _sprite;
public ReadyManifestUi()
{
RobustXamlLoader.Load(this);
_jobCategories = new Dictionary<string, BoxContainer>();
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
_playerManager = IoCManager.Resolve<ISharedPlayerManager>();
_entitySystem = IoCManager.Resolve<IEntitySystemManager>();
_sprite = _entitySystem.GetEntitySystem<SpriteSystem>();
}
// Currently rebuilds the UI every time it's updated, should probably split department lookup and job counts into separate functions
public void RebuildUI(Dictionary<ProtoId<JobPrototype>, int> jobCounts)
{
ReadyManifestListing.DisposeAllChildren();
_jobCategories.Clear();
var departments = new List<DepartmentPrototype>();
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
{
if (department.EditorHidden)
continue;
departments.Add(department);
}
departments.Sort(DepartmentUIComparer.Instance);
foreach (var department in departments)
{
var departmentName = Loc.GetString($"department-{department.ID}");
if (!_jobCategories.TryGetValue(department.ID, out var category))
{
category = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
Name = department.ID,
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
("departmentName", departmentName))
};
category.AddChild(new Label()
{
StyleClasses = { "LabelBig" },
Text = Loc.GetString($"department-{department.ID}")
});
_jobCategories[department.ID] = category;
ReadyManifestListing.AddChild(category);
}
var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
foreach (var job in jobs)
{
var gridContainer = new GridContainer()
{
HorizontalExpand = true,
Columns = 2
};
var jobContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
};
var title = new RichTextLabel()
{
HorizontalExpand = true
};
title.SetMessage(job.LocalizedName + ":");
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 0, 4, 0)
};
var readyCount = new RichTextLabel()
{
HorizontalExpand = true
};
if (jobCounts.ContainsKey(job.ID))
{
var jobCount = jobCounts[job.ID];
var color = jobCount > 0 ? Color.White : Color.Red;
readyCount.SetMessage(jobCount.ToString(), null, color);
}
else
{
readyCount.SetMessage("0", null, Color.Red);
}
var jobIcon = _prototypeManager.Index(job.Icon);
icon.Texture = _sprite.Frame0(jobIcon.Icon);
jobContainer.AddChild(icon);
jobContainer.AddChild(title);
gridContainer.AddChild(jobContainer);
gridContainer.AddChild(readyCount);
category.AddChild(gridContainer);
}
}
}
}
@@ -38,12 +38,10 @@ public sealed class ConstantsTest
Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
$"GasAbbreviations size is not equal to TotalNumberOfGases.");
// the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
// the ID for each gas has to correspond to a value in the Gas enum (converted to a string)
foreach (var gas in gasProtos)
{
var validInteger = int.TryParse(gas.ID, out var number);
Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
Assert.That(Enum.TryParse<Gas>(gas.ID, out _), $"GasPrototype {gas.ID} has an invalid ID. It must correspond to a value in the {nameof(Gas)} enum.");
}
});
});
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Reflection;
@@ -64,17 +66,16 @@ namespace Content.IntegrationTests.Tests.DoAfter
var server = pair.Server;
await server.WaitIdleAsync();
var entityManager = server.ResolveDependency<IEntityManager>();
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent();
// That it finishes successfully
await server.WaitPost(() =>
{
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
#pragma warning disable NUnit2045 // Interdependent assertions.
Assert.That(doAfterSystem.TryStartDoAfter(args));
Assert.That(ev.Cancelled, Is.False);
@@ -92,23 +93,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var ev = new TestDoAfterEvent();
await server.WaitPost(() =>
{
var tickTime = 1.0f / timing.TickRate;
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
if (!doAfterSystem.TryStartDoAfter(args, out var id))
{
Assert.Fail();
return;
}
Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
Assert.That(!ev.Cancelled);
doAfterSystem.Cancel(id);
@@ -121,5 +116,67 @@ namespace Content.IntegrationTests.Tests.DoAfter
await pair.CleanReturnAsync();
}
/// <summary>
/// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
/// includes the correct interacting entities.
/// </summary>
[Test]
public async Task TestGetInteractingEntities()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.EntMan;
var timing = server.ResolveDependency<IGameTiming>();
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
var interactionSystem = entityManager.System<SharedInteractionSystem>();
var ev = new TestDoAfterEvent();
EntityUid mob = default;
EntityUid target = default;
EntityUid mob2 = default;
EntityUid mob3 = default;
EntityUid target2 = default;
await server.WaitPost(() =>
{
// Spawn two targets to interact with
target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
// Spawn a mob which is interacting with the first target
mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args));
// Spawn two more mobs which are interacting with the second target
mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args2));
mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
Assert.That(doAfterSystem.TryStartDoAfter(args3));
});
var list = new HashSet<EntityUid>();
interactionSystem.GetEntitiesInteractingWithTarget(target, list);
Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
await server.WaitPost(() =>
{
entityManager.DeleteEntity(mob);
entityManager.DeleteEntity(mob2);
entityManager.DeleteEntity(mob3);
entityManager.DeleteEntity(target);
entityManager.DeleteEntity(target2);
});
await pair.CleanReturnAsync();
}
}
}
@@ -49,6 +49,9 @@ public abstract partial class InteractionTest
public static implicit operator EntitySpecifier(string prototype)
=> new(prototype, 1);
public static implicit operator EntitySpecifier(EntProtoId prototype)
=> new(prototype.Id, 1);
public static implicit operator EntitySpecifier((string, int) tuple)
=> new(tuple.Item1, tuple.Item2);
@@ -54,6 +54,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryCharger
- type: entity
@@ -68,6 +69,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
- type: entity
@@ -85,6 +87,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
node: output
- type: BatteryCharger
@@ -110,6 +113,7 @@ namespace Content.IntegrationTests.Tests.Power
maxSupply: 1000
supplyRampTolerance: 1000
- type: Battery
netsync: false
maxCharge: 1000
startingCharge: 1000
- type: Transform
@@ -119,6 +123,7 @@ namespace Content.IntegrationTests.Tests.Power
id: ApcDummy
components:
- type: Battery
netsync: false
maxCharge: 10000
startingCharge: 10000
- type: PowerNetworkBattery
@@ -380,6 +385,8 @@ namespace Content.IntegrationTests.Tests.Power
const float startingCharge = 100_000;
PowerNetworkBatteryComponent netBattery = default!;
EntityUid generatorEnt = default!;
EntityUid consumerEnt = default!;
BatteryComponent battery = default!;
PowerConsumerComponent consumer = default!;
@@ -395,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(generatorEnt);
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
@@ -441,7 +448,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (200 + 100) / 2.0 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -460,7 +468,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -576,6 +585,8 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid batteryEnt = default!;
PowerSupplierComponent supplier = default!;
BatteryComponent battery = default!;
@@ -591,8 +602,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
var netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
@@ -615,7 +626,8 @@ namespace Content.IntegrationTests.Tests.Power
{
// half a second @ 500 W = 250
// 50% efficiency, so 125 J stored total.
Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(125).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
});
});
@@ -633,6 +645,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -653,9 +668,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -694,7 +709,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
const int expectedSpent = 200;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -711,6 +727,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -731,9 +750,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -772,7 +791,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
const int expectedSpent = 400;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -1223,6 +1243,9 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid substationEnt = default!;
EntityUid apcEnt = default!;
PowerNetworkBatteryComponent substationNetBattery = default!;
BatteryComponent apcBattery = default!;
@@ -1242,9 +1265,9 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
var generatorSupplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
substationNetBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(substationEnt);
@@ -1262,8 +1285,9 @@ namespace Content.IntegrationTests.Tests.Power
{
Assert.Multiple(() =>
{
var currentCharge = batterySys.GetCharge((apcEnt, apcBattery));
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
});
});
@@ -3,15 +3,16 @@ using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization;
namespace Content.IntegrationTests.Tests.Power;
[Explicit]
public sealed class StationPowerTests
{
/// <summary>
@@ -19,29 +20,32 @@ public sealed class StationPowerTests
/// </summary>
private const float MinimumPowerDurationSeconds = 10 * 60;
// Begin DeltaV Additions - DeltaV maps for testing
private static readonly string[] GameMaps =
[
"Fland",
"Meta",
"Packed",
"Omega",
"Bagel",
"Box",
"Core",
"Marathon",
"Saltern",
"Reach",
"Train",
"Oasis",
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge",
"Convex",
"Relic",
"Academy",
"Arena",
"Asterisk",
"Byoin",
"Chibi",
"Division",
"Edge",
"Elegance",
"Glacier",
"Hammurabi",
"TheHive",
"Lighthouse",
"Micro",
"Ovni",
"Pebble",
"Shoukou",
"Submarine",
"Terra",
"Tortuga",
];
// Begin DeltaV Additions - DeltaV maps for testing
[Explicit]
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestStationStartingPowerWindow(string mapProtoId)
{
@@ -54,6 +58,7 @@ public sealed class StationPowerTests
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var batterySys = entMan.System<BatterySystem>();
// Load the map
await server.WaitAssertion(() =>
@@ -77,7 +82,8 @@ public sealed class StationPowerTests
if (node.NodeGroup is not IBasePowerNet group)
continue;
networks.TryGetValue(group.NetworkNode, out var charge);
networks[group.NetworkNode] = charge + battery.CurrentCharge;
var currentCharge = batterySys.GetCharge((uid, battery));
networks[group.NetworkNode] = charge + currentCharge;
}
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;
@@ -100,6 +106,54 @@ public sealed class StationPowerTests
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
});
await pair.CleanReturnAsync();
}
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestApcLoad(string mapProtoId)
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
});
var server = pair.Server;
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var xform = entMan.System<TransformSystem>();
// Load the map
await server.WaitAssertion(() =>
{
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(mapProto, out var mapId, opts);
});
// Wait long enough for power to ramp up, but before anything can trip
await pair.RunSeconds(2);
// Check that no APCs start overloaded
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
Assert.Multiple(() =>
{
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
{
// Uncomment the following line to log starting APC load to the console
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
else
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
}
});
await pair.CleanReturnAsync();
}
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Interaction;
using Content.Shared.Movement.Pulling.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Puller;
#nullable enable
public sealed class InteractingEntitiesTest : InteractionTest
{
private static readonly EntProtoId MobHuman = "MobHuman";
/// <summary>
/// Spawns a Target mob, and a second mob which drags it,
/// and checks that the dragger is considered to be interacting with the dragged mob.
/// </summary>
[Test]
public async Task PullerIsConsideredInteractingTest()
{
await SpawnTarget(MobHuman);
var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
var pullSys = SEntMan.System<PullingSystem>();
await Server.WaitAssertion(() =>
{
Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
$"{puller} failed to start pulling {Target}");
});
var list = new HashSet<EntityUid>();
Server.System<SharedInteractionSystem>()
.GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
}
}
@@ -108,7 +108,7 @@ public sealed class DockTest : ContentUnitTest
{
mapGrid = entManager.AddComponent<MapGridComponent>(map.MapUid);
entManager.DeleteEntity(map.Grid);
var path = new ResPath("/Maps/Shuttles/emergency.yml");
var path = new ResPath("/Maps/_DV/Shuttles/Evac/ntes_dart.yml"); // DeltaV - Made evac DV specific, Was: /Maps/Shuttles/emergency.yml
Assert.That(entManager.System<MapLoaderSystem>().TryLoadGrid(otherMap.MapId, path, out var grid));
shuttle = grid!.Value.Owner;
@@ -0,0 +1,263 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Containers;
using Content.Shared.Item;
using Content.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Storage;
public sealed class StorageTest
{
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
/// </summary>
[Test]
public async Task StorageSizeArbitrageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var entMan = server.ResolveDependency<IEntityManager>();
var itemSys = entMan.System<SharedItemSystem>();
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
storage.Whitelist != null ||
storage.MaxItemSize == null ||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
continue;
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
$"Found storage arbitrage on {proto.ID}");
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestStorageFillPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
continue;
foreach (var entry in storage.Contents)
{
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
}
}
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
await Assert.MultipleAsync(async () =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
StorageComponent? storage = null;
ItemComponent? item = null;
var size = 0;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("Storage", out storage))
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
return;
}
proto.TryGetComponent("Item", out item);
size = GetFillSize(fill, false, protoMan, itemSys);
});
if (storage == null)
continue;
var maxSize = storage.MaxItemSize;
if (storage.MaxItemSize == null)
{
if (item?.Size == null)
{
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
}
else
{
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
var index = Math.Max(0, curIndex - 1);
maxSize = allSizes[index].ID;
}
}
if (maxSize == null)
continue;
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
foreach (var entry in fill.Contents)
{
if (entry.PrototypeId == null)
continue;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
continue;
ItemComponent? entryItem = null;
await server.WaitPost(() =>
{
fillItem.TryGetComponent("Item", out entryItem);
});
if (entryItem == null)
continue;
Assert.That(protoMan.Index(entryItem.Size).Weight,
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
}
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForEntityStorageFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
if (entStorage == null)
return;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");
});
}
await pair.CleanReturnAsync();
}
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
if (entry.PrototypeId == null)
return 0;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
{
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
return 0;
}
if (getCount)
return entry.Amount;
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
return 0;
}
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
var totalSize = 0;
var groups = new Dictionary<string, int>();
foreach (var entry in fill.Contents)
{
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
if (entry.GroupId == null)
totalSize += size;
else
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
}
return totalSize + groups.Values.Sum();
}
/// <summary>
/// Tests that prototypes are not using multiple container fill components at the same time.
/// </summary>
[Test]
public async Task NoMultipleContainerFillsTest()
{
await using var pair = await PoolManager.GetServerClient();
var compFact = pair.Server.ResolveDependency<IComponentFactory>();
Assert.Multiple(() =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<EntityTableContainerFillComponent>())
{
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
Assert.That(!proto.HasComponent<ContainerFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
}
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<ContainerFillComponent>())
{
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
}
});
await pair.CleanReturnAsync();
}
}
@@ -1,240 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.Storage.Components;
using Content.Shared.Item;
using Content.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
public sealed class StorageTest
{
/// <summary>
/// Can an item store more than itself weighs.
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
/// </summary>
[Test]
public async Task StorageSizeArbitrageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var entMan = server.ResolveDependency<IEntityManager>();
var itemSys = entMan.System<SharedItemSystem>();
await server.WaitAssertion(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
storage.Whitelist != null ||
storage.MaxItemSize == null ||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
continue;
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
$"Found storage arbitrage on {proto.ID}");
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestStorageFillPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
continue;
foreach (var entry in storage.Contents)
{
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
}
}
});
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
allSizes.Sort();
await Assert.MultipleAsync(async () =>
{
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<EntityStorageComponent>(compFact))
continue;
StorageComponent? storage = null;
ItemComponent? item = null;
var size = 0;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("Storage", out storage))
{
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
return;
}
proto.TryGetComponent("Item", out item);
size = GetFillSize(fill, false, protoMan, itemSys);
});
if (storage == null)
continue;
var maxSize = storage.MaxItemSize;
if (storage.MaxItemSize == null)
{
if (item?.Size == null)
{
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
}
else
{
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
var index = Math.Max(0, curIndex - 1);
maxSize = allSizes[index].ID;
}
}
if (maxSize == null)
continue;
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
foreach (var entry in fill.Contents)
{
if (entry.PrototypeId == null)
continue;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
continue;
ItemComponent? entryItem = null;
await server.WaitPost(() =>
{
fillItem.TryGetComponent("Item", out entryItem);
});
if (entryItem == null)
continue;
Assert.That(protoMan.Index(entryItem.Size).Weight,
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
}
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestSufficientSpaceForEntityStorageFill()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var id = compFact.GetComponentName<StorageFillComponent>();
var itemSys = entMan.System<SharedItemSystem>();
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
{
if (proto.HasComponent<StorageComponent>(compFact))
continue;
await server.WaitAssertion(() =>
{
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
if (entStorage == null)
return;
var size = GetFillSize(fill, true, protoMan, itemSys);
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
$"{proto.ID} storage fill is too large.");
});
}
await pair.CleanReturnAsync();
}
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
if (entry.PrototypeId == null)
return 0;
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
{
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
return 0;
}
if (getCount)
return entry.Amount;
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
return 0;
}
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
{
var totalSize = 0;
var groups = new Dictionary<string, int>();
foreach (var entry in fill.Contents)
{
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
if (entry.GroupId == null)
totalSize += size;
else
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
}
return totalSize + groups.Values.Sum();
}
}
}
@@ -3,11 +3,13 @@ using System.Collections.Generic;
using Content.Server.VendingMachines;
using Content.Server.Wires;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Containers;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.EntityTable;
using Content.Shared.Prototypes;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Robust.Shared.GameObjects;
@@ -115,6 +117,7 @@ namespace Content.IntegrationTests.Tests
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var entityTable = server.EntMan.System<EntityTableSystem>();
await server.WaitAssertion(() =>
{
@@ -134,17 +137,23 @@ namespace Content.IntegrationTests.Tests
restocks.Add(proto.ID);
}
// Collect all the prototypes with StorageFills referencing those entities.
// Collect all the prototypes with EntityTableContainerFills referencing those entities.
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
if (!proto.TryGetComponent<EntityTableContainerFillComponent>(out var storage, compFact))
continue;
var containers = storage.Containers;
if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) // We only care about this container type.
continue;
List<string> restockStore = new();
foreach (var spawnEntry in storage.Contents)
foreach (var spawnEntry in entityTable.GetSpawns(container))
{
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
restockStore.Add(spawnEntry.PrototypeId);
if (restocks.Contains(spawnEntry))
restockStore.Add(spawnEntry);
}
if (restockStore.Count > 0)
@@ -153,7 +162,7 @@ namespace Content.IntegrationTests.Tests
// Iterate through every CargoProduct and make sure each
// prototype with a restock component is referenced in a
// purchaseable entity with a StorageFill.
// purchaseable entity with an EntityTableContianerFill.
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
{
if (restockStores.ContainsKey(proto.Product))
@@ -28,7 +28,9 @@ public sealed class ShipyardTest
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
var value = pricing.AppraiseGrid(shuttle.Value);
@@ -57,7 +59,9 @@ public sealed class ShipyardTest
{
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
{
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
var console = FindComponent<ShuttleConsoleComponent>(entities, shuttle.Value);
@@ -0,0 +1,801 @@
using System.Collections.Generic;
using System.Reflection;
using Content.Server._DV.Traits;
using Content.Shared._DV.Traits;
using Content.Shared._DV.Traits.Conditions;
using Content.Shared._DV.Traits.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Nutrition.Components;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests._DV;
/// <summary>
/// Comprehensive integration tests for the trait system.
/// Tests all conditions, effects, and validation logic.
/// </summary>
[TestFixture]
[TestOf(typeof(TraitSystem))]
public sealed partial class TraitSystemTest
{
[TestPrototypes]
private const string Prototypes = @"
# Test Trait Categories
- type: traitCategory
id: TestCategoryUnlimited
name: trait-dysgraphia-name
maxTraits: null
maxPoints: null
- type: traitCategory
id: TestCategoryLimited
name: trait-dysgraphia-name
maxTraits: 2
maxPoints: 10
# Test Traits - Conditions
- type: trait
id: TestTraitHasComp
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
conditions:
- !type:HasCompCondition
component: Hunger
effects:
- !type:AddCompsEffect
components:
- type: Test
# Test Traits - Effects
- type: trait
id: TestTraitAddComps
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
effects:
- !type:AddCompsEffect
components:
- type: Test
- type: Hunger
- type: trait
id: TestTraitOverrideComps
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
effects:
- !type:OverrideCompsEffect
components:
- type: Hunger
- type: trait
id: TestTraitRemComps
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
effects:
- !type:RemCompsEffect
components:
- Hunger
- Thirst
- type: trait
id: TestTraitSpawnItem
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
effects:
- !type:SpawnItemInHandEffect
item: Pen
# Test Traits - Validation
- type: trait
id: TestTraitConflictA
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
conditions:
- !type:HasTraitCondition
trait: TestTraitConflictB
invert: true
effects:
- !type:AddCompsEffect
components:
- type: Test
- type: trait
id: TestTraitConflictB
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryUnlimited
cost: 0
effects:
- !type:AddCompsEffect
components:
- type: Test
- type: trait
id: TestTraitLimited1
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryLimited
cost: 5
effects:
- !type:AddCompsEffect
components:
- type: Test
- type: trait
id: TestTraitLimited2
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryLimited
cost: 5
effects:
- !type:AddCompsEffect
components:
- type: Test
- type: trait
id: TestTraitLimited3
name: trait-dysgraphia-name
description: trait-dysgraphia-name
category: TestCategoryLimited
cost: 5
effects:
- !type:AddCompsEffect
components:
- type: Test
";
#region Condition Tests
[RegisterComponent]
private sealed partial class TestComponent : Component;
[Test]
public async Task HasCompCondition_WithComponent_ReturnsTrue()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
entMan.AddComponent<HungerComponent>(player);
var condition = new HasCompCondition { Component = "Hunger" };
var ctx = CreateContext(entMan, protoMan, factory, player);
Assert.That(condition.Evaluate(ctx), Is.True, "HasCompCondition should return true when component exists");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task HasCompCondition_WithoutComponent_ReturnsFalse()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new HasCompCondition { Component = "Hunger" };
var ctx = CreateContext(entMan, protoMan, factory, player);
Assert.That(condition.Evaluate(ctx),
Is.False,
"HasCompCondition should return false when component doesn't exist");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task HasCompCondition_Inverted_ReturnsOpposite()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
entMan.AddComponent<HungerComponent>(player);
var condition = new HasCompCondition { Component = "Hunger", Invert = true };
var ctx = CreateContext(entMan, protoMan, factory, player);
Assert.That(condition.Evaluate(ctx),
Is.False,
"Inverted HasCompCondition should return false when component exists");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task HasJobCondition_MatchingJob_ReturnsTrue()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new HasJobCondition { Job = "MedicalDoctor" };
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
Assert.That(condition.Evaluate(ctx), Is.True, "HasJobCondition should return true for matching job");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task HasJobCondition_DifferentJob_ReturnsFalse()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new HasJobCondition { Job = "MedicalDoctor" };
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
Assert.That(condition.Evaluate(ctx), Is.False, "HasJobCondition should return false for different job");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task InDepartmentCondition_JobInDepartment_ReturnsTrue()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new InDepartmentCondition { Department = "Medical" };
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
Assert.That(condition.Evaluate(ctx),
Is.True,
"InDepartmentCondition should return true when job is in department");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task InDepartmentCondition_JobNotInDepartment_ReturnsFalse()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new InDepartmentCondition { Department = "Medical" };
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
Assert.That(condition.Evaluate(ctx),
Is.False,
"InDepartmentCondition should return false when job is not in department");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task IsSpeciesCondition_MatchingSpecies_ReturnsTrue()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new IsSpeciesCondition { Species = "Human" };
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Human");
Assert.That(condition.Evaluate(ctx), Is.True, "IsSpeciesCondition should return true for matching species");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task IsSpeciesCondition_DifferentSpecies_ReturnsFalse()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var condition = new IsSpeciesCondition { Species = "Human" };
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Vox");
Assert.That(condition.Evaluate(ctx),
Is.False,
"IsSpeciesCondition should return false for different species");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
#endregion
#region Effect Tests
[Test]
public async Task AddCompsEffect_AddsComponents()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
Assert.That(entMan.HasComponent<HungerComponent>(player),
Is.False,
"Player should not start with HungerComponent");
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
foreach (var effect in trait.Effects)
{
effect.Apply(ctx);
}
Assert.That(entMan.HasComponent<HungerComponent>(player),
Is.True,
"AddCompsEffect should add HungerComponent");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task AddCompsEffect_DoesNotOverwrite()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
foreach (var effect in trait.Effects)
{
effect.Apply(ctx);
}
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
Assert.That(hungerAfter,
Is.SameAs(hungerBefore),
"AddCompsEffect should not replace existing component instance");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task OverrideCompsEffect_OverwritesComponent()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitOverrideComps"));
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
foreach (var effect in trait.Effects)
{
effect.Apply(ctx);
}
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
Assert.That(hungerAfter,
Is.Not.SameAs(hungerBefore),
"OverrideCompsEffect should replace existing component instance");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task RemCompsEffect_RemovesComponents()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
var factory = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
entMan.AddComponent<HungerComponent>(player);
entMan.AddComponent<ThirstComponent>(player);
Assert.That(entMan.HasComponent<HungerComponent>(player),
Is.True,
"Player should start with HungerComponent");
Assert.That(entMan.HasComponent<ThirstComponent>(player),
Is.True,
"Player should start with ThirstComponent");
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitRemComps"));
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
foreach (var effect in trait.Effects)
{
effect.Apply(ctx);
}
Assert.That(entMan.HasComponent<HungerComponent>(player),
Is.False,
"RemCompsEffect should remove HungerComponent");
Assert.That(entMan.HasComponent<ThirstComponent>(player),
Is.False,
"RemCompsEffect should remove ThirstComponent");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task SpawnItemInHandEffect_SpawnsItem()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ResolveDependency<IPrototypeManager>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity("MobHuman", MapCoordinates.Nullspace);
var handsSys = entMan.System<SharedHandsSystem>();
var hands = entMan.GetComponent<HandsComponent>(player);
Assert.That(handsSys.GetActiveItem((player, hands)), Is.Null, "Player should start with empty hands");
var traitSys = entMan.System<TraitSystem>();
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitSpawnItem"));
// We need to use reflection to call the private ApplyTrait method
var method = typeof(TraitSystem).GetMethod("ApplyTrait",
BindingFlags.NonPublic | BindingFlags.Instance);
method?.Invoke(traitSys, new object[] { player, trait });
var item = handsSys.GetActiveItem((player, hands));
Assert.That(item, Is.Not.Null, "SpawnItemInHandEffect should spawn item in hand");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
#endregion
#region Validation Tests
[Test]
public async Task RespectsConflicts()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoMan = server.ProtoMan;
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
var profile = new HumanoidCharacterProfile()
.WithTraitPreference("TestTraitConflictA", protoMan)
.WithTraitPreference("TestTraitConflictB", protoMan);
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var disabledTraits = new Dictionary<ProtoId<TraitPrototype>, List<string>>();
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, profile.TraitPreferences, null, null, null, profile, disabledTraits });
Assert.Multiple(() =>
{
Assert.That(validTraits?.Count, Is.EqualTo(1), "Only one conflicting trait should be valid");
Assert.That(validTraits.Contains("TestTraitConflictB"), Is.True, "Non-complaining trait should be kept");
Assert.That(disabledTraits.ContainsKey("TestTraitConflictA"), Is.True, "Trait with conflicts should be rejected");
});
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task RespectsCategoryLimits()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
// TestCategoryLimited has maxTraits: 2
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
{
"TestTraitLimited1",
"TestTraitLimited2",
"TestTraitLimited3",
};
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task RespectsCategoryPointLimits()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
// TestCategoryLimited has maxPoints: 10, each trait costs 5
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
{
"TestTraitLimited1",
"TestTraitLimited2",
"TestTraitLimited3", // This would exceed the 10 point limit
};
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxPoints limit");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task ChecksConditionsOnSpawn()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
entMan.AddComponent<HungerComponent>(player);
// Trait requires HungerComponent
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
{
"TestTraitHasComp",
};
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.True, "Trait with met condition should be valid");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task RejectsTraitsWithUnmetConditions()
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
await server.WaitAssertion(() =>
{
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
// Player does NOT have HungerComponent
// Trait requires HungerComponent
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
{
"TestTraitHasComp",
};
var traitSys = entMan.System<TraitSystem>();
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
BindingFlags.NonPublic | BindingFlags.Instance);
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
Assert.That(validTraits?.Contains("TestTraitHasComp"),
Is.False,
"Trait with unmet condition should be rejected");
entMan.DeleteEntity(player);
});
await pair.CleanReturnAsync();
}
#endregion
#region Helper Methods
private static TraitConditionContext CreateContext(
IEntityManager entMan,
IPrototypeManager protoMan,
IComponentFactory factory,
EntityUid player,
ProtoId<JobPrototype>? jobId = null,
ProtoId<SpeciesPrototype>? speciesId = null)
{
return new TraitConditionContext
{
Player = player,
Session = null,
EntMan = entMan,
Proto = protoMan,
CompFactory = factory,
LogMan = IoCManager.Resolve<ILogManager>(),
JobId = jobId,
SpeciesId = speciesId,
};
}
private static TraitEffectContext CreateEffectContext(
IEntityManager entMan,
IPrototypeManager protoMan,
IComponentFactory factory,
EntityUid player)
{
return new TraitEffectContext
{
Player = player,
EntMan = entMan,
Proto = protoMan,
CompFactory = factory,
LogMan = IoCManager.Resolve<ILogManager>(),
Transform = entMan.GetComponent<TransformComponent>(player),
};
}
#endregion
}
+39
View File
@@ -0,0 +1,39 @@
// File to store DeltaV-specific database models
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database;
// ReSharper disable once InconsistentNaming
public static class DVModel
{
/// <summary>
/// Stores tip dismissal preferences for a player, tracking which tips they've marked
/// as "Don't show again".
/// </summary>
[Table("dv_seen_tips")]
[Index(nameof(PlayerUserId), nameof(TipProtoId), IsUnique = true)]
public class SeenTip
{
public int Id { get; set; }
/// <summary>
/// The player's user ID (GUID). References the Player table.
/// </summary>
public Guid PlayerUserId { get; set; }
/// <summary>
/// The prototype ID of the tip that was dismissed.
/// </summary>
[MaxLength(64)]
public string TipProtoId { get; set; } = string.Empty;
/// <summary>
/// When the tip was dismissed.
/// </summary>
public DateTime DismissedAt { get; set; }
}
}
File diff suppressed because it is too large Load Diff

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