Compare commits

..
Author SHA1 Message Date
Vanessa 5ed135c796 lol 2026-06-29 10:21:38 -05:00
BriBroooandpre-commit-ci[bot] 448b755b75 Fixes messageId console errors (#6143)
* Fixes messageId console errors

Replaced paper.yml HydroCo's notices in the paper-misc.ftl file and moved the [bold] to the last line to prevent error messages in the console

* [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-06-29 09:59:09 -05:00
Delta-V bot 977e47b434 Automatic changelog update 2026-06-29 16:06:38 +02:00
Vanessa ee18b71805 Various Crew Monitor Fixes/Tweaks (#6139)
* Various Crew Monitor Fixes/Tweaks

* comment

* comment

* Increased wireless ranges
2026-06-29 14:06:18 +00:00
29b64c1f7e Suffix and other entity naming megafix (#6091)
* Suffix and other entity naming megafix

* migration

* others

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-06-29 14:00:08 +00:00
Delta-V bot a99d488b04 Automatic changelog update 2026-06-29 15:39:43 +02:00
f18d6803b9 PORT: DV's Pool Episode - Longue Chairs + Swim Rings + Pool Tiles & Water (#6113)
* Pool-Themed Decorations: Lounge Chairs, Umbrellas, Tiles, Swim Rings, and Pool Water (#2071)

<!-- Guidelines:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->
<!-- NOTE: All code submitted to this repository is ALWAYS licensed
under the AGPL-3.0-or-later license.
The REUSE Specification headers or separate .license files indicate a
secondary license (e.g., MPL or MIT) solely to facilitate
integration for projects that do not use the AGPL license. This
secondary license does not replace the fact that AGPL-3.0-or-later
remains the primary and binding license.
Uncomment and modify the following line if you wish to change the
license from the default of AGPL.-->
<!--- LICENSE: AGPL -->
<!-- What did you change? -->

This PR adds a bunch of, well, mapper toys basically, but normal players
can obtain them too. They are:
- Pool tile and dark pool tile; obtainable via the cutter machine.
- Pool water; unobtainable without mapping/admin tools, but it's 1%
chlorine.
- Lounge chairs, constructed with steel and cloth. Deconstructible with
a screwdriver.
- Beach umbrellas, constructed with rods and cloth. Deconstructible with
a screwdriver.
- Swim rings, which can be ordered from Logistics. They're sort of like
the emergency comfort units (pillows), but they're inflatable rings and
you can pick them up.

<!-- Discuss how this would affect game balance or explain why it was
changed. Link any relevant discussions or issues. -->
These assets are needed for an upcoming map I'm making and I'm giving
them to the community early out of the kindness of my heart

<!-- Summary of code changes for easier review. -->
All YML and sprites babyyy

- Entity prototypes for chairs, umbrellas, pool water, swim rings, and
the pool tile stack items
- Some construction graphs
- A cargo product
- A floor definition
- Recipes for the tile cutter

Pool water IS drinkable because it's extremely funny. Keep in mind that
50u of pool water will only give you like 2 poison damage

<!-- Attach media if the PR makes ingame changes (clothing, items,
features, etc).
Small fixes/refactors are exempt. Media may be used in SS14 progress
reports with credit. -->

Preview of the stuff:

<img width="713" height="541" alt="image"
src="https://github.com/user-attachments/assets/42f1acc8-cc83-4356-a8e0-3c715245d120"
/>

<img width="697" height="666" alt="image"
src="https://github.com/user-attachments/assets/8597017a-9d03-4419-80c5-cf475f3206d1"
/>

sorry i forgot the pink ring

<img width="253" height="179" alt="image"
src="https://github.com/user-attachments/assets/7f738f8e-42b4-4ee4-aa75-9f08b67ded0c"
/>

sorry i forgot the white chair

<img width="206" height="140" alt="image"
src="https://github.com/user-attachments/assets/6fabe541-4821-4e88-8d64-a99f463c5253"
/>

Cargo:

<img width="633" height="461" alt="image"
src="https://github.com/user-attachments/assets/b8ae9b89-4d2f-4326-9814-d000d5e88809"
/>

Crafting menu:

<img width="266" height="454" alt="image"
src="https://github.com/user-attachments/assets/cc480ee6-0e0d-4055-93c3-e5a1c4e0687c"
/>

<img width="409" height="211" alt="image"
src="https://github.com/user-attachments/assets/41970a43-1ba8-4898-a96c-deb87a05a409"
/>

<img width="409" height="211" alt="image"
src="https://github.com/user-attachments/assets/ffc00874-e330-49eb-8aba-972c6d223a3a"
/>

Pool water:

<img width="680" height="463" alt="image"
src="https://github.com/user-attachments/assets/9abc48b0-8358-46de-89ef-41fc64989787"
/>

<!-- Confirm the following by placing an X in the brackets [X]: -->
- [x] I have read and am following the [Pull Request and Changelog
Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [x] I have added media to this PR or it does not require an ingame
showcase.
- [x] I have tested any changes or additions.
- [x] I can confirm this PR contains no AI-generated content, and did
not use any AI-generated content.
<!-- You should understand that not following the above may get your PR
closed at maintainer’s discretion -->

<!-- List any breaking changes, including namespaces, public
class/method/field changes, prototype renames; and provide instructions
for fixing them.
This will be posted in #codebase-changes. -->

Nope

**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.
Changelog must have a 🆑 symbol, so the bot recognizes the changes and
adds them to the game's changelog. -->
🆑
- add: 13 colors of lounge chairs can be constructed out of steel and
cloth.
- add: 8 colors of beach umbrellas can be constructed out of rods and
cloth.
- add: Pool tile (and dark pool tile) can be produced at the cutter
machine.
- add: Logistics can now order a crate of bulk swim rings, in 11
possible colors!
- add: For mappers (and admins): there is a new "pool water" entity that
is a nice green-cyan color and contains about 1% chlorine.

---------

Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>

* Remove Umbrella + Fixes from Port

* Pool Ring Additions

* Buff Fishing Vendor

* grr

* its no longer 2am

---------

Co-authored-by: portfiend <109661617+portfiend@users.noreply.github.com>
Co-authored-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
2026-06-29 08:39:22 -05:00
Delta-V bot db38e48754 Automatic changelog update 2026-06-29 15:27:30 +02:00
MD360 3acbf512be Borgs must amogus. (#6126)
* We must scream

* +1 License

* Moved to _DV audio

* + Comment

* forgot space
2026-06-29 13:27:10 +00:00
pathetic meowmeowandVanessa cb956f605f Use the actual backported engine fix (#6138)
* Revert "I hate submodules. Why did it have to be submodules? (#6117)"

This reverts commit d3b13a6c83.

* from one workaround to another

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-06-29 13:43:52 +02:00
Delta-V bot e6e094a7a1 Automatic changelog update 2026-06-29 11:46:17 +02:00
Manoob b7d8476473 Make pigs drop bacon and adjust a few recipes to do with bacon/pork (#6122)
* Make pigs drop bacon and adjust a few recipes to do with bacon/pork

* Only one piece of bacon for rice and pork!

Signed-off-by: Manoob <manu.demoen@gmail.com>

* Make pigs drop bacon AND meat

* Add 3 bacon to the chef-vend inventories

---------

Signed-off-by: Manoob <manu.demoen@gmail.com>
2026-06-29 11:45:57 +02:00
Delta-V bot 3df4bfaeb7 Automatic changelog update 2026-06-29 08:05:44 +02:00
Apachito 29b46efa16 Rebalanced Tide Crafting (#5818) 2026-06-29 02:05:24 -04:00
Vanessa e7607e1227 I love submodules. Always use submodules. (#6119)
Revert "I hate submodules. Why did it have to be submodules? (#6117)"

This reverts commit d3b13a6c83.
2026-06-29 00:16:42 -05:00
Delta-V bot 460dbe1fc1 Automatic changelog update 2026-06-29 02:54:28 +02:00
pathetic meowmeow c7c6f1a67a Add nodecrawling, implement ventcrawl with it (#5830)
* node crawling movement

* more node crawling

* quality assurance

* do-after it

* i'm ratted king

* we don't need to serialize node stuff

* rename and ser/de fix

* review fixes
2026-06-29 00:54:08 +00:00
Delta-V bot 495107e5e0 Automatic changelog update 2026-06-28 21:15:54 +02:00
pathetic meowmeowandpre-commit-ci[bot] 8366dde288 Rewrite ion laws (#6121)
* Rewrite ion laws

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

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

* edits

* get gooned

* jumpsuit clarity

* review and tests

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-28 21:15:35 +02:00
Delta-V bot 5f9315f43b Automatic changelog update 2026-06-28 18:42:54 +02:00
Stxcking c21ddfeb41 Tweaks: New Fishing Vendor Stock and Price Changes (#6114)
* man

* Update fishing_points.yml
2026-06-28 11:42:34 -05:00
Delta-V bot 9b0eef0805 Automatic changelog update 2026-06-28 18:42:21 +02:00
StxckingandRadezolid 36bc3d0b04 The Disappearance of Hatsune Miku (#5790)
* I HATE FUN

* uh

* make it actually parent

* Update Resources/Prototypes/_DV/Catalog/Fills/Boxes/general.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: Stxcking <217132821+Stxcking@users.noreply.github.com>

---------

Signed-off-by: Stxcking <217132821+Stxcking@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
2026-06-28 11:42:01 -05:00
Delta-V bot e9b6634747 Automatic changelog update 2026-06-28 15:26:15 +02:00
Vape d1390f5e98 Update tips.ftl (#6133) 2026-06-28 15:25:56 +02:00
Delta-V bot 196e2a52e3 Automatic changelog update 2026-06-28 14:19:16 +02:00
Proxy also works :3 401ae01321 Reduce Holoparasite Cost (#6115)
Update uplink_catalog.yml
2026-06-28 14:18:56 +02:00
github-actions[bot]andDeltaV-Bot 793236f12b Update Credits (#6135)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2026-06-28 04:52:00 +02:00
Delta-V bot 229491aa46 Automatic changelog update 2026-06-28 02:39:01 +02:00
Vape a09e09ac0f Remove mention of "morphotypes" (#6134) 2026-06-27 19:38:41 -05:00
Delta-V bot e59b37b081 Automatic changelog update 2026-06-27 22:18:26 +02:00
Vape b359f44c15 Fix: Fixes to both Harpy and Moth (#6131)
Bug Fix
2026-06-27 15:18:06 -05:00
Delta-V bot 932dd5e429 Automatic changelog update 2026-06-26 19:23:56 +02:00
wesker 3001253e43 Synthetic Emotes Fix (#6125)
* separate synthetic emote

* fix duplicate IDs (server error)

* Update speech_emotes.yml

* maybe this

* maybe keep the removed things from upstream lol

* imagine yamlops-ing when the problem was your trash code all along
2026-06-26 19:23:37 +02:00
Vanessa d3b13a6c83 I hate submodules. Why did it have to be submodules? (#6117)
Living on a prayer.
2026-06-25 15:07:33 -05:00
Delta-V bot 2ab0163598 Automatic changelog update 2026-06-25 20:04:07 +02:00
pathetic meowmeow 6cdeb3d9cf Suit sensors, but in space? Perish the thought. (#6054) 2026-06-25 12:59:49 -05:00
Delta-V bot d7543bd24a Automatic changelog update 2026-06-25 18:17:46 +02:00
f7f800c439 Tweaked Synthetic (#6090)
* actually not overhauled but tweaked i think

* forgot to stage this one

* make shocking 60% chance instead of 80%, and make shock dmg 5-20 random.

* update comment

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

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

* okay this should work + remember to mention new changesa asdas asd

* I ALREADY STAGED ALL

* important summary comments to give more important info when someone's
too lazy to check the rule

* fix some stuff

* signs of shock

* Update Resources/Prototypes/_DV/Traits/meta.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: wesker <53323309+weskified@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Traits/meta.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: wesker <53323309+weskified@users.noreply.github.com>

* Update Resources/Locale/en-US/_DV/traits/traits.ftl

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: wesker <53323309+weskified@users.noreply.github.com>

* changessss

---------

Signed-off-by: wesker <53323309+weskified@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-06-25 11:17:26 -05:00
Delta-V bot b6ed84325f Automatic changelog update 2026-06-25 17:37:53 +02:00
ManoobandVanessa 61aee79dc2 Replace nachos recipes (#6099)
* make nachos from tacos and change how cheesy and chili variants get made

* Change WizDen Contentfreeze recipelimit

* Update Resources/Prototypes/_DV/Recipes/Cooking/deep_fryer_recipes.yml

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

* Moved nacho metamorph to DV folder

* newlline, keep file as it was :^)

---------

Signed-off-by: Manoob <manu.demoen@gmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-06-25 10:37:33 -05:00
Delta-V bot 55fe6026ea Automatic changelog update 2026-06-25 17:28:10 +02:00
pathetic meowmeowandVanessa 6dfa12a509 AI? Being able to send faxes? Perish the thought. (#6064)
* AI? Being able to send faxes? Perish the thought.

* Update Resources/Prototypes/Entities/Mobs/Player/silicon.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-06-25 15:27:49 +00:00
Delta-V bot 485fd14f05 Automatic changelog update 2026-06-25 16:07:32 +02:00
Theo 031b37ffcb Washing machines can hold huge items (#6086)
washer size huge
2026-06-25 09:07:12 -05:00
Delta-V bot 06365bdbf0 Automatic changelog update 2026-06-25 15:10:58 +02:00
Tmanzxd 7f18c5f346 Commented out Surgeon Job Requirement for CMO (#6112) 2026-06-25 13:10:38 +00:00
Delta-V bot d407652eb0 Automatic changelog update 2026-06-25 04:28:46 +02:00
Manoob 9108e2c9b5 Fix Low metabolism rate modifiers from truncating to zero (#6104) 2026-06-24 21:28:26 -05:00
VelcroboyandVelcroboy c7ef614f3d Edit towel spawner (#6106)
* Edit towel spawner

* numbers

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-06-24 21:26:51 -05:00
VelcroboyandVelcroboy fa735f7669 Fix Chapel Access (#6105)
Fix chapel access

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-06-24 20:12:36 -05:00
Delta-V bot 6de6421636 Automatic changelog update 2026-06-25 00:03:18 +02:00
Vanessa 3cb24c8b68 Custom Client-Side Radio Colors (#6101) 2026-06-24 22:02:58 +00:00
Delta-V bot 9fad412075 Automatic changelog update 2026-06-24 15:15:21 +02:00
Vanessa fbc3104836 Fix Infinite Cloaking (#6085)
Fix infinite cloaking because I guess power draw doesn't count as using the battery charge.
2026-06-24 15:15:01 +02:00
Delta-V bot e231d9d67c Automatic changelog update 2026-06-24 15:08:40 +02:00
Stxcking 468d929c4b Tweak: AAC Subject Button Colors Return + Words Capitalized (#5567)
* Nuked StyleNano + Added colors to sheetlet

SubjectButtonColors table to define colors (same hex as the ones in StyleNano)
SubjectColorRules to loop rules for applying colors

* Capitalize every single phrase

CONSISTENCY RAHH
2026-06-24 15:08:20 +02:00
Delta-V bot 2f1aaff32c Automatic changelog update 2026-06-24 00:35:07 +02:00
Vanessa 4f49ccde68 Sleepers no longer have an annoucement (#6066)
* Sleepers no longer have an annoucement.

* NT sleepers too.
2026-06-23 18:34:47 -04:00
Vanessaandpre-commit-ci[bot] cb3b74e49e Station-Specific Department Cleanup (#6078)
* Removed some jobs from station specific jobs because they are on all stations.

* Department Prototype Cleanup

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

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

* Actually fix brigmedic on the crew monitor

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-23 18:34:44 -04:00
Vanessaandpre-commit-ci[bot] 7d15a39a77 Fixed emergency access locale string (#6044)
* Fixed emergency access locale string

* [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-06-23 18:23:22 -04:00
Delta-V bot 3ac816fee1 Automatic changelog update 2026-06-24 00:19:37 +02:00
Delta-V bot b909d9a2e5 Automatic changelog update 2026-06-23 17:27:27 +02:00
Theo 7547752159 Broken bottle slicing (#6092)
add comps
2026-06-23 10:27:07 -05:00
Manoob 9b7819288e Added a fix that resets the cook timers on fryer power changed (#6075) 2026-06-23 09:17:06 -05:00
Delta-V bot 424a4569d8 Automatic changelog update 2026-06-23 14:50:11 +02:00
wesker 12827c0e98 TraitSystem require fix (#6097)
how did i even forget this
2026-06-23 12:49:51 +00:00
Delta-V bot d1e61ba0a2 Automatic changelog update 2026-06-23 12:40:37 +02:00
652e52dbca Nuke/CosCult Global Music and SFX (#5918)
* Extended ServerGlobalSoundSystem and exposed its lies. Now its actually global.

* Fixed Epsilon event rule namespacing and formatting.

* Cosmic Cult Music Updates

* Few more CosCult changes.

* Nuke and Alert Changes

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

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

* Update Content.Server/_DV/CosmicCult/EntitySystems/CosmicChantrySystem.cs

Co-authored-by: Sir Warock <67167466+SirWarock@users.noreply.github.com>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>

* Update Content.Server/_DV/Audio/ServerGlobalSoundSystem.cs

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.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: Sir Warock <67167466+SirWarock@users.noreply.github.com>
2026-06-23 12:40:17 +02:00
Delta-V bot e7fe4865de Automatic changelog update 2026-06-22 20:06:07 +02:00
A111Vojta 84b600a39f Harmony lanyards (#6035)
* Lanyards Part1

* Lanyards Part 2

* Lanyards Part 3

* Fix

* Lanyards - Adding Comments

* replace plaintext with LocIds

* expanded the lanyard slot blacklist
2026-06-22 20:05:47 +02:00
Delta-V bot 31201c44ba Automatic changelog update 2026-06-22 18:48:51 +02:00
Theo 4f9f93eb87 Slugs structural increase TWO! (#6068)
* number up

* comment formatting
2026-06-22 11:48:31 -05:00
Delta-V bot f8a4d823b9 Automatic changelog update 2026-06-22 13:37:10 +02:00
Proxy also works :3 c61d2c6a58 guidebook shields (#6070) 2026-06-22 06:36:50 -05:00
Delta-V bot e25181e1bc Automatic changelog update 2026-06-22 13:11:55 +02:00
7c052442a8 Traits UI Modifications, Visuals, and a new Condition (#6082)
* traits ui things

* fluent

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

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

* Update Resources/Locale/en-US/_DV/traits/traits-ui.ftl

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: wesker <53323309+weskified@users.noreply.github.com>

* i hate toby

* newline

* pattern matching

* made simpler

* 2sleepy4myowngood

* kl;jasglkknf.,/.sa/.mmmmmmmmm

---------

Signed-off-by: wesker <53323309+weskified@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-06-22 11:11:35 +00:00
Delta-V bot 19b0de35dd Automatic changelog update 2026-06-22 10:46:57 +02:00
wesker 1c66c1fe2e Fix 8 0'Clock Ghosts to be Mono (#6084)
* fix jukebox to be mono

* ACTUALLY fix this, holy shit audacity
2026-06-22 10:46:37 +02:00
Delta-V bot ef46675a2f Automatic changelog update 2026-06-22 06:23:31 +02:00
Halo3moth c0b6929c9a lower pitbull vent spawn rate (#6081) 2026-06-22 04:23:12 +00:00
Vanessa c7aa50cf85 Update C# Contribution Examples and Guidelines (#6039)
Updated C# guidelines.
2026-06-21 15:59:47 +02:00
Delta-V bot c69fb99600 Automatic changelog update 2026-06-21 15:48:33 +02:00
VelcroboyandVelcroboy 3ca37d9c2b Map Edits: The Hive (#6018)
* Map Edits: The Hive

* review changes

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2026-06-21 15:48:12 +02:00
github-actions[bot]andDeltaV-Bot 8ba993d1a9 Update Credits (#6072)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2026-06-21 05:30:08 +02:00
Delta-V bot edd1500bf3 Automatic changelog update 2026-06-20 19:14:31 +02:00
einknusprigestoastandpre-commit-ci[bot] 2ec64360b7 ERT Security Loadout overhaul (#6049)
* ERT Selectors Amber Sec

* Added SRT Loadouts

* Added SRT Loadouts

* Added ERT selectors

* ERT Loadout tweaks

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

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

* Changed Omnitool to be ToolDebug + Multitool

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-20 19:14:11 +02:00
Delta-V bot e0540c6205 Automatic changelog update 2026-06-20 17:02:34 +02:00
7953b0469a I SCREAM, YOU SCREAM, WE ALL SCREAM FOR ICE CREAM (#5943)
* Initial commit.

* Minor tweaks to names, added caramel bottle.

* Fixing open state of caramel bottle.

* JSON cleanup.

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

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

* Minor tweak to cookie dough ice cream recipe. Removed unused sprites.

* Swapping out updated sprites for rainbow sherbet, tropical sundae, neapolitan.

* Removing plushies.zip.

* Moved frozen treats to their own guidebook section under drinks to reduce clutter.

* Fixing flavor references, adding icecreamflavors.yml to contain definitions, corrected loadout ID references in loadoutgroups.

* Committing loadout changes that slipped through the cracks.

* Corrected casing in reference to RSI.

* Changed recipe for caramel to be just sugar to prevent conflict with Sigynate's recipe.

* So I'm kinda dumb and somehow it didn't occur to me that making sugar alone yield caramel would actually not only not fix the issue with Syginate but also wreck it for so many other chems, oopsie daisy, we're swapping it for Saline for a more shelf-stable salted caramel sauce.

* Tweaking ratio for Caramel, also, what are the odds that the tempermental build just works after this?

* At this point I'm just curious if a different build will fail this time.

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

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

* Update Resources/Prototypes/Catalog/VendingMachines/Inventories/bardrobe.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/Catalog/VendingMachines/Inventories/bardrobe.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/Catalog/VendingMachines/Inventories/boozeomat.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/Catalog/VendingMachines/Inventories/boozeomat.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Clothing/Uniforms/jumpsuits.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/_DV/Entities/Objects/Consumable/Drinks/frozen_treats.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Update Resources/Prototypes/Guidebook/references.yml

Co-authored-by: Radezolid <snappednexus@gmail.com>
Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>

* Review changes

* Pascal-ify-ing flavor IDs.

* Update Resources/Prototypes/_DV/Entities/Objects/Consumable/Drinks/frozen_treats.yml

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

---------

Signed-off-by: robobozu <240598948+robobozu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2026-06-20 15:02:13 +00:00
Delta-V bot a3d3d23600 Automatic changelog update 2026-06-20 16:24:55 +02:00
pathetic meowmeow 5ccfce1e7f NanoChat? Outside the same grid? Perish the thought. (#6057) 2026-06-20 09:24:35 -05:00
Delta-V bot 879cbf43f5 Automatic changelog update 2026-06-20 16:20:24 +02:00
einknusprigestoastandpre-commit-ci[bot] 9c84627db5 Added CentComm Turrets (#6069)
* Added CentComm Turrets

* [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-06-20 09:20:04 -05:00
707 changed files with 19484 additions and 5655 deletions
+1
View File
@@ -32,6 +32,7 @@ jobs:
run: |
cd RobustToolbox/
git submodule update --init --recursive
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
+1
View File
@@ -29,6 +29,7 @@ jobs:
run: |
cd RobustToolbox
git fetch --depth=1
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Install dependencies
run: dotnet restore
+1
View File
@@ -23,6 +23,7 @@ jobs:
run: |
cd RobustToolbox/
git submodule update --init --recursive
rm Robust.Shared.Tests/Networking/NetEncryptionDoSTest.cs # DeltaV - work around bug in the backport
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
+78 -18
View File
@@ -73,28 +73,88 @@ If you are adding a lot of C# code, then take advantage of partial classes. Put
Otherwise, **add comments on or around any changed lines.**
A comment on a new imported namespace:
### Single-Line Changes
Format should look like this.
```cs
using Content.Server.Psionics.Glimmer; // DeltaV
/* Importing Namespaces - Include optional comment if its not obvious what its being used for. */
using Content.Server._DV.Psionics.Glimmer; // DeltaV
using Content.Shared.Damage.Systems; // DeltaV - Addition of HandHeldArmor
/* Changing an upstream line - Same line as the change */
if (!TryComp<EyeComponent>(ent, out var eye) || _disabled) // DeltaV - check if disabled
/* Adding - Either same line or above the line. */
EnsureComp<PotentialPsionicComponent>(entity); // Deltav - Psionics
/* "Deleting" - Don't actually delete, just comment out and say why. This only applies to upstream code. */
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
```
A pair of comments enclosing a block of added code:
> * Its pretty obvious in the example above that importing `Content.Server._DV.Psionics.Glimmer` means we'll be interacting with glimmer so putting `// DeltaV - Add Glimmer` is needlessly redundant.
> * It's not as obvious what the `Content.Shared.Damage.Systems` namespace is used for, since its so broad, so adding a comment what feature is using it helps.
> * Actual code changes should almost always include the comment after ``// DeltaV`.
### Multi-Line Changes
Depending on how much you are editing, putting a comment on EACH line may be excessive, so if you have a larger block of code you are changing, denote it like so:
```cs
private EntityUid Slice(...)
// BEGIN DeltaV - Remove innate radio and radios from pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
{
...
_transform.SetLocalRotation(sliceUid, 0);
// DeltaV - start of deep frier stuff
var slicedEv = new FoodSlicedEvent(user, uid, sliceUid);
RaiseLocalEvent(uid, ref slicedEv);
// DeltaV - end of deep frier stuff
...
if (_inventory.TryGetSlotEntity(target, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(target, $"pocket{i}", true, true);
}
RemComp<ActiveRadioComponent>(target); // If the zombie has an innate radio, get rid of it.
// END DeltaV
```
> * Denoting these with a BEGIN and END clearly shows they are block of code without having to read the entire comment. This makes it easier to tell when you're dealing with single-line comments versus a block with merging in conflicts.
> * Case and order of the first two words is less of a concern. `// DeltaV Begin` or `// Begin DeltaV` will work fine too.
> * Try to make your blocks as small as possible, but use your discretion.
> * If you deleting multiple lines, use line comments (``//``) if its a few lines but if its a larger block (like commenting out an entire function), it is preferable to use block comments (`/* */`).
#### Soft Exceptions to the Multi-Line "Rules"
Some multi-line changes can use a single-line comment in certain scenarios. But if you are UNSURE, just use `// BEGIN DeltaV` and `// END DeltaV` comments like the previous section does and it'll be fine.
I'll give some examples.
```cs
/* This change comments out 3 lines but only needs a single line comment because commenting out the if statement implies that its logic will be commented out too. */
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
// {
// _profileEditor.RefreshTraits();
// }
/* Same principle here. This adds two lines but the if statement implies the next line so commenting both lines isn't really needed. */
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
```
### New Methods or Component Variables
Sometimes, you'll need to implement a whole new method or component variable and instead of wrapping it in `// BEGIN DeltaV` and `// END DeltaV`, you can just denote that it's a DeltaV function in the summary block before the function. This denotes the WHOLE function as a DeltaV addition.
```cs
/* New Method Example */
/// <summary>
/// DeltaV - Handle revealing ninja if cloaked when attacked by a hitscan attack.
/// </summary>
private void OnNinjaAttacked(Entity<SpaceNinjaComponent> ent, ref DamageChangedEvent args)
{
...
}
/* New Component Variable Example */
/// <summary>
/// DeltaV - If disabled the action will not disable when no charges remain. Use if you want to handle no charges differently.
/// </summary>
[DataField]
public bool DisableWhenEmpty = true;
```
In short:
* Use `// BEGIN DeltaV` and `// END Delta` to denote a *block* of changes.
* Keep blocks as small as possible.
* Use `// DeltaV` on or before the line if its not a block of changes.
* Use exceptions when they make sense.
### Changing Upstream Localization Fluent .ftl files
**Move all changed locale strings to a new DeltaV file** - use a `.ftl` file in the `_DV` folder. Comment out the old strings in the upstream file, and explain that they were moved.
@@ -104,10 +164,10 @@ Example:
Commented out old string in `Resources\Locale\en-US\xenoarchaeology\artifact-analyzer.ftl`
```
# DeltaV - moved to _DV file
#analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
#}[/color][/font]
# analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
# }[/color][/font]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`
+10
View File
@@ -7,6 +7,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Radio.Components; // DeltaV - Client-side Radio Colors
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -158,6 +159,15 @@ namespace Content.Client.Examine
// since there's probably one open already if it's coming in from the server.
var entity = GetEntity(ev.EntityUid);
// BEGIN DeltaV - Client-Side Radio Colors
// This *seems* a bit hacky, but we basically don't want to overwrite what's been pushed on the client-side,
// because the server will send us the server-side colors (default). I've yet to find a scenario where the
// server and client don't match, so until then, this is probably fine, if a little hacky.
if (HasComp<EncryptionKeyHolderComponent>(entity) || HasComp<EncryptionKeyComponent>(entity))
return;
// END DeltaV
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
}
@@ -1,5 +1,6 @@
using Content.Client.Pinpointer.UI;
using Robust.Client.Graphics;
using Robust.Client.Player; // DeltaV
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
@@ -7,6 +8,9 @@ namespace Content.Client.Medical.CrewMonitoring;
public sealed partial class CrewMonitoringNavMapControl : NavMapControl
{
[Dependency] private IPlayerManager _playerManager = default!; // DeltaV
private readonly SharedTransformSystem _xform; // DeltaV
public NetEntity? Focus;
public Dictionary<NetEntity, string> LocalizedNames = new();
@@ -15,6 +19,9 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
public CrewMonitoringNavMapControl() : base()
{
IoCManager.InjectDependencies(this); // DeltaV - Gotta inject!
_xform = EntManager.System<SharedTransformSystem>(); // DeltaV - Gotta inject!
WallColor = new Color(192, 122, 196);
TileColor = new(71, 42, 72);
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
@@ -77,4 +84,27 @@ public sealed partial class CrewMonitoringNavMapControl : NavMapControl
_trackedEntityLabel.Text = string.Empty;
_trackedEntityPanel.Visible = false;
}
/// <summary>
/// DeltaV - Do some things before the base NavMap Draw and also force the redraw of the map
/// after if the MapUid was null when the CrewMonitor UI was opened.
/// </summary>
/// <param name="handle"></param>
protected override void Draw(DrawingHandleScreen handle)
{
// MapUid will not have a value if the crew monitor UI was activated off-grid.
var forceMapUpdate = false;
if (!MapUid.HasValue && _playerManager.LocalEntity is { } player)
{
MapUid = _xform.GetGrid(player);
forceMapUpdate = true;
}
base.Draw(handle);
// We need to call this after Draw() because UpdateNavMap has functions that uses variables
// set in Draw()
if (forceMapUpdate)
UpdateNavMap();
}
}
@@ -1,13 +1,16 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Medical.CrewMonitoring"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:crewMonitoring="clr-namespace:Content.Client._DV.CrewMonitoring"
Title="{Loc crew-monitoring-ui-title}"
Resizable="False"
SetSize="1210 700"
MinSize="1210 700">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True">
<ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20"/>
<ui:CrewMonitoringNavMapControl Name="NavMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20" Visible="False" />
<crewMonitoring:CrewMonitoringShuttleControl Name="ShuttleMap" HorizontalExpand="True" VerticalExpand="True" Margin="5 20" Visible="False" />
<BoxContainer Orientation="Vertical" Margin="0 0 10 0">
<controls:StripeBack>
<PanelContainer>
@@ -5,6 +5,7 @@ using Content.Client.Pinpointer.UI;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Medical.SuitSensor;
using Content.Shared.Station; // DeltaV - crew monitoring radar
using Content.Shared.StatusIcon;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
@@ -27,6 +28,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SharedTransformSystem _transformSystem;
private readonly SpriteSystem _spriteSystem;
private readonly SharedStationSystem _station; // DeltaV - crew monitoring radar
private NetEntity? _trackedEntity;
private bool _tryToScrollToListFocus;
@@ -39,8 +41,10 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
_transformSystem = _entManager.System<SharedTransformSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
_station = _entManager.System<SharedStationSystem>(); // DeltaV - crew monitoring radar
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
ShuttleMap.NavMap = NavMap; // DeltaV - crew monitoring radar
}
public void Set(string stationName, EntityUid? mapUid)
@@ -50,8 +54,10 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
NavMap.MapUid = xform.GridUid;
else
NavMap.Visible = false;
// Begin DeltaV - handle navmap visibility based on current position
// else
// NavMap.Visible = false;
// End DeltaV - handle navmap visibility based on current position
StationName.AddStyleClass("LabelBig");
StationName.Text = stationName;
@@ -62,6 +68,28 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
{
base.FrameUpdate(args);
// Begin DeltaV - handle navmap visibility based on current position
var ent = ShuttleMap.Owner;
// If an entity is being tracked, use them as the focus, since they may be off-station
// and we want to use the nav map if they are.
if (_trackedEntity is { } trackedEntity)
ent = _entManager.GetEntity(trackedEntity);
// We need to make sure the entity is valid because for some reason, it *could* be an
// invalid entity and GetOwningStation will throw an exception.
if (ent.HasValue && ent.Value.IsValid() && _station.GetOwningStation(ent) is not null)
{
NavMap.Visible = true;
ShuttleMap.Visible = false;
}
else
{
NavMap.Visible = false;
ShuttleMap.Visible = true;
}
// End DeltaV - handle navmap visibility based on current position
if (_tryToScrollToListFocus)
TryToScrollToFocus();
}
@@ -69,6 +97,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
public void ShowSensors(List<SuitSensorStatus> sensors, EntityUid monitor, EntityCoordinates? monitorCoords)
{
ClearOutDatedData();
ShuttleMap.Owner = monitor; // DeltaV - crew monitoring radar
// No server label
if (sensors.Count == 0)
@@ -304,7 +333,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
jobContainer.AddChild(jobLabel);
// Add user coordinates to the navmap
if (coordinates != null && NavMap.Visible && _blipTexture != null)
if (coordinates != null && _blipTexture != null) // DeltaV - don't filter the navmap visibility
{
NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid,
new NavMapBlip
@@ -323,6 +352,14 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
if (_trackedEntity == sensor.SuitSensorUid)
{
_trackedEntity = null;
// BEGIN DeltaV - center on crew monitor user if deselecting
// This fixes some weird bug where if you deselect a tracked entity, it will
// just not draw the map at all in some cases.
if (ShuttleMap.Owner is { } handheld &&
_transformSystem.TryGetMapOrGridCoordinates(handheld, out var userCoords) &&
userCoords is { } coords)
NavMap.CenterToCoordinates(coords);
// END DeltaV
}
else
@@ -10,6 +10,7 @@
<tabs:AudioTab Name="AudioTab" />
<tabs:AccessibilityTab Name="AccessibilityTab" />
<tabs:AdminOptionsTab Name="AdminOptionsTab" />
<dtabs:RadioTab Name="RadioTab" /> <!-- DeltaV - Client-side Radio Options -->
<dtabs:DeltaTab Name="DeltaTab" /> <!-- DeltaV -->
</TabContainer>
</DefaultWindow>
@@ -21,7 +21,8 @@ namespace Content.Client.Options.UI
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-accessibility"));
Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-admin"));
Tabs.SetTabTitle(6, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
Tabs.SetTabTitle(6, Loc.GetString("ui-options-tab-radio")); // DeltaV - Radio Channel Colors
Tabs.SetTabTitle(7, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
UpdateTabs();
}
@@ -36,6 +37,7 @@ namespace Content.Client.Options.UI
AccessibilityTab.Control.ReloadValues();
AudioTab.Control.ReloadValues();
AdminOptionsTab.Control.ReloadValues();
RadioTab.Control.ReloadValues(); // DeltaV - Radio Channel Colors
DeltaTab.Control.ReloadValues(); // DeltaV
}
}
@@ -56,8 +56,8 @@ public partial class NavMapControl : MapGridControl
// Constants
protected float UpdateTime = 1.0f;
protected float MaxSelectableDistance = 10f;
protected float MinDragDistance = 5f;
public float MaxSelectableDistance = 10f; // DeltaV - accessible to CrewMonitoringShuttleControl
public float MinDragDistance = 5f; // DeltaV - accessible to CrewMonitoringShuttleControl
protected static float MinDisplayedRange = 8f;
protected static float MaxDisplayedRange = 128f;
protected static float DefaultDisplayedRange = 48f;
@@ -193,6 +193,13 @@ public partial class NavMapControl : MapGridControl
_recenter.Disabled = false;
}
// Begin DeltaV - we need access to that from outside
public void TrackEntity(NetEntity? entity)
{
TrackedEntitySelectedAction?.Invoke(entity);
}
// End DeltaV - we need access to that from outside
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
-111
View File
@@ -159,35 +159,6 @@ namespace Content.Client.Stylesheets
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
// DeltaV - AAC button variables
public static readonly string CommandButtonClass = "CommandButton";
public static readonly string EngineeringButtonClass = "EngineeringButton";
public static readonly string EpistemicsButtonClass = "EpistemicsButton";
public static readonly string JusticeButtonClass = "JusticeButton";
public static readonly string LogisticsButtonClass = "LogisticsButton";
public static readonly string MedicalButtonClass = "MedicalButton";
public static readonly string SecurityButtonClass = "SecurityButton";
public static readonly string ServiceButtonClass = "ServiceButton";
// DeltaV - AAC button colors
public static readonly Color CommandButtonColorDefault = Color.FromHex("#404A58");
public static readonly Color CommandColorHovered = Color.FromHex("#4F587B");
public static readonly Color EngineeringButtonColorDefault = Color.FromHex("#77684B");
public static readonly Color EngineeringColorHovered = Color.FromHex("#776D71");
public static readonly Color EpistemicsButtonColorDefault = Color.FromHex("#6F5973");
public static readonly Color EpistemicsColorHovered = Color.FromHex("#71638E");
public static readonly Color LogisticsButtonColorDefault = Color.FromHex("#61503A");
public static readonly Color LogisticsColorHovered = Color.FromHex("#675C64");
public static readonly Color JusticeButtonColorDefault = Color.FromHex("#4F3D4C");
public static readonly Color JusticeColorHovered = Color.FromHex("#5C4B5A");
public static readonly Color MedicalButtonColorDefault = Color.FromHex("#49687D");
public static readonly Color MedicalColorHovered = Color.FromHex("#556E95");
public static readonly Color SecurityButtonColorDefault = Color.FromHex("#724449");
public static readonly Color SecurityColorHovered = Color.FromHex("#745370");
public static readonly Color ServiceButtonColorDefault = Color.FromHex("#607952");
public static readonly Color ServiceColorHovered = Color.FromHex("#667A76");
// End DeltaV
public const string StyleClassCrewManifestGender = "CrewManifestGender"; //Delta-V - Manifest pronouns
// i'm not sure what the missing symbols were referencing, and this is getting obseleted anyway so:
@@ -1677,88 +1648,6 @@ namespace Content.Client.Stylesheets
BackgroundColor = FancyTreeSelectedRowColor,
}),
// DeltaV - AAC button styles
Element<ContainerButton>()
.Class(CommandButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, CommandButtonColorDefault),
Element<ContainerButton>()
.Class(CommandButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, CommandColorHovered),
Element<ContainerButton>()
.Class(EngineeringButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, EngineeringButtonColorDefault),
Element<ContainerButton>()
.Class(EngineeringButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, EngineeringColorHovered),
Element<ContainerButton>()
.Class(EpistemicsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, EpistemicsButtonColorDefault),
Element<ContainerButton>()
.Class(EpistemicsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, EpistemicsColorHovered),
Element<ContainerButton>()
.Class(LogisticsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, LogisticsButtonColorDefault),
Element<ContainerButton>()
.Class(LogisticsButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, LogisticsColorHovered),
Element<ContainerButton>()
.Class(MedicalButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, MedicalButtonColorDefault),
Element<ContainerButton>()
.Class(MedicalButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, MedicalColorHovered),
Element<ContainerButton>()
.Class(SecurityButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, SecurityButtonColorDefault),
Element<ContainerButton>()
.Class(SecurityButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, SecurityColorHovered),
Element<ContainerButton>()
.Class(ServiceButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, ServiceButtonColorDefault),
Element<ContainerButton>()
.Class(ServiceButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, ServiceColorHovered),
Element<ContainerButton>()
.Class(JusticeButtonClass)
.Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(Control.StylePropertyModulateSelf, JusticeButtonColorDefault),
Element<ContainerButton>()
.Class(JusticeButtonClass)
.Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(Control.StylePropertyModulateSelf, JusticeColorHovered),
// End DeltaV
// Silicon law edit ui
Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel)
.Prop(Label.StylePropertyFontColor, NanoGold),
@@ -32,6 +32,21 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
}
}
// Begin DeltaV - node crawling
private Type[] _types = new Type[] { };
[ViewVariables]
public Type[] Types
{
get => _types;
set
{
_types = value;
UpdateAll();
}
}
// End DeltaV - node crawling
public override void Initialize()
{
base.Initialize();
@@ -65,6 +80,17 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
var revealed = !covered || ShowAll || scannerRevealed;
// Begin DeltaV - node crawling
foreach (var type in _types)
{
if (!HasComp(uid, type))
continue;
revealed = true;
break;
}
// End DeltaV - node crawling
// set visibility & color of each layer
foreach (var layer in args.Sprite.AllLayers)
{
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions; // DeltaV - Client-Side Radio Channel Colors
using Content.Client.Administration.Managers;
using Content.Client.Chat;
using Content.Client.Chat.Managers;
@@ -839,8 +840,38 @@ public sealed partial class ChatUIController : UIController
}
}
/// <summary>
/// DeltaV - Regex expression to match on the chat message text in order to replace it with another color.
/// </summary>
private static readonly Regex RadioChatColorRegex = new(@"color=#?([a-fA-F0-9]{8})", RegexOptions.Compiled);
/// <summary>
/// DeltaV - Replaces the channel color hex code in a message with the client-side hex color
/// overridden by <see cref="Content.Client._DV.Radio.RadioChannelColorSystem"/> .
/// </summary>
/// <param name="message">The message to find and replace the color hex code in.</param>
/// <param name="channelProtoId">The proto ID of the channel. Basically the replacement color.</param>
/// <returns>The message with the color hex code replaced, if it exists.</returns>
private string ReplaceRadioChannelColorFromServer(string message, string channelProtoId)
{
if (_prototypeManager.Resolve<RadioChannelPrototype>(channelProtoId, out var channel))
{
return RadioChatColorRegex.Replace(message, $"color={channel.Color.ToHex()}");
}
return message;
}
public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
{
// BEGIN DeltaV
// Needs to happen before ANY other color or pattern matching so that we don't screw up the other colors in the message.
if (msg.Channel == ChatChannel.Radio && msg.RadioChannelProtoId is { } channel)
{
msg.WrappedMessage = ReplaceRadioChannelColorFromServer(msg.WrappedMessage, channel);
}
// END DeltaV
// color the name unless it's something like "the old man"
if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Whisper) && _chatNameColorsEnabled)
{
@@ -12,6 +12,19 @@ namespace Content.Client._DV.AACTablet.UI;
[CommonSheetlet]
public sealed class AACSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig
{
private static readonly (string StyleClass, Color Normal, Color Hover)[] SubjectButtonColors =
[
// Department colors
("CommandButton", Color.FromHex("#404A58"), Color.FromHex("#4F587B")),
("EngineeringButton", Color.FromHex("#77684B"), Color.FromHex("#776D71")),
("EpistemicsButton", Color.FromHex("#6F5973"), Color.FromHex("#71638E")),
("JusticeButton", Color.FromHex("#4F3D4C"), Color.FromHex("#5C4B5A")),
("LogisticsButton", Color.FromHex("#61503A"), Color.FromHex("#675C64")),
("MedicalButton", Color.FromHex("#49687D"), Color.FromHex("#556E95")),
("SecurityButton", Color.FromHex("#724449"), Color.FromHex("#745370")),
("ServiceButton", Color.FromHex("#607952"), Color.FromHex("#667A76")),
];
public override StyleRule[] GetRules(T sheet, object config)
{
// Create style boxes
@@ -81,6 +94,21 @@ public sealed class AACSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, I
.Prop(Control.StylePropertyModulateSelf, Color.DarkRed),
]);
SubjectColorRules(rules);
return rules.ToArray();
}
// AAC - Makes sure colors are applied to subject buttons
private static void SubjectColorRules(List<StyleRule> rules)
{
foreach (var (styleClass, normal, hover) in SubjectButtonColors)
{
rules.AddRange([
E<ContainerButton>().Class(ContainerButton.StyleClassButton).Class(styleClass)
.PseudoNormal().Prop(Control.StylePropertyModulateSelf, normal),
E<ContainerButton>().Class(ContainerButton.StyleClassButton).Class(styleClass)
.PseudoHovered().Prop(Control.StylePropertyModulateSelf, hover),
]);
}
}
}
@@ -218,7 +218,7 @@ public sealed partial class AACWindow : FancyWindow
MaxSize = new Vector2(ButtonWidth, ButtonWidth),
ClipText = false,
HorizontalExpand = true,
StyleClasses = { styleClass }
StyleClasses = { ContainerButton.StyleClassButton, styleClass }
};
var buttonLabel = new RichTextLabel
@@ -0,0 +1,180 @@
using System.Numerics;
using Content.Client.Pinpointer.UI;
using Content.Client.Shuttles.UI;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Client._DV.CrewMonitoring;
public sealed class CrewMonitoringShuttleControl : BaseShuttleControl
{
[Dependency] private readonly IMapManager _mapManager = default!;
private readonly SharedShuttleSystem _shuttles;
private readonly SharedTransformSystem _transform;
public EntityUid? Owner;
public NavMapControl? NavMap;
private List<Entity<MapGridComponent>> _grids = new();
public CrewMonitoringShuttleControl() : base(64f, 256f, 256f)
{
_shuttles = EntManager.System<SharedShuttleSystem>();
_transform = EntManager.System<SharedTransformSystem>();
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
DrawBacking(handle);
DrawCircles(handle);
if (Owner is not { } owner || NavMap is not { } navMap)
return;
var xformQuery = EntManager.GetEntityQuery<TransformComponent>();
var fixturesQuery = EntManager.GetEntityQuery<FixturesComponent>();
var bodyQuery = EntManager.GetEntityQuery<PhysicsComponent>();
var iffQuery = EntManager.GetEntityQuery<IFFComponent>();
if (!xformQuery.TryGetComponent(owner, out var xform)
|| xform.MapID == MapId.Nullspace)
{
return;
}
var coordinates = new EntityCoordinates(owner, Vector2.Zero);
var mapPos = _transform.ToMapCoordinates(coordinates);
var ourEntRot = Angle.Zero;
var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
Matrix3x2.Invert(ourEntMatrix, out var worldToShuttle);
var shuttleToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
var worldToView = worldToShuttle * shuttleToView;
var viewBounds = new Box2Rotated(new Box2(-WorldRange, -WorldRange, WorldRange, WorldRange).Translated(mapPos.Position), ourEntRot, mapPos.Position);
var viewAABB = viewBounds.CalcBoundingBox();
_grids.Clear();
_mapManager.FindGridsIntersecting(xform.MapID, new Box2(mapPos.Position - MaxRadarRangeVector, mapPos.Position + MaxRadarRangeVector), ref _grids, approx: true, includeMap: false);
foreach (var grid in _grids)
{
if (!fixturesQuery.HasComponent(grid))
continue;
var body = bodyQuery.GetComponent(grid);
iffQuery.TryComp(grid, out var iff);
if (!_shuttles.CanDraw(grid, body, iff))
continue;
var curGridToWorld = _transform.GetWorldMatrix(grid);
var curGridToView = curGridToWorld * worldToShuttle * shuttleToView;
var gridAABB = curGridToWorld.TransformBox(grid.Comp.LocalAABB);
if (!gridAABB.Intersects(viewAABB))
continue;
var labelColor = _shuttles.GetIFFColor(grid, self: false, iff);
DrawGrid(handle, curGridToView, grid, labelColor);
}
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
var box = SizeBox;
foreach (var (coord, value) in navMap.TrackedCoordinates)
{
if (!lit || !value.Visible)
continue;
var blipPos = _transform.ToMapCoordinates(coord);
if (mapPos.MapId == MapId.Nullspace)
continue;
handle.DrawCircle(Vector2.Clamp(Vector2.Transform(blipPos.Position, worldToView), box.TopLeft, box.BottomRight), float.Sqrt(MinimapScale) * 2f, value.Color);
}
foreach (var blip in navMap.TrackedEntities.Values)
{
if (blip.Blinks && !lit)
continue;
var blipPos = _transform.ToMapCoordinates(blip.Coordinates);
if (mapPos.MapId == MapId.Nullspace)
continue;
handle.DrawCircle(Vector2.Clamp(Vector2.Transform(blipPos.Position, worldToView), box.TopLeft, box.BottomRight), float.Sqrt(MinimapScale) * 2f, blip.Color);
}
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (Owner is not { } owner || NavMap is null)
return;
if (!EntManager.TryGetComponent<TransformComponent>(owner, out var xform) || xform.MapID == MapId.Nullspace)
{
return;
}
var ourEntRot = Angle.Zero;
var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
Matrix3x2.Invert(ourEntMatrix, out var worldToShuttle);
var shuttleToView = Matrix3x2.CreateScale(new Vector2(MinimapScale, -MinimapScale)) * Matrix3x2.CreateTranslation(MidPointVector);
var worldToView = worldToShuttle * shuttleToView;
Matrix3x2.Invert(worldToView, out var viewToWorld);
if (args.Function == EngineKeyFunctions.UIClick)
{
if (NavMap.TrackedEntities.Count == 0)
return;
if ((StartDragPosition - args.PointerLocation.Position).Length() > NavMap.MinDragDistance)
return;
var localPosition = args.PointerLocation.Position - GlobalPixelPosition;
var worldPosition = Vector2.Transform(localPosition, viewToWorld);
var closestEntity = NetEntity.Invalid;
var closestDistance = float.PositiveInfinity;
foreach (var (currentEntity, blip) in NavMap.TrackedEntities)
{
if (!blip.Selectable)
continue;
var currentDistance = (_transform.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length();
if (closestDistance < currentDistance || currentDistance * MinimapScale > NavMap.MaxSelectableDistance)
continue;
closestEntity = currentEntity;
closestDistance = currentDistance;
}
if (closestDistance > NavMap.MaxSelectableDistance || !closestEntity.IsValid())
return;
NavMap.TrackEntity(closestEntity);
}
else if (args.Function == EngineKeyFunctions.UIRightClick)
{
NavMap.TrackEntity(null);
}
}
}
@@ -0,0 +1,43 @@
using Content.Client.SubFloor;
using Content.Shared._DV.NodeCrawl;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client._DV.NodeCrawl;
public sealed class NodeCrawlSystem : SharedNodeCrawlSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SubFloorHideSystem _subfloor = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeCrawlerComponent, LocalPlayerAttachedEvent>(OnAttached);
SubscribeLocalEvent<NodeCrawlerComponent, LocalPlayerDetachedEvent>(OnDetached);
SubscribeLocalEvent<NodeCrawlerComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAttached(Entity<NodeCrawlerComponent> ent, ref LocalPlayerAttachedEvent args)
{
if (ent.Comp.Mover is not null)
_subfloor.Types = ent.Comp.RevealedComponents;
}
private void OnDetached(Entity<NodeCrawlerComponent> ent, ref LocalPlayerDetachedEvent args)
{
_subfloor.Types = new Type[] { };
}
private void OnAfterAutoHandleState(Entity<NodeCrawlerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_player.LocalEntity != ent)
return;
if (ent.Comp.Mover is not null)
_subfloor.Types = ent.Comp.RevealedComponents;
else
_subfloor.Types = new Type[] { };
}
}
@@ -0,0 +1,87 @@
<tabs:RadioTab xmlns="https://spacestation14.io"
xmlns:tabs="clr-namespace:Content.Client._DV.Options.UI.Tabs"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<!-- Common -->
<Label Text="{Loc 'ui-options-radio-header-common'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCommonColorSlider"
Example="{Loc 'ui-options-radio-example-common'}"/>
<Button Name="ResetCommonColorButton" Text="{Loc 'ui-options-radio-reset-common'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Command -->
<Label Text="{Loc 'ui-options-radio-header-command'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCommandColorSlider"
Example="{Loc 'ui-options-radio-example-command'}"/>
<Button Name="ResetCommandColorButton" Text="{Loc 'ui-options-radio-reset-command'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Security -->
<Label Text="{Loc 'ui-options-radio-header-security'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioSecurityColorSlider"
Example="{Loc 'ui-options-radio-example-security'}"/>
<Button Name="ResetSecurityColorButton" Text="{Loc 'ui-options-radio-reset-security'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Medical -->
<Label Text="{Loc 'ui-options-radio-header-medical'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioMedicalColorSlider"
Example="{Loc 'ui-options-radio-example-medical'}"/>
<Button Name="ResetMedicalColorButton" Text="{Loc 'ui-options-radio-reset-medical'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Engineering -->
<Label Text="{Loc 'ui-options-radio-header-engineering'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioEngineeringColorSlider"
Example="{Loc 'ui-options-radio-example-engineering'}"/>
<Button Name="ResetEngineeringColorButton" Text="{Loc 'ui-options-radio-reset-engineering'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Science -->
<Label Text="{Loc 'ui-options-radio-header-science'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioScienceColorSlider"
Example="{Loc 'ui-options-radio-example-science'}"/>
<Button Name="ResetScienceColorButton" Text="{Loc 'ui-options-radio-reset-science'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Cargo -->
<Label Text="{Loc 'ui-options-radio-header-cargo'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioCargoColorSlider"
Example="{Loc 'ui-options-radio-example-cargo'}"/>
<Button Name="ResetCargoColorButton" Text="{Loc 'ui-options-radio-reset-cargo'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Service -->
<Label Text="{Loc 'ui-options-radio-header-service'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioServiceColorSlider"
Example="{Loc 'ui-options-radio-example-service'}"/>
<Button Name="ResetServiceColorButton" Text="{Loc 'ui-options-radio-reset-service'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- Binary -->
<Label Text="{Loc 'ui-options-radio-header-binary'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioBinaryColorSlider"
Example="{Loc 'ui-options-radio-example-binary'}"/>
<Button Name="ResetBinaryColorButton" Text="{Loc 'ui-options-radio-reset-binary'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- DeltaV - Justice -->
<Label Text="{Loc 'ui-options-radio-header-justice'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioJusticeColorSlider"
Example="{Loc 'ui-options-radio-example-justice'}"/>
<Button Name="ResetJusticeColorButton" Text="{Loc 'ui-options-radio-reset-justice'}" MaxWidth="300" Margin="0 4 0 0" />
<!-- DeltaV - Prison -->
<Label Text="{Loc 'ui-options-radio-header-prison'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="RadioPrisonColorSlider"
Example="{Loc 'ui-options-radio-example-prison'}"/>
<Button Name="ResetPrisonColorButton" Text="{Loc 'ui-options-radio-reset-prison'}" MaxWidth="300" Margin="0 4 0 0" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</tabs:RadioTab>
@@ -0,0 +1,55 @@
using Content.Client.Options.UI;
using Content.Shared._DV.CCVars;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class RadioTab : Control
{
[Dependency] private readonly IConfigurationManager _config = default!; // DeltaV
public RadioTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
// Having so many color sliders on one page is a lot. We should only really try
// to have common/departmental channels here, or it just becomes WAAAAY too much.
Control.AddOptionColorSlider(DCCVars.RadioColorCommon, RadioCommonColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorCommand, RadioCommandColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorSecurity, RadioSecurityColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorMedical, RadioMedicalColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorEngineering, RadioEngineeringColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorScience, RadioScienceColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorCargo, RadioCargoColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorService, RadioServiceColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorBinary, RadioBinaryColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorJustice, RadioJusticeColorSlider);
Control.AddOptionColorSlider(DCCVars.RadioColorPrison, RadioPrisonColorSlider);
ResetCommonColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCommon);
ResetCommandColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCommand);
ResetSecurityColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorSecurity);
ResetMedicalColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorMedical);
ResetEngineeringColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorEngineering);
ResetScienceColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorScience);
ResetCargoColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorCargo);
ResetServiceColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorService);
ResetBinaryColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorBinary);
ResetJusticeColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorJustice);
ResetPrisonColorButton.OnPressed += _ => OnResetColorButton(DCCVars.RadioColorPrison);
Control.Initialize();
}
private void OnResetColorButton(CVarDef<string> cvar)
{
_config.SetCVar(cvar, cvar.DefaultValue);
Control.ReloadValues();
}
}
@@ -0,0 +1,49 @@
using Content.Shared._DV.CCVars;
using Content.Shared.Radio;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Radio;
public sealed partial class RadioChannelColorSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
// Feels like there should be a better way to do this but I'd rather not add a client-side cvar ID or reference
// to a prototype in Content.Shared, since I don't want people thinking they can ever use it on the server-side.
private static readonly ProtoId<RadioChannelPrototype> CommonChannelProto = "Common";
private static readonly ProtoId<RadioChannelPrototype> CommandChannelProto = "Command";
private static readonly ProtoId<RadioChannelPrototype> SecurityChannelProto = "Security";
private static readonly ProtoId<RadioChannelPrototype> MedicalChannelProto = "Medical";
private static readonly ProtoId<RadioChannelPrototype> EngineeringChannelProto = "Engineering";
private static readonly ProtoId<RadioChannelPrototype> ScienceChannelProto = "Science";
private static readonly ProtoId<RadioChannelPrototype> CargoChannelProto = "Supply";
private static readonly ProtoId<RadioChannelPrototype> ServiceChannelProto = "Service";
private static readonly ProtoId<RadioChannelPrototype> BinaryChannelProto = "Binary";
private static readonly ProtoId<RadioChannelPrototype> JusticeChannelProto = "Justice";
private static readonly ProtoId<RadioChannelPrototype> PrisonChannelProto = "Prison";
public override void Initialize()
{
base.Initialize();
_config.OnValueChanged(DCCVars.RadioColorCommon, (value) => { SetChannelColor(CommonChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorCommand, (value) => { SetChannelColor(CommandChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorSecurity, (value) => { SetChannelColor(SecurityChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorMedical, (value) => { SetChannelColor(MedicalChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorEngineering, (value) => { SetChannelColor(EngineeringChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorScience, (value) => { SetChannelColor(ScienceChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorCargo, (value) => { SetChannelColor(CargoChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorService, (value) => { SetChannelColor(ServiceChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorBinary, (value) => { SetChannelColor(BinaryChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorJustice, (value) => { SetChannelColor(JusticeChannelProto, value); }, true);
_config.OnValueChanged(DCCVars.RadioColorPrison, (value) => { SetChannelColor(PrisonChannelProto, value); }, true);
}
private void SetChannelColor(ProtoId<RadioChannelPrototype> channel, string newColor)
{
if (Color.TryParse(newColor, out var color))
_proto.Index(channel).Color = color;
}
}
@@ -0,0 +1,17 @@
using Content.Shared.Labels.Components;
namespace Content.Client._DV.Silicons;
public static class NameHelpers
{
extension(IEntityManager self)
{
public string PaperName(EntityUid paper)
{
if (self.TryGetComponent<LabelComponent>(paper, out var label) && label.CurrentLabel is { } labelText)
return labelText;
return self.GetComponent<MetaDataComponent>(paper).EntityName;
}
}
}
@@ -0,0 +1,144 @@
using System.IO;
using Content.Shared._DV.Silicons;
using Content.Shared.Fax;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Containers;
namespace Content.Client._DV.Silicons;
[UsedImplicitly]
public sealed class StationAiFaxBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[Dependency] private readonly IFileDialogManager _fileDialogManager = default!;
private StationAiFaxWindow? _window;
private List<StationAiFaxPageWindow> _windows = new();
private bool _dialogIsOpen;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<StationAiFaxWindow>();
_window.FileButtonPressed += OnFileButtonPressed;
_window.BlankButtonPressed += OnBlankButtonPressed;
_window.OpenSendWindowPressed += OnOpenSendWindowPressed;
_window.ExaminePressed += OnExaminePressed;
_window.ShredPressed += OnShredPressed;
_window.DuplicatePressed += OnDuplicatePressed;
UpdateFaxes(EntMan.System<SharedContainerSystem>().EnsureContainer<Container>(Owner, StationAiFaxComponent.Container, out _));
}
private async void OnFileButtonPressed()
{
if (_dialogIsOpen)
return;
_dialogIsOpen = true;
var filters = new FileDialogFilters(new FileDialogFilters.Group("txt"));
await using var file = await _fileDialogManager.OpenFile(filters, FileAccess.Read);
_dialogIsOpen = false;
if (_window == null || _window.Disposed || file == null)
{
return;
}
using var reader = new StreamReader(file);
var firstLine = await reader.ReadLineAsync();
string? label = null;
var content = await reader.ReadToEndAsync();
if (firstLine is { })
{
if (firstLine.StartsWith('#'))
{
label = firstLine[1..].Trim();
}
else
{
content = firstLine + "\n" + content;
}
}
SendMessage(new FaxFileMessage(
label?[..Math.Min(label.Length, FaxFileMessageValidation.MaxLabelSize)],
content[..Math.Min(content.Length, FaxFileMessageValidation.MaxContentSize)],
false));
}
private void OnBlankButtonPressed()
{
SendMessage(new FaxFileMessage(null, string.Empty, false));
}
private void OnOpenSendWindowPressed(EntityUid entity)
{
var window = new StationAiFaxPageWindow(entity, EntMan);
_windows.Add(window);
window.RefreshButtonPressed += OnRefreshButtonPressed;
window.PeerSelected += OnPeerSelected;
window.SendButtonPressed += OnSendDocument;
if (UiSystem.TryGetUiState<FaxUiState>(Owner, UiKey, out var state))
{
window.UpdateState(state);
}
window.OpenCentered();
}
private void OnSendDocument(EntityUid entity)
{
SendMessage(new StationAiFaxSendMessage(EntMan.GetNetEntity(entity)));
}
private void OnExaminePressed(EntityUid entity)
{
SendPredictedMessage(new StationAiFaxExamineMessage(EntMan.GetNetEntity(entity)));
}
private void OnShredPressed(EntityUid entity)
{
SendPredictedMessage(new StationAiFaxShredMessage(EntMan.GetNetEntity(entity)));
}
private void OnDuplicatePressed(EntityUid entity)
{
SendPredictedMessage(new StationAiFaxDuplicateMessage(EntMan.GetNetEntity(entity)));
}
private void OnRefreshButtonPressed()
{
SendMessage(new FaxRefreshMessage());
}
private void OnPeerSelected(string address)
{
SendMessage(new FaxDestinationMessage(address));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not FaxUiState cast)
return;
_window?.UpdateState(cast);
foreach (var window in _windows)
{
window.UpdateState(cast);
}
}
public void UpdateFaxes(Container container)
{
_window?.UpdateFaxes(container);
}
}
@@ -0,0 +1,14 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client._DV.UserInterfaces"
xmlns:xe="clr-namespace:Content.Client.UserInterface.XamlExtensions"
StyleClasses="PanelLight"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="8">
<Label Name="PaperName" HorizontalExpand="True" />
<ui:IconButton Name="Shred" StyleClasses="OpenRight" Icon="{xe:Tex '/Textures/Interface/shredder.svg.192dpi.png'}" />
<ui:IconButton Name="Duplicate" StyleClasses="OpenBoth" Icon="{xe:Tex '/Textures/Interface/files.svg.192dpi.png'}" />
<ui:IconButton Name="Examine" StyleClasses="OpenBoth" Icon="{xe:Tex '/Textures/Interface/eye.svg.192dpi.png'}" />
<ui:IconButton Name="Send" StyleClasses="OpenLeft" Icon="{xe:Tex '/Textures/Interface/send.svg.192dpi.png'}" />
</BoxContainer>
</PanelContainer>
@@ -0,0 +1,27 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Silicons;
[GenerateTypedNameReferences]
public sealed partial class StationAiFaxItem : PanelContainer
{
public string? Text { get => PaperName.Text; set => PaperName.Text = value; }
public bool CanSend { get => !Duplicate.Disabled; set => Duplicate.Disabled = !value; }
public event Action? ShredPressed;
public event Action? DuplicatePressed;
public event Action? ExaminePressed;
public event Action? SendPressed;
public StationAiFaxItem()
{
RobustXamlLoader.Load(this);
Shred.OnPressed += _ => ShredPressed?.Invoke();
Duplicate.OnPressed += _ => DuplicatePressed?.Invoke();
Examine.OnPressed += _ => ExaminePressed?.Invoke();
Send.OnPressed += _ => SendPressed?.Invoke();
}
}
@@ -0,0 +1,64 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'fax-machine-ui-page-window'}"
MinSize="380 220">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<PanelContainer StyleClasses="PanelDark" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'fax-machine-ui-sending'}"
VerticalAlignment="Bottom"
HorizontalExpand="True" />
<Label Name="SendingLabel"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Align="Right"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'fax-machine-ui-from'}"
VerticalAlignment="Bottom"
HorizontalExpand="True" />
<Label Name="FromLabel"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Align="Right"/>
</BoxContainer>
<BoxContainer
Orientation="Horizontal">
<Label Text="{Loc 'fax-machine-ui-to'}"
VerticalAlignment="Bottom"
HorizontalExpand="True"/>
<BoxContainer Orientation="Horizontal">
<OptionButton Name="PeerSelector"
StyleClasses="OpenRight" />
<Button StyleClasses="OpenLeft"
Name="RefreshButton"
ToolTip="{Loc 'fax-machine-ui-refresh-button'}">
<TextureButton
MouseFilter="Ignore"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Scale="0.9 0.9"
StyleClasses="RefreshButton" />
</Button>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" SeparationOverride="8" Margin="8">
<Button Name="SendButton"
Text="{Loc 'fax-machine-ui-send-button'}"
HorizontalExpand="True"
Disabled="True" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,70 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Fax;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Silicons;
[GenerateTypedNameReferences]
public sealed partial class StationAiFaxPageWindow : FancyWindow
{
public event Action? RefreshButtonPressed;
public event Action<string>? PeerSelected;
public event Action<EntityUid>? SendButtonPressed;
public StationAiFaxPageWindow(EntityUid uid, IEntityManager entityManager)
{
RobustXamlLoader.Load(this);
SendingLabel.Text = entityManager.PaperName(uid);
RefreshButton.OnPressed += _ => RefreshButtonPressed?.Invoke();
SendButton.OnPressed += _ => SendButtonPressed?.Invoke(uid);
PeerSelector.OnItemSelected += args =>
PeerSelected?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
}
public void UpdateState(FaxUiState state)
{
SendButton.Disabled = !state.CanSendCooldown;
FromLabel.Text = state.DeviceName;
if (state.AvailablePeers.Count == 0)
{
PeerSelector.AddItem(Loc.GetString("fax-machine-ui-no-peers"));
PeerSelector.Disabled = true;
}
if (PeerSelector.Disabled && state.AvailablePeers.Count != 0)
{
PeerSelector.Clear();
PeerSelector.Disabled = false;
}
// always must be selected destination
if (string.IsNullOrEmpty(state.DestinationAddress) && state.AvailablePeers.Count != 0)
{
PeerSelected?.Invoke(state.AvailablePeers.First().Key);
return;
}
if (state.AvailablePeers.Count != 0)
{
PeerSelector.Clear();
foreach (var (address, name) in state.AvailablePeers)
{
var id = AddPeerSelect(name, address);
if (address == state.DestinationAddress)
PeerSelector.Select(id);
}
}
}
private int AddPeerSelect(string name, string address)
{
PeerSelector.AddItem(name);
PeerSelector.SetItemMetadata(PeerSelector.ItemCount - 1, address);
return PeerSelector.ItemCount - 1;
}
}
@@ -0,0 +1,37 @@
using Content.Shared._DV.Silicons;
using Content.Shared.Fax;
using Robust.Client.GameObjects;
using Robust.Shared.Containers;
namespace Content.Client._DV.Silicons;
public sealed class StationAiFaxSystem : SharedStationAiFaxSystem
{
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiFaxComponent, EntRemovedFromContainerMessage>(OnRemoved);
}
protected override void OnInserted(Entity<StationAiFaxComponent> ent, ref EntInsertedIntoContainerMessage args)
{
base.OnInserted(ent, ref args);
if (!_userInterface.TryGetOpenUi<StationAiFaxBoundUserInterface>(ent.Owner, FaxUiKey.Key, out var bui))
return;
bui.UpdateFaxes(_container.EnsureContainer<Container>(ent, StationAiFaxComponent.Container, out _));
}
private void OnRemoved(Entity<StationAiFaxComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (!_userInterface.TryGetOpenUi<StationAiFaxBoundUserInterface>(ent.Owner, FaxUiKey.Key, out var bui))
return;
bui.UpdateFaxes(_container.EnsureContainer<Container>(ent, StationAiFaxComponent.Container, out _));
}
}
@@ -0,0 +1,29 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'fax-machine-ui-window'}"
MinSize="500 600">
<BoxContainer Orientation="Vertical" Name="NoFaxes" HorizontalAlignment="Center" VerticalAlignment="Center" SeparationOverride="8">
<Label StyleClasses="PlaceholderText" Text="{Loc 'fax-machine-ui-no-faxes'}" Align="Center" />
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center">
<Button Name="NoFaxesBlank" Text="{Loc 'fax-machine-ui-add-blank-file'}" HorizontalExpand="True" StyleClasses="OpenLeft" />
<Button Name="NoFaxesUpload" Text="{Loc 'fax-machine-ui-upload-file'}" HorizontalExpand="True" StyleClasses="OpenRight" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Name="YesFaxes" HorizontalExpand="True" VerticalExpand="True">
<PanelContainer StyleClasses="PanelDark" VerticalExpand="True" HorizontalExpand="True">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Name="FaxList" Margin="8" VerticalAlignment="Top" HorizontalExpand="True" SeparationOverride="8" Orientation="Vertical" />
</ScrollContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" SeparationOverride="8" Margin="8">
<Button Name="YesFaxesBlank" Text="{Loc 'fax-machine-ui-add-blank-file'}" HorizontalAlignment="Center" />
<Button Name="YesFaxesUpload" Text="{Loc 'fax-machine-ui-upload-file'}" HorizontalAlignment="Center" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,92 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Fax;
using Content.Shared.Labels.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.Silicons;
[GenerateTypedNameReferences]
public sealed partial class StationAiFaxWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public event Action? BlankButtonPressed;
public event Action? FileButtonPressed;
public event Action<EntityUid>? OpenSendWindowPressed;
public event Action<EntityUid>? ExaminePressed;
public event Action<EntityUid>? ShredPressed;
public event Action<EntityUid>? DuplicatePressed;
public bool CanSend
{
get;
set
{
if (field == value)
return;
field = value;
foreach (var item in FaxList.Children)
{
var castItem = (StationAiFaxItem)item;
castItem.CanSend = value;
}
NoFaxesBlank.Disabled = !value;
NoFaxesUpload.Disabled = !value;
YesFaxesBlank.Disabled = !value;
YesFaxesUpload.Disabled = !value;
}
}
public StationAiFaxWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
NoFaxesBlank.OnPressed += _ => BlankButtonPressed?.Invoke();
NoFaxesUpload.OnPressed += _ => FileButtonPressed?.Invoke();
YesFaxesBlank.OnPressed += _ => BlankButtonPressed?.Invoke();
YesFaxesUpload.OnPressed += _ => FileButtonPressed?.Invoke();
}
public void UpdateFaxes(Robust.Shared.Containers.Container container)
{
FaxList.RemoveAllChildren();
foreach (var item in container.ContainedEntities)
{
var page = new StationAiFaxItem
{
Text = _entityManager.TryGetComponent<LabelComponent>(item, out var label) &&
label.CurrentLabel is { } labelText
? labelText
: _entityManager.GetComponent<MetaDataComponent>(item).EntityName,
CanSend = CanSend,
};
FaxList.AddChild(page);
page.ExaminePressed += () => ExaminePressed?.Invoke(item);
page.ShredPressed += () => ShredPressed?.Invoke(item);
page.DuplicatePressed += () => DuplicatePressed?.Invoke(item);
page.SendPressed += () => OpenSendWindowPressed?.Invoke(item);
}
if (container.ContainedEntities.Count == 0)
{
YesFaxes.Visible = false;
NoFaxes.Visible = true;
}
else
{
YesFaxes.Visible = true;
NoFaxes.Visible = false;
}
}
public void UpdateState(FaxUiState state)
{
CanSend = state.CanSendCooldown;
}
}
@@ -108,6 +108,70 @@ public sealed partial class TraitCategory : BoxContainer
{
CategoryPointsLabel.Visible = false;
}
// Lock unselected entries whenever the category cap is reached so the
// player gets a clear visual cue instead of silently failing to equip.
UpdateCategoryFullState();
}
/// <summary>
/// Propagates category-cap lock state to every entry.
/// <para>
/// Selected entries are never locked - they are the traits filling the cap
/// and must stay interactive so the player can deselect them.
/// </para>
/// <para>
/// Unselected entries are locked when:
/// <list type="bullet">
/// <item><see cref="TraitCategoryPrototype.MaxTraits"/> is defined and already reached, OR</item>
/// <item><see cref="TraitCategoryPrototype.MaxPoints"/> is defined and this specific
/// trait's cost would push spending over the limit.</item>
/// </list>
/// </para>
/// </summary>
private void UpdateCategoryFullState()
{
var traitCapReached = _category.MaxTraits.HasValue && SelectedCount >= _category.MaxTraits.Value;
foreach (var (_, entry) in _traitEntries)
{
if (entry.IsSelected)
{
// Selected traits are never locked by category state.
entry.SetLockedByCategory(false);
continue;
}
var locked = traitCapReached;
// Even when the trait count cap hasn't been hit, lock this specific entry
// if adding its cost would exceed the category's point budget.
if (!locked && _category.MaxPoints.HasValue)
locked = PointsSpent + entry.TraitCost > _category.MaxPoints.Value;
entry.SetLockedByCategory(locked);
}
}
/// <summary>
/// Called by <see cref="TraitsTab"/> whenever the player's global point budget changes.
/// Locks any unselected entry whose cost the player can no longer afford.
/// <para>
/// Free (0-cost) and negative-cost traits are never locked - they cost nothing or
/// actually give points back. Selected traits are also never locked here; they
/// were affordable when picked and must stay interactive to be deselected.
/// </para>
/// </summary>
public void UpdateGlobalPointsLock(int remainingPoints)
{
foreach (var (_, entry) in _traitEntries)
{
// A selected trait is always unlocked here - same reasoning as category cap:
// it was paid for at selection time and must stay interactive.
// Traits that cost nothing or give points back are never locked by budget.
var shouldLock = !entry.IsSelected && entry.TraitCost > remainingPoints;
entry.SetLockedByPoints(shouldLock);
}
}
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
@@ -171,11 +235,11 @@ public sealed partial class TraitCategory : BoxContainer
/// 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)
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
foreach (var (_, entry) in _traitEntries)
{
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences);
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, selectedTraits);
}
// Update stats since some traits may have been deselected
+160 -39
View File
@@ -27,6 +27,9 @@ public sealed partial class TraitEntry : PanelContainer
private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
private bool _isLockedByCategory;
private bool _isLockedByPoints;
public TraitEntry(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
@@ -59,9 +62,17 @@ public sealed partial class TraitEntry : PanelContainer
foreach (var condition in _trait.Conditions)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
if (condition is TraitDependencyCondition depCond)
{
// Use GetTooltips() to get individual lines without duplication
tooltips.AddRange(depCond.GetTooltips(_prototype, _loc));
}
else
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
}
}
if (tooltips.Count > 0)
@@ -91,39 +102,92 @@ public sealed partial class TraitEntry : PanelContainer
/// <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)
public void UpdateConditionsMet(
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
_failedConditionTooltips.Clear();
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),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences),
_ => true,
};
bool result;
// Apply inversion
result ^= condition.Invert;
if (condition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
// CheckDependencyCondition adds its own tooltips directly
}
else
{
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
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
// Apply inversion for non-dependency conditions
result ^= condition.Invert;
if (!result)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
if (!result)
{
MeetsConditions = false;
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
UpdateDisabledState();
}
/// <summary>
/// Checks a TraitDependencyCondition and adds failure tooltips directly.
/// Returns true if the condition passes.
/// </summary>
private bool CheckDependencyCondition(TraitDependencyCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
var passed = true;
foreach (var conflict in condition.Conflicts)
{
if (selectedTraits == null || !selectedTraits.Contains(conflict))
continue;
if (_prototype.TryIndex(conflict, out var conflictProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-conflict",
("trait", _loc.GetString(conflictProto.Name))));
}
passed = false;
}
foreach (var required in condition.Requires)
{
if (selectedTraits != null && selectedTraits.Contains(required))
continue;
if (_prototype.TryIndex(required, out var requiredProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-required",
("trait", _loc.GetString(requiredProto.Name))));
}
passed = false;
}
return passed;
}
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
{
if (!_prototype.TryIndex(speciesId, out var species))
@@ -159,7 +223,12 @@ public sealed partial class TraitEntry : PanelContainer
return antagPreferences.Contains(condition.Antag);
}
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
private bool CheckAnyOfCondition(
AnyOfCondition condition,
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
if (condition.Conditions.Count == 0)
return false;
@@ -167,19 +236,27 @@ public sealed partial class TraitEntry : PanelContainer
// 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
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences), // Recursive!
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
_ => true,
};
bool result;
// Apply child's inversion
result ^= childCondition.Invert;
if (childCondition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
}
else
{
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),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
result ^= childCondition.Invert;
}
// If any child passes, the AnyOf passes
if (result)
@@ -190,16 +267,50 @@ public sealed partial class TraitEntry : PanelContainer
return false;
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the category's trait or
/// points-cap state changes. Locking only applies to <em>unselected</em>
/// entries - the selected traits are the ones filling the cap and must
/// remain interactive so the player can deselect them.
/// </summary>
public void SetLockedByCategory(bool locked)
{
if (_isLockedByCategory == locked)
return;
_isLockedByCategory = locked;
UpdateDisabledState();
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the player's global point budget
/// changes. Locks unselected entries whose cost the player can no longer afford.
/// Free (0-cost) and negative-cost traits are never locked by this.
/// </summary>
public void SetLockedByPoints(bool locked)
{
if (_isLockedByPoints == locked)
return;
_isLockedByPoints = locked;
UpdateDisabledState();
}
private void UpdateDisabledState()
{
if (!MeetsConditions)
var isSelected = TraitCheckbox.Pressed;
var conditionLocked = !MeetsConditions;
var categoryLocked = _isLockedByCategory && !isSelected;
var pointsLocked = _isLockedByPoints && !isSelected;
var isDisabled = conditionLocked || categoryLocked || pointsLocked;
if (isDisabled)
{
// Hide checkbox, show lock icon
TraitCheckbox.Visible = false;
LockIcon.Visible = true;
// Deselect if conditions no longer met
if (TraitCheckbox.Pressed)
if (isSelected && conditionLocked)
{
_isUpdating = true;
TraitCheckbox.Pressed = false;
@@ -211,14 +322,24 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Update tooltip to show failed conditions
if (_failedConditionTooltips.Count > 0)
// Tooltip priority for conditions: condition failures > category full > insufficient points.
if (conditionLocked && _failedConditionTooltips.Count > 0)
{
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
("requirements", string.Join("\n", _failedConditionTooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (categoryLocked)
{
var tooltipText = Loc.GetString("trait-category-full-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (pointsLocked)
{
var tooltipText = Loc.GetString("trait-insufficient-points-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
}
else
{
+18 -8
View File
@@ -154,14 +154,18 @@ public sealed partial class TraitsTab : BoxContainer
}
}
// Check conflicts
foreach (var conflict in trait.Conflicts)
// Check conflicts via TraitDependencyCondition
var depCondition = trait.Conditions.OfType<Content.Shared._DV.Traits.Conditions.TraitDependencyCondition>().FirstOrDefault();
if (depCondition != null)
{
if (!_selectedTraits.Contains(conflict))
continue;
foreach (var conflict in depCondition.Conflicts)
{
if (!_selectedTraits.Contains(conflict))
continue;
RevertTraitToggle(traitId);
return;
RevertTraitToggle(traitId);
return;
}
}
_selectedTraits.Add(traitId);
@@ -176,6 +180,7 @@ public sealed partial class TraitsTab : BoxContainer
}
UpdateGlobalStats();
UpdateAllConditions();
UpdateCategoryStats(trait.Category);
OnTraitsChanged?.Invoke(_selectedTraits);
}
@@ -211,7 +216,7 @@ public sealed partial class TraitsTab : BoxContainer
// If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage);
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
@@ -235,6 +240,11 @@ public sealed partial class TraitsTab : BoxContainer
> 0f => "TraitsProgressBarLow",
_ => "TraitsProgressBarEmpty"
});
foreach (var (_, categoryUi) in _categoryUis)
{
categoryUi.UpdateGlobalPointsLock(remainingPoints);
}
}
private void OnProgressBarParentResized()
@@ -281,7 +291,7 @@ public sealed partial class TraitsTab : BoxContainer
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);
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
}
RecalculateStats();
@@ -0,0 +1,7 @@
<Button xmlns="https://spacestation14.io">
<TextureRect Name="TextureRect"
TextureScale="0.5 0.5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="KeepCentered"/>
</Button>
@@ -0,0 +1,17 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._DV.UserInterfaces;
[GenerateTypedNameReferences]
public sealed partial class IconButton : Button
{
public Texture? Icon { get => TextureRect.Texture; set => TextureRect.Texture = value; }
public IconButton()
{
RobustXamlLoader.Load(this);
}
}
@@ -24,7 +24,7 @@ public sealed class WizdenContentFreeze
var protoMan = server.ProtoMan;
var recipesCount = protoMan.Count<FoodRecipePrototype>();
var recipesLimit = 269; // DeltaV - was 218
var recipesLimit = 266; // DeltaV - was 218
if (recipesCount > recipesLimit)
{
@@ -262,32 +262,6 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", ntAgent, Loc.GetString("admin-verb-text-make-NTAgent")),
};
args.Verbs.Add(agent);
args.Verbs.Add(new ()
{
Text = Loc.GetString("admin-verb-make-HeadInsurgent.text"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
Act = () =>
{
_antag.ForceMakeAntag<NTAgentRuleComponent>(targetPlayer, "UprisingRule", "HeadInsurgent");
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-HeadInsurgent.message"),
});
args.Verbs.Add(new ()
{
Text = Loc.GetString("admin-verb-make-HeadLoyalist.text"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "Nanotrasen"),
Act = () =>
{
_antag.ForceMakeAntag<NTAgentRuleComponent>(targetPlayer, "UprisingRule", "HeadLoyalist");
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-HeadLoyalist.message"),
});
// End DeltaV Additions
// start DeltaV Additions - add hitman
var hitmanName = Loc.GetString("admin-verb-make-hitman");
@@ -6,6 +6,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Player; // DeltaV
namespace Content.Server.AlertLevel;
@@ -191,7 +192,8 @@ public sealed class AlertLevelSystem : EntitySystem
{
if (detail.Sound != null)
{
var filter = _stationSystem.GetInOwningStation(station);
//var filter = _stationSystem.GetInOwningStation(station); // DeltaV - Global Annoucements
var filter = Filter.Empty().AddAllPlayers(); // DeltaV - Global Annoucements
_audio.PlayGlobal(detail.Sound, filter, true, detail.Sound.Params);
}
else
@@ -202,7 +204,7 @@ public sealed class AlertLevelSystem : EntitySystem
if (announce)
{
_chatSystem.DispatchStationAnnouncement(station, announcementFull, playDefaultSound: playDefault,
_chatSystem.DispatchGlobalAnnouncement(announcementFull, playSound: playDefault, // DeltaV - Global Annoucements
colorOverride: detail.Color, sender: stationName);
}
@@ -23,8 +23,7 @@ public sealed partial class AntagSelectionSystem
/// </summary>
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
[NotNullWhen(true)] out AntagSelectionDefinition? definition,
int? players = null,
string? preferredRole = null) // DeltaV - preferrec roles
int? players = null)
{
definition = null;
@@ -40,11 +39,6 @@ public sealed partial class AntagSelectionSystem
// It needs to track selected minds for each definition independently.
foreach (var def in ent.Comp.Definitions)
{
// Begin DeltaV - preferred roles
if (preferredRole is not null && !def.PrefRoles.Contains(preferredRole))
continue;
// End DeltaV - preferred roles
var target = GetTargetAntagCount(ent, null, def);
if (mindCount < target)
@@ -342,11 +336,11 @@ public sealed partial class AntagSelectionSystem
/// This technically is a gamerule-ent-less way to make an entity an antag.
/// You should almost never be using this.
/// </summary>
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule, string? preferredRole = null) where T : Component // DeltaV - preferred antag roles
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
{
var rule = ForceGetGameRuleEnt<T>(defaultRule);
if (!TryGetNextAvailableDefinition(rule, out var def, preferredRole: preferredRole)) // DeltaV - preferred antag roles
if (!TryGetNextAvailableDefinition(rule, out var def))
def = rule.Comp.Definitions.Last();
MakeAntag(rule, player, def.Value);
}
@@ -7,7 +7,7 @@ using Robust.Shared.Player;
namespace Content.Server.Audio;
public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
public sealed partial class ServerGlobalSoundSystem : SharedGlobalSoundSystem // DeltaV - Made Partial
{
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
@@ -184,6 +184,11 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
var rate = entry.MetabolismRate * group.MetabolismRateModifier;
// BEGIN DeltaV - fix FixedPoint2 truncation flooring low rateModifiers to zero
if (rate == FixedPoint2.Zero && entry.MetabolismRate > FixedPoint2.Zero && group.MetabolismRateModifier > 0f)
rate = FixedPoint2.Epsilon;
// END DeltaV
// Remove $rate, as long as there's enough reagent there to actually remove that much
mostToRemove = FixedPoint2.Clamp(rate, 0, quantity);
+13 -8
View File
@@ -5,6 +5,7 @@ using Content.Server.DeviceNetwork.Systems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Tools;
using Content.Shared._DV.Fax; // DeltaV - fax
using Content.Shared._DV.Pager; // DeltaV - pagers
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
@@ -90,10 +91,10 @@ public sealed class FaxSystem : EntitySystem
{
base.Update(frameTime);
var query = EntityQueryEnumerator<FaxMachineComponent, ApcPowerReceiverComponent>();
while (query.MoveNext(out var uid, out var fax, out var receiver))
var query = EntityQueryEnumerator<FaxMachineComponent>();
while (query.MoveNext(out var uid, out var fax)) // DeltaV - faxes aren't necessarily powered
{
if (!receiver.Powered)
if (TryComp<ApcPowerReceiverComponent>(uid, out var receiver) && !receiver.Powered) // DeltaV - faxes aren't necessarily powered
continue;
ProcessPrintingAnimation(uid, frameTime, fax);
@@ -392,7 +393,7 @@ public sealed class FaxSystem : EntitySystem
var canCopy = isPaperInserted &&
component.SendTimeoutRemaining <= 0 &&
component.InsertingTimeRemaining <= 0;
var state = new FaxUiState(component.FaxName, component.KnownFaxes, canSend, canCopy, isPaperInserted, component.DestinationFaxAddress);
var state = new FaxUiState(component.FaxName, component.KnownFaxes, canSend, canCopy, isPaperInserted, component.DestinationFaxAddress, component.SendTimeoutRemaining <= 0); // DeltaV - AI fax
_userInterface.SetUiState(uid, FaxUiKey.Key, state);
}
@@ -461,7 +462,7 @@ public sealed class FaxSystem : EntitySystem
/// Copies the paper in the fax. A timeout is set after copying,
/// which is shared by the send button.
/// </summary>
public void Copy(EntityUid uid, FaxMachineComponent? component, FaxCopyMessage args)
public void Copy(EntityUid uid, FaxMachineComponent? component, BoundUserInterfaceMessage args, EntityUid? item = null) // DeltaV - station AI fax
{
if (!Resolve(uid, ref component))
return;
@@ -469,7 +470,7 @@ public sealed class FaxSystem : EntitySystem
if (component.SendTimeoutRemaining > 0)
return;
var sendEntity = component.PaperSlot.Item;
var sendEntity = item ?? component.PaperSlot.Item; // DeltaV - station AI fax
if (sendEntity == null)
return;
@@ -511,7 +512,7 @@ public sealed class FaxSystem : EntitySystem
/// Sends message to addressee if paper is set and a known fax is selected
/// A timeout is set after sending, which is shared by the copy button.
/// </summary>
public void Send(EntityUid uid, FaxMachineComponent? component, FaxSendMessage args)
public void Send(EntityUid uid, FaxMachineComponent? component, BoundUserInterfaceMessage args, EntityUid? item = null) // DeltaV - station AI fax
{
if (!Resolve(uid, ref component))
return;
@@ -519,7 +520,7 @@ public sealed class FaxSystem : EntitySystem
if (component.SendTimeoutRemaining > 0)
return;
var sendEntity = component.PaperSlot.Item;
var sendEntity = item ?? component.PaperSlot.Item; // DeltaV - station AI fax
if (sendEntity == null)
return;
@@ -635,6 +636,10 @@ public sealed class FaxSystem : EntitySystem
}
_adminLogger.Add(LogType.Action, LogImpact.Low, $"\"{component.FaxName}\" {ToPrettyString(uid):tool} printed {ToPrettyString(printed):subject}: {printout.Content}");
// Begin DeltaV - notify on fax printing
var evt = new FaxPrintedEvent(printed);
RaiseLocalEvent(uid, ref evt);
// End DeltaV - notify on fax printing
}
private void NotifyAdmins(string faxName)
+11 -11
View File
@@ -327,15 +327,15 @@ public sealed class NukeSystem : EntitySystem
// should play
if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !ResolvedSoundSpecifier.IsNullOrEmpty(_selectedNukeSong))
{
_sound.DispatchStationEventMusic(uid, _selectedNukeSong, StationEventMusicType.Nuke);
_sound.DispatchGlobalEventMusic(_selectedNukeSong, StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
nuke.PlayedNukeSong = true;
}
// play alert sound if time is running out
if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
{
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(nuke.AlertSound), new AudioParams{Volume = -5f});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.PlayGlobal(_audio.ResolveSound(nuke.AlertSound), new AudioParams { Volume = -5f }); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
nuke.PlayedAlertSound = true;
UpdateAppearance(uid, nuke);
}
@@ -475,7 +475,7 @@ public sealed class NukeSystem : EntitySystem
/// <summary>
/// Force a nuclear bomb to start a countdown timer
/// </summary>
public void ArmBomb(EntityUid uid, NukeComponent? component = null, string? announcementId = null) // DeltaV - custom announcements
public void ArmBomb(EntityUid uid, NukeComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
@@ -500,13 +500,13 @@ public sealed class NukeSystem : EntitySystem
_selectedNukeSong = _audio.ResolveSound(component.ArmMusic);
// warn a crew
var announcement = Loc.GetString(announcementId ?? "nuke-component-announcement-armed", // DeltaV - custom announcements
var announcement = Loc.GetString("nuke-component-announcement-armed",
("time", (int) component.RemainingTime),
("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((uid, nukeXform)))));
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false, null, Color.Red); // DeltaV - Global Nuke Annoucement
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
_sound.PlayGlobal(_audio.ResolveSound(component.ArmSound)); // DeltaV - Global Nuke Music
_nukeSongLength = (float) _audio.GetAudioLength(_selectedNukeSong).TotalSeconds;
// turn on the spinny light
@@ -544,11 +544,11 @@ public sealed class NukeSystem : EntitySystem
// warn a crew
var announcement = Loc.GetString("nuke-component-announcement-unarmed");
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false); // DeltaV - Global Nuke Announcement
component.PlayedNukeSong = false;
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.PlayGlobal(_audio.ResolveSound(component.DisarmSound)); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
// reset nuke remaining time to either itself or the minimum time, whichever is higher
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
@@ -610,7 +610,7 @@ public sealed class NukeSystem : EntitySystem
OwningStation = transform.GridUid,
});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
Del(uid);
}
@@ -157,7 +157,8 @@ public sealed class RadioSystem : EntitySystem
message,
wrappedMessage,
NetEntity.Invalid,
null);
null,
radioChannelProto: channel.ID); // DeltaV - Add RadioChannel for committing sins
var chatMsg = new MsgChatMessage { Message = chat };
var ev = new RadioReceiveEvent(message, messageSource, channel, radioSource, chatMsg);
@@ -209,7 +210,7 @@ public sealed class RadioSystem : EntitySystem
}
/// <inheritdoc cref="TelecomServerComponent"/>
private bool HasActiveServer(MapId mapId, string channelId)
public bool HasActiveServer(MapId mapId, string channelId) // DeltaV - we need this
{
var servers = EntityQuery<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent, TransformComponent>();
foreach (var (_, keys, power, transform) in servers)
@@ -1,4 +1,5 @@
using Content.Server.StationEvents.Components;
using Content.Shared._DV.IonLaws; // DeltaV - ion laws
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Dataset;
@@ -20,6 +21,7 @@ public sealed class IonStormSystem : EntitySystem
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SiliconLawSystem _siliconLaw = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IonLawSystem _ionLaw = default!; // DeltaV - ion laws
// funny
private static readonly ProtoId<DatasetPrototype> Threats = "IonStormThreats";
@@ -93,7 +95,7 @@ public sealed class IonStormSystem : EntitySystem
}
// generate a new law...
var newLaw = GenerateLaw();
var newLaw = _ionLaw.GenerateLaw(_robustRandom); // GenerateLaw(); DeltaV - inject our own
// see if the law we add will replace a random existing law or be a new glitched order one
if (laws.Laws.Count > 0 && _robustRandom.Prob(target.ReplaceChance))
@@ -6,4 +6,41 @@ namespace Content.Server.StationEvents.Components;
[RegisterComponent]
public sealed partial class IonStormRuleComponent : Component
{
// DV start - Synthetic tweaks
/// <summary>
/// The chance that a synth gets electrocuted by the ion storm.
/// </summary>
[DataField]
public float SynthElectrocutionChance = 0.6f;
/// <summary>
/// The minimum delay before a synth affected by the ion storm gets electrocuted. (in seconds)
/// </summary>
[DataField]
public int SynthElectrocutionDelayMin = 3;
/// <summary>
/// The maximum delay before a synth affected by the ion storm gets electrocuted. (in seconds)
/// </summary>
[DataField]
public int SynthElectrocutionDelayMax = 10;
/// <summary>
/// The minimum damage dealt by the synth electrocution. (shock damage)
/// </summary>
[DataField]
public int SynthElectrocutionDamageMin = 5;
/// <summary>
/// The maximum damage dealt by the synth electrocution. (shock damage)
/// </summary>8
[DataField]
public int SynthElectrocutionDamageMax = 10;
/// <summary>
/// How long the synth is stunned for by the electrocution. (in seconds)
/// </summary>
[DataField]
public int SynthElectrocutionStunDuration = 3;
// DV end - synthetic tweaks
}
@@ -6,9 +6,12 @@ using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Station.Components;
// CD - start synth trait
using Content.Server.Chat.Managers;
using Content.Server.Electrocution; // DV - added for synth trait
using Content.Shared.Chat;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Content.Shared._ES.Sparks; // Delta-V change - added for synth trait (where they get affected by ion storms)
using Timer = Robust.Shared.Timing.Timer; // DV - used for synth trait delayed damage
// CD - end synth trait
namespace Content.Server.StationEvents.Events;
@@ -17,6 +20,8 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
{
[Dependency] private readonly IonStormSystem _ionStorm = default!;
[Dependency] private readonly IChatManager _chatManager = default!; // CD - Used for synth trait
[Dependency] private readonly ElectrocutionSystem _electrocution = default!; // DV - used for synth trait
[Dependency] private readonly ESSparksSystem _esSparks = default!; // DV - used for synth trait
protected override void Started(EntityUid uid, IonStormRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
@@ -26,18 +31,32 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
return;
// CD - Go through everyone with the SynthComponent and inform them a storm is happening.
// DV - buuuuuut only if IonStormAffected is present so that not all synthetics are affected by this
// but they are still notified about incoming ion storms regardless.
var synthQuery = EntityQueryEnumerator<SynthComponent>();
while (synthQuery.MoveNext(out var ent, out var synthComp))
{
if (RobustRandom.Prob(synthComp.AlertChance))
continue;
if (!TryComp<ActorComponent>(ent, out var actor))
continue;
var msg = Loc.GetString("station-event-ion-storm-synth");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Yellow);
if (RobustRandom.Prob(synthComp.AlertChance))
{
var msg = Loc.GetString("station-event-ion-storm-synth");
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg));
_chatManager.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Yellow);
}
// DV start - super duper fucked up thing for synths
if (synthComp.IonStormAffected && RobustRandom.Prob(comp.SynthElectrocutionChance))
{
var delay = RobustRandom.Next(TimeSpan.FromSeconds(comp.SynthElectrocutionDelayMin), TimeSpan.FromSeconds(comp.SynthElectrocutionDelayMax)); // they'll never know when it'll hit them
Timer.Spawn(delay, () =>
{
_electrocution.TryDoElectrocution(ent, null, RobustRandom.Next(comp.SynthElectrocutionDamageMin, comp.SynthElectrocutionDamageMax), TimeSpan.FromSeconds(comp.SynthElectrocutionStunDuration), true, ignoreInsulation: true);
_esSparks.DoSparks(ent);
});
}
// DV end
}
// CD - End of synth trait
@@ -1,84 +0,0 @@
using Content.Server.Revolutionary.Components;
using Content.Shared._DV.Uprising;
using Content.Shared.Cuffs.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Content.Shared.Station;
namespace Content.Server.Uprising;
public sealed class CommandStatusObjectiveSystem : EntitySystem
{
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedStationSystem _station = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CommandStatusObjectiveComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private bool CheckIncapacitated(EntityUid uid)
{
if (TryComp<CuffableComponent>(uid, out var cuffed) && cuffed.CuffedHandCount > 0)
return true;
if (!TryComp<MobStateComponent>(uid, out var state))
return true;
if (state.CurrentState is MobState.Dead or MobState.Invalid)
return true;
if (_station.GetOwningStation(uid) == null)
return true;
return false;
}
private void OnGetProgress(Entity<CommandStatusObjectiveComponent> ent, ref ObjectiveGetProgressEvent args)
{
var total = 0;
var counted = 0;
var query = AllEntityQuery<MindContainerComponent, CommandStaffComponent>();
while (query.MoveNext(out var uid, out var mind, out _))
{
if (mind.Mind is not { } mindUid)
continue;
total++;
var converted = false;
foreach (var role in ent.Comp.ConvertedRoles)
{
if (!EntityManager.ComponentFactory.TryGetRegistration(role, out var roleReg))
{
Log.Error($"Role component not found for CrewConversionObjective: {role}");
continue;
}
if (!_role.MindHasRole(mindUid, roleReg.Type, out _))
continue;
converted = true;
break;
}
var incapacitated = CheckIncapacitated(uid);
if (converted && !incapacitated || !converted && ent.Comp.ShouldUnconvertedBeIncapacitated && incapacitated)
{
counted++;
}
}
if (total == 0)
return;
args.Progress = (float)counted / (float)total;
}
}
+10 -1
View File
@@ -7,7 +7,16 @@ namespace Content.Server._CD.Traits;
public sealed partial class SynthComponent : Component
{
/// <summary>
/// The chance that the synth is alerted of an ion storm
/// Whether this synthetic is affected by ion storms (electrocution/sparks and
/// alerts when one occurs). False by default. set true by the
/// "Susceptible to Ion Storms" meta trait.
/// </summary>
[DataField]
public bool IonStormAffected;
/// <summary>
/// The chance that the synth is alerted of an ion storm. Only relevant if
/// <see cref="IonStormAffected"/> is true.
/// </summary>
[DataField]
public float AlertChance = 0.3f;
+27 -2
View File
@@ -1,8 +1,12 @@
using Content.Server.Body.Systems;
using Content.Server.Database;
using Content.Server.Chat.Systems;
using Content.Shared.Body.Components;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Chat.TypingIndicator;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Speech.Components;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Server._CD.Traits;
@@ -12,15 +16,22 @@ public sealed class SynthSystem : EntitySystem
// Begin DeltaV - make strings static readonly
private static readonly ProtoId<TypingIndicatorPrototype> RobotTypingIndicator = "robot";
private static readonly ProtoId<ReagentPrototype> SynthBloodReagent = "SynthBlood";
private static readonly ProtoId<TagPrototype> SyntheticEmotesTag = "SyntheticEmotes";
private static readonly ProtoId<EmoteSoundsPrototype> SyntheticEmoteSounds = "SyntheticEmoteSounds";
// End DeltaV
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ChatSystem _chat = default!;
private EmoteSoundsPrototype? _syntheticEmoteSounds; // delta-v - proper synthe emote implementation
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SynthComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<SynthComponent, EmoteEvent>(OnEmote); // delta-v - proper synthe emote implementation
}
private void OnStartup(EntityUid uid, SynthComponent component, ComponentStartup args)
@@ -38,5 +49,19 @@ public sealed class SynthSystem : EntitySystem
_bloodstream.ChangeBloodReagents((uid, bloodstream), new([new(SynthBloodReagent, bloodstream.BloodReferenceSolution.Volume)]));
}
// End DeltaV
_tag.AddTag(uid, SyntheticEmotesTag); // delta-v - proper synthe emote implementation
}
}
// Start DeltaV - proper synthe emote implementation
private void OnEmote(EntityUid uid, SynthComponent component, ref EmoteEvent args)
{
if (args.Handled)
return;
_syntheticEmoteSounds ??= _proto.Index(SyntheticEmoteSounds);
args.Handled = _chat.TryPlayEmoteSound(uid, _syntheticEmoteSounds, args.Emote);
}
// End DeltaV
}
@@ -0,0 +1,73 @@
using Content.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map;
namespace Content.Server.Audio;
/// <summary>
/// Extends upstream's Content.Server/Audio/ServerGlobalSoundSystem.cs.
/// </summary>
public sealed partial class ServerGlobalSoundSystem
{
/// <summary>
/// DeltaV - Plays a sound globally for all players, no matter where they are.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobal(ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
RaiseNetworkEvent(msg);
}
public void StopGlobalEventMusic(StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
RaiseNetworkEvent(msg);
}
public void DispatchGlobalEventMusic(SoundSpecifier sound, StationEventMusicType type)
{
DispatchGlobalEventMusic(_audio.ResolveSound(sound), type);
}
public void DispatchGlobalEventMusic(ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
RaiseNetworkEvent(msg);
}
/// <summary>
/// DeltaV - Plays a sound globally for all players on a specified Map.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobalOnMap(MapId map, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void StopMapEventMusic(MapId map, StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void DispatchMapEventMusic(MapId map, SoundSpecifier sound, StationEventMusicType type)
{
DispatchMapEventMusic(map, _audio.ResolveSound(sound), type);
}
public void DispatchMapEventMusic(MapId map, ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
}
@@ -6,12 +6,6 @@ namespace Content.Server._DV.CartridgeLoader.Cartridges;
[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
public sealed partial class NanoChatCartridgeComponent : Component
{
/// <summary>
/// Station entity to keep track of.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// The NanoChat card to keep track of.
/// </summary>
@@ -2,9 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.Power.Components;
using Content.Server.Radio;
using Content.Server.Station.Systems;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.CCVar;
@@ -30,7 +29,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly RadioSystem _radio = default!;
private EntityQuery<PdaComponent> _pdaQuery;
private EntityQuery<NanoChatCardComponent> _cardQuery;
@@ -456,9 +455,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
// First verify we can send from this device
var channel = _prototype.Index(sender.Comp.RadioChannel);
var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
RaiseLocalEvent(ref sendAttemptEvent);
if (sendAttemptEvent.Cancelled)
if (!CanSend(sender))
return (true, new List<Entity<NanoChatCardComponent>>());
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
@@ -487,26 +484,19 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (receiverCart.Card != recipient.Owner)
continue;
// Check if devices are on same station/map
var recipientStation = _station.GetOwningStation(receiverUid);
var senderStation = _station.GetOwningStation(sender);
var receiverMapId = Transform(receiverUid).MapID;
var senderMapId = Transform(sender).MapID;
// Both entities must be on a station
if (recipientStation == null || senderStation == null)
// Must be on the same map unless long range is allowed.
if (!channel.LongRange && receiverMapId != senderMapId)
continue;
// Must be on same map/station unless long range allowed
if (!channel.LongRange && recipientStation != senderStation)
continue;
// Needs telecomms
if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
// Check if telecommunications are active on both ends
if (!_radio.HasActiveServer(receiverMapId, receiverCart.RadioChannel) || !_radio.HasActiveServer(senderMapId, sender.Comp.RadioChannel))
continue;
// Check if recipient can receive
var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
RaiseLocalEvent(ref receiveAttemptEv);
if (receiveAttemptEv.Cancelled)
if (!CanReceive(sender, receiverUid))
continue;
// Found valid cartridge that can receive
@@ -519,21 +509,26 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
}
/// <summary>
/// Checks if there are any active telecomms servers on the given station
/// Tests if a NanoChat cartridge can send messages
/// </summary>
private bool HasActiveServer(EntityUid station)
/// <param name="sender">The NanoChat cartridge trying to send</param>
private bool CanSend(Entity<NanoChatCartridgeComponent> sender)
{
// I have no idea why this isn't public in the RadioSystem
var query =
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
var sendAttemptEvent = new RadioSendAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender);
RaiseLocalEvent(ref sendAttemptEvent);
return !sendAttemptEvent.Cancelled;
}
while (query.MoveNext(out var uid, out _, out _, out var power))
{
if (_station.GetOwningStation(uid) == station && power.Powered)
return true;
}
return false;
/// <summary>
/// Tests if a receiver can receive from a given NanoChat cartridge
/// </summary>
/// <param name="sender">The NanoChat cartridge trying to send</param>
/// <param name="receiver">The receiver cartridge trying to receive</param>
private bool CanReceive(Entity<NanoChatCartridgeComponent> sender, EntityUid receiver)
{
var receiveAttemptEv = new RadioReceiveAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender, receiver);
RaiseLocalEvent(ref receiveAttemptEv);
return !receiveAttemptEv.Cancelled;
}
/// <summary>
@@ -688,16 +683,15 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
{
List<NanoChatRecipient>? contacts;
if (_station.GetOwningStation(loader) is { } station)
{
ent.Comp.Station = station;
if (CanSend(ent) && _radio.HasActiveServer(Transform(ent).MapID, ent.Comp.RadioChannel))
{
contacts = [];
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
{
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName && _station.GetOwningStation(entityId) == station)
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
}
@@ -706,7 +700,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var borgQuery = AllEntityQuery<NanoChatCardComponent, BorgChassisComponent>();
while (borgQuery.MoveNext(out var borgId, out var borgChatCard, out var _))
{
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(borgId) == station)
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(borgId).EntityName));
}
@@ -715,7 +709,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var aiQuery = AllEntityQuery<NanoChatCardComponent, StationAiHeldComponent>();
while (aiQuery.MoveNext(out var aiId, out var aiChatCard, out var _))
{
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(aiId) == station)
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(aiId).EntityName));
}
@@ -521,7 +521,7 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
{
finComp.CurrentState = FinaleState.Unavailable;
_popup.PopupCoordinates(Loc.GetString("cosmiccult-monument-powerdown"), Transform(gameruleMonument).Coordinates, PopupType.Large);
_sound.StopStationEventMusic(gameruleMonument, StationEventMusicType.CosmicCult);
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
_monument.UpdateMonumentAppearance(gameruleMonument, false);
}
@@ -82,11 +82,9 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
_corrupting.SetCorruptionTime((uid, corruptingComp), TimeSpan.FromSeconds(1));
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 2);
_sound.DispatchStationEventMusic(uid, comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchStationAnnouncement(uid,
Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
_sound.DispatchGlobalEventMusic(comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null, Color.FromHex("#cae8e8"));
var stationUid = _station.GetStationInMap(Transform(uid).MapID);
if (stationUid != null)
@@ -128,8 +126,8 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
if (stationUid != null)
_alert.SetLevel(stationUid.Value, "green", true, true, true);
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(comp.CancelEventSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.CosmicCult);
_sound.PlayGlobal(_audio.ResolveSound(comp.CancelEventSound));
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
uid.Comp.CurrentState = FinaleState.ReadyFinale;
if (TryComp<CosmicCorruptingComponent>(uid, out var corruptingComp))
@@ -8,6 +8,7 @@ using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -25,6 +26,7 @@ public sealed class CosmicChantrySystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
/// <summary>
@@ -79,11 +81,11 @@ public sealed class CosmicChantrySystem : EntitySystem
comp.SpawnTimer = _timing.CurTime + comp.SpawningTime;
comp.CountdownTimer = _timing.CurTime + comp.EventTime;
_sound.PlayGlobalOnStation(ent, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchStationAnnouncement(ent,
Loc.GetString("cosmiccult-chantry-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
var targetMap = _transform.GetMapId(ent.Owner);
_sound.PlayGlobalOnMap(targetMap, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-chantry-location",
("location", indicatedLocation)), null, false, null, Color.FromHex("#cae8e8"));
if (_mind.TryGetMind(comp.InternalVictim, out _, out var mind))
mind.PreventGhosting = true;
@@ -79,13 +79,13 @@ public sealed class MonumentSystem : SharedMonumentSystem
{
comp.SongTimer = null;
if (comp.SelectedSong is { } song)
_sound.DispatchStationEventMusic(uid, song, StationEventMusicType.CosmicCult);
_sound.DispatchGlobalEventMusic(song, StationEventMusicType.CosmicCult);
}
if (comp.CurrentState == FinaleState.ActiveFinale && comp.FinaleAnnounceCheck && comp.FinaleTimer - _timing.CurTime < comp.VisualsThreshold)
{
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 3);
_chatSystem.DispatchStationAnnouncement(uid, Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
comp.FinaleAnnounceCheck = false;
}
@@ -0,0 +1,26 @@
using Content.Server.Administration;
using Content.Shared._DV.IonLaws;
using Content.Shared.Administration;
using Robust.Shared.Random;
using Robust.Shared.Toolshed;
namespace Content.Server._DV.IonLaws;
[ToolshedCommand, AdminCommand(AdminFlags.Fun)]
public sealed class IonLawCommand : ToolshedCommand
{
[Dependency] private readonly IRobustRandom _random = default!;
private IonLawSystem? _ionLaw;
[CommandImplementation("generate")]
public IEnumerable<string> GenerateNames(int count)
{
_ionLaw ??= GetSys<IonLawSystem>();
for (var i = 0; i < count; i++)
{
yield return _ionLaw.GenerateLaw(_random);
}
}
}
@@ -90,6 +90,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
private void OnPowerChanged(Entity<DeepFryerComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
ResetCookingItemsStartTime(ent);
}
private void OnThrowHitBy(Entity<DeepFryerComponent> ent, ref ThrowHitByEvent args)
@@ -674,4 +675,26 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
// Add the split solution to the food
Solution.AddSolution(foodSolutionEnt.Value, transferredSolution);
}
/// <summary>
/// Resets the starttime for cooking items (on power change)
/// </summary>
private void ResetCookingItemsStartTime(Entity<DeepFryerComponent> ent)
{
if (_power.IsPowered(ent.Owner) && !ent.Comp.WasPreviouslyPowered)
{
// Power just came back on - reset all cooking timers
foreach (var itemUid in ent.Comp.CookingItems.Keys.ToList())
{
var cookingItem = ent.Comp.CookingItems[itemUid];
cookingItem.TimeStarted = _timing.CurTime;
ent.Comp.CookingItems[itemUid] = cookingItem;
}
ent.Comp.WasPreviouslyPowered = true;
}
else if (!_power.IsPowered(ent.Owner) && ent.Comp.WasPreviouslyPowered)
{
ent.Comp.WasPreviouslyPowered = false;
}
}
}
@@ -0,0 +1,101 @@
using System.Numerics;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared._DV.NodeCrawl;
using Content.Shared.Atmos;
using Content.Shared.NodeContainer;
namespace Content.Server._DV.NodeCrawl;
public sealed class NodeCrawlSystem : SharedNodeCrawlSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrawlableNodeComponent, NodeGroupsRebuilt>(OnNodeGroupsRebuilt);
SubscribeLocalEvent<NodeCrawlerComponent, InhaleLocationEvent>(OnInhaleLocation);
SubscribeLocalEvent<NodeCrawlerComponent, ExhaleLocationEvent>(OnExhaleLocation);
SubscribeLocalEvent<NodeCrawlerComponent, AtmosExposedGetAirEvent>(OnGetAir);
}
private Entity<NodeContainerComponent>? GetNodeContainer(Entity<NodeCrawlerComponent> crawler)
{
if (!TryComp<NodeCrawlerMovementComponent>(crawler.Comp.Mover, out var mover) || mover.Node is not { } node)
return null;
if (!TryComp<NodeContainerComponent>(node, out var nodeContainer))
return null;
return (node, nodeContainer);
}
private GasMixture? GetAir(Entity<NodeCrawlerComponent> crawler)
{
if (GetNodeContainer(crawler) is not { } nodeContainer)
return null;
foreach (var containedNode in nodeContainer.Comp.Nodes.Values)
{
if (containedNode is not PipeNode pipe)
continue;
return pipe.Air;
}
return null;
}
private void OnInhaleLocation(Entity<NodeCrawlerComponent> ent, ref InhaleLocationEvent args)
{
if (GetAir(ent) is not { } air)
return;
args.Gas = air;
}
private void OnExhaleLocation(Entity<NodeCrawlerComponent> ent, ref ExhaleLocationEvent args)
{
if (GetAir(ent) is not { } air)
return;
args.Gas = air;
}
private void OnGetAir(Entity<NodeCrawlerComponent> ent, ref AtmosExposedGetAirEvent args)
{
if (args.Handled || GetAir(ent) is not { } air)
return;
args.Gas = air;
args.Handled = true;
}
private void OnNodeGroupsRebuilt(Entity<CrawlableNodeComponent> ent, ref NodeGroupsRebuilt args)
{
if (!TryComp<NodeContainerComponent>(ent, out var nodeContainer))
return;
ent.Comp.DeadEnd = false;
var set = new HashSet<EntityUid>();
foreach (var node in nodeContainer.Nodes.Values)
{
foreach (var reachable in node.ReachableNodes)
{
set.Add(reachable.Owner);
}
if (node is PipeNode pipeNode &&
node.ReachableNodes.Count != BitOperations.PopCount((uint)pipeNode.CurrentPipeDirection))
{
ent.Comp.DeadEnd = true;
}
}
ent.Comp.ReachableNodes = set;
Dirty(ent);
}
}
@@ -0,0 +1,39 @@
using Content.Server.Fax;
using Content.Shared._DV.Silicons;
using Content.Shared.Fax;
using Content.Shared.Fax.Components;
namespace Content.Server._DV.Silicons;
public sealed class StationAiFaxSystem : SharedStationAiFaxSystem
{
[Dependency] private readonly FaxSystem _fax = default!;
public override void Initialize()
{
base.Initialize();
Subs.BuiEvents<StationAiFaxComponent>(FaxUiKey.Key,
subs =>
{
subs.Event<StationAiFaxSendMessage>(OnSend);
subs.Event<StationAiFaxDuplicateMessage>(OnDuplicate);
});
}
private void OnSend(Entity<StationAiFaxComponent> ent, ref StationAiFaxSendMessage args)
{
if (GetDocument(ent, args.Document) is not { } document)
return;
_fax.Send(ent, Comp<FaxMachineComponent>(ent), args, document);
}
private void OnDuplicate(Entity<StationAiFaxComponent> ent, ref StationAiFaxDuplicateMessage args)
{
if (GetDocument(ent, args.Document) is not { } document)
return;
_fax.Copy(ent, Comp<FaxMachineComponent>(ent), args, document);
}
}
@@ -1,9 +1,9 @@
using Content.Server.Power.Components;
using Content.Server.StationEvents.Events;
using Content.Server._DV.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
namespace Content.Server._DV.StationEvents.Components;
/// <summary>
/// When fired, turns off power on the station for a few seconds, playing <see cref="EpsilonEventRuleComponent.PowerOffSound"/>
@@ -1,14 +1,15 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Server.StationEvents.Events;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server._DV.StationEvents.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.StationEvents.Events
{
namespace Content.Server._DV.StationEvents.Events;
public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleComponent>
{
[Dependency] private readonly ApcSystem _apcSystem = default!;
@@ -25,7 +26,7 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
return;
component.AffectedStation = chosenStation.Value;
// Plays the power off sound to the station.
// Plays the power off sound for those who are on the station.
_sound.PlayGlobalOnStation(component.AffectedStation, _audio.ResolveSound(component.PowerOffSound));
var query = AllEntityQuery<ApcComponent, TransformComponent>();
@@ -70,4 +71,3 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
_alertLevelSystem.SetLevel(component.AffectedStation, "epsilon", true, true, true);
}
}
}
+2 -1
View File
@@ -109,7 +109,8 @@ public sealed class TraitSystem : EntitySystem
JobId = jobId,
SpeciesId = speciesId,
Profile = profile,
StatusEffects = _statusEffects
StatusEffects = _statusEffects,
SelectedTraits = selectedTraits
};
foreach (var traitId in selectedTraits)
@@ -1,8 +0,0 @@
namespace Content.Server._DV.Uprising;
[RegisterComponent, Access(typeof(UprisingRuleSystem))]
public sealed partial class UprisingConsoleComponent : Component
{
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(5);
}
@@ -1,54 +0,0 @@
using Content.Shared._DV.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server._DV.Uprising;
[RegisterComponent, Access(typeof(UprisingRuleSystem)), AutoGenerateComponentPause]
public sealed partial class UprisingRuleComponent : Component
{
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? NukeAnnouncementAt;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? NukeTimeAt;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? FirstWarningAt;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? ImpendingWarningAt;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? FinalWarningAt;
[DataField]
public TimeSpan NukeAnnouncementDelay = TimeSpan.FromMinutes(30);
[DataField]
public TimeSpan NukeTimeDelay = TimeSpan.FromMinutes(90);
[DataField]
public TimeSpan FirstWarning = TimeSpan.FromMinutes(30);
[DataField]
public TimeSpan ImpendingWarning = TimeSpan.FromMinutes(15);
[DataField]
public TimeSpan FinalWarning = TimeSpan.FromMinutes(5);
[DataField]
public float NukeDuration = 60f;
[DataField]
public (UprisingSide, TimeSpan)? SideWinsAt;
[DataField]
public UprisingSide? SideWon;
[DataField]
public Dictionary<UprisingSide, TimeSpan> SideTimes = new()
{
{ UprisingSide.Insurgent , TimeSpan.FromMinutes(3) },
{ UprisingSide.Loyalist , TimeSpan.FromMinutes(3) },
};
}
@@ -1,341 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Antag;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Nuke;
using Content.Server.Pinpointer;
using Content.Server.Roles;
using Content.Shared._DV.Roles;
using Content.Shared._DV.Uprising;
using Content.Shared.Chat;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.DoAfter;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Content.Shared.Roles;
using Content.Shared.Verbs;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server._DV.Uprising;
public sealed class UprisingRuleSystem : GameRuleSystem<UprisingRuleComponent>
{
[Dependency] private readonly SharedChatSystem _chat = default!;
[Dependency] private readonly NukeSystem _nuke = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LoyalistRoleComponent, GetBriefingEvent>(OnGetLoyalistBriefing);
SubscribeLocalEvent<InsurgentRoleComponent, GetBriefingEvent>(OnGetInsurgentBriefing);
SubscribeLocalEvent<UprisingVictoryObjectiveComponent, ObjectiveGetProgressEvent>(OnUprisingVictoryProgress);
SubscribeLocalEvent<UprisingConsoleComponent, GetVerbsEvent<AlternativeVerb>>(OnGetConsoleVerbs);
SubscribeLocalEvent<UprisingConsoleComponent, UprisingArmDoAfter>(OnArm);
SubscribeLocalEvent<UprisingConsoleComponent, UprisingDisarmDoAfter>(OnDisarm);
}
private void OnUprisingVictoryProgress(Entity<UprisingVictoryObjectiveComponent> ent, ref ObjectiveGetProgressEvent args)
{
if (GetRule() is not { } rule)
return;
if (rule.Comp1.SideWinsAt is var (side, _) && ent.Comp.Side == side)
args.Progress = 0.5f;
else if (rule.Comp1.SideWon == ent.Comp.Side)
args.Progress = 1f;
else
args.Progress = 0f;
}
protected override void Started(EntityUid uid, UprisingRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
component.NukeAnnouncementAt = Timing.CurTime + component.NukeAnnouncementDelay;
component.NukeTimeAt = Timing.CurTime + component.NukeTimeDelay;
component.FirstWarningAt = component.NukeTimeAt - component.FirstWarning;
component.ImpendingWarningAt = component.NukeTimeAt - component.ImpendingWarning;
component.FinalWarningAt = component.NukeTimeAt - component.FinalWarning;
}
protected override void ActiveTick(EntityUid uid, UprisingRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.NukeAnnouncementAt is { } nukeAnnouncement && nukeAnnouncement <= Timing.CurTime)
{
var time = component.NukeTimeAt.HasValue ? component.NukeTimeAt.Value - Timing.CurTime : component.NukeTimeDelay;
_chat.DispatchGlobalAnnouncement(Loc.GetString("uprising-announcement-nuke-announcement", ("time", (int)Math.Round(time.TotalMinutes))), colorOverride: Color.Gold);
component.NukeAnnouncementAt = null;
}
if (component.FirstWarningAt is { } firstWarning && firstWarning <= Timing.CurTime)
{
var time = component.NukeTimeAt.HasValue ? component.NukeTimeAt.Value - Timing.CurTime : component.FirstWarning;
_chat.DispatchGlobalAnnouncement(Loc.GetString("uprising-announcement-first-warning", ("time", (int)Math.Round(time.TotalMinutes))), colorOverride: Color.Gold);
component.FirstWarningAt = null;
}
if (component.ImpendingWarningAt is { } impendingWarning && impendingWarning <= Timing.CurTime)
{
var time = component.NukeTimeAt.HasValue ? component.NukeTimeAt.Value - Timing.CurTime : component.ImpendingWarning;
_chat.DispatchGlobalAnnouncement(Loc.GetString("uprising-announcement-impending-warning", ("time", (int)Math.Round(time.TotalMinutes))), colorOverride: Color.Gold);
component.ImpendingWarningAt = null;
}
if (component.FinalWarningAt is { } finalWarning && finalWarning <= Timing.CurTime)
{
var time = component.NukeTimeAt.HasValue ? component.NukeTimeAt.Value - Timing.CurTime : component.FinalWarning;
_chat.DispatchGlobalAnnouncement(Loc.GetString("uprising-announcement-final-warning", ("time", (int)Math.Round(time.TotalMinutes))), colorOverride: Color.Gold);
component.FinalWarningAt = null;
}
if (component.NukeTimeAt is { } nukeTime && nukeTime <= Timing.CurTime)
{
component.NukeTimeAt = null;
var query = EntityQueryEnumerator<NukeComponent>();
while (query.MoveNext(out var bombUid, out var bomb))
{
_nuke.SetRemainingTime(bombUid, component.NukeDuration, bomb);
_itemSlots.SetLock(bombUid, bomb.DiskSlot, false);
_itemSlots.TryEject(bombUid, bomb.DiskSlot, null, out _, true);
_itemSlots.SetLock(bombUid, bomb.DiskSlot, true);
_nuke.ArmBomb(bombUid, bomb, "uprising-announcement-nuke-armed");
}
}
if (component.SideWinsAt is var (winningSide, winningTime) && winningTime <= Timing.CurTime)
{
component.SideWinsAt = null;
var query = EntityQueryEnumerator<NukeComponent>();
while (query.MoveNext(out var bombUid, out var bomb))
{
_nuke.DisarmBomb(bombUid, bomb);
}
component.SideWon = winningSide;
_chat.DispatchGlobalAnnouncement(
Loc.GetString($"uprising-announcement-victory-announcement-{winningSide}"),
Loc.GetString($"uprising-announcement-victory-announcement-{winningSide}.sender"),
colorOverride: Color.FromXaml(Loc.GetString($"uprising-announcement-victory-announcement-{winningSide}.color")));
GameTicker.EndRound();
}
}
protected override void AppendRoundEndText(EntityUid uid,
UprisingRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
if (component.SideWinsAt is var (partialSide, _))
{
args.AddLine(Loc.GetString($"uprising-outcome-{partialSide}-partial"));
}
else if (component.SideWon is { } winningSide)
{
args.AddLine(Loc.GetString($"uprising-outcome-{winningSide}-full"));
}
else
{
args.AddLine(Loc.GetString("uprising-outcome-none"));
}
args.AddLine(Loc.GetString("uprising-leads"));
var antags = _antag.GetAntagIdentifiers(uid);
var leads = new HashSet<EntityUid>();
foreach (var (mind, sessionData, name) in antags)
{
leads.Add(mind);
if (_role.MindHasRole<UprisingRoleComponent>(mind, out var it))
args.AddLine(Loc.GetString($"uprising-leads-user.{it.Value.Comp2.Side}", ("name", name), ("user", sessionData.UserName)));
else
args.AddLine(Loc.GetString("uprising-leads-user.Unknown", ("name", name), ("user", sessionData.UserName)));
}
args.AddLine(string.Empty);
var lines = new Dictionary<UprisingSide, List<string>>();
foreach (var value in Enum.GetValues<UprisingSide>())
{
lines[value] = new();
}
var minds = EntityQueryEnumerator<MindComponent>();
while (minds.MoveNext(out var mindUid, out var mindComponent))
{
if (leads.Contains(mindUid))
continue;
if (!_role.MindHasRole<UprisingRoleComponent>((mindUid, mindComponent), out var role) || mindComponent.OriginalOwnerUserId is not { } owner)
continue;
if (!_player.TryGetPlayerData(owner, out var data))
continue;
lines[role.Value.Comp2.Side].Add(Loc.GetString($"uprising-members-user", ("name", mindComponent.CharacterName ?? string.Empty), ("user", data.UserName)));
}
foreach (var value in Enum.GetValues<UprisingSide>())
{
args.AddLine(Loc.GetString($"uprising-members.{value}"));
foreach (var line in lines[value])
{
args.AddLine(line);
}
args.AddLine(string.Empty);
}
}
private void OnGetLoyalistBriefing(Entity<LoyalistRoleComponent> ent, ref GetBriefingEvent args)
{
args.Append(Loc.GetString("antag-Loyalist.briefing"));
}
private void OnGetInsurgentBriefing(Entity<InsurgentRoleComponent> ent, ref GetBriefingEvent args)
{
args.Append(Loc.GetString("antag-Insurgent.briefing"));
}
private Entity<UprisingRuleComponent, GameRuleComponent>? GetRule()
{
var enumerator = QueryActiveRules();
while (enumerator.MoveNext(out var uid, out _, out var uprising, out var gamerule))
{
return (uid, uprising, gamerule);
}
return null;
}
private void StartWinning(UprisingSide side, EntityUid console)
{
if (GetRule() is not { } rule)
return;
var location = _navMap.GetNearestBeaconString(console);
var announcement = Loc.GetString($"uprising-announcement-start-victory-announcement-{side}",
("time", (int) Math.Round(rule.Comp1.SideTimes[side].TotalSeconds)),
("location", FormattedMessage.RemoveMarkupOrThrow(location)));
_chat.DispatchGlobalAnnouncement(
announcement,
Loc.GetString($"uprising-announcement-start-victory-announcement-{side}.sender"),
colorOverride: Color.FromXaml(Loc.GetString($"uprising-announcement-start-victory-announcement-{side}.color")));
rule.Comp1.SideWinsAt = (side, Timing.CurTime + rule.Comp1.SideTimes[side]);
}
private void StopWinning()
{
if (GetRule() is not { } rule || rule.Comp1.SideWinsAt is not var (side, time))
return;
rule.Comp1.SideTimes[side] = Timing.CurTime - time;
_chat.DispatchGlobalAnnouncement(
Loc.GetString($"uprising-announcement-interrupted-victory-announcement-{side}"),
Loc.GetString($"uprising-announcement-interrupted-victory-announcement-{side}.sender"),
colorOverride: Color.FromXaml(Loc.GetString($"uprising-announcement-interrupted-victory-announcement-{side}.color")));
rule.Comp1.SideWinsAt = null;
}
private void OnGetConsoleVerbs(Entity<UprisingConsoleComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (GetRule() is not { } rule)
return;
var user = args.User;
if (rule.Comp1.SideWinsAt is var (disarmSide, _))
{
args.Verbs.Add(new ()
{
Act = () => StartDisarming(ent, user),
Text = Loc.GetString($"uprising-disarm-verb.{disarmSide}"),
});
return;
}
if (!CheckCanArm(args.User, out var armSide))
return;
args.Verbs.Add(new()
{
Act = () => StartArming(ent, user),
Text = Loc.GetString($"uprising-arm-verb.{armSide}"),
});
}
private bool CheckCanArm(EntityUid user, [NotNullWhen(true)] out UprisingSide? side)
{
side = null;
if (_mind.GetMind(user) is not { } mind || !TryComp<MindComponent>(mind, out var mindComp))
return false;
if (!_role.MindHasRole<UprisingRoleComponent>(mind, out var uprisingRole))
return false;
foreach (var objective in mindComp.Objectives)
{
if (!HasComp<UprisingArmRequiredObjectiveComponent>(objective))
continue;
if (_objectives.GetProgress(objective, (mind, mindComp)) is not >= 1)
return false;
}
side = uprisingRole.Value.Comp2.Side;
return true;
}
private void StartDisarming(Entity<UprisingConsoleComponent> ent, EntityUid user)
{
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
ent.Comp.Delay,
new UprisingDisarmDoAfter(),
ent.Owner));
}
private void StartArming(Entity<UprisingConsoleComponent> ent, EntityUid user)
{
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
ent.Comp.Delay,
new UprisingArmDoAfter(),
ent.Owner));
}
private void OnArm(Entity<UprisingConsoleComponent> ent, ref UprisingArmDoAfter args)
{
if (args.Handled || args.Cancelled || !CheckCanArm(args.User, out var side))
return;
StartWinning(side.Value, ent);
}
private void OnDisarm(Entity<UprisingConsoleComponent> ent, ref UprisingDisarmDoAfter args)
{
if (args.Handled || args.Cancelled)
return;
StopWinning();
}
}
+9 -1
View File
@@ -41,7 +41,13 @@ namespace Content.Shared.Chat
[NonSerialized]
public bool Read;
public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
/// <summary>
/// DeltaV - If not null, contains the radio channel proto ID that the message was sent on.
/// Used to replace the channel color client-side.
/// </summary>
public string? RadioChannelProtoId = null;
public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, string? radioChannelProto = null) // DeltaV - added radioChannelProto
{
Channel = channel;
Message = message;
@@ -52,6 +58,7 @@ namespace Content.Shared.Chat
MessageColorOverride = colorOverride;
AudioPath = audioPath;
AudioVolume = audioVolume;
RadioChannelProtoId = radioChannelProto; // DeltaV
}
public ChatMessage(ChatMessage copyFrom)
@@ -66,6 +73,7 @@ namespace Content.Shared.Chat
AudioPath = copyFrom.AudioPath;
AudioVolume = copyFrom.AudioVolume;
Read = copyFrom.Read;
RadioChannelProtoId = copyFrom.RadioChannelProtoId; // DeltaV
}
}
@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Ghost;
using Content.Shared.Interaction;
using Content.Shared.Inventory; // Harmony - for lanyards
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
@@ -293,7 +294,7 @@ namespace Content.Shared.Examine
/// If you're pushing multiple messages that should be grouped together (or ordered in some way),
/// call <see cref="PushGroup"/> before pushing and <see cref="PopGroup"/> when finished.
/// </summary>
public sealed class ExaminedEvent : EntityEventArgs
public sealed class ExaminedEvent : EntityEventArgs, IInventoryRelayEvent //Harmony InventoryRelayEvent for Lanyards
{
/// <summary>
/// The message that will be displayed as the examine text.
@@ -529,6 +530,7 @@ namespace Content.Shared.Examine
}
private record ExamineMessagePart(FormattedMessage Message, int Priority, bool DoNewLine, string? Group);
public SlotFlags TargetSlots { get; } = SlotFlags.NECK; // Harmony - For targeting lanyards.
}
@@ -63,7 +63,7 @@ public sealed partial class FaxMachineComponent : Component
/// Sound to play when fax printing new message
/// </summary>
[DataField]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
public SoundSpecifier? PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg"); // DeltaV - nullable
/// <summary>
/// Sound to play when fax successfully send message
+4 -1
View File
@@ -17,13 +17,15 @@ public sealed class FaxUiState : BoundUserInterfaceState
public bool IsPaperInserted { get; }
public bool CanSend { get; }
public bool CanCopy { get; }
public bool CanSendCooldown; // DeltaV - AI fax
public FaxUiState(string deviceName,
Dictionary<string, string> peers,
bool canSend,
bool canCopy,
bool isPaperInserted,
string? destAddress)
string? destAddress,
bool canSendCooldown) // DeltaV - AI fax
{
DeviceName = deviceName;
AvailablePeers = peers;
@@ -31,6 +33,7 @@ public sealed class FaxUiState : BoundUserInterfaceState
CanSend = canSend;
CanCopy = canCopy;
DestinationAddress = destAddress;
CanSendCooldown = canSendCooldown; // DeltaV - AI fax
}
}
@@ -33,6 +33,7 @@ using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Wieldable;
using Content.Shared.Zombies;
using Content.Shared.Examine; //Harmony
namespace Content.Shared.Inventory;
@@ -64,6 +65,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ExaminedEvent>(RelayInventoryEvent); // Harmony - added for lanyards.
// by-ref events
SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);
@@ -1,6 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement; //Harmony Lanyards
using Content.Shared.Inventory; //Harmony Lanyards
using Content.Shared.Labels.Components;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Paper;
@@ -8,6 +10,7 @@ using Content.Shared.Tag; // DeltaV
using Robust.Shared.Containers;
using Robust.Shared.Prototypes; // DeltaV
using Robust.Shared.Utility;
using System.Linq; //Harmony Lanyards
namespace Content.Shared.Labels.EntitySystems;
@@ -34,6 +37,11 @@ public sealed partial class LabelSystem : EntitySystem
SubscribeLocalEvent<PaperLabelComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, ExaminedEvent>(OnExamined);
// Harmony - OnExamined can now be inventory relayed from the neck slot for lanyard implementation,
// getting the entity's pronouns and changing the label inspection text to reflect it being from a lanyard.
// This would cause any other neck slot item with the label component to be described as a lanyard on inspection,
// but currently no others exist.
SubscribeLocalEvent<PaperLabelComponent, InventoryRelayedEvent<ExaminedEvent>>((e, c, ev) => OnExaminedInInventory(e, c, ev.Args));
}
private void OnLabelCompMapInit(Entity<LabelComponent> ent, ref MapInitEvent args)
@@ -123,9 +131,65 @@ public sealed partial class LabelSystem : EntitySystem
args.PushMarkup(Loc.GetString("comp-paper-label-has-label"));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony addition begins - shows which stamps have been applied to a label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-label-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
// Harmony addition ends
}
}
// Harmony addition begins - version of OnExamined for when the event is inventory relayed. Used when reading from a worn lanyard.
private void OnExaminedInInventory(EntityUid uid, PaperLabelComponent comp, ExaminedEvent args)
{
if (comp.LabelSlot.Item is not { Valid: true } item)
return;
using (args.PushGroup(nameof(PaperLabelComponent)))
{
// UID's parent is saved to be used for localisation grammar when the label is from a lanyard, since the text is changed.
var user = Comp<TransformComponent>(uid).ParentUid;
if (!args.IsInDetailsRange)
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-cant-read", ("user", Identity.Entity(user, EntityManager))));
return;
}
if (!TryComp(item, out PaperComponent? paper))
// Assuming yaml has the correct entity whitelist, this should not happen.
return;
if (string.IsNullOrWhiteSpace(paper.Content))
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-blank", ("user", Identity.Entity(user, EntityManager))));
return;
}
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard", ("user", Identity.Entity(user, EntityManager))));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony - shows which stamps have been applied to a lanyard's label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-lanyard-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
}
}
// Harmony Addition Ends
// Not ref-sub due to being used for multiple subscriptions.
private void OnContainerModified(EntityUid uid, PaperLabelComponent label, ContainerModifiedMessage args)
{
@@ -68,6 +68,11 @@ public abstract class SharedSuitSensorSystem : EntitySystem
/// <returns>True if the sensor is assigned to a station or assigning it was successful. False otherwise.</returns>
public bool CheckSensorAssignedStation(Entity<SuitSensorComponent> sensor)
{
// Begin DeltaV - fallback
if (sensor.Comp.StationId.HasValue && _stationSystem.GetOwningStation(sensor.Owner) is null)
return true;
// End DeltaV - fallback
if (!sensor.Comp.StationId.HasValue && Transform(sensor.Owner).GridUid == null)
return false;
@@ -348,7 +353,7 @@ public abstract class SharedSuitSensorSystem : EntitySystem
var transform = ent.Comp2;
// check if sensor is enabled and worn by user
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || !HasComp<MobStateComponent>(sensor.User) || transform.GridUid == null)
if (sensor.Mode == SuitSensorMode.SensorOff || sensor.User == null || !HasComp<MobStateComponent>(sensor.User)) // DeltaV - don't block on grids here
return null;
// try to get mobs id from ID slot
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared._DV.Movement; // DeltaV
using Content.Shared._DV.NodeCrawl; // DeltaV - Node Crawling
using Content.Shared.ActionBlocker;
using Content.Shared.CCVar;
using Content.Shared.Friction;
@@ -50,6 +51,7 @@ public abstract partial class SharedMoverController : VirtualController
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tags = default!;
[Dependency] private readonly TileMovementSystem _tileMovement = default!; // DeltaV
[Dependency] private readonly NodeCrawlerMovementSystem _nodeCrawlerMovement = default!; // DeltaV - node crawling
protected EntityQuery<CanMoveInAirComponent> CanMoveInAirQuery;
protected EntityQuery<FootstepModifierComponent> FootstepModifierQuery;
@@ -264,6 +266,11 @@ public abstract partial class SharedMoverController : VirtualController
return;
// End DeltaV Additions
// Begin DeltaV Additions - node crawling
if (_nodeCrawlerMovement.TryTick((uid, mover, physicsComponent, xform)))
return;
// End DeltaV Additions
var touching = false;
// Whether we use tilefriction or not
if (weightless || inAirHelpless)
+1 -1
View File
@@ -276,7 +276,7 @@ public sealed class PaperSystem : EntitySystem
_appearance.SetData(entity, PaperVisuals.Status, status, appearance);
}
private void UpdateUserInterface(Entity<PaperComponent> entity)
public void UpdateUserInterface(Entity<PaperComponent> entity) // DeltaV - station AI
{
_uiSystem.SetUiState(entity.Owner, PaperUiKey.Key, new PaperBoundUserInterfaceState(entity.Comp.Content, entity.Comp.StampedBy, entity.Comp.Mode));
}
+2 -2
View File
@@ -65,8 +65,8 @@ public enum CollisionGroup
TabletopMachineLayer = Opaque | BulletImpassable,
// Airlocks, windoors, firelocks
GlassAirlockLayer = HighImpassable | MidImpassable | BulletImpassable | InteractImpassable,
AirlockLayer = Opaque | GlassAirlockLayer,
GlassAirlockLayer = HighImpassable | MidImpassable | LowImpassable | BulletImpassable | InteractImpassable, // DeltaV - we have ventcrawling
AirlockLayer = Opaque | LowImpassable | GlassAirlockLayer, // DeltaV - we have ventcrawling
// Airlock assembly
HumanoidBlockLayer = HighImpassable | MidImpassable,
@@ -120,10 +120,23 @@ public abstract partial class SharedBatterySystem : EntitySystem
var rechargerQuery = EntityQueryEnumerator<BatterySelfRechargerComponent, BatteryComponent>();
while (rechargerQuery.MoveNext(out var uid, out var recharger, out var battery))
{
// BEGIN DeltaV - Battery Drain
// Currently, NextAutoRecharge is only set when there's an explicit power draw from
// the battery, and it doesn't count a negative charge rate as battery charge being used
// so, this is to patch that in the case that the drain exceeds the recharge.
if (recharger.AutoRechargePauseTime > TimeSpan.Zero && battery.ChargeRate < 0)
{
TrySetChargeCooldown((uid, recharger));
continue;
}
// END DeltaV
if (recharger.NextAutoRecharge == null || curTime < recharger.NextAutoRecharge)
continue;
recharger.NextAutoRecharge = null; // Don't refresh every tick.
Dirty(uid, recharger);
RefreshChargeRate((uid, battery)); // Cooldown is over, apply the new recharge rate.
}
@@ -32,4 +32,10 @@ public sealed partial class PowerCellDrawComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public float UseCharge;
/// <summary>
/// DeltaV - If the battery's recharge should be paused while power draw is active.
/// </summary>
[DataField, AutoNetworkedField]
public bool PauseRecharging;
}
@@ -17,8 +17,14 @@ public sealed partial class PowerCellSystem
ent.Comp.Enabled = enabled;
Dirty(ent, ent.Comp);
if (TryGetBatteryFromSlot(ent.Owner, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
// BEGIN DeltaV - Allow PowerDraw to prevent recharge
if (TryGetBatteryFromSlot(ent.Owner, out var battery) && battery is { } existingBattery)
{
_battery.RefreshChargeRate(existingBattery.AsNullable());
if (ent.Comp.PauseRecharging) _battery.TrySetChargeCooldown(existingBattery.Owner);
}
// END DeltaV
}
@@ -24,7 +24,7 @@ public sealed partial class RadioChannelPrototype : IPrototype
public int Frequency { get; private set; } = 0;
[DataField("color")]
public Color Color { get; private set; } = Color.Lime;
public Color Color { get; set; } = Color.Lime; // DeltaV - Client-side radio channel colors, no longer private set
[IdDataField, ViewVariables]
public string ID { get; private set; } = default!;
@@ -101,6 +101,12 @@ public sealed partial class RatKingComponent : Component
{ RatKingOrderType.CheeseEm, "RatKingCommandCheeseEm" },
{ RatKingOrderType.Loose, "RatKingCommandLoose" }
};
/// <summary>
/// DeltaV - Servants within this radius will join the Rat King when vent crawling
/// </summary>
[DataField]
public float VentCrawlRecruitRadius = 4f;
}
[Serializable, NetSerializable]
@@ -415,7 +415,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem
public bool CanOpen(EntityUid user, EntityUid target, bool silent = false, EntityStorageComponent? component = null)
{
if (!Resolve(target, ref component))
if (!Resolve(target, ref component, false)) // DeltaV - Prevent HTNOperators from failing tests
return false;
if (!HasComp<HandsComponent>(user))
@@ -1,14 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.Roles.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.Uprising;
public sealed partial class ConversionAcceptActionEvent : InstantActionEvent
{
[DataField(required: true)]
public EntProtoId MindRole;
[DataField(required: true)]
public EntProtoId<MindRoleComponent> RemoveMindRole;
}
@@ -1,33 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Robust.Shared.Network;
namespace Content.Shared.Uprising;
public sealed class ConversionAcceptActionSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly INetManager _net = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindContainerComponent, ConversionAcceptActionEvent>(OnAcceptConversion);
}
private void OnAcceptConversion(Entity<MindContainerComponent> ent, ref ConversionAcceptActionEvent args)
{
if (_mind.GetMind(ent) is not { } mind)
return;
args.Handled = true;
if (_net.IsServer)
{
_role.MindAddRole(mind, args.MindRole);
_role.MindRemoveRole(mind, args.RemoveMindRole);
}
}
}
@@ -1,13 +0,0 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared.Uprising;
public sealed partial class ConversionOfferActionEvent : EntityTargetActionEvent
{
[DataField(readOnly: true)]
public List<Type> IncompatibleMindRoleTypes;
[DataField]
public EntProtoId AcceptAction;
}
@@ -1,35 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
namespace Content.Shared.Uprising;
public sealed class ConversionOfferActionSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MindRoleComponent, ConversionOfferActionEvent>(OnOffer);
}
private void OnOffer(Entity<MindRoleComponent> ent, ref ConversionOfferActionEvent args)
{
if (_mind.GetMind(args.Target) is not { } mind)
return;
foreach (var incompatible in args.IncompatibleMindRoleTypes)
{
if (_role.MindHasRole(mind, incompatible, out _))
return;
}
_actions.AddAction(args.Target, args.AcceptAction);
args.Handled = true;
}
}
@@ -1,6 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Actions;
[RegisterComponent, NetworkedComponent]
public sealed partial class DeleteOnPerformedActionComponent : Component;
@@ -1,18 +0,0 @@
using Content.Shared.Actions.Events;
namespace Content.Shared._DV.Actions;
public sealed class DeleteOnPerformedActionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeleteOnPerformedActionComponent, ActionPerformedEvent>(OnActionPerformed);
}
private void OnActionPerformed(Entity<DeleteOnPerformedActionComponent> ent, ref ActionPerformedEvent args)
{
PredictedQueueDel(ent);
}
}
@@ -0,0 +1,73 @@
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
namespace Content.Shared._DV.CCVars;
public sealed partial class DCCVars
{
public static readonly CVarDef<string> RadioColorCommon =
CVarDef.Create("deltav.options.radio.common_color",
"#2cdb2c",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Common radio channel.");
public static readonly CVarDef<string> RadioColorCommand =
CVarDef.Create("deltav.options.radio.command_color",
"#fcdf03",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Command radio channel.");
public static readonly CVarDef<string> RadioColorSecurity =
CVarDef.Create("deltav.options.radio.security_color",
"#8c93f5",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Security radio channel.");
public static readonly CVarDef<string> RadioColorMedical =
CVarDef.Create("deltav.options.radio.medical_color",
"#57b8f0",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Medical radio channel.");
public static readonly CVarDef<string> RadioColorEngineering =
CVarDef.Create("deltav.options.radio.engineering_color",
"#ff733c",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Engineering radio channel.");
public static readonly CVarDef<string> RadioColorScience =
CVarDef.Create("deltav.options.radio.science_color",
"#cd7ccd",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Science radio channel.");
public static readonly CVarDef<string> RadioColorCargo =
CVarDef.Create("deltav.options.radio.cargo_color",
"#b48b57",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Cargo radio channel.");
public static readonly CVarDef<string> RadioColorService =
CVarDef.Create("deltav.options.radio.service_color",
"#539c00",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Service radio channel.");
public static readonly CVarDef<string> RadioColorBinary =
CVarDef.Create("deltav.options.radio.binary_color",
"#5ed7aa",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Binary radio channel.");
public static readonly CVarDef<string> RadioColorJustice =
CVarDef.Create("deltav.options.radio.justice_color",
"#c70667",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Justice radio channel.");
public static readonly CVarDef<string> RadioColorPrison =
CVarDef.Create("deltav.options.radio.prison_color",
"#FFA500",
CVar.CLIENTONLY | CVar.ARCHIVE,
"The color of the Prison radio channel.");
}
@@ -0,0 +1,4 @@
namespace Content.Shared._DV.Fax;
[ByRefEvent]
public readonly record struct FaxPrintedEvent(EntityUid Fax);
@@ -0,0 +1,74 @@
using System.Linq;
using System.Globalization;
using Content.Shared.Dataset;
using Content.Shared.Humanoid;
using Content.Shared.Random.Helpers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared._DV.IonLaws;
public sealed class IonLawSystem : EntitySystem
{
[Dependency] private readonly ILocalizationManager _localization = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
private const string Culture = "en-US";
public override void Initialize()
{
base.Initialize();
var culture = new CultureInfo(Culture);
_localization.AddFunction(culture, "PICK", FormatPick);
_localization.AddFunction(culture, "PICK-ENTITY", FormatPickEntity);
}
private ILocValue FormatPick(LocArgs args)
{
var rand = ((LocValueRandom)args.Args[0]).Value;
return rand.Pick(args.Args.Skip(1).ToList());
}
private ILocValue FormatPickEntity(LocArgs args)
{
var rand = ((LocValueRandom)args.Args[0]).Value;
var players = ((LocValueEntityList)args.Args[1]).Value;
if (players.Count == 0)
{
return new LocValueString(Loc.GetString("dv-ion-law-target-fallback", ("random", new LocValueRandom(rand))));
}
return new LocValueEntity(rand.PickAndTake(players));
}
private static readonly ProtoId<LocalizedDatasetPrototype> IonLawTemplates = "IonLawTemplates";
public string GenerateLaw(IRobustRandom random)
{
var template = random.PickId(_proto.Index(IonLawTemplates));
var players = _player.Sessions
.Where(x => HasComp<HumanoidAppearanceComponent>(x.AttachedEntity))
.Select(x => x.AttachedEntity!.Value)
.ToList();
return Loc.GetString(template, ("random", new LocValueRandom(random)), ("players", new LocValueEntityList(players)));
}
}
public sealed record LocValueRandom(IRobustRandom Value) : LocValue<IRobustRandom>(Value)
{
public override string Format(LocContext ctx)
{
throw new InvalidOperationException("Attempted to format a IRobustRandom");
}
}
public sealed record LocValueEntityList(List<EntityUid> Value) : LocValue<List<EntityUid>>(Value)
{
public override string Format(LocContext ctx)
{
throw new InvalidOperationException("Attempted to format a List<EntityUid>");
}
}
@@ -137,6 +137,12 @@ public sealed partial class DeepFryerComponent : Component
/// <seealso cref="ProfessionalChefComponent"/>
[DataField]
public float MissChance = 0.25f;
/// <summary>
/// Check for whether the cooking timer needs to be reset after powering down
/// </summary>
[DataField]
public bool WasPreviouslyPowered = true;
}
/// <summary>
@@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Represents an entity in a node network that can be crawled in
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedNodeCrawlSystem), typeof(NodeCrawlerMovementSystem))]
public sealed partial class CrawlableNodeComponent : Component
{
/// <summary>
/// Other entities with <see cref="CrawlableNodeComponent" /> that can be reached from this one
/// </summary>
[ViewVariables, AutoNetworkedField]
public HashSet<EntityUid> ReachableNodes = new();
/// <summary>
/// Whether this node has an unconnected node and should be exited from on movement
/// </summary>
[ViewVariables, AutoNetworkedField]
public bool DeadEnd;
/// <summary>
/// All entities with <see cref="NodeCrawlerMovementComponent" /> that are associated with this one
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<EntityUid> Crawlers = new();
}
@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Marker component to use on atmos-related devices that node crawlers can enter/exit out of.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class NodeCrawlVentAccessComponent : Component;
@@ -0,0 +1,44 @@
using Content.Shared.DoAfter;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Handles entities that can enter and exit node-constrained movement.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(SharedNodeCrawlSystem))]
public sealed partial class NodeCrawlerComponent : Component
{
/// <summary>
/// The mover this crawler is currently being carried by, if any
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Mover;
/// <summary>
/// Components of entities to reveal while inside a mover
/// </summary>
[DataField(readOnly: true)]
public Type[] RevealedComponents;
/// <summary>
/// Whitelist for entities that will be considered as exit nodes.
/// </summary>
[DataField]
public EntityWhitelist? ExitNodes;
/// <summary>
/// How long it takes to enter a node.
/// </summary>
[DataField]
public TimeSpan EnterDelay = TimeSpan.FromSeconds(1);
}
[Serializable, NetSerializable]
public sealed partial class NodeCrawlEnterDoAfterEvent : SimpleDoAfterEvent;
[ByRefEvent]
public readonly record struct NodeCrawlerStartedCrawlingEvent(Entity<NodeCrawlerMovementComponent> Mover);
@@ -0,0 +1,42 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Handles node-confined movement for an entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(NodeCrawlerMovementSystem))]
public sealed partial class NodeCrawlerMovementComponent : Component
{
/// <summary>
/// The current node.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Node;
/// <summary>
/// The target node being moved to.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? TargetNode;
/// <summary>
/// The crawler being carried by this entity.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? HeldCrawler;
/// <summary>
/// The required angle to be within for deciding which node to move from the target direction
/// </summary>
[DataField]
public double RequiredAngle = Math.PI / 4f;
}
/// <summary>
/// Event raised when a node crawler arrives at a node entity.
/// </summary>
/// <param name="Node">The arrived-at node.</param>
[ByRefEvent]
public readonly record struct NodeCrawlerArrivedAtNodeEvent(EntityUid Node);
@@ -0,0 +1,195 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Shared._DV.NodeCrawl;
public sealed class NodeCrawlerMovementSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedNodeCrawlSystem _nodeCrawl = default!;
public bool TryTick(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent> sharedMover)
{
if (!TryComp<NodeCrawlerMovementComponent>(sharedMover, out var crawler) || crawler.Node is null)
return false;
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover = (sharedMover.Owner, sharedMover.Comp1, sharedMover.Comp2, sharedMover.Comp3, crawler);
if (mover.Comp4.TargetNode is { } target)
OngoingMovement(mover, target);
else
StartMovement(mover);
return true;
}
private void StartMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover)
{
if (GetDestination(mover, mover.Comp1.HeldMoveButtons) is not { } target)
{
if (mover.Comp4.Node is not { } node)
return;
var nodeComp = Comp<CrawlableNodeComponent>(node);
if (!nodeComp.DeadEnd)
return;
if (mover.Comp4.HeldCrawler is not { } crawler)
return;
_nodeCrawl.ExitNodeCrawl((crawler, Comp<NodeCrawlerComponent>(crawler)));
return;
}
mover.Comp4.TargetNode = target;
Dirty(mover, mover.Comp4);
OngoingMovement(mover, target);
}
private void StopMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover)
{
_physics.SetLinearVelocity(mover, Vector2.Zero, body: mover.Comp2);
_physics.SetAngularVelocity(mover, 0, body: mover.Comp2);
}
private void OngoingMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target)
{
var speed = MoveSpeed(mover);
if (ReachedDestination(mover, target, speed))
{
StopMovement(mover);
SetNode((mover, mover), target);
mover.Comp4.TargetNode = null;
Dirty(mover, mover.Comp4);
if (TryComp<MovementRelayTargetComponent>(mover, out var movementTarget))
{
var evt = new NodeCrawlerArrivedAtNodeEvent(target);
RaiseLocalEvent(movementTarget.Source, ref evt);
}
StartMovement(mover);
return;
}
UpdateMovement(mover, target, speed);
}
private float MoveSpeed(Entity<InputMoverComponent> mover)
{
var moveSpeed = CompOrNull<MovementSpeedModifierComponent>(mover);
var walkSpeed = moveSpeed?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
var sprintSpeed = moveSpeed?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
return mover.Comp.Sprinting ? sprintSpeed : walkSpeed;
}
private void UpdateMovement(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target,
float speed)
{
var delta = _transform.GetWorldPosition(target) - _transform.GetWorldPosition(mover.Comp3);
var facing = Angle.FromWorldVec(delta);
_transform.SetWorldRotation(mover.Comp3, facing);
var velocity = delta;
velocity.Normalize();
velocity *= speed;
_physics.SetCanCollide(mover, false);
_physics.SetLinearVelocity(mover, velocity, body: mover.Comp2);
_physics.SetAngularVelocity(mover, 0, body: mover.Comp2);
}
private bool ReachedDestination(
Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> mover,
EntityUid target,
float speed)
{
var delta = _transform.GetWorldPosition(mover.Comp3) - _transform.GetWorldPosition(target);
return delta.EqualsApprox(Vector2.Zero, speed * 0.01f);
}
private EntityUid? GetDestination(Entity<InputMoverComponent, PhysicsComponent, TransformComponent, NodeCrawlerMovementComponent> ent, MoveButtons buttons)
{
if ((buttons & MoveButtons.AnyDirection) == 0)
return null;
var target = _mover.DirVecForButtons(buttons);
target = _mover.GetParentGridAngle(ent.Comp1).RotateVec(target);
if (ent.Comp4.Node is not { } node || !Exists(node) || !TryComp<CrawlableNodeComponent>(node, out var nodeCrawl))
return null;
var nodeXform = Transform(node);
var nodeWorld = _transform.GetWorldPosition(nodeXform);
var smallestTarget = EntityUid.Invalid;
var largestDot = 0d;
foreach (var reachable in nodeCrawl.ReachableNodes)
{
var reachableXform = Transform(reachable);
var reachableWorld = _transform.GetWorldPosition(reachableXform);
var delta = reachableWorld - nodeWorld;
delta.Normalize();
var deltaTargetDot = Vector2.Dot(delta, target);
if (deltaTargetDot < largestDot)
continue;
smallestTarget = reachable;
largestDot = deltaTargetDot;
}
if (!smallestTarget.Valid || largestDot <= Math.Cos(ent.Comp4.RequiredAngle))
return null;
return smallestTarget;
}
public void SetNode(Entity<NodeCrawlerMovementComponent> ent, EntityUid? node)
{
if (ent.Comp.Node == node)
return;
if (ent.Comp.Node is { } oldNode)
{
var oldNodeComp = Comp<CrawlableNodeComponent>(oldNode);
oldNodeComp.Crawlers.Remove(ent);
Dirty(oldNode, oldNodeComp);
}
if (node is { } newNode)
{
var newNodeComp = Comp<CrawlableNodeComponent>(newNode);
newNodeComp.Crawlers.Add(ent);
Dirty(newNode, newNodeComp);
}
ent.Comp.Node = node;
Dirty(ent);
}
public void SetHeldCrawler(Entity<NodeCrawlerMovementComponent> ent, EntityUid? held)
{
if (ent.Comp.HeldCrawler == held)
return;
ent.Comp.HeldCrawler = held;
Dirty(ent);
}
}
@@ -0,0 +1,229 @@
using Content.Shared.DoAfter;
using Content.Shared.Eye;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.RatKing;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.NodeCrawl;
/// <summary>
/// Manages entry & exit of node crawlers into node networks
/// </summary>
public abstract class SharedNodeCrawlSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly NodeCrawlerMovementSystem _nodeCrawler = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
private const string MoverContainer = "mover-container";
private static readonly EntProtoId MoverProto = "DVNodeCrawlMover";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrawlableNodeComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<NodeCrawlerComponent, NodeCrawlEnterDoAfterEvent>(OnNodeCrawlEntryDoAfter);
SubscribeLocalEvent<NodeCrawlerComponent, NodeCrawlerArrivedAtNodeEvent>(OnArrivedAtNode);
SubscribeLocalEvent<NodeCrawlerComponent, GetVisMaskEvent>(OnGetVisMask);
SubscribeLocalEvent<CrawlableNodeComponent, ComponentShutdown>(OnCrawlableShutdown);
SubscribeLocalEvent<NodeCrawlerMovementComponent, ComponentShutdown>(OnMovementShutdown);
SubscribeLocalEvent<NodeCrawlerComponent, ComponentShutdown>(OnCrawlerShutdown);
SubscribeLocalEvent<CrawlableNodeComponent, AnchorStateChangedEvent>(OnCrawlableAnchorChanged);
SubscribeLocalEvent<RatKingComponent, NodeCrawlerStartedCrawlingEvent>(OnRatKingStartedCrawling);
}
private void OnGetVerbs(Entity<CrawlableNodeComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
var user = args.User;
var target = args.Target;
if (!TryComp<NodeCrawlerComponent>(user, out var nodeCrawler))
return;
if (!_entityWhitelist.IsWhitelistPass(nodeCrawler.ExitNodes, target))
return;
if (!args.CanAccess)
return;
args.Verbs.Add(new AlternativeVerb
{
Act = () => StartEntryDoAfter((user, nodeCrawler), target),
Text = Loc.GetString("node-crawl-enter", ("target", target)),
});
}
private void StartEntryDoAfter(Entity<NodeCrawlerComponent> ent, EntityUid target)
{
var doAfterArgs = new DoAfterArgs(EntityManager, ent.Owner, ent.Comp.EnterDelay, new NodeCrawlEnterDoAfterEvent(), ent.Owner, target)
{
BreakOnMove = true,
BreakOnDamage = true,
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
private void OnNodeCrawlEntryDoAfter(Entity<NodeCrawlerComponent> ent, ref NodeCrawlEnterDoAfterEvent args)
{
if (args.Cancelled || args.Target is not { } target)
return;
NodeCrawl(ent, target);
}
private void NodeCrawl(Entity<NodeCrawlerComponent> ent, EntityUid target)
{
if (!_net.IsServer)
return;
var mover = Spawn(MoverProto, Transform(target).Coordinates);
var crawler = Comp<NodeCrawlerMovementComponent>(mover);
var container = _container.GetContainer(mover, MoverContainer);
_container.Insert(ent.Owner, container);
ent.Comp.Mover = mover;
Dirty(ent);
var evt = new NodeCrawlerStartedCrawlingEvent((mover, crawler));
RaiseLocalEvent(ent, ref evt);
_nodeCrawler.SetNode((mover, crawler), target);
_nodeCrawler.SetHeldCrawler((mover, crawler), ent);
_mover.SetRelay(ent, mover);
_physics.SetCanCollide(ent.Owner, false);
_physics.SetCanCollide(mover, false);
_eye.RefreshVisibilityMask(ent.Owner);
}
/// <summary>
/// Causes this node crawler to exit its node crawl.
/// </summary>
/// <param name="ent">The crawler to exit node-crawl from.</param>
public void ExitNodeCrawl(Entity<NodeCrawlerComponent> ent)
{
if (ent.Comp.Mover is not { } mover)
return;
ent.Comp.Mover = null;
Dirty(ent);
var container = _container.GetContainer(mover, MoverContainer);
_container.Remove(ent.Owner, container);
foreach (var other in _container.EmptyContainer(container))
{
if (!TryComp<NodeCrawlerComponent>(other, out var otherCrawler))
continue;
otherCrawler.Mover = null;
Dirty(other, otherCrawler);
}
RemComp<RelayInputMoverComponent>(ent);
if (_net.IsServer && !TerminatingOrDeleted(mover))
QueueDel(mover); // deletion isn't predicted because client queued deletion doesn't interact well with container stuff
_physics.SetCanCollide(ent.Owner, true);
_eye.RefreshVisibilityMask(ent.Owner);
}
private void OnArrivedAtNode(Entity<NodeCrawlerComponent> ent, ref NodeCrawlerArrivedAtNodeEvent args)
{
if (!_entityWhitelist.IsWhitelistPass(ent.Comp.ExitNodes, args.Node))
return;
ExitNodeCrawl(ent);
}
private void OnGetVisMask(Entity<NodeCrawlerComponent> ent, ref GetVisMaskEvent args)
{
if (ent.Comp.Mover is null)
return;
args.VisibilityMask |= (int)VisibilityFlags.Subfloor;
}
private void OnCrawlableShutdown(Entity<CrawlableNodeComponent> ent, ref ComponentShutdown args)
{
foreach (var crawler in ent.Comp.Crawlers)
{
var movement = Comp<NodeCrawlerMovementComponent>(crawler);
if (movement.HeldCrawler is not { } held)
continue;
_nodeCrawler.SetNode((crawler, movement), null);
ExitNodeCrawl((held, Comp<NodeCrawlerComponent>(held)));
}
}
private void OnMovementShutdown(Entity<NodeCrawlerMovementComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.Node is { } node)
{
var nodeComp = Comp<CrawlableNodeComponent>(node);
nodeComp.Crawlers.Remove(ent);
Dirty(node, nodeComp);
}
if (ent.Comp.HeldCrawler is { } crawler)
{
ExitNodeCrawl((crawler, Comp<NodeCrawlerComponent>(crawler)));
}
}
private void OnCrawlerShutdown(Entity<NodeCrawlerComponent> ent, ref ComponentShutdown args)
{
ExitNodeCrawl(ent);
}
private void OnCrawlableAnchorChanged(Entity<CrawlableNodeComponent> ent, ref AnchorStateChangedEvent args)
{
if (args.Anchored)
return;
foreach (var crawler in ent.Comp.Crawlers)
{
var movement = Comp<NodeCrawlerMovementComponent>(crawler);
if (movement.HeldCrawler is not { } held)
continue;
ExitNodeCrawl((held, Comp<NodeCrawlerComponent>(held)));
}
}
private void OnRatKingStartedCrawling(Entity<RatKingComponent> ent, ref NodeCrawlerStartedCrawlingEvent args)
{
var entities = new HashSet<Entity<RatKingServantComponent>>();
_entityLookup.GetEntitiesInRange(Transform(ent).Coordinates,
ent.Comp.VentCrawlRecruitRadius,
entities);
var container = _container.GetContainer(args.Mover, MoverContainer);
foreach (var servant in entities)
{
if (servant.Comp.King != ent)
continue;
_container.Insert(servant.Owner, container);
}
}
}
@@ -1,3 +1,4 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DeviceNetwork.Systems;
@@ -54,5 +55,11 @@ public sealed class PageSenderSystem : EntitySystem
{ PagerConstants.DataBody, body },
};
_deviceNetwork.QueuePacket(ent, null, payload);
if (HasComp<PagerComponent>(ent))
{
var evt = new CartridgeLoaderNotificationSentEvent(Loc.GetString("pager-self-notification"), body);
RaiseLocalEvent(ent, ref evt);
}
}
}

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