Compare commits

..
Author SHA1 Message Date
Tobias Berger c1cc812cd3 Better admin logging for nanochat 2025-12-14 21:09:23 +01:00
Tobias Berger e273a1fa3a Add some more nanochat admin logging 2025-12-14 20:21:00 +01:00
Tobias Berger e297168eef Fix emoji some more 2025-12-12 10:43:22 +01:00
Tobias Berger 984a5384a5 Revert "Show numbers in message sender label"
This reverts commit 9e617db031.
2025-12-11 16:51:02 +01:00
Tobias Berger b37476785c Only lookup recipients inside a PDA 2025-12-11 16:40:32 +01:00
Tobias Berger 997d5b691f Disable NanoChat UI when no ID inserted 2025-12-11 15:40:48 +01:00
Tobias Berger 63e37a76bf Contact list update on create 2025-12-11 15:14:39 +01:00
Tobias Berger 66b8a7f74b Emoji ID for debugging 2025-12-11 14:19:12 +01:00
Tobias Berger 35b07aeacd Fix non-localized string 2025-12-11 14:05:49 +01:00
Tobias Berger fe2a30456f Escape nanochat messages 2025-12-11 14:04:25 +01:00
Tobias Berger e23d6f3ba7 Sort instead of Order 2025-12-11 12:44:50 +01:00
Tobias Berger f167435cfc Fix doubled sender number 2025-12-11 12:02:31 +01:00
Tobias Berger 3c93bfc943 Move 'view member' button 2025-12-11 12:02:12 +01:00
Tobias Berger 9e617db031 Show numbers in message sender label 2025-12-11 03:13:05 +01:00
Tobias Berger 258dd35754 Improve emoji insertion UX 2025-12-11 03:04:26 +01:00
Tobias Berger 126ee2ac35 Improve padding of NanoChat Message list 2025-12-11 03:04:15 +01:00
Tobias Berger 188e895d6a Clean up Funky commments in _DV 2025-12-11 03:03:36 +01:00
Tobias Berger 08ecb92fdc Clear emoji cache when prototypes reload 2025-12-11 02:51:38 +01:00
Tobias Berger 683b9db3d9 Touch up NanoChatRecipient 2025-12-11 02:50:50 +01:00
Tobias Berger 880c1bed31 Fix Emoji alignment 2025-12-11 02:48:22 +01:00
Tobias Berger aba4972866 Fix emoji 2025-12-10 19:51:01 +01:00
Tobias Berger 2758b202fa Remove redundant subscription 2025-12-10 18:40:32 +01:00
Tobias Berger f797d0bf8d Cherrypick fixes 2025-12-10 18:07:31 +01:00
Tobias Berger 5602dc5089 optimize nanochat emojis 2025-12-10 18:01:02 +01:00
Evaisa 19722eeec7 Fallback for listing sender names.
Sender names now show up if they are in your contacts, even if they do not have List Number enabled.
If their name can not be obtained then it will show "Unknown#0000" rather than just the nanochat number.

(cherry picked from commit 90775680d8b7b2d95636dd1027e56d48118e55e1)
2025-12-10 17:55:47 +01:00
Evaisa fc558e41ea compiles properly now
(cherry picked from commit a000c7b35937bbfc94909cf2da092e90a27161f1)
2025-12-10 17:55:47 +01:00
Evaisa daf070f876 Fix issues with nanochat
When pAI and PDA owner open nanochat at the same time it should no longer break.
"Invite To Group" list now shows accurate/live data instead of outdated cached recipient data
Popups should no longer stay open when switching apps or closing the PDA.
Group chats should now properly show when messages failed to deliver.
Invite menus now show job titles.
Invite menus are now sorted by department.

(cherry picked from commit f23580452a0a43cc105e5642ead35999a820c675)
2025-12-10 17:55:44 +01:00
funkystationbot 2cd02f1ea8 chore: Automatically update REUSE headers
(cherry picked from commit 661e8136af4c7b31d657531b667043e35b84190a)
2025-12-10 17:51:17 +01:00
Evaisa e1ce26896e Emoji definitions are done in yaml instead.
Emojis are now defined through yaml, hope I did not miss any.

(cherry picked from commit 48936a63248e7e76855157a36ff6214faa4c6051)
2025-12-10 17:51:17 +01:00
funkystationbot bd2245eee3 chore: Automatically update REUSE headers
(cherry picked from commit 84d2cc3a1f31878e5d18ebde63928d63f3b4dd95)
2025-12-10 17:51:13 +01:00
Evaisa 0b03791672 fixes
yippee

(cherry picked from commit 2d847cd7df0780f439cec6782414c86d859b0a56)
2025-12-10 17:50:35 +01:00
Evaisa 7250214259 Fix small bug
- Ownership did not get transferred when clicking the delete button in a group chat.

(cherry picked from commit d7ccc986155221dbbfb948ed0694347e72c50369)
2025-12-10 17:50:15 +01:00
Evaisa 46c3b5079d Fix some emojis not working because of RSI stuff & Bug with regular chats
- Regular chats were received as the wrong person.
- Certain emotes didn't load outside of debug builds seemingly.

(cherry picked from commit deb2b584c77b1715a1adb1327c52f80046f7c79a)
2025-12-10 17:50:15 +01:00
Evaisa 3e618923ea Added group chats and emojis
- You can now create group chats in NanoChat
- You can also send your meta friends your favourite :godo: emote.

(cherry picked from commit eedb378b9f33033faf4e9dd139f4b2366dcf0632)
2025-12-10 17:50:02 +01:00
Delta-V bot 36bc5a387d Automatic changelog update 2025-12-10 16:49:09 +01:00
Field Command fcc48538b1 Pebble Hotfix 2: Small Fixes (#4924)
Small Fixes
2025-12-10 09:48:49 -06:00
Delta-V bot 543ddb718e Automatic changelog update 2025-12-10 16:26:09 +01:00
pootslap 6d352688f7 Harpy emote tweaks. (#4915)
* gendered emotes + bird noises

* yawn fix

* Moved to _DV
2025-12-10 09:25:50 -06:00
Delta-V bot 20f282cd33 Automatic changelog update 2025-12-10 15:59:46 +01:00
verybigman 177a12f8d8 Mowed Astrosnow (#4921)
* idk

* cleanup after

* tada
2025-12-10 08:59:26 -06:00
Delta-V bot 9e9fd50bb9 Automatic changelog update 2025-12-10 15:57:57 +01:00
Sir Warock be03177c2e Made Rollerbeds immune to steptriggers (#4933) 2025-12-10 08:57:37 -06:00
Delta-V bot 7612a7dcf9 Automatic changelog update 2025-12-10 09:13:02 +01:00
Vanessa 0f921dc115 EntropicDegen Fix (#4931)
lol
2025-12-10 08:12:42 +00:00
Delta-V bot 496467c7ba Automatic changelog update 2025-12-10 09:05:52 +01:00
Stxcking f70b80e88a Add more festive holiday clothing to AutoDrobe (#4909)
* sprites galore

* no Santa monkey

* autodrobe

* cost up
2025-12-10 08:05:32 +00:00
Minerva 3526da8a62 Fixes coffe typo (#4930) 2025-12-09 21:00:08 -06:00
Delta-V bot ccde7f3164 Automatic changelog update 2025-12-09 20:44:47 +01:00
Colin-Tel 4b60952386 Chibi update 08DEC2025 (#4914)
* omg chibi so kawaii!~ >w<

* take a wild fucking guess

* cameras, fixrotations, tilewindows
2025-12-09 19:44:27 +00:00
Delta-V bot 0df8d7e238 Automatic changelog update 2025-12-09 19:31:27 +01:00
Stxcking 89cffdf305 Syndicat ghost role description tweak (#4889)
* final tweaks I promise.

* minor desc change requests

* typo buh
2025-12-09 18:31:07 +00:00
Delta-V bot bee774ad98 Automatic changelog update 2025-12-09 18:56:36 +01:00
snowywinters d1ee7499eb AI HUD and Comms Update (#4905)
* test one

* Removed mind shield hud
2025-12-09 11:56:16 -06:00
Delta-V bot 838c3c490d Automatic changelog update 2025-12-09 17:45:06 +01:00
MD360 9ba19a023f 100% Ion resistance on Ninja Suit and Visor NVG. (#4916)
* Add NVG to visor, Ion Immunity and no EMP armor ignore

* Comments..

* .ftl from up to _DV

* Rewrote visor description.

* Remove ftl newline, forgor desc comment.
2025-12-09 10:44:47 -06:00
67c127ecee Vulnerable String Sanitization (Prevent cmdlink) (#4922)
* Fix label markup escapeing (#40600)

fix

* Sanitize NanoChat

* Escape PDA Owner/Job

* Escape ID Examine

* Early merge of https://github.com/space-wizards/space-station-14/pull/41799

---------

Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-09 15:38:10 +00:00
Delta-V bot da74750c7d Automatic changelog update 2025-12-08 20:34:42 +01:00
verybigman 12f561b292 Reinforced Tinted Windows Bug Fix (#4918)
fix
2025-12-08 19:34:23 +00:00
Delta-V bot 87887e9555 Automatic changelog update 2025-12-08 16:42:26 +01:00
Field Command 7c02535bd1 Byoin TEG burn chamber edits! (#4911)
Update byoin.yml
2025-12-08 09:42:06 -06:00
Delta-V bot f58547b51a Automatic changelog update 2025-12-08 16:34:41 +01:00
a28ae76d34 Feroxi: Blood in the Water (#4902)
* Added shark vision to Feroxi.

* Fixed a bug if someone was holding a container of blood.

* Updated pulse time and useDelay for the pulse.

* Updated guidebook

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

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

* Cleanup

* More clean-up

* Removed vision goggles sound from ability

* Got rid of an unused namespace

* last fix I swear

* Apply suggestions from code review

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

* Apply suggestion from @Toby222

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

* Removed SharedLightSystem from SharkVisionOverlay

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-08 16:34:21 +01:00
Delta-V bot ebb91609ab Automatic changelog update 2025-12-08 16:33:34 +01:00
Vanessaandpre-commit-ci[bot] 6f288da78f Syndie borg fixes (#4913)
* Moved law boards to their own file, similar to the structure that upstream uses.

* Fixed syndicate medical borg ghost role. Moved DV lawsets.

* [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>
2025-12-08 16:33:14 +01:00
Delta-V bot c51f259c7d Automatic changelog update 2025-12-08 15:49:09 +01:00
c9ea4aeb77 xenoarch: node scanner UI overhaul (#4814)
* xenoarch: node scanner UI overhaul

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

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

* Update Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs

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

* Update Content.Shared/Xenoarchaeology/Artifact/SharedXenoArtifactSystem.Unlock.cs

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

---------

Signed-off-by: Charlie Morley <cmorley191@gmail.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>
2025-12-08 08:48:49 -06:00
Delta-V bot e7afcda1a8 Automatic changelog update 2025-12-08 15:47:27 +01:00
Charlie Morley b56ca6e773 xenoarch: remove impossible artifact trigger combinations (#4740)
* xenoarch trigger combination blacklists

* minor review comments

* review comments
2025-12-08 08:47:07 -06:00
Charlie Morleyandpre-commit-ci[bot] 14f6ebbd14 Protect arrivals and evac from meteors (slightly) (#4820)
* Protect arrivals and evac from meteors (slightly)

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

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

* change meteor protection zone to a MarkerBase

* [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>
2025-12-08 08:41:56 -06:00
Delta-V bot 7ede56f59e Automatic changelog update 2025-12-08 15:37:42 +01:00
087dc9b9c0 Give custom laws to Salv Borg (#4866)
* Give custom laws to Salv Borg

Give a custom lawset to salvage borg

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

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

* updated laws explain it better and look cleaner

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

* Update laws.ftl

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

---------

Signed-off-by: BungleBoss <100389635+BunglesnortBoss@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>
2025-12-08 08:37:23 -06:00
Delta-V bot 26bd34ad1b Automatic changelog update 2025-12-08 15:35:18 +01:00
DorragonandVanessa 4346034b4c Adds nightvision to rat kings and xenos (#4881)
* ISeeDeadPeople

* Apply suggestions from code review

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

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2025-12-08 14:34:57 +00:00
Delta-V bot 9c232542fd Automatic changelog update 2025-12-08 14:59:12 +01:00
e945f5869a Harpies Flightless No More! (#4888)
* Initial port of EE Harpy flying (#919)

* Added better friction and acceleration modifiers.

* Moved server's flight system to shared. I don't know why it isn't in the first place.

* Shifting things around.

* Code tweaks to make it better

* Comment fixes

* Fixed footsteps and footstep sounds

* Added a quick conditional if InitialStaminaCost greater than zero

* Removed a pop-up that wasn't showing

* Additional adjustments

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

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

* Fixed a few more systems to call the Flying system instead of the component

* Comment consistency. DeltaV comments where I made it better. EE comments where they originally had things

* YAML fixes

* Guidebook

* Apply suggestions from code review

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

* Removed DoAfterDashEvent

* Fixed suggestions

* Updated comment

* Added new copyright free wingflaps with proper attribution

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

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

* Appeased YAML gods

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-08 14:58:52 +01:00
Tobias Berger 1b7d405ecb Manual changelog update 2025-12-08 14:56:44 +01:00
Tobias Berger 12584f2291 Manual changelog update 2025-12-08 12:05:31 +01:00
Delta-V bot 4ab4662ffe Automatic changelog update 2025-12-08 10:01:34 +01:00
verybigmanandTobias Berger d287951f1e Reinforced Tinted Windows (#4875)
* completion

* fix

* moved directories

* fixed

* Apply suggestion from @Toby222

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

---------

Signed-off-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-08 10:01:15 +01:00
Delta-V bot d3e011e9b0 Automatic changelog update 2025-12-08 07:08:57 +01:00
VelcroboyandVelcroboy 566382e75f It's a Tortuga Xmas! (#4893)
* It's a Tortuga Xmas!

* ...

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2025-12-08 07:08:37 +01:00
Delta-V bot 6dbb71c5c7 Automatic changelog update 2025-12-08 07:08:11 +01:00
VelcroboyandVelcroboy 306607e12c Hivemas (#4898)
* Hivemas

* variantize

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2025-12-08 07:07:51 +01:00
turtlemutt 4f39809ea9 Winterhouse (#4910)
Update lighthouse.yml
2025-12-07 23:39:42 -06:00
Delta-V bot 88055bb823 Automatic changelog update 2025-12-08 06:30:37 +01:00
This_Is_Gray 0f388ece51 It Snew On Lady Elegance (Christmas PR) (#4903)
* Update elegance.yml

* Update elegance.yml

* those who snow

* nukes the tattered cloak for real

* Update elegance.yml
2025-12-07 23:30:18 -06:00
3720e89c52 Xenoarch tweaks (#4486)
* Initial glimmer change

* Added glimmer multiplier

* Adds scalar of research properly like the old glimmer system

* Left over code

* Glimmer changes for Analysis Console UI

* Accidental Namespace added

* Fixed small Math issue with glimmer values

* Fixed reference to Nyano

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml

Co-authored-by: KOTOB <59124164+kotobdev@users.noreply.github.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml

Co-authored-by: KOTOB <59124164+kotobdev@users.noreply.github.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Shared/Xenoarchaeology/Equipment/Components/ArtifactAnalyzerComponent.cs

Co-authored-by: KOTOB <59124164+kotobdev@users.noreply.github.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Fixed Deltav comment references

Went though various files and added comments to signify where delta v changes are to the upstream code

* More DeltaV Comment fixes

Added a few more comments for DeltaV specific changes, I also removed an unused line of code, and renamed to variables for better clarity

* Commenting and moving one file to _DV

I cant read apparently and  only saw some fixes requested, here are the rest apologies

* Update Content.Shared/Xenoarchaeology/Equipment/Components/ArtifactAnalyzerComponent.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Shared/Xenoarchaeology/Equipment/Components/AnalysisConsoleComponent.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Shared/Content.Shared.csproj

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Shared/_DV/Xenoarchaeology/BUI/AnalysisConsoleBoundUserInterfaceState.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Resources/Locale/en-US/xenoarchaeology/artifact-analyzer.ftl

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Addressed issues of cleaning up a function to one that already exists, restoring an old comment from upstream, and moving localization data to _DV folder

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Update Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs

Co-authored-by: Charlie Morley <cmorley191@gmail.com>
Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>

* Missed one line to bring over to _DV

* Fixed github and IDE complains about null ref

---------

Signed-off-by: SirSmith148 <64059804+SirSmith148@users.noreply.github.com>
Co-authored-by: KOTOB <59124164+kotobdev@users.noreply.github.com>
Co-authored-by: Charlie Morley <cmorley191@gmail.com>
2025-12-07 21:12:01 -07:00
Delta-V bot 3e56f86b00 Automatic changelog update 2025-12-08 01:04:06 +01:00
5060736956 Atmos Ops: dP Guidebook Entry (#4906)
Atmos dP Guidebook Entry (#40194)

* Add Atmos dP guidebook

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

---------


(cherry picked from commit a93f6b8cdf)

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-12-07 18:03:46 -06:00
ConstantlyConfused d86ae4278e Make the cane loadout group include the librarian cane (#4904) 2025-12-07 22:58:03 +01:00
Cepelinas1 a595421915 Demetashield epsilon part 2 (#4481)
* demetashield epsilon part 2

* bug fix

* annoucement.yml fix

* some redundant stuff removed

fix
2025-12-07 13:12:48 -07:00
Delta-V bot bcde66d934 Automatic changelog update 2025-12-07 19:03:11 +01:00
beck-thompson 9b00ed5737 Fix respawn timer (#4895)
* fix
2025-12-07 19:02:52 +01:00
Delta-V bot e253f855e1 Automatic changelog update 2025-12-07 18:54:53 +01:00
Stxckingandpre-commit-ci[bot] 4a9a9f4be5 Add Chemical, Paramedic, and Surgeon wall lockers (#4899)
* woo wall lockers!

* chemical

* clean up

* [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>
2025-12-07 18:54:34 +01:00
github-actions[bot]andDeltaV-Bot 0975ff525c Update Credits (#4897)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2025-12-07 18:45:59 +01:00
Delta-V bot 49b60728db Automatic changelog update 2025-12-07 12:46:04 +01:00
Halo3moth 3ea3cc3ff9 Always bigger fi- oh nevermind (Feroxi Emote) (#4877)
* feroxi changes

Wideswing,10 pierce, tail thump, better storage of body emotes for feroxis

* Direction Changes

Removed wideswing dropped feroxi melee back to 7

Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>

---------

Signed-off-by: Halo3moth <mdavid.wininger@gmail.com>
2025-12-07 05:45:45 -06:00
Dorragon fed1c5cfc3 Centcomm roles QOL (#4880)
* first commit yippee

* ERT now gets centcomm implants too. Also fixes ERT not having death rattle implants
2025-12-07 05:34:16 -06:00
Delta-V bot 1209f820ee Automatic changelog update 2025-12-07 10:47:33 +01:00
Proxy also works :3andVanessa 1fb4b9871a kill speedboots (#4883)
* remove speedboots

* no more reverse engineering

* Update Resources/Prototypes/Research/civilianservices.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Proxy also works :3 <98499019+Proxysseia@users.noreply.github.com>

---------

Signed-off-by: Proxy also works :3 <98499019+Proxysseia@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2025-12-07 11:47:14 +02:00
Delta-V bot 420a9266ed Automatic changelog update 2025-12-07 08:09:54 +01:00
29a7d080d4 Make pride clothing be able to chameleon into similar pieces of pride clothing (#4845)
* Expedite gender reassignment (#36894)

* Remove Misgendering (#40425)

fix misgendering

* make pride scarfs and cloaks chameleon

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

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

* add missing comments

* Attempt Toby's suggestion: rename chameleon stuff

* rename the chameleon verb & window

---------

Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-07 00:09:34 -07:00
Delta-V bot f976232d0c Automatic changelog update 2025-12-07 07:40:28 +01:00
Field Command b23d763547 Midpoint Festive Season (#4891)
* Midpoint Festive Season

* Made frosty lights not be powered by festive magic
2025-12-07 06:40:08 +00:00
HTML/Crystal d273cdba9b Give dummy avali and chitnid moth eye sprites (#4884)
1 commit ops
2025-12-06 23:07:19 -07:00
VelcroboyandVelcroboy 51035e28dd Fix mapping section in CONTRIBUTING.md (#4894)
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2025-12-06 21:27:52 +00:00
Delta-V bot b87ec6b1e4 Automatic changelog update 2025-12-06 06:33:59 +01:00
VelcroboyandVelcroboy 246cf08462 Map Hotfix: Tortuga (#4878)
* Map Hotfix: Tortuga

* invalid

* Circuit imprinter

* increase detective 1->2

* return of the circuit imprinter

* fish power

---------

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2025-12-06 05:33:40 +00:00
Delta-V bot 9645a5ca82 Automatic changelog update 2025-12-06 05:40:22 +01:00
VelcroboyandVelcroboy 0e4502b973 Map Edits: The Hive (#4887)
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
2025-12-06 05:40:02 +01:00
biddygelson d4e0857063 Academy 1.3.0 (#4863)
* * make festive for winturrr
* fixed emergency light in viro checkpoint
* fixed always on light outside dispo

* fixed light
2025-12-05 19:27:23 -06:00
Delta-V bot 448a10f27f Automatic changelog update 2025-12-06 02:25:56 +01:00
SirrRequiem 7c91aa2f9d Terra: The Holiday Episode (#4843)
SNOW
2025-12-05 19:25:36 -06:00
Delta-V bot d5bf0abb33 Automatic changelog update 2025-12-06 02:25:14 +01:00
Field Command 3b30bcdbd5 Pebble hotfix 1: Added ore processor (#4874)
* Added ore processor

* Forgot grills
2025-12-05 19:24:55 -06:00
turtlemutt 7eabecb3b6 Update lighthouse.yml (#4849) 2025-12-05 19:23:53 -06:00
Cepelinas1 86e02dbae5 fix errors in SOP and space law in wiki, again (#4753)
* first commit

* fix
2025-12-05 20:10:53 +01:00
Delta-V bot 8c5713d756 Automatic changelog update 2025-12-05 18:56:58 +01:00
fa095884ee Space Law / SoP in guidebook! (#4857)
* Add files via upload

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Add files via upload

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Add files via upload

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Add files via upload

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Add files via upload

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Updated this to fix a no longer existent .xml

* Fixed a stash issue lol

* man im sloppy, fixed a title issue lol

* Made both books have the AlertProcedure guide entry

* added the Alert Procedure

* Normalised line endings to LF

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

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

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

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

* Update Resources/Prototypes/_DV/Guidebook/CompanyPolicy.yml

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

* Update Resources/Prototypes/_DV/Guidebook/CompanyPolicy.yml

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

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/GeneralRegulations.xml

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

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/RightsandResponsibilities.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/selfDefence.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update Resources/ServerInfo/Guidebook/_DV/SPLAW/CompanyPolicy/PotentialThreats.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Fixed most of my sloppy YAML and guidebook entries as suggested.

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

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

* Edited alot of entries and YMLs to mirror changes.
Removed alot of unnecessary speechmarks

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

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

* Update Resources/ServerInfo/Guidebook/_DV/SpaceLaw/CompanyPolicy/DangersFromOurCompetitors.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Fixed my YAML

* Added the AND to SecurityandJustice

* Fixed my linktexts

* fixed no filepath

* made a commanding officer entry

* Fixing and adding some entries

* more fixes

* bye bye empty.xml!

* ac

* Made entries, marked some in bold and finally updated the medical reference book!

* fixed indentation

* Update CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update CodeofConduct.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update AppearanceStandards.xml

Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>

* Update books.yml

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

---------

Signed-off-by: PureBreadBagel <146181383+PureBreadBagel@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
Co-authored-by: Cepelinas1 <kakelis01@gmail.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2025-12-05 17:56:38 +00:00
Charlie Morley c0819086f5 CONTRIBUTING.md - .ftl _DV guideline & reorganized comment section (#4882)
* CONTRIBUTING.md - reorganized, and added ftl separate file guideline

* minor tweak

* helps when your example is actually relevant

* CONTRIBUTING review comment - features to changes

Signed-off-by: Charlie Morley <cmorley191@gmail.com>

* triple yaml example combo

---------

Signed-off-by: Charlie Morley <cmorley191@gmail.com>
2025-12-05 14:14:57 +00:00
Delta-V bot 867c2d1be4 Automatic changelog update 2025-12-05 13:56:29 +01:00
3c0fa71f86 Holofan Rebalance (and Standardizing Holoprojectors) (#4734)
* Holofan re-balance. No longer infinite but recharge.

Original Implementation by MajorMoth. Modified and expanded on by ShepardToTheStars. Holofans Changes (#1394)

<!-- 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? -->
- Holofans are now timed
- Holobarriers are now charge based, and produce a timed barrier

<!-- Discuss how this would affect game balance or explain why it was
changed. Link any relevant discussions or issues. -->
Direction request to avoid them being up all shift and doing atmos
shenanigans.

<!-- Summary of code changes for easier review. -->
New components and systems to handle detailed descriptions on holofan
barriers. Bunch of .yml changes.

Because both holograms can now either be destroyed, or despawn after 5
minutes, a complete rewrite of the way they're produced was necessary.
The projectors still have 6 uses and can recharge 1 use every 2 minutes.
Clicking on a holo barrier/fan with the correct projector will delete
it, and add a charge back to the projector.

<!-- 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. -->
N/A

<!-- 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 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. -->

**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. -->

🆑 Jakumba
- add: Added a warning text when inspecting a Holofan and Holobarrier
with duration before they expire
- tweak: Holobarrier Projectors now work on a charge base system
- tweak: Holofans and Holobarriers now last for five minutes when placed

---------

Co-authored-by: Falcon <falcon@zigtag.dev>
Co-authored by: Vanessa <vanessalouwagie@gmail.com>

* Added a CanPickup variable to the component to disallow picking up holoprojections.

* Hid the empty version of HoloprojectorEngineering

* YAML cleanup

* Holoprojection locale update

* Old logic cleanup

* Fixed holobarrier sprite

* Words

* Fixed hidden projector in DV namespace

* Excluded TimedDespawnDetailed from test that already excluded the TimedDespawn comp

* PR #4734 feedback changes

* oops. Actually put StopTimer in TimedDespawnDetailed

---------

Co-authored-by: Jakumba <16194688+Jakumba@users.noreply.github.com>
Co-authored-by: Falcon <falcon@zigtag.dev>
2025-12-05 12:56:09 +00:00
Delta-V bot 1179c52b26 Automatic changelog update 2025-12-05 05:48:49 +01:00
KOTOB 275413e268 Discourage LRP warops announcements (#4876)
no fun allowed
2025-12-04 22:48:29 -06:00
Delta-V bot 7649c9ecde Automatic changelog update 2025-12-05 03:59:16 +01:00
Stxckingandpre-commit-ci[bot] 4c1ffd2cee Fishing Vendor and Fish Basket (#4862)
* everything rah

* vending.png

* recovered files

* clean up

* .

* final touches?

* [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>
2025-12-04 20:58:57 -06:00
Delta-V bot 5183d9fcc1 Automatic changelog update 2025-12-05 03:49:51 +01:00
HTML/Crystal 7b5f735051 Give chitnids moth eye sprites (#4835)
1 line ops
2025-12-04 20:49:31 -06:00
Delta-V bot 3e55e8ccc3 Automatic changelog update 2025-12-05 03:40:55 +01:00
Vanessaandpre-commit-ci[bot] 0dad42c119 Silvia can equip medhuds and IDs. Fixed her slipping too. (#4867)
* Silvia can see health bars. Fixed her slipping too.

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

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

* Added inventory slots to Silvia.

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

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

* PR Feedback

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-04 20:40:36 -06:00
Delta-V bot a8848872cf Automatic changelog update 2025-12-05 03:37:39 +01:00
verybigman 147188ebca Various melee weapon tweaks and modifications. (#4806)
* part uno

* tada

* aaaaa

* a

* last one i swear

* i lied

* error fixed

* fixed conflict for real this time

* choppas, rickenbacker, and pickaxe reverted

* fixed for real this time

* fixed comments

---------

Signed-off-by: verybigman <171044200+verybigman311@users.noreply.github.com>
2025-12-04 20:37:20 -06:00
Delta-V bot 967108a2d9 Automatic changelog update 2025-12-05 03:14:20 +01:00
Stxcking c12e669b14 Add prescription glasses to loadouts + diagnostic hud (#4871)
* everything buh

* a bunch of loadout changes

* .rsi

* changes request + prisonguard startinggear
2025-12-04 20:14:00 -06:00
Delta-V bot f6e855f4a3 Automatic changelog update 2025-12-05 03:05:12 +01:00
Charlie Morley 2cd12c7b9e fix duplicate spawns from anomaly (undrillable rock bug) (#37833) (#4873)
* prevent anomalies from spawning duplicate entities or tiles on a tile

* deltav comments
2025-12-04 20:04:53 -06:00
Delta-V bot e4ab6d7106 Automatic changelog update 2025-12-05 02:59:05 +01:00
Vanessa 48ee9ed999 Fixed agent visor not able to see silicon and kitsune health bars (#4864)
* Fixed nukie visor not able to see silicon and kitsune health bars

* Fixed syndicate medical borgs too

* Additional commenting
2025-12-05 01:58:46 +00:00
Delta-V bot 98eeeb654a Automatic changelog update 2025-12-05 02:44:55 +01:00
Vanessa 8932a19003 Early merge of bomb planting tweak (#41208) (#4861) 2025-12-04 19:44:35 -06:00
Delta-V bot cbec20b6c7 Automatic changelog update 2025-12-04 23:29:19 +01:00
verybigman f971a9c9b3 Addition of Shrapnel to Frag Grenade Projectiles (#4856)
added functionality
2025-12-04 16:28:59 -06:00
Delta-V bot ef1dffd653 Automatic changelog update 2025-12-04 19:42:40 +01:00
verybigman 0f1a7f69e8 Ultimate Wielded Sprites Update (and other fun things) (#4851)
* lasers done

* fixed drinks

* one done

* enforcer complete

* messed up the sprites

* fixed suitstorage

* edit

* electric boogaloo

* save

* IK done

* guess who forgot to edit the json

* every gun now has wield sprites

* kammerer is BROKEN

* finally free

* changes
2025-12-04 12:42:20 -06:00
Delta-V bot 404956c873 Automatic changelog update 2025-12-04 17:59:28 +01:00
73363b9ff5 More Chat Indicators (#4679)
* emote whisper and OOC bubbles

Signed-off-by: Nesoryo-jpeg <joem365289@gmail.com>

* Update meta.json

added entries for OOC, Emote, and Whisper

Signed-off-by: Nesoryo-jpeg <joem365289@gmail.com>

* Update meta.json

added copyright info for OOC, Emote, and Whispering

Signed-off-by: Nesoryo-jpeg <joem365289@gmail.com>

* Update typing_indicator.yml

Added Whisper, OOC, and Emote indicators

Signed-off-by: Nesoryo-jpeg <joem365289@gmail.com>

* added indicator logic

* added radio sprite state

* initialized logic for chat bubbles

* refresh indicator change

* fixed emote sprite

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

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

* nicer comments

* cleaned up and sorted namespaces

* oh my god

* cleanup...

* cleanup two

* cleanup 3 (my god)

* YESSSS

---------

Signed-off-by: Nesoryo-jpeg <joem365289@gmail.com>
Co-authored-by: Nesoryo-jpeg <joem365289@gmail.com>
Co-authored-by: nkokic <nikola1.kokic@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-04 10:59:08 -06:00
Delta-V bot ed21b2e04d Automatic changelog update 2025-12-04 14:28:17 +01:00
Dorragonandpre-commit-ci[bot] 36025fe065 There's always bigger fish in the sea (#4826)
* Oh my god there are sharks here

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

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

* I don't even know man

* Fix spaces

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

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

* fixes fixes fixes

* English language was invented to sell more dictionaries

* eveny

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-04 14:27:57 +01:00
Delta-V bot 7ad4ff91a4 Automatic changelog update 2025-12-04 12:18:26 +01:00
e7c2f7a32a Small Borg ID Chips Rework (#4821)
* Removed Basic borg IDs.

* Added ID chips to derelict borgs so they don't have to beg to get into the station.

* Added the ability to unlock derelict syndicate cyborgs to roboticists

* Lathes now create borg ID chips with all Borg AA permissions.

* Added new component for Borg ID chips to prevent them from being copied.

* Fixed words

* Added BorgIdCard to BaseIdChip

* Logic hard

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

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

* Added BorgModulesDeltaV to exofabs

* Fixed build error

* Update Content.Shared/_DV/Access/Components/BorgIdCardComponent.cs

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

* PR #4821 feedback

* Forgot to rename a file.

---------

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-04 12:18:07 +01:00
Delta-V bot 1d3bc96e1f Automatic changelog update 2025-12-04 09:27:57 +01:00
WoljifandTobias Berger 8e20fdc54b Buffed the dragon (#4781)
* Buffed the dragon

* Move dragon modifier set to _DV

* Tweaks requested by direction

* Update Resources/Prototypes/Entities/Mobs/Player/dragon.yml

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

---------

Signed-off-by: Woljif <243318409+Woljif@users.noreply.github.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-12-04 09:27:37 +01:00
Minerva f6f8a085e3 Cherrypick: Prefixes surgical caps with their color (#41681) (#4868)
Prefixes surgical caps with their color (#41681)
2025-12-04 00:27:05 -06:00
This_Is_Gray fe553d0061 AI Camera + Misc. Elegance "Hot"fix (#4860)
* Update elegance.yml

* Update elegance.yml
2025-12-03 23:52:08 -06:00
Delta-V bot 995d8b51e6 Automatic changelog update 2025-12-04 06:07:06 +01:00
Field Command 6214826354 Pebble is not small station anymore! (#4850)
Pebble is not small station anymore
2025-12-03 23:06:46 -06:00
Delta-V bot cbb2d827a0 Automatic changelog update 2025-12-04 06:06:33 +01:00
Field Command a982f9d6db Update to NTES BC20 evac shuttle for Lighthouse (#4842)
BC20 Update
2025-12-03 23:06:13 -06:00
Delta-V bot 774f203deb Automatic changelog update 2025-12-04 04:26:59 +01:00
verybigman dae3e5fe26 Christmas Lamp Rotation Fix (#4865)
lampfixed
2025-12-03 21:26:40 -06:00
Minerva e8f181cb44 Prefixes DV-specific surgical cap names with their color (#4859) 2025-12-03 23:36:02 +00:00
Delta-V bot 42524f7030 Automatic changelog update 2025-12-03 23:35:18 +01:00
Field Command 076e4b1a5e The shipyard will haunt me for millennia (#4858)
* Appraiser, why you be like this?

* small edit
2025-12-03 16:34:59 -06:00
Delta-V bot 12912a55ec Automatic changelog update 2025-12-03 21:26:12 +01:00
Stxcking baddc5b782 Fix syndicat pointless description (#4854)
Update pointless.yml
2025-12-03 14:25:52 -06:00
Delta-V bot b50015430e Automatic changelog update 2025-12-03 17:08:46 +01:00
Vanessa 10cb0f3cd2 Stasicillin Forced Sleeping Fix (#4823)
* This probably works.

* Updated duration of stasicillian sleep.
2025-12-03 16:08:26 +00:00
Delta-V bot 7836c82461 Automatic changelog update 2025-12-03 08:08:11 +01:00
Proxy also works :3 14b3561bab remove species locks in uplink store (#4759)
* remove racism

* Update uplink_catalog.yml

* larynx instead of syrinx
2025-12-03 01:07:52 -06:00
Delta-V bot 7dd8a86b9d Automatic changelog update 2025-12-03 06:32:20 +01:00
Stxckingandpre-commit-ci[bot] 60928a137b Syndicat Returns (#4731)
* meow

* whoops

* meta,json

* forgot

* i cant spell sobs

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

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

* resolved conflict

* Revert "resolved conflict"

This reverts commit 3fcf22b163.

* Update plastic.yml

* Update plastic.yml

* Update plastic.yml

* Update plastic.yml

* Update plastic.yml

* Update plastic.yml

* changes request

---------

Signed-off-by: Stxcking <tamthaotran5@outlook.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-02 23:32:01 -06:00
Delta-V bot f1b568385f Automatic changelog update 2025-12-03 06:02:06 +01:00
ConstantlyConfused bd7aaf4ea2 Add aroace cloaks to the Pride-o-mat (#4847)
* Make Aroace cloaks accessible through the pride-o-mat

* slightly increase price
2025-12-02 23:01:47 -06:00
Minerva 9f6826ca6b Trait typofixes (#4840)
* Trait typofixes

* This typo slipped past me
2025-12-02 20:13:43 -06:00
Charlie Morley 489eae6a0f add MeteorLargeDeltaV to entity spawn menu (#4822)
* add MeteorLargeDeltaV to entity spawn menu

* meteor suffix
2025-12-02 23:32:56 +00:00
Delta-V bot 7ab87401db Automatic changelog update 2025-12-03 00:30:07 +01:00
Field Commandandpre-commit-ci[bot] 1cbb219722 Lots of edits and naming conventions to grids (#4828)
* lost of fixes

- Standardized all shuttles.
- Standardized shuttle naming convention for shipyard shuttles.
- Standardized shuttle naming convention for cargo shuttles.
- Fixed Midpoint wrongly named holopad in the reporter's office.

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

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

* Fixed shipyard pricing

* Margin was apparently not enough

* Not enough margin still

* Hopefully this will solve the margins

* Applied requested changes

* Those darn margins are so hard to predict

* Named the trader

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-02 17:29:47 -06:00
Delta-V bot c8c2614445 Automatic changelog update 2025-12-02 23:26:46 +01:00
Vanessa 3570b40a18 Rat lungs don't scale by quantity of miasma (#4836)
Changed scaleByQuantity to false to nerf rat lungs.

Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2025-12-02 23:26:27 +01:00
Delta-V bot 2a4fb27248 Automatic changelog update 2025-12-02 15:20:57 +01:00
Vape f912e02e85 Updates Patreon Link (#4833) 2025-12-02 15:20:37 +01:00
Delta-V bot cee50ea403 Automatic changelog update 2025-12-02 06:33:22 +01:00
Field Command d15ef11237 Pebble's Reincarnation! (#4807)
* Pebble V2

* Added missing job spawns

* Added in missing intercoms

* Hopefully finals polish

* Fixed so evac can dock properly

* Final polish

* Grammar fixer
2025-12-01 23:33:02 -06:00
Delta-V bot 2d6dad77ce Automatic changelog update 2025-12-02 05:42:39 +01:00
Bonktrauma 489466e2c6 Bring Ovni back (#4602)
* Bring Ovni back

* Update ovni.yml

* Made archivists room bigger, changed AI core a bit

* Update ovni.yml

* Update ovni.yml

* removed invalid

* attributions file

* Made requested changes

* removed invalid..?

* Update ovni.yml

* removed invalid

* Minor fixes

* fix invalid, hopefully for the last time..

* fixes, still need to do signs, its next

blah

* final fixes.. i hope

beh

* attributions fix

* Update deltav_map_attributions.yml
2025-12-01 22:42:19 -06:00
Delta-V bot 90ce1ca21f Automatic changelog update 2025-12-02 01:13:27 +01:00
BungleBossandVanessa 4468b2dcbb Give Cyborgs increased base weightless speed, longer battery life, ability to self-weld. (#4819)
* Increase Weightless speed.

Give Cyborgs a smidge more health (10 HP) and increase their acceleration in space and their acceleration to 1->1.25. Enabled the ability for them to self weld but will require multiple welds to reach full health.

* Update Content.Shared/Movement/Components/MovementSpeedModifierComponent.cs

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

* Update Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>

---------

Signed-off-by: BungleBoss <100389635+BunglesnortBoss@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
2025-12-01 18:13:07 -06:00
Delta-V bot 84f4adef37 Automatic changelog update 2025-12-01 22:20:57 +01:00
Apachito 6f37d1a5d6 Buffed Make Shift fishing rod (#4827) 2025-12-01 15:20:35 -06:00
Delta-V bot 890e1e3700 Automatic changelog update 2025-12-01 14:34:05 +01:00
Cepelinas1 fd761c7aa9 Remove possession charges stacking (#4808)
* first commit

* armaments to contraband
2025-12-01 13:33:46 +00:00
Proxy also works :3 d398c966c7 removes mentions of detainment (#4722)
* removes mentions of detainment

* ?

* ??

* Daughter Babylon, doomed to destruction, happy is the one who repays you according to what you have done to us. Happy is the one who seizes your infants and dashes them against the rocks.

---------

Signed-off-by: Proxy also works :3 <98499019+Proxysseia@users.noreply.github.com>
2025-12-01 13:01:14 +00:00
Vanessa ed38eae313 Fixed a client error with stamped biscuits and corporate slips (#4812)
fixed a client error with stamped biscuits and corporate slips
2025-12-01 08:58:24 +01:00
Delta-V bot 42bcbb3fc0 Automatic changelog update 2025-12-01 08:57:22 +01:00
Vanessaandslarticodefast 7f942fa97a Make crawl speed depend on your free hand count (#41458) (#4772)
* Make crawl speed depend on your free hand count (#41458)

* crawl speed

* I can't spell

* Reduced the penalty by half. Zero hands taken is 50% move speed.

* forgot a few F's lol

* comment fix

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-12-01 08:57:02 +01:00
Delta-V bot a101aabb54 Automatic changelog update 2025-11-30 19:10:35 +01:00
Charlie Morleyandpre-commit-ci[bot] b4782c2b21 xenoarch: hide some node effects until unlocked (#4748)
* xenoarch: hide some node effects until unlocked

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

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

* minor review comments

* added missing deltav comments

* test failures: forgot DataField annotation on XAEDetails.AllowLockedEffectHiding, and LocId SpecificTip field needed default

* AnalysisConsoleMenu.xaml.cs copypasta mistake (that'll teach me to push before testing)

* arti effect hiding increased to 70%

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-30 12:10:16 -06:00
Delta-V bot d1ee65c21c Automatic changelog update 2025-11-30 19:00:52 +01:00
William LemonandTobias Berger 0e897d8715 Fractured Form Patches (#4582)
* Fix

* Update Content.Server/_DV/Abilities/Psionics/FracturedFormPowerSystem.cs

Co-authored-by: Tobias Berger <toby@tobot.dev>
Signed-off-by: William Lemon <William.Lemon2@gmail.com>

* Imaguine trusting Github to make a change

* Uhn

---------

Signed-off-by: William Lemon <William.Lemon2@gmail.com>
Co-authored-by: Tobias Berger <toby@tobot.dev>
2025-11-30 12:00:33 -06:00
Delta-V bot a59daf05d7 Automatic changelog update 2025-11-30 18:46:31 +01:00
Corynandpre-commit-ci[bot] eed64813ee Add Minor Mass Mind Swap (#4728)
* Add Minor Mass Mind Swap

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

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

* Move away from Nyanotrasen namespace, Add Default Value for isTemporary

* Raise Glimmer Requirmeents from 500 to 750, Reduce Max pairs from 5 to 3

* Review Changes

* Moving null up

* Changes as Direction requested

* Remove non-fitting comment

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-30 11:46:11 -06:00
Delta-V bot 9e261a7065 Automatic changelog update 2025-11-30 18:00:23 +01:00
Charlie Morley eb13a3721d psychokinetic scream LogPowerUsed (#4816) 2025-11-30 11:00:04 -06:00
Delta-V bot 08273e5da1 Automatic changelog update 2025-11-30 17:46:57 +01:00
Sir Warock 5914ff6e44 Make the Adv. Truncheon Advanced (#4736) 2025-11-30 10:46:38 -06:00
Delta-V bot 82ca2142e2 Automatic changelog update 2025-11-30 17:44:43 +01:00
verybigman 4159ed8d7c New Decoration: The nefarious Lava Fountain (#4792)
* work in progress

* bugs all gone!

* cough

* we are finally done or something

* sprite fixed
2025-11-30 10:44:24 -06:00
Delta-V bot d89ff2a824 Automatic changelog update 2025-11-30 17:41:51 +01:00
verybigmanandpre-commit-ci[bot] 4f2f881f35 Various beverage additions, tweaks, and modifications. (#4780)
* minor additions complete

* Added a bunch of fun thingies

* yet another commit

* finally free

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

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

* oops

* minor sprite issue fix award or something

* changes complete to comments and credits

* forgot to do this

* last minute sprite fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-30 10:41:31 -06:00
Delta-V bot 712135779d Automatic changelog update 2025-11-30 17:37:54 +01:00
Proxy also works :3 0fe298d0e3 Make visitors no longer KoS (#4763) 2025-11-30 10:37:35 -06:00
Delta-V bot 53c8e457c8 Automatic changelog update 2025-11-30 17:32:02 +01:00
pootslap e6f521ea13 Change age appearance (#4783)
* Update SpeciesPrototype.cs

* Update SpeciesPrototype.cs

* Update SpeciesPrototype.cs

* added comments

hope I did it right

Signed-off-by: pootslap <211648338+pootslap@users.noreply.github.com>

---------

Signed-off-by: pootslap <211648338+pootslap@users.noreply.github.com>
2025-11-30 10:31:42 -06:00
Delta-V bot 095b364c40 Automatic changelog update 2025-11-30 17:30:53 +01:00
Charlie MorleyandTheGrimbeeper 969ed2599a xenoarch: early merge radiation trigger fix (#4817) Fix radiation damage being misattributed to radiation receiver (caused artifacts to not be triggered by ambient rads) #41065
Fix radiation damage being misattributed to radiation receiver (caused artifacts to not be triggered by ambient rads) (#41065)

* Xenoartifact: Fixed ambient radiation damage not triggering

Fixed ambient radiation damage not triggering artifact.

* Revert "Xenoartifact: Fixed ambient radiation damage not triggering"

This reverts commit 30e5c7cdb49c15574b49ddd1a1f7b1768abd2614.

* Fix radiation damage misattribution

(cherry picked from commit dd9a1de77f)

Co-authored-by: TheGrimbeeper <thegrimbeeper.11@gmail.com>
2025-11-30 10:30:34 -06:00
github-actions[bot]andDeltaV-Bot 2ae8858a41 Update Credits (#4813)
Co-authored-by: DeltaV-Bot <github@deltav.gay>
2025-11-30 16:29:25 +00:00
Delta-V bot cc95373474 Automatic changelog update 2025-11-30 17:24:45 +01:00
Vanessa 12b750aa91 Added double-tapping stand-up bind to push yourself up. (#4802) 2025-11-30 10:24:25 -06:00
MD360 293239834c Add magnets to remaining lathes. (#4799)
* Add magnet to 11 lathes and ore box unfinished fully.

* Remove ore box part.

* Fix the linter..

* Added magnet turn off on eject.

* Redo magnet to base and fix seperate bug.
2025-11-30 10:11:24 -06:00
767 changed files with 311686 additions and 106308 deletions
+6 -5
View File
@@ -6,12 +6,13 @@ It is highly recommended to read the '''[[Standard Operating Procedure|Standard
* '''[[Standard Operating Procedure#Controlled Items|Controlled Items]]:''' If a member of station crew requires access to a Controlled Item for use in their job, or for regular function, they must obtain the signed and stamped approval of a relevant member of Station Command or the CO. The latter is able to override the decisions of Station Command in this regard, however.
* '''[[Standard Operating Procedure#Entity Definitions|Entity]]:''' Any thing with distinct, independent existence.
* '''[[Standard Operating Procedure#Entity Definitions|Employee]]:''' An entity who has an employment contract with a Company.
* '''[[Standard Operating Procedure#Entity Definitions|Pet]]:''' Any Entity that is provided to Company's Space Stations - either through the station's Logistics Department, or brought on board by the crew - for the purposes of emotional support.
* '''[[Standard Operating Procedure#Security Jurisdiction|Security Jurisdiction]]:''' Station law enforcement officers are bound by Space Law; they cannot operate outside of standard accessible areas, or perform searches, unless such action is allowed by either Alert Procedure or a valid warrant.
* '''[[Standard Operating Procedure#Detainment and Arrest|Detainment & Arrest]]:''' Members of law enforcement may detain entities under '''[[Standard Operating Procedure#Probable Cause|Probable Cause]]''' of criminal activity. These detainees must be informed of the crime they are suspected of committing, and may be moved to a secure location and/or restrained while law enforcement determine if a crime was committed. Such detention should last no longer than five minutes. Any entity may be arrested, during which they are restrained, informed of their charges, and taken to the brig for search and processing under '''[[Standard Operating Procedure#Brig Procedure|Brig Procedure]]'''.
* '''[[Standard Operating Procedure#Arrest|Arrest]]:'''Any entity may be arrested, during which they are restrained, informed of their charges, and taken to the brig for search and processing under '''[[Standard Operating Procedure#Brig Procedure|Brig Procedure]]'''.
* '''[[Standard Operating Procedure#Sentencing|Sentencing]]:''' Any member of law enforcement is allowed to pass sentence on Misdemeanor, Felony, and Grand Felony charges. The Chief Justice, or in their absence, the Head of Security are allowed to pass sentence on all Charges.
* '''[[Standard Operating Procedure#Treatment of Prisoners|Treatment of Prisoners]]:''' Prisoners must be provided with food, water, clothing, free movement within the brig, adequate medical care, access to the Common and Prison radio channels, and moral, spiritual, or legal counseling if requested and available. If the brig becomes uninhabitable, prisoners must be securely and safely relocated to another area until the brig is functional once more. Prisoners sentenced to Extended Confinement have additional rights and firmer restrictions that must be upheld by law enforcement officials.
* '''[[Standard Operating Procedure#Paroles and Pardons|Paroles & Pardons]]:''' Parole may be offered under the discretion of the Warden, Head of Security, or CO. A parole hearing requires only a Judge, as discussed within Trial Procedure. Pardons may be issued by the arresting officer, the Judge of a trial, by the Warden (to those within the permanent brig), or by the CO, when deemed in the best interest of the crew or vessel, or when the circumstances of the crime warrant suspension of sentence.
* '''[[Standard Operating Procedure#Pressing Charges|Pressing Charges]]:''' Any Employee may press charges against any other entity. Individuals may only be charged with a crime if it can be proveny beyond reasonable doubt that they have committed the crime.
* '''[[Standard Operating Procedure#Pressing Charges|Pressing Charges]]:''' Any Employee may press charges against any other entity. Individuals may only be charged with a crime if it can be proven beyond reasonable doubt that they have committed the crime.
* '''[[Standard Operating Procedure#Trial Procedure|Trial Procedure]]:''' A criminal trial’s Judge must be, in sequential order: The Chief Justice, the Court Clerk, an impartial member of Justice or Station Command, or the Warden. The Prosecution should ideally be the Prosecutor, or an available member of Station Command or Station Security in that order. The Defense should ideally be a member of the Justice department assigned to the defendant, though they are allowed to represent themselves if they wish. If insufficient crew are available, the Judge is fully authorized and strongly encouraged to pass a sentence of Extended Confinement until a proper trial can be held.
* '''[[Alert Procedure]]:''' Codes Blue, Red, Gamma, and Delta authorize Security to carry progressively more advanced equipment, and loosen '''[[Standard Operating Procedure#Rules of Engagement|Rules of Engagement]]''' regarding their use. The former two codes disallow openly carrying firearms by non-Security personnel; the latter two suspend many rights to privacy, and grant Security the power to do whatever neccessary to retake or scuttle the station.
@@ -210,7 +211,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style= "border:1px solid black;"| [[File:SL_Kidnapping.png]]
|style= "border:1px solid black;"| Kidnapping
|style= "border:1px solid black;"| 16 minutes
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an entity against its will.
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an Employee against its will.
|-
| style="border:1px solid black;" | 303
| style="border:1px solid black;" | WIP
@@ -310,7 +311,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|style= "border:1px solid black;"| WIP
!style= "border:1px solid black;"| Identity Theft
|style= "border:1px solid black;"| 5 minutes
|style= "border:1px solid black;"| To maliciously assume the identity of an entity, such as through writing, attire, and/or speech.
|style= "border:1px solid black;"| To maliciously assume the identity of an Employee, such as through writing, attire, and/or speech.
|}
@@ -329,7 +330,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | [[File:SL_AnimalCruelty.png]]
! style="border: 1px solid black;" | {{anchor|Animal Cruelty}}Animal Cruelty
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To inflict unnecessary suffering on a sapient being with malicious intent.
| style="border: 1px solid black;" | To inflict unnecessary suffering on a Pet with malicious intent.
|-
|style= "border:1px solid black;"| 102
|style= "border:1px solid black;"| WIP
+9 -10
View File
@@ -8,7 +8,6 @@ The terms specified below are defined for further use within the procedures.
:* Command Staff/Station Command: Members of the Command department who lead a department aboard the station.
:* Crew: All Employees employed by Station Command, or by NT to reside on the station and perform an established function therein. This includes any Employee listed on the Crew Manifest or issued a functional NT identification and access card. Individuals imprisoned by NT, along with those imprisoned during the course of a shift for violations of Space Law, are considered Crew.
:* Judge: An individual legally appointed to preside over a hearing, as specified within Hearing Procedure.
:* Detainee: Any Employee whose free movement has been curtailed by law enforcement on suspicion of criminal activity. They have not, however, been officially charged with a crime.
:* Arrestee: Any Employee who has been formally charged with a crime.
:* Prisoner: Any Employee who has been sentenced for the commission of a crime, or awaiting a hearing for a capital offense.
:* Permanent Brig/Permabrig/Brig: A secure location for the long-term confinement of Prisoners. Typically outfitted to sustain the basic survival needs of the Prisoners within.
@@ -22,7 +21,7 @@ The following definitions are provided to clarify the legal protections- and obl
== Entity ==
Any individual with a distinct existence. Entities may also encompass a plurality of identities contained within a single physical form. NanoTrasen does not require Entities be offered the protections of Standard Operating Procedure, Space Law, or Rules of Engagement unless further defined below. Security Officers are encouraged to be reasonable with their use of force regarding circumstances, especially if Entity's termination would leave lawsuits or tarnish reputation for NanoTrasen.
== Pet ==
Any Entity that is provided to NanoTrasen Space Stations- either through the station's Logistics Department, or brought on board by the crew- for the purposes of emotional support. Any harm against such a Pet may render the offender liable to the crime of Animal Cruelty.
Any Entity that is provided to NanoTrasen Space Stations - either through the station's Logistics Department, or brought on board by the crew - for the purposes of emotional support. Any harm against such a Pet may render the offender liable to the crime of Animal Cruelty.
Examples of Pets include, but are not limited to:
:* Renault the Fox
:* Smile the Rainbow Slime
@@ -101,27 +100,27 @@ A list of such Substances follow:
== Controlled Armaments ==
Controlled Armaments are grouped into categories of increasing severity.
Controlled Armaments are grouped into categories of increasing severity. If the offender possesses several categories of contraband, only the highest severity of said items should be charged.
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed, adapted, or used to cause physical harm in close combat
* Any object or equipment specifically designed or adapted to protect against physical harm caused by weapons, projectiles, or other forms of direct assault
* Any object or instrument specifically designed or adapted to incapacitate or disorient by using light or sound without causing permanent injury
* Any object or equipment specifically designed or adapted for explicitly training purposes that would otherwise fall under another category
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig and further legal action as the situation demands.''
* Any object or instrument specifically designed or adapted to cause physical harm in close combat, which employs a powered system to enhance performance
* Any manually-operated ballistic arms that possess an internal magazine or feeding system, or lack an internal magazine or feeding system altogether
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
* Any manually-operated ballistic arms that use an external magazine or feeding system
* Any semi-automatic ballistic arms not easily concealable within a pocket or coat
* Any object or instrument specifically designed or adapted for explicitly less-lethal purposes that would otherwise fall under another category
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
* Any semi-automatic ballistic arms that are easily concealable
* Any fully-automatic ballistic arms
@@ -129,7 +128,7 @@ Controlled Armaments are grouped into categories of increasing severity.
* Any object or device specifically designed or adapted to cause physical harm through detonation of an explosive charge
* Any object or equipment specifically designed or adapted to protect against both a space environment and physical harm caused by weapons, projectiles, or other forms of direct assault
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig for each separate and distinct item, confiscation of the offending item(s), and further legal action as the situation demands.''
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig and further legal action as the situation demands.''
* Any crew-served or emplaced weapon
* Any object or instrument specifically designed or adapted to launch large-calibre explosives or projectiles for offensive or defensive purposes
@@ -218,7 +217,7 @@ Should the mutiny be unsuccessful, only the head officer(s) who approved the mut
Should the mutiny be successful, the head officer(s) who approved the mutiny must select a new Commanding Officer per Line of Succession. The new CO will then sentence the previous heads of staff for their crimes per Space Law.
== Foreign Vessels ==
Should civilian and military vessels receive a docking request from another neutral vessel, they may approve or deny such a request. Any vessel may, with consent from the docking entity, be searched by law enforcement upon arrival, and its crew granted free movement accompanied by an escort at the discretion of the CO or law enforcement officials.
Should civilian and military vessels receive a docking request from another neutral vessel, they may approve or deny such a request. Any vessel may, with consent from the docking entity, be searched by law enforcement upon arrival, and its crew granted free movement accompanied by an escort at the discretion of the CO or law enforcement officials. Crew of vessels legally docked on NanoTrasen Stations are considered to be in contract with NanoTrasen and as such are protected by Space Law, Standard operating Procedure and the Rules of Engagement.
Crew of foreign vessels may be refused entry, at any time and for any reason, and must be returned to, or be escorted to, their own vessel where applicable. Failure to comply may constitute trespass. Docking vessels bearing a warrant or commission represent an exception, and may not be denied entry or be forced to submit to a search.
@@ -253,7 +252,7 @@ Martial Law may only be declared by Central Command. Under Martial Law Security
* authorised to take whatever actions deemed necessary to neutralise or repel station threats.
* authorized to summarily execute station threats.
* not bound to the Rules of Engagement.
* not obligated to provide any medical care to prisoners and detainees, including revival.
* not obligated to provide any medical care to prisoners, including revival.
== Armory Procedure ==
The armory and its usage are governed by the Warden. The Warden is responsible for stocking, distributing, and recording the collection of weapons used by the station during the shift. They are also afforded the authority to arm security at their discretion, so long as the weaponry distributed is in appropriate response to [[Alert Procedure]].
+76 -24
View File
@@ -22,39 +22,57 @@ Examples:
# Changes to upstream files
If you make a change to an upstream C# or YAML file **you must add comments on or around the changed lines**.
The comments should clarify what changed, to make conflict resolution simpler when a file is changed upstream.
Follow a few guidelines when modifying non-DeltaV files, to help us manage our project. (files that are not in `_DV` or `Nyano` folders)
For YAML specifically, if you add a new component to a prototype add the comment to the `type: ...` line.
If you just modify some fields of a component, comment the fields instead.
Primarily, **add comments on or around all new or changed lines** in upstream files. Explain what was changed to make resolving merge conflicts easier; we regularly merge new upstream changes into our project.
For C# files, if you are adding a lot of code try to put it in a partial class when it makes sense.
### Changing Upstream YAML .yml files
The exception to this is early merging commits that are going to be cherry picked in the future regardless, there's no harm in leaving them as-is.
**Add comments on or around any changed lines.**
As an aside, fluent (.ftl) files **do not support comments on the same line** as a locale value, so be careful when changing them.
If you add a new component to a prototype, add an explanation to the `type: ...` line. Example:
## Examples of comments in upstream files
A single line comment on a changed yml field:
```yml
- type: entity
parent: BasePDA
id: SciencePDA
name: epistemics PDA # DeltaV - Epistemics Department replacing Science
parent: MobSiliconBase
id: MobSupplyBot
components:
- type: InteractionPopup # DeltaV - Make supplybots pettable
interactSuccessString: petting-success-supplybot
interactFailureString: petting-failure-supplybot
interactSuccessSound:
path: /Audio/Ambience/Objects/periodic_beep.ogg
```
A pair of comments enclosing a list of added items to starting gear:
Whereas if you just modify some fields of a component, comment the fields instead, using inline or block comments. Examples:
```yml
storage:
back:
- EmergencyRollerBedSpawnFolded
- type: entityTable
id: FillLockerWarden
table: !type:AllSelector
children:
- id: ClothingHandsGlovesCombat
- id: ClothingShoesBootsSecurityMagboots # DeltaV - Added security magboots.
- id: ClothingShoesBootsJack
#- id: ClothingOuterCoatWarden # DeltaV - removed for incongruence
#- id: ClothingOuterWinterWarden # DeltaV - removed for incongruence
- id: RubberStampWarden
- id: DoorRemoteArmory
- id: HoloprojectorSecurity
# Begin DeltaV additions
- BodyBagFolded
- Portafib
- id: WeaponEnergyShotgun
- id: BoxPDAPrisoner
- id: LunchboxSecurityFilledRandom
prob: 0.3
# End DeltaV additions
```
### Changing Upstream C# .cs files
If you are adding a lot of C# code, then take advantage of partial classes. Put the new code in its own file in the `_DV` folder, if it makes sense.
Otherwise, **add comments on or around any changed lines.**
A comment on a new imported namespace:
```cs
using Content.Server.Psionics.Glimmer; // DeltaV
@@ -77,6 +95,40 @@ private EntityUid Slice(...)
}
```
### 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.
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]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`
```
analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
[vagueandspecific] {$vagueInfo} ({$specificInfo})
[vagueonly] {$vagueInfo} (unable to detect details)
[simple] {$specificInfo}
[hidden] Unable to detect (unlock to discover)
*[noinfo] Unlock nodes to gain info
}[/color][/font]
```
Also keep in mind that fluent (.ftl) files **do not support comments on the same line** as a locale value, so be careful when commenting.
### Early merges
We mostly merge upstream changes in big chunks (e.g. a month of upstream PRs at a time), but urgent changes can be merged early, separately.
Early merges are an exception to the above rules - if cherry-picking a PR for an early merge, you don't need to add `#DeltaV` comments, since the code is coming directly from upstream without any changes.
# Mapping
If you want to make changes to a map, get in touch with its maintainer to make sure you don't both make changes at the same time.
@@ -92,11 +144,11 @@ Please limit changelogs on map PRs to **significant** map alterations or additio
Format for map PRs looks like:
```
:cl: Yourname
MAPS: Mapname
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
MAPS:
- add: Mapname: Added fun!
- remove: Mapname: Removed fun!
- tweak: Mapname: Changed fun!
- fix: Mapname: Fixed fun!
```
# Before you submit
@@ -59,6 +59,9 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
SendCartridgeUiReadyEvent(_activeProgram.Value);
}
if (_activeCartridgeUI is IDisposable disposableUi) // Funky: Handle cleanup for popups and such.
disposableUi.Dispose();
_activeCartridgeUI = ui;
_activeUiFragment?.Dispose();
_activeUiFragment = control;
@@ -121,8 +124,13 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
{
base.Dispose(disposing);
if (disposing)
if (disposing) // Funky: Handle cleanup for popups and such.
{
if (_activeCartridgeUI is IDisposable disposableUi)
disposableUi.Dispose();
_activeUiFragment?.Dispose();
}
}
protected CartridgeComponent? RetrieveCartridgeComponent(EntityUid? cartridgeUid)
@@ -1,8 +1,11 @@
using Content.Client.Message;
using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
namespace Content.Client.CartridgeLoader.Cartridges;
@@ -14,6 +17,18 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
public event Action? OnNotificationSwithPressed;
// DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
private static readonly Type[] AllowedTags =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
];
public NewsReaderUiFragment()
{
RobustXamlLoader.Load(this);
@@ -31,16 +46,17 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
Author.Visible = true;
PageName.Text = article.Title;
PageText.SetMarkupPermissive(article.Content);
PageText.SetMessage(FormattedMessage.FromMarkupPermissive(article.Content), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
PageNum.Text = $"{targetNum}/{totalNum}";
NotificationSwitch.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
string shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
var shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
ShareTime.SetMarkup(Loc.GetString("news-read-ui-time-prefix-text") + " " + shareTime);
Author.SetMarkup(Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author")); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
Prev.Disabled = targetNum <= 1;
Next.Disabled = targetNum >= totalNum;
@@ -58,6 +58,13 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
{
_menu?.UpdateState(targets, st.SelectedId);
}
// Begin Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
if (_menu is { } menu && EntMan.TryGetComponent<ChameleonClothingComponent>(Owner, out var chameleon) && chameleon.WindowTitleOverride is { } titleOverride)
{
_menu.Title = Loc.GetString(titleOverride);
}
// End Delta-V
}
private void OnIdSelected(string selectedId)
@@ -1,10 +1,12 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.RichText; // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -18,6 +20,18 @@ public sealed partial class ArticleEditorPanel : Control
private bool _preview;
// DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
private static readonly Type[] AllowedTags =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
];
public ArticleEditorPanel()
{
RobustXamlLoader.Load(this);
@@ -29,7 +43,7 @@ public sealed partial class ArticleEditorPanel : Control
// Customize scrollbar width and margin. This is not possible in xaml
var scrollbar = ContentField.GetChild(1);
scrollbar.SetWidth = 6f;
scrollbar.Margin = new Thickness(9, 0, 2 , 0);
scrollbar.Margin = new Thickness(9, 0, 2, 0);
RichTextInfoLabel.TooltipSupplier = sender =>
{
@@ -81,7 +95,9 @@ public sealed partial class ArticleEditorPanel : Control
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
var articleBody = Rope.Collapse(ContentField.TextRope); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), AllowedTags); // DeltaV - Sanitize markup; see https://github.com/space-wizards/space-station-14/pull/41799
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
@@ -19,5 +19,9 @@
Align="Center"
Access="Public"/>
</BoxContainer>
<Label Name='WarningLabel'
Align="Center"
StyleClasses="WindowFooterText"
Text="{Loc 'war-declarator-standards-warning'}" /> <!-- DeltaV - warops LRP announcement warning -->
</BoxContainer>
</controls:FancyWindow>
+3 -3
View File
@@ -146,7 +146,7 @@ namespace Content.Client.PDA
if (state.PdaOwnerInfo.ActualOwnerName != null)
{
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName;
_pdaOwner = FormattedMessage.EscapeText(state.PdaOwnerInfo.ActualOwnerName); // DeltaV - Escape actual owner
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("actualOwnerName", _pdaOwner)));
PdaOwnerLabel.Visible = true;
@@ -159,8 +159,8 @@ namespace Content.Client.PDA
if (state.PdaOwnerInfo.IdOwner != null || state.PdaOwnerInfo.JobTitle != null)
{
_owner = state.PdaOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown");
_jobTitle = state.PdaOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned");
_owner = FormattedMessage.EscapeText(state.PdaOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")); // DeltaV - Escape owner and job
_jobTitle = FormattedMessage.EscapeText(state.PdaOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned")); // DeltaV - Escape owner and job
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
("owner", _owner),
("jobTitle", _jobTitle)));
@@ -714,6 +714,13 @@ public sealed partial class ChatUIController : UIController
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);
else
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel);
// DeltaV begin - refresh indicator change
if (CurrentChannel != prefixChannel)
_typingIndicator?.ClientSubmittedChatText();
CurrentChannel = prefixChannel;
// DeltaV end
}
public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text)
@@ -238,6 +238,7 @@ public partial class ChatBox : UIWidget
return;
ChatInput.ChannelSelector.Select(toSelect);
_controller.CurrentChannel = toSelect; // DeltaV - Alt Chat Indicators
}
private void OnInputKeyBindDown(GUIBoundKeyEventArgs args)
@@ -270,12 +271,14 @@ public partial class ChatBox : UIWidget
_controller.UpdateSelectedChannel(this);
// Warn typing indicator about change
_controller.NotifyChatTextChange();
// _controller.NotifyChatTextChange(); // DeltaV
_controller.NotifySpecificChatTextChange(SelectedChannel); // DeltaV - Alt Chat Indicators
}
private void OnFocusEnter(LineEditEventArgs args)
{
// Warn typing indicator about focus
_controller.CurrentChannel = SelectedChannel; // DeltaV - Alt Chat Indicators
_controller.NotifyChatFocus(true);
}
@@ -3,12 +3,10 @@ using Content.Client.Ghost;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Client.UserInterface.Systems.Ghost.Widgets;
using Content.Shared.Ghost;
using Robust.Client.Console; // Frontier
using Robust.Shared.Console; // Frontier
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Content.Client._Corvax.Respawn; // Frontier
using Content.Shared._NF.CCVar; // Frontier
using Content.Shared._Corvax.Respawn; // Frontier
using Robust.Shared.Configuration; // Frontier
namespace Content.Client.UserInterface.Systems.Ghost;
@@ -21,7 +19,9 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
[Dependency] private readonly IConfigurationManager _cfg = default!; // Frontier
[UISystemDependency] private readonly GhostSystem? _system = default;
[UISystemDependency] private readonly RespawnSystem? _respawn = default; // Frontier
// Updated when get death time from the server.
private TimeSpan? DeathTime;
private GhostGui? Gui => UIManager.GetActiveUIWidgetOrNull<GhostGui>();
@@ -32,6 +32,9 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
// DeltaV
SubscribeNetworkEvent<RespawnResetEvent>(OnRespawnReseted);
}
private void OnScreenLoad()
@@ -64,23 +67,14 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
system.GhostRoleCountUpdated -= OnRoleCountUpdated;
}
// Begin Frontier
public void OnSystemLoaded(RespawnSystem system)
{
system.RespawnReseted += OnRespawnReseted;
}
public void OnSystemUnloaded(RespawnSystem system)
{
system.RespawnReseted -= OnRespawnReseted;
}
private void OnRespawnReseted()
// Begin DeltaV
private void OnRespawnReseted(RespawnResetEvent ev, EntitySessionEventArgs args)
{
DeathTime = ev.Time;
UpdateGui();
UpdateRespawn(_respawn?.RespawnResetTime);
UpdateRespawn();
}
// End Frontier
// End DeltaV
public void UpdateGui()
{
@@ -94,9 +88,9 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
}
// Begin Frontier
private void UpdateRespawn(TimeSpan? timeOfDeath)
private void UpdateRespawn()
{
Gui?.UpdateRespawn(timeOfDeath);
Gui?.UpdateRespawn(DeathTime);
}
// End Frontier
@@ -116,7 +110,7 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
return;
Gui.Visible = true;
UpdateRespawn(_respawn?.RespawnResetTime); // Frontier
UpdateRespawn(); // Frontier, DeltaV
UpdateGui();
}
@@ -203,11 +197,4 @@ public sealed class GhostUIController : UIController, IOnSystemChanged<GhostSyst
{
_system?.OpenGhostRoles();
}
// Begin Frontier
private void RespawnPressed()
{
IoCManager.Resolve<IClientConsoleHost>().RemoteExecuteCommand(null, "ghostrespawn");
}
// End Frontier
}
@@ -1,7 +1,8 @@
using Content.Shared.Research.Components;
using Content.Shared._DV.Xenoarchaeology.BUI;// DeltaV
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
using JetBrains.Annotations;
using Robust.Client.UserInterface; // DeltaV
namespace Content.Client.Xenoarchaeology.Ui;
@@ -54,5 +55,15 @@ public sealed class AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKe
_consoleMenu?.Dispose();
}
// DeltaV
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not AnalysisConsoleBoundUserInterfaceState consoleState)
return;
_consoleMenu?.UpdateState(consoleState.Mult, consoleState.Ratio);
}
}
@@ -29,7 +29,9 @@
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<RichTextLabel Name="GlimmerSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/> <!-- DeltaV -->
<RichTextLabel Name="ExtractionSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<RichTextLabel Name="MultLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/> <!-- DeltaV -->
</BoxContainer>
</PanelContainer>
</BoxContainer>
@@ -1,3 +1,4 @@
using System.Numerics; // DeltaV
using System.Text;
using Content.Client.Message;
using Content.Client.Resources;
@@ -21,7 +22,7 @@ namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(3);
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(5);// DeltaV
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
@@ -36,8 +37,14 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
private Entity<XenoArtifactNodeComponent>? _currentNode;
private TimeSpan? _hideExtractInfoIn;
private int _extractionSum;
// Begin DeltaV - Variables for extraction calculation display
private int _extractionSum;
private int _glimmerSum;
private float _multValue;
private float _ratioValue;
private bool _pressed;
// End DeltaV
public event Action? OnServerSelectionButtonPressed;
public event Action? OnExtractButtonPressed;
@@ -65,7 +72,12 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
OnServerSelectionButtonPressed?.Invoke();
};
ExtractButton.OnPressed += StartExtract;
// DeltaV
ExtractButton.OnPressed += _ =>
{
_pressed = true;
OnExtractButtonPressed?.Invoke();
};
}
/// <summary>
@@ -84,7 +96,20 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
Update(_owner);
}
private void StartExtract(BaseButton.ButtonEventArgs obj)
// DeltaV
public void UpdateState(float mult, float ratio)
{
_multValue = mult;
_ratioValue = ratio;
if (_pressed)
{
_pressed = false;
StartExtract();
return;
}
}
private void StartExtract() // DeltaV - args changed
{
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
@@ -92,7 +117,8 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ExtractContainer.Visible = true;
NodeViewContainer.Visible = false;
_extractionSum = 0;
_extractionSum = 0;// DeltaV
_glimmerSum = 0;// DeltaV
var extractionMessage = new FormattedMessage();
var nodes = _xenoArtifact.GetAllNodes(artifact.Value);
@@ -100,16 +126,21 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
var count = 0;
foreach (var node in nodes)
{
var pointValue = _xenoArtifact.GetResearchValue(node);
var pointValue = _xenoArtifact.GetResearchValue(node) * _multValue;// DeltaV
var glimmerValue = (pointValue / _ratioValue / _multValue);// DeltaV
if (pointValue <= 0)
continue;
count++;
var nodeId = _xenoArtifact.GetNodeId(node);
var text = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", pointValue));
extractionMessage.AddMarkupOrThrow(text);
_extractionSum += (int)pointValue;// DeltaV
_glimmerSum += (int)(glimmerValue);// DeltaV
var textPoints = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", (int)(pointValue)));
var textGlimmer = Loc.GetString("analysis-console-glimmer-value", ("id", nodeId), ("value", (int)(glimmerValue)));// DeltaV
extractionMessage.AddMarkupOrThrow(textPoints);
extractionMessage.PushNewline();
extractionMessage.AddMarkupOrThrow(textGlimmer);// DeltaV
extractionMessage.PushNewline();
}
@@ -120,10 +151,12 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ExtractionResearchLabel.SetMessage(extractionMessage);
ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));
ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));// DeltaV
GlimmerSumLabel.SetMarkup(Loc.GetString("analysis-console-glimmer-sum", ("value", _glimmerSum)));// DeltaV
MultLabel.SetMarkup(Loc.GetString("analysis-console-glimmer-mult", ("value", _multValue.ToString("F2"))));// DeltaV
_audio.PlayGlobal(_owner.Comp.ScanFinishedSound, _owner, AudioParams.Default.WithVolume(1f));
OnExtractButtonPressed?.Invoke();
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -202,14 +235,34 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
var hasInfo = _xenoArtifact.HasUnlockedPredecessor(artifact.Value, node.Value);
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", hasInfo),
("info", _ent.GetComponentOrNull<MetaDataComponent>(node.Value)?.EntityDescription ?? string.Empty)));
// DeltaV - start of hide locked node effects
var specificInfo = node.Value.Comp.EffectTipSpecific ?? "xenoarch-effect-tip-unknown";
if (!hasInfo || (node.Value.Comp.LockedEffectTipHidden && node.Value.Comp.Locked))
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", hasInfo ? XenoArtifactEffectVisibility.Hidden : XenoArtifactEffectVisibility.NoInfo)));
}
else if (!node.Value.Comp.LockedEffectTipHidden && node.Value.Comp.LockedEffectTipVague && node.Value.Comp.EffectTipVague != null)
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", node.Value.Comp.Locked ? XenoArtifactEffectVisibility.VagueOnly : XenoArtifactEffectVisibility.VagueAndSpecific),
("specificInfo", Loc.GetString(specificInfo)),
("vagueInfo", Loc.GetString(node.Value.Comp.EffectTipVague))));
}
else
{
EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
("state", XenoArtifactEffectVisibility.Simple),
("specificInfo", Loc.GetString(specificInfo))));
}
// DeltaV - end of hide locked node effects
var predecessorNodes = _xenoArtifact.GetPredecessorNodes(artifact.Value.Owner, node.Value);
if (!hasInfo)
{
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", false)));
// DeltaV - start of hide locked node effects
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", XenoArtifactEffectVisibility.NoInfo)));
// DeltaV - end of hide locked node effects
}
else
{
@@ -229,5 +282,16 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
ClassValueLabel.SetMarkup(Loc.GetString("analysis-console-info-class-value",
("class", Loc.GetString($"artifact-node-class-{Math.Min(6, predecessorNodes.Count + 1)}"))));
}
// DeltaV - start of hide locked node effects
public enum XenoArtifactEffectVisibility : sbyte
{
NoInfo = 0,
Hidden = 1,
Simple = 2,
VagueOnly = 3,
VagueAndSpecific = 4,
}
// DeltaV - end of hide locked node effects
}
@@ -1,3 +1,4 @@
using Content.Client._DV.Xenoarchaeology.Ui;
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
@@ -7,15 +8,19 @@ namespace Content.Client.Xenoarchaeology.Ui;
/// </summary>
public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
// DeltaV - start of node scanner overhaul
[ViewVariables]
private NodeScannerDisplay? _scannerDisplay;
private DVNodeScannerDisplay? _scannerDisplay;
// DeltaV - end of node scanner overhaul
/// <inheritdoc />
protected override void Open()
{
base.Open();
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
// DeltaV - start of node scanner overhaul
_scannerDisplay = this.CreateWindow<DVNodeScannerDisplay>();
// DeltaV - end of node scanner overhaul
_scannerDisplay.SetOwner(Owner);
}
@@ -1,20 +0,0 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="305 180"
SetSize="305 180"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Orientation="Horizontal">
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="45 25 0 0" MinHeight="47" />
<GridContainer Name="ActiveNodesList" Columns="4" Rows="2" Visible="True" MinHeight="72" />
</BoxContainer>
<controls:StripeBack>
<Label Name="ArtifactStateLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>
@@ -1,150 +0,0 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class NodeScannerDisplay : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IGameTiming _timing= default!;
private readonly SharedXenoArtifactSystem _artifact;
private TimeSpan? _nextUpdate;
private EntityUid _owner;
private TimeSpan _updateFromAttachedFrequency;
private readonly HashSet<string> _triggeredNodeNames = new();
public NodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_updateFromAttachedFrequency = scannerComponent.DisplayDataUpdateInterval;
_owner = scannerEntityUid;
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if(_nextUpdate != null && _timing.CurTime < _nextUpdate)
return;
_nextUpdate = _timing.CurTime + _updateFromAttachedFrequency;
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
{
Update(false, ArtifactState.None);
return;
}
var attachedArtifactEnt = connectedScanner.AttachedTo;
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
return;
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
_triggeredNodeNames.Clear();
ArtifactState artifactState;
if (unlockingComponent == null)
{
var timeToUnlockAvailable = artifactComponent.NextUnlockTime - _timing.CurTime;
artifactState = timeToUnlockAvailable > TimeSpan.Zero
? ArtifactState.Cooldown
: ArtifactState.Ready;
}
else
{
var triggeredIndexes = unlockingComponent.TriggeredNodeIndexes;
foreach (var triggeredIndex in triggeredIndexes)
{
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
_triggeredNodeNames.Add(triggeredNodeName);
}
artifactState = ArtifactState.Unlocking;
}
Update(true, artifactState, _triggeredNodeNames);
}
/// <summary>
/// Updates labels with scanned artifact data and list of triggered nodes from component.
/// </summary>
private void Update(bool isConnected, ArtifactState artifactState, HashSet<string>? triggeredNodeNames = null)
{
ArtifactStateLabel.Text = GetStateText(artifactState);
NodeScannerState.Text = isConnected
? Loc.GetString("node-scanner-artifact-connected")
: Loc.GetString("node-scanner-artifact-non-connected");
ActiveNodesList.Children.Clear();
if (triggeredNodeNames == null)
return;
if (triggeredNodeNames.Count > 0)
{
// show list of triggered nodes instead of 'no data' placeholder
NoActiveNodeDataLabel.Visible = false;
ActiveNodesList.Visible = true;
foreach (var nodeId in triggeredNodeNames)
{
var nodeLabel = new Button
{
Text = nodeId,
Margin = new Thickness(15, 5, 0, 0),
MaxHeight = 40,
Disabled = true
};
ActiveNodesList.Children.Add(nodeLabel);
}
}
else
{
// clear list of activated nodes (done previously), show 'no data' placeholder
NoActiveNodeDataLabel.Visible = true;
ActiveNodesList.Visible = false;
}
}
private string GetStateText(ArtifactState state)
{
return state switch
{
ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
ArtifactState.Unlocking => Loc.GetString("node-scanner-artifact-state-unlocking"),
ArtifactState.Cooldown => Loc.GetString("node-scanner-artifact-state-cooldown"),
_ => throw new ArgumentException("Invalid state"),
};
}
}
@@ -1,22 +0,0 @@
using Content.Shared._Corvax.Respawn;
namespace Content.Client._Corvax.Respawn;
public sealed class RespawnSystem : EntitySystem
{
public TimeSpan? RespawnResetTime { get; private set; }
public event Action? RespawnReseted;
public override void Initialize()
{
SubscribeNetworkEvent<RespawnResetEvent>(OnRespawnReset);
}
private void OnRespawnReset(RespawnResetEvent e)
{
RespawnResetTime = e.Time;
RespawnReseted?.Invoke();
}
}
+2 -1
View File
@@ -6,6 +6,7 @@ namespace Content.Client._DV.Biscuit;
public sealed class BiscuitSystem : VisualizerSystem<BiscuitVisualsComponent>
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
protected override void OnAppearanceChange(EntityUid uid, BiscuitVisualsComponent component,
ref AppearanceChangeEvent args)
@@ -15,7 +16,7 @@ public sealed class BiscuitSystem : VisualizerSystem<BiscuitVisualsComponent>
_appearance.TryGetData(uid, BiscuitStatus.Cracked, out bool cracked);
args.Sprite.LayerSetVisible(BiscuitVisualLayers.Top, !cracked);
_sprite.LayerSetVisible((uid, args.Sprite), BiscuitVisualLayers.Top, !cracked);
}
}
@@ -26,6 +26,16 @@
</PanelContainer.PanelOverride>
</PanelContainer>
<!-- Group chat icon -->
<TextureRect Name="GroupIcon"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
MinSize="16 16"
MaxSize="16 16"
Stretch="KeepAspectCentered"
VerticalAlignment="Center"
Margin="4 0 6 0"
Visible="False" />
<!-- Text container -->
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
@@ -4,6 +4,7 @@ using Content.Shared.Access.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
@@ -38,10 +39,12 @@ public sealed partial class NanoChatEntry : BoxContainer
_pressHandler = _ => OnPressed?.Invoke(_number);
ChatButton.OnPressed += _pressHandler;
NameLabel.Text = SharedNanoChatSystem.Truncate(recipient.Name, _maxNameLength);
JobLabel.Text = SharedNanoChatSystem.Truncate(recipient.JobTitle ?? "", _maxIdJobLength);
JobLabel.Visible = !string.IsNullOrEmpty(recipient.JobTitle);
// FIXME: Truncate and Escape can lead to stray `\`, if the truncate cuts a markup tag in half
NameLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.Name, _maxNameLength));
JobLabel.Text = FormattedMessage.EscapeText(SharedNanoChatSystem.Truncate(recipient.JobTitle ?? string.Empty, _maxIdJobLength));
JobLabel.Visible = !string.IsNullOrWhiteSpace(JobLabel.Text);
UnreadIndicator.Visible = recipient.HasUnread;
GroupIcon.Visible = recipient.IsGroup;
ChatButton.ModulateSelfOverride = isSelected ? NanoChatMessageBubble.OwnMessageColor : null;
}
@@ -1,9 +1,12 @@
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
@@ -26,12 +29,67 @@ public sealed partial class NanoChatLookupView : PanelContainer
return;
}
for (var idx = 0; idx < contacts.Count; idx++)
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
var noDepartmentContacts = new List<NanoChatRecipient>();
foreach (var contact in contacts)
{
var contact = contacts[idx];
var isEvenRow = idx % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
if (!string.IsNullOrWhiteSpace(contact.Department) &&
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
{
departmentGroups.GetOrNew(dept).Add(contact);
}
else
{
noDepartmentContacts.Add(contact);
}
}
var rowIndex = 0;
foreach (var (department, departmentContacts) in departmentGroups.OrderBy(entry => entry.Key, DepartmentUIComparer.Instance))
{
departmentContacts.Sort();
// Add department header
ContactsList.AddChild(new Label
{
Text = Loc.GetString(department.Name),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
});
foreach (var contact in departmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
// Add contacts without departments at the end
if (noDepartmentContacts.Count > 0)
{
noDepartmentContacts.Sort();
if (departmentGroups.Count > 0)
{
ContactsList.AddChild(new Label
{
Text = Loc.GetString("nano-chat-other-department"),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, 8, 0, 2)
});
}
foreach (var contact in noDepartmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, state, isEvenRow, OnStartChat);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
}
@@ -42,9 +100,13 @@ public sealed partial class NanoChatLookupView : PanelContainer
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label()
{
Text = contact.Name,
Text = displayName,
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
@@ -1,5 +1,6 @@
<cartridges:NanoChatMessageBubble
xmlns="https://spacestation14.io"
xmlns:nanochatui="clr-namespace:Content.Client._DV.NanoChat.UI"
xmlns:cartridges="clr-namespace:Content.Client._DV.CartridgeLoader.Cartridges"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
HorizontalExpand="True">
@@ -7,31 +8,34 @@
<BoxContainer Name="MessageContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<!-- Left spacer for other's messages -->
<Control Name="LeftSpacer"
MinSize="12 0" />
<!-- Left spacer for other's messages -->
<Control MinSize="12 0" Name="LeftSpacer" />
<!-- Message panel -->
<BoxContainer Name="MessageBox"
Orientation="Vertical"
MaxWidth="320"
HorizontalExpand="True">
<!-- Sender name -->
<Label HorizontalAlignment="Left"
Margin="10,0,10,2"
Name="SenderNameLabel"
StyleClasses="LabelSmall"
Visible="False" />
<PanelContainer Name="MessagePanel"
MaxWidth="320"
HorizontalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"
ContentMarginTopOverride="6"
ContentMarginBottomOverride="6"
BorderThickness="1">
<graphics:StyleBoxFlat BorderThickness="1">
<!-- Colors set in code based on message sender -->
</graphics:StyleBoxFlat>
</PanelContainer.PanelOverride>
<RichTextLabel Name="MessageText"
HorizontalExpand="True" />
HorizontalExpand="True"
Margin="10,6,10,6" />
</PanelContainer>
<!-- Delivery failed text -->
@@ -1,14 +1,19 @@
using System.Text.RegularExpressions;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
using Content.Client._Funkystation.NanoChat;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NanoChatMessageBubble : BoxContainer
{
private static readonly Regex EmojiRegex = new(@":(\w+):", RegexOptions.Compiled);
public static readonly Color OwnMessageColor = Color.FromHex("#173717d9"); // Dark green
public static readonly Color OtherMessageColor = Color.FromHex("#252525d9"); // Dark gray
public static readonly Color BorderColor = Color.FromHex("#40404066"); // Subtle border
@@ -20,7 +25,7 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
RobustXamlLoader.Load(this);
}
public void SetMessage(NanoChatMessage message, bool isOwnMessage)
public void SetMessage(NanoChatMessage message, bool isOwnMessage, string? senderName = null, bool showSenderName = false)
{
if (MessagePanel.PanelOverride is not StyleBoxFlat)
return;
@@ -30,8 +35,20 @@ public sealed partial class NanoChatMessageBubble : BoxContainer
style.BackgroundColor = isOwnMessage ? OwnMessageColor : OtherMessageColor;
style.BorderColor = BorderColor;
// Set message content
MessageText.Text = message.Content;
var contentWithEmoji = EmojiRegex.Replace(FormattedMessage.EscapeText(message.Content), "[emoji=\"$1\"]");
MessageText.SetMessage(
FormattedMessage.FromMarkupPermissive(contentWithEmoji),
new[] { typeof(EmojiTag) }
);
SenderNameLabel.Visible = showSenderName && !string.IsNullOrEmpty(senderName);
if (SenderNameLabel.Visible)
{
SenderNameLabel.Text = senderName;
SenderNameLabel.Modulate = TextColor;
}
MessageText.Modulate = TextColor;
// Show delivery failed text if needed (only for own messages)
@@ -5,7 +5,7 @@ using Robust.Client.UserInterface;
namespace Content.Client._DV.CartridgeLoader.Cartridges;
public sealed partial class NanoChatUi : UIFragment
public sealed partial class NanoChatUi : UIFragment, IDisposable
{
private NanoChatUiFragment? _fragment;
@@ -30,6 +30,12 @@ public sealed partial class NanoChatUi : UIFragment
_fragment?.UpdateState(cast);
}
public void Dispose()
{
// Close all popups when switching apps or closing the PDA
_fragment?.CloseAllPopups();
}
private static void SendNanoChatUiMessage(NanoChatUiMessageType type,
uint? number,
string? content,
@@ -60,6 +60,17 @@
Margin="0 0 4 0"
StyleClasses="OpenBoth"
ToolTip="{Loc nano-chat-new-chat}" />
<Button Name="CreateGroupChatButton"
MinSize="32 32"
MaxSize="32 32"
Margin="0 0 4 0"
StyleClasses="OpenBoth"
ToolTip="{Loc nano-chat-create-group}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<Button Name="LookupButton"
MaxSize="32 32"
StyleClasses="OpenBoth"
@@ -84,12 +95,20 @@
</BoxContainer>
</controls:StripeBack>
<Label Name="NoIdLabel"
Text="{Loc 'nano-chat-no-id-card'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
VerticalExpand="True"
HorizontalExpand="True"
Visible="True" />
<!-- Main content split -->
<BoxContainer Name="ChatView"
Orientation="Horizontal"
VerticalExpand="True"
HorizontalExpand="True"
Margin="0 5 0 0">
Margin="0 5 0 0"
Visible="False">
<!-- Left panel: Chat list -->
<PanelContainer StyleClasses="AngleRect"
VerticalExpand="True"
@@ -167,6 +186,18 @@
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<!-- View Members should take place of edit button in group chats, to avoid layout shift when moving between normal and group -->
<Button Name="ViewMembersButton"
MaxSize="32 32"
Visible="False"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-view-members}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/members.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<Button Name="DeleteChatButton"
MaxSize="32 32"
Visible="False"
@@ -197,11 +228,34 @@
MinSize="18 18" />
</Control>
</Button>
<Button Name="InviteToGroupButton"
MaxSize="32 32"
Visible="False"
StyleClasses="OpenBoth"
Margin="0 0 4 0"
ToolTip="{Loc nano-chat-invite-to-group}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/_Funkystation/Interface/NanoChat/invite.png"
Stretch="KeepAspectCentered"
MinSize="18 18" />
</Button>
<!-- Input row -->
<LineEdit Name="MessageInput"
PlaceHolder="{Loc nano-chat-message-placeholder}"
HorizontalExpand="True"
StyleClasses="OpenRight" />
<!-- Emoji Picker -->
<Button Name="EmojiButton"
MinSize="32 32"
MaxSize="32 32"
StyleClasses="OpenBoth"
Margin="4 0 0 0"
ToolTip="{Loc nano-chat-emoji-picker}">
<TextureRect StyleClasses="ButtonSquare"
TexturePath="/Textures/Interface/Emotes/laugh.png"
Stretch="KeepAspectCentered"
MinSize="24 24" />
</Button>
<Button Name="SendButton"
MinSize="32 32"
Disabled="True"
@@ -1,7 +1,18 @@
// SPDX-FileCopyrightText: 2024 Skubman <ba.fallaria@gmail.com>
// SPDX-FileCopyrightText: 2024 Tadeo <td12233a@gmail.com>
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
// SPDX-FileCopyrightText: 2025 Icepick <122653407+Icepicked@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 corresp0nd <46357632+corresp0nd@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 taydeo <td12233a@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.CCVar;
using Content.Client._Funkystation.NanoChat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -18,10 +29,14 @@ public sealed partial class NanoChatUiFragment : BoxContainer
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private int _maxNameLength;
private int _maxIdJobLength;
private readonly int _maxNameLength;
private readonly int _maxIdJobLength;
private readonly NewChatPopup _newChatPopup;
private readonly EditChatPopup _editChatPopup;
private readonly CreateGroupChatPopup _createGroupChatPopup;
private readonly InviteToGroupPopup _inviteToGroupPopup;
private readonly GroupMembersPopup _groupMembersPopup;
private readonly EmojiPickerPopup _emojiPickerPopup;
private uint? _currentChat;
private uint? _pendingChat;
private uint _ownNumber;
@@ -30,6 +45,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
private Dictionary<uint, NanoChatRecipient> _recipients = [];
private Dictionary<uint, List<NanoChatMessage>> _messages = [];
private HashSet<uint> _mutedChats = [];
private List<NanoChatRecipient>? _contacts; // Used in order to show names for people not in recipients
public event Action<NanoChatUiMessageType, uint?, string?, string?>? OnMessageSent;
@@ -43,6 +59,10 @@ public sealed partial class NanoChatUiFragment : BoxContainer
_newChatPopup = new(_maxNameLength, _maxIdJobLength);
_editChatPopup = new(_maxNameLength, _maxIdJobLength);
_createGroupChatPopup = new(_maxNameLength);
_inviteToGroupPopup = new();
_groupMembersPopup = new();
_emojiPickerPopup = new();
SetupEventHandlers();
}
@@ -58,12 +78,57 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.EditChat, number, name, job);
};
_createGroupChatPopup.OnGroupCreated += (name) =>
{
OnMessageSent?.Invoke(NanoChatUiMessageType.CreateGroupChat, null, name, null);
};
_inviteToGroupPopup.OnInvite += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.InviteToGroup, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnKick += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.KickFromGroup, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnAdmin += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.AdminUser, currentChat, number.ToString(), null);
};
_groupMembersPopup.OnDeadmin += (number) =>
{
if (_currentChat is not uint currentChat)
return;
OnMessageSent?.Invoke(NanoChatUiMessageType.DeadminUser, currentChat, number.ToString(), null);
};
_emojiPickerPopup.OnEmojiSelected += InsertEmoji;
NewChatButton.OnPressed += _ =>
{
_newChatPopup.ClearInputs();
_newChatPopup.OpenCentered();
};
CreateGroupChatButton.OnPressed += _ =>
{
_createGroupChatPopup.ClearInputs();
_createGroupChatPopup.OpenCentered();
};
MuteChatButton.OnPressed += _ =>
{
if (_currentChat is not uint currentChat)
@@ -134,8 +199,39 @@ public sealed partial class NanoChatUiFragment : BoxContainer
MessageInput.OnTextEntered += _ => SendMessage();
SendButton.OnPressed += _ => SendMessage();
EmojiButton.OnPressed += _ => OpenEmojiPicker();
EditChatButton.OnPressed += _ => BeginEditChat();
DeleteChatButton.OnPressed += _ => DeleteCurrentChat();
InviteToGroupButton.OnPressed += _ => OpenInvitePopup();
ViewMembersButton.OnPressed += _ => OpenMembersPopup();
}
private void OpenEmojiPicker()
{
// Close any existing emoji picker to prevent duplicates (shouldn't be happening anyway but it sure did)
if (_emojiPickerPopup.IsOpen)
_emojiPickerPopup.Close();
_emojiPickerPopup.OpenCentered();
}
private void InsertEmoji(string emoji)
{
MessageInput.InsertAtCursor(emoji);
MessageInput.GrabKeyboardFocus();
}
/// <summary>
/// Closes all open popups. Called when switching apps or closing the PDA.
/// </summary>
public void CloseAllPopups()
{
_emojiPickerPopup.Close();
_inviteToGroupPopup.Close();
_groupMembersPopup.Close();
_newChatPopup.Close();
_editChatPopup.Close();
_createGroupChatPopup.Close();
}
private void ToggleView()
@@ -156,7 +252,7 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (_recipients.Count == 0)
return;
var orderedRecipients = _recipients.OrderBy(r => r.Value.Name).Select(r => r.Key).ToArray();
var orderedRecipients = _recipients.OrderBy(r => r.Value).Select(r => r.Key).ToArray();
var currentChatIndex = (direction, _currentChat) switch
{
(CycleDirection.Up, null) => _recipients.Count,
@@ -252,6 +348,89 @@ public sealed partial class NanoChatUiFragment : BoxContainer
OnMessageSent?.Invoke(NanoChatUiMessageType.DeleteChat, activeChat, null, null);
}
private void OpenInvitePopup()
{
if (_currentChat is not uint currentChat ||
!_recipients.TryGetValue(currentChat, out var recipient) ||
!recipient.IsGroup)
return;
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var members = recipient.Members ?? new HashSet<uint>();
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
_inviteToGroupPopup.ClearSearch();
_inviteToGroupPopup.OpenCentered();
}
private void OpenMembersPopup()
{
if (_currentChat is not uint currentChat ||
!_recipients.TryGetValue(currentChat, out var recipient) ||
!recipient.IsGroup)
return;
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var members = recipient.Members ?? new HashSet<uint>();
var admins = recipient.Admins ?? new HashSet<uint>();
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
_groupMembersPopup.OpenCentered();
}
private void RefreshOpenPopups()
{
// If popup is open, refresh it or close if no longer a member
if (_inviteToGroupPopup.IsOpen)
{
if (_currentChat is uint currentChat &&
_recipients.TryGetValue(currentChat, out var recipient) &&
recipient.IsGroup)
{
var members = recipient.Members ?? new HashSet<uint>();
if (!members.Contains(_ownNumber))
{
_inviteToGroupPopup.Close();
}
else
{
// Use the contacts list from the server (same as Look Up Numbers)
// Don't fall back to recipients as they contain outdated cached information
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
_inviteToGroupPopup.SetContacts(availableContacts, members, _ownNumber);
}
}
else
{
_inviteToGroupPopup.Close();
}
}
if (_groupMembersPopup.IsOpen)
{
if (_currentChat is uint currentChat &&
_recipients.TryGetValue(currentChat, out var recipient) &&
recipient.IsGroup)
{
var members = recipient.Members ?? new HashSet<uint>();
if (!members.Contains(_ownNumber))
{
_groupMembersPopup.Close();
}
else
{
// Use the contacts list from the server (same as Look Up Numbers)
// Don't fall back to recipients as they contain outdated cached information
var availableContacts = _contacts ?? new List<NanoChatRecipient>();
var admins = recipient.Admins ?? new HashSet<uint>();
_groupMembersPopup.SetMembers(availableContacts, members, _ownNumber, recipient.CreatorId, admins);
}
}
else
{
_groupMembersPopup.Close();
}
}
}
private void BeginEditChat()
{
if (_currentChat is not uint currentChat)
@@ -294,17 +473,40 @@ public sealed partial class NanoChatUiFragment : BoxContainer
var activeChat = _pendingChat ?? _currentChat;
var hasActiveChat = activeChat != null;
var isGroupChat = false;
var canManageGroup = false;
NanoChatRecipient recipient = default;
var hasRecipient = false;
if (hasActiveChat && activeChat != null && _recipients.TryGetValue(activeChat.Value, out recipient))
{
hasRecipient = true;
isGroupChat = recipient.IsGroup;
canManageGroup = isGroupChat && (recipient.CreatorId == _ownNumber || recipient.Admins?.Contains(_ownNumber) == true);
}
// Update UI state
MessagesScroll.Visible = hasActiveChat;
CurrentChatName.Visible = !hasActiveChat;
MessageInputContainer.Visible = hasActiveChat;
DeleteChatButton.Visible = hasActiveChat;
EditChatButton.Visible = hasActiveChat;
EditChatButton.Visible = hasActiveChat && !isGroupChat;
DeleteChatButton.Disabled = !hasActiveChat;
if (activeChat != null && _recipients.TryGetValue(activeChat.Value, out var recipient))
InviteToGroupButton.Visible = canManageGroup;
ViewMembersButton.Visible = isGroupChat;
if (hasRecipient)
{
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
if (isGroupChat)
{
CurrentChatName.Text = recipient.Name;
}
else
{
CurrentChatName.Text = recipient.Name + (string.IsNullOrEmpty(recipient.JobTitle) ? "" : $" ({recipient.JobTitle})");
}
}
else
{
@@ -321,14 +523,44 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (activeChat == null || !messages.TryGetValue(activeChat.Value, out var chatMessages))
return;
var isGroupChat = _recipients.TryGetValue(activeChat.Value, out var recipient) && recipient.IsGroup;
foreach (var message in chatMessages)
{
var messageBubble = new NanoChatMessageBubble();
messageBubble.SetMessage(message, message.SenderId == _ownNumber);
MessageList.AddChild(messageBubble);
var isOwnMessage = message.SenderId == _ownNumber;
// For group chats, show sender name for other people's messages
string? senderName = null;
if (isGroupChat && !isOwnMessage)
{
// Try to get sender name from contacts first, then recipients
if (_recipients.TryGetValue(message.SenderId, out var senderRecipient))
{
senderName = senderRecipient.Name;
}
// If not in recipients, try contacts list (station-wide directory)
if (senderName == null && _contacts != null)
{
var contact = _contacts.FirstOrDefault(c => c.Number == message.SenderId);
if (contact.Number != 0)
senderName = contact.Name;
}
// Fallback to "Unknown" with number if name not found
if (senderName == null)
{
senderName = $"{Loc.GetString("nano-chat-unknown-sender")}#{message.SenderId:D4}";
}
}
messageBubble.SetMessage(message, isOwnMessage, senderName, isGroupChat && !isOwnMessage);
// Add spacing between messages
MessageList.AddChild(new Control { MinSize = new Vector2(0, 4) });
if (MessageList.ChildCount > 0)
MessageList.AddChild(new Control { Name = "MessagePadding", MinSize = new Vector2(0, 4) });
MessageList.AddChild(messageBubble);
}
MessageList.InvalidateMeasure();
@@ -357,13 +589,41 @@ public sealed partial class NanoChatUiFragment : BoxContainer
ListNumberButton.Pressed = _listNumber;
}
private void CheckValidNumber()
{
if (_ownNumber == 0)
{
NoIdLabel.Visible = true;
ChatView.Visible = false;
LookupView.Visible = false;
MuteButton.Disabled = true;
NewChatButton.Disabled = true;
CreateGroupChatButton.Disabled = true;
LookupButton.Disabled = true;
ListNumberButton.Disabled = true;
}
else if (NoIdLabel.Visible)
{
NoIdLabel.Visible = false;
ToggleView();
MuteButton.Disabled = false;
NewChatButton.Disabled = false;
CreateGroupChatButton.Disabled = false;
LookupButton.Disabled = false;
ListNumberButton.Disabled = false;
}
}
public void UpdateState(NanoChatUiState state)
{
_ownNumber = state.OwnNumber;
_notificationsMuted = state.NotificationsMuted;
_listNumber = state.ListNumber;
_mutedChats = state.MutedChats;
OwnNumberLabel.Text = $"#{state.OwnNumber:D4}";
_contacts = state.Contacts;
OwnNumberLabel.Text = $"#{_ownNumber:D4}";
UpdateMuteButton();
UpdateListNumber();
@@ -387,10 +647,15 @@ public sealed partial class NanoChatUiFragment : BoxContainer
if (_pendingChat == null)
_currentChat = state.CurrentChat;
UpdateCurrentChat();
UpdateMuteChatButton();
// Update recipients first, then update UI that depends on it
UpdateChatList(state.Recipients);
UpdateMessages(state.Messages);
UpdateCurrentChat();
UpdateMuteChatButton();
LookupView.UpdateContactList(state);
CheckValidNumber();
RefreshOpenPopups();
}
}
+130
View File
@@ -0,0 +1,130 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
namespace Content.Client._DV.NanoChat.UI;
/// <summary>
/// Extremely specialized version of <see cref="Robust.Client.UserInterface.Controls.TextureRect" /> designed for drawing Textures at fixed size with an additional offset
/// Circumvents the problem of not having "negative mergins" on TextureRect for manual alignment
/// This is fucking evil, please do not use this :3
/// </summary>
public sealed class EmojiControl : Control
{
public const string StylePropertyTexture = "texture";
public const string StylePropertyOffset = "offset";
private Texture? _texture;
private Vector2? _offset;
private string? _texturePath;
public string TexturePath
{
set
{
Texture = IoCManager.Resolve<IResourceCache>().GetResource<TextureResource>(value);
_texturePath = value;
}
}
protected override void OnThemeUpdated()
{
if (_texturePath != null) Texture = Theme.ResolveTexture(_texturePath);
base.OnThemeUpdated();
}
/// <summary>
/// The texture to draw.
/// </summary>
public Texture? Texture
{
get
{
if (_texture is null)
{
if (TryGetStyleProperty(StylePropertyTexture, out Texture? texture))
{
return texture;
}
}
return _texture;
}
set
{
var oldSize = _texture?.Size;
_texture = value;
if (value?.Size != oldSize)
{
InvalidateMeasure();
}
}
}
/// <summary>
/// Vertical offset of the texture
/// </summary>
public Vector2 Offset
{
get
{
if (_offset is null)
{
if (TryGetStyleProperty(StylePropertyOffset, out Vector2 offset))
return offset;
}
return _offset ?? Vector2.Zero;
}
set
{
var oldOffset = _offset;
_offset = value;
if (oldOffset != value)
{
InvalidateArrange();
}
}
}
protected override void ArrangeCore(UIBox2 finalRect)
{
finalRect.Left += Offset.X;
finalRect.Right += Offset.X;
finalRect.Top += Offset.Y;
finalRect.Bottom += Offset.Y;
base.ArrangeCore(finalRect);
}
protected override void Draw(DrawingHandleScreen handle)
{
var texture = Texture;
if (texture is null)
return;
handle.DrawTextureRect(texture, GetDrawDimensions());
}
private UIBox2 GetDrawDimensions()
{
var (texWidth, texHeight) = Texture?.Size ?? Vector2i.Zero;
var width = texWidth * (PixelSize.Y / texHeight);
var height = (float)PixelSize.Y;
if (width > PixelSize.X)
{
width = PixelSize.X;
height = texHeight * (PixelSize.X / texWidth);
}
var size = new Vector2(width, height);
var position = (PixelSize - size) / 2 + Offset;
return UIBox2.FromDimensions(position, size);
}
}
@@ -0,0 +1,187 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stealth;
using Content.Shared._DV.Overlays.Components;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client._Goobstation.Overlays;
public sealed class SharkVisionOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private readonly TransformSystem _transform;
private readonly StealthSystem _stealth;
private readonly ContainerSystem _container;
private readonly SharedSolutionContainerSystem _solution;
private readonly SpriteSystem _sprite;
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly List<SharkVisionRenderEntry> _entries = [];
private EntityUid? _lightEntity;
public float LightRadius;
public SharkVisionComponent? Comp;
public SharkVisionOverlay()
{
IoCManager.InjectDependencies(this);
_container = _entity.System<ContainerSystem>();
_transform = _entity.System<TransformSystem>();
_stealth = _entity.System<StealthSystem>();
_solution = _entity.System<SharedSolutionContainerSystem>();
_sprite = _entity.System<SpriteSystem>();
ZIndex = -1;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture is null || Comp is null)
return;
var worldHandle = args.WorldHandle;
var eye = args.Viewport.Eye;
if (eye == null)
return;
var player = _player.LocalEntity;
if (!_entity.TryGetComponent(player, out TransformComponent? playerXform))
return;
var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime);
var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime);
var mapId = eye.Position.MapId;
var eyeRot = eye.Rotation;
GetVisionEntities(Comp.BloodPrototypes, mapId, eyeRot);
foreach (var entry in _entries)
{
Render(entry.Ent, entry.Map, worldHandle, entry.EyeRot, Comp.Color, alpha);
}
worldHandle.SetTransform(Matrix3x2.Identity);
}
private void GetVisionEntities(ProtoId<ReagentPrototype>[] bloodPrototypes, MapId mapId, Angle eyeRot)
{
_entries.Clear();
var entities = _entity.EntityQueryEnumerator<SolutionContainerManagerComponent, SpriteComponent, TransformComponent>();
while (entities.MoveNext(out var uid, out var body, out var sprite, out var xform))
{
if (!CanSee(uid, sprite))
continue;
// Luckily, players are also SolutionContainerManagerComponent, so let's check for a bloodstream and check for bleeds
if (_entity.TryGetComponent<BloodstreamComponent>(uid, out var bloodstream) && bloodstream.BleedAmount <= 0)
continue;
// Should always be true but I'm just simplifying a future if statement
if (!_entity.TryGetComponent<SolutionContainerManagerComponent>(uid, out var solutionContainer))
continue;
if (solutionContainer.Containers == null)
continue;
var bloodFound = false;
// Someone calculate the Big O notation of this lmao
foreach (var individualContainer in solutionContainer.Containers)
{
if (_solution.TryGetSolution((uid, solutionContainer), individualContainer, out var _, out var solution))
{
var reagentsInContainer = solution.GetReagentPrototypes(_proto).ToDictionary();
bloodFound = reagentsInContainer.Keys.Any(reagentKey => bloodPrototypes.Any(x => reagentKey == x));
}
if (bloodFound)
break; // We don't need to keep searching if the entity has blood in at least one container
}
if (!bloodFound)
continue;
var entity = uid;
// Parent container check
if (_container.TryGetOuterContainer(uid, xform, out var container))
{
var owner = container.Owner;
if (_entity.TryGetComponent<SpriteComponent>(owner, out var ownerSprite)
&& _entity.TryGetComponent<TransformComponent>(owner, out var ownerXform))
{
entity = owner;
sprite = ownerSprite;
xform = ownerXform;
}
}
if (_entries.Any(e => e.Ent.Owner == entity))
continue;
_entries.Add(new SharkVisionRenderEntry((entity, sprite, xform), mapId, eyeRot));
}
}
private void Render(Entity<SpriteComponent, TransformComponent> ent,
MapId? map,
DrawingHandleWorld handle,
Angle eyeRot,
Color color,
float alpha)
{
var (uid, sprite, xform) = ent;
if (xform.MapID != map || !CanSee(uid, sprite))
return;
var position = _transform.GetWorldPosition(xform);
var rotation = _transform.GetWorldRotation(xform);
var originalColor = sprite.Color;
_sprite.SetColor((ent, ent.Comp1), color.WithAlpha(alpha));
_sprite.RenderSprite((ent, ent.Comp1), handle, eyeRot, rotation, position);
_sprite.SetColor((ent, ent.Comp1), originalColor);
}
private bool CanSee(EntityUid uid, SpriteComponent sprite)
{
return sprite.Visible && (!_entity.TryGetComponent(uid, out StealthComponent? stealth) ||
_stealth.GetVisibility(uid, stealth) > 0.5f);
}
public void ResetLight(bool checkFirstTimePredicted = true)
{
if (_lightEntity == null || checkFirstTimePredicted && !_timing.IsFirstTimePredicted)
return;
_entity.DeleteEntity(_lightEntity);
_lightEntity = null;
}
}
public record struct SharkVisionRenderEntry(
Entity<SpriteComponent, TransformComponent> Ent,
MapId? Map,
Angle EyeRot);
@@ -0,0 +1,113 @@
using Content.Client.Overlays;
using Content.Shared._DV.Overlays.Components;
using Content.Shared._Goobstation.Overlays;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Robust.Client.Graphics;
namespace Content.Client._Goobstation.Overlays;
public sealed class SharkVisionSystem : EquipmentHudSystem<SharkVisionComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private SharkVisionOverlay _sharkOverlay = default!;
private BaseSwitchableOverlay<SharkVisionComponent> _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharkVisionComponent, SwitchableOverlayToggledEvent>(OnToggle);
_sharkOverlay = new SharkVisionOverlay();
_overlay = new BaseSwitchableOverlay<SharkVisionComponent>();
}
protected override void OnRefreshComponentHud(Entity<SharkVisionComponent> ent,
ref RefreshEquipmentHudEvent<SharkVisionComponent> args)
{
if (!ent.Comp.IsEquipment)
base.OnRefreshComponentHud(ent, ref args);
}
protected override void OnRefreshEquipmentHud(Entity<SharkVisionComponent> ent,
ref InventoryRelayedEvent<RefreshEquipmentHudEvent<SharkVisionComponent>> args)
{
if (ent.Comp.IsEquipment)
base.OnRefreshEquipmentHud(ent, ref args);
}
private void OnToggle(Entity<SharkVisionComponent> ent, ref SwitchableOverlayToggledEvent args)
{
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<SharkVisionComponent> args)
{
base.UpdateInternal(args);
SharkVisionComponent? tvComp = null;
var lightRadius = 0f;
foreach (var comp in args.Components)
{
if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime))
continue;
if (tvComp == null)
tvComp = comp;
else if (!tvComp.DrawOverlay && comp.DrawOverlay)
tvComp = comp;
else if (tvComp.DrawOverlay == comp.DrawOverlay && tvComp.PulseTime > 0f && comp.PulseTime <= 0f)
tvComp = comp;
lightRadius = MathF.Max(lightRadius, 1);
}
UpdateSharkOverlay(tvComp, lightRadius);
UpdateOverlay(tvComp);
}
protected override void DeactivateInternal()
{
base.DeactivateInternal();
_sharkOverlay.ResetLight(false);
UpdateOverlay(null);
UpdateSharkOverlay(null, 0f);
}
private void UpdateSharkOverlay(SharkVisionComponent? comp, float lightRadius)
{
_sharkOverlay.LightRadius = lightRadius;
_sharkOverlay.Comp = comp;
switch (comp)
{
case not null when !_overlayMan.HasOverlay<SharkVisionOverlay>():
_overlayMan.AddOverlay(_sharkOverlay);
break;
case null:
_overlayMan.RemoveOverlay(_sharkOverlay);
_sharkOverlay.ResetLight();
break;
}
}
private void UpdateOverlay(SharkVisionComponent? tvComp)
{
_overlay.Comp = tvComp;
switch (tvComp)
{
case { DrawOverlay: true } when !_overlayMan.HasOverlay<BaseSwitchableOverlay<SharkVisionComponent>>():
_overlayMan.AddOverlay(_overlay);
break;
case null or { DrawOverlay: false }:
_overlayMan.RemoveOverlay(_overlay);
break;
}
// Night vision overlay is prioritized
_overlay.IsActive = !_overlayMan.HasOverlay<BaseSwitchableOverlay<NightVisionComponent>>();
}
}
@@ -4,6 +4,9 @@ using Content.Client.CharacterInfo;
using Content.Shared.CCVar;
using Content.Shared._DV.CCVars;
using Content.Shared.Dataset;
using Content.Shared.Chat;
using Content.Shared.Chat.TypingIndicator;
using Robust.Shared.Prototypes;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
@@ -12,6 +15,12 @@ namespace Content.Client.UserInterface.Systems.Chat;
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{
public ChatSelectChannel CurrentChannel = ChatSelectChannel.None;
private static readonly ProtoId<TypingIndicatorPrototype> WhisperID = "whisper";
private static readonly ProtoId<TypingIndicatorPrototype> EmoteID = "emote";
private static readonly ProtoId<TypingIndicatorPrototype> OocID = "ooc";
private static readonly ProtoId<TypingIndicatorPrototype> RadioID = "radio";
/// <summary>
/// Gets Invoked whenever the autofilled highlights have changed.
/// Used to populate the preview in the channel selector window.
@@ -98,4 +107,38 @@ public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSys
if (shouldSave)
_config.SaveToFile();
}
/// <summary>
/// Notifies and sets what type of typing indicator should be put.
/// </summary>
public void NotifySpecificChatTextChange(ChatSelectChannel selectedChannel)
{
var channel = CurrentChannel;
if (CurrentChannel == ChatSelectChannel.None)
channel = selectedChannel;
switch (channel)
{
case ChatSelectChannel.Whisper:
_typingIndicator?.ClientAlternateTyping(WhisperID);
break;
case ChatSelectChannel.Radio:
_typingIndicator?.ClientAlternateTyping(RadioID);
break;
case ChatSelectChannel.Emotes:
_typingIndicator?.ClientAlternateTyping(EmoteID);
break;
case ChatSelectChannel.LOOC:
case ChatSelectChannel.OOC:
_typingIndicator?.ClientAlternateTyping(OocID);
break;
default:
_typingIndicator?.ClientChangedChatText();
break;
}
}
}
@@ -0,0 +1,39 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="350 400"
SetSize="350 400"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Name="ProgressBarContainer" Orientation="Horizontal" Margin="0 5 0 0" HorizontalExpand="True" Visible="False" >
<Label Name="ProgressBarLabel" Text="{Loc 'node-scan-timer'}" Margin="20 0 0 0" />
<ProgressBar
Name="ProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20"
Margin="10 0 20 0"
HorizontalExpand="True" >
</ProgressBar>
</BoxContainer>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" >
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="15 15 0 0" MinHeight="47" HorizontalExpand="True" HorizontalAlignment="Center" />
<BoxContainer Name="ActiveNodesList" Orientation="Vertical" HorizontalExpand="True" Visible="False" MinHeight="72" />
</BoxContainer>
<PanelContainer Name="SystemWarningPanel"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
HorizontalExpand="True"
Margin="10 0 10 5"
Visible="False" >
<BoxContainer Orientation="Vertical" HorizontalExpand="True" >
<RichTextLabel Name="SystemWarningLabel" Margin="5 8 5 0" HorizontalAlignment="Center" />
<RichTextLabel Name="SystemWarningSublabel" Margin="5 3 5 8" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -0,0 +1,250 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client._DV.Xenoarchaeology.Ui;
/// <summary>
/// DeltaV replacement for NodeScannerDisplay, the UI of the artifact node scanner.
/// Displays extra information about the artifact unlocking phase.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DVNodeScannerDisplay : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IGameTiming _timing= default!;
private readonly SharedXenoArtifactSystem _artifact;
private readonly SpriteSystem _spriteSystem;
private Texture? _blipTexture;
private EntityUid _owner;
private float animatedTimeRemaining = 0;
private float animatedTimeRemainingVelocity = 0;
public DVNodeScannerDisplay()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_artifact = _ent.System<SharedXenoArtifactSystem>();
_spriteSystem = _ent.System<SpriteSystem>();
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
_owner = scannerEntityUid;
_blipTexture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
}
/// <inheritdoc />
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// reset
NoActiveNodeDataLabel.Visible = true;
NodeScannerState.Text = Loc.GetString("node-scanner-artifact-non-connected");
ProgressBarContainer.Visible = false;
ActiveNodesList.Visible = false;
SystemWarningPanel.Visible = false;
if (!_ent.TryGetComponent(_owner, out NodeScannerConnectedComponent? connectedScanner))
return;
var attachedArtifactEnt = connectedScanner.AttachedTo;
if (!_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactComponent? artifactComponent))
return;
_ent.TryGetComponent(attachedArtifactEnt, out XenoArtifactUnlockingComponent? unlockingComponent);
// header banner
NodeScannerState.Text =
unlockingComponent != null
? Loc.GetString("node-scanner-artifact-state-unlocking")
: _timing.CurTime < artifactComponent.NextUnlockTime
? Loc.GetString("node-scanner-artifact-state-cooldown")
: Loc.GetString("node-scanner-artifact-state-ready");
// time remaining progress bar
var unlockTimeRemaining =
unlockingComponent != null
? MathF.Max(0f, (float)(unlockingComponent.EndTime - _timing.CurTime).TotalSeconds)
: 0f;
// damped-oscillator-style system for animating sudden jumps in time remaining
animatedTimeRemainingVelocity += 0.001f * (unlockTimeRemaining - animatedTimeRemaining) - 0.1f * animatedTimeRemainingVelocity;
animatedTimeRemaining = MathF.Max(0f, animatedTimeRemaining + animatedTimeRemainingVelocity);
// sigmoid-style function to make sure progress bar never goes over 100%
ProgressBar.Value = 2f / (1f + MathF.Exp(-0.08f * animatedTimeRemaining)) - 1f;
// node data and warning panel
(List<int> triggeredIndexesOrdered, HashSet<int> triggeredIndexesRelated)? unlockingDataToShow = null;
if (unlockingComponent != null)
{
unlockingDataToShow = (
unlockingComponent.TriggeredNodeIndexesOrdered,
unlockingComponent.TriggeredNodeIndexesRelated
);
}
else if (
artifactComponent.LastUnlockingEndTime != null
&& _timing.CurTime - artifactComponent.LastUnlockingEndTime < TimeSpan.FromSeconds(60)
)
{
unlockingDataToShow = (
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered,
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated
);
}
if (
unlockingDataToShow is (List<int> triggeredIndexesOrdered, HashSet<int> triggeredIndexesRelated)
&& triggeredIndexesOrdered.Count > 0
)
{
// show triggered node data instead of 'no data' placeholder
NoActiveNodeDataLabel.Visible = false;
ProgressBarContainer.Visible = true;
ActiveNodesList.Visible = true;
// node list
ActiveNodesList.Children.Clear();
foreach (var triggeredIndex in triggeredIndexesOrdered)
{
var node = _artifact.GetNode((attachedArtifactEnt, artifactComponent), triggeredIndex);
var nodeControl = new Button
{
Margin = new Thickness(15, 5, 15, 0),
MaxHeight = 40,
Disabled = true,
HorizontalExpand = true
};
ActiveNodesList.AddChild(nodeControl);
var mainContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
};
nodeControl.AddChild(mainContainer);
// mimics suitCoordsIndicator in CrewMonitoringWindow
var relatednessIndicator = new TextureRect()
{
Texture = _blipTexture,
TextureScale = new Vector2(0.25f, 0.25f),
Modulate = triggeredIndexesRelated.Contains(triggeredIndex) ? Color.LimeGreen : Color.DarkRed,
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
mainContainer.AddChild(relatednessIndicator);
var triggerTip = Loc.GetString(node.Comp.TriggerTip!);
var triggeredNodeName = (_ent.GetComponentOrNull<NameIdentifierComponent>(node)?.Identifier ?? 0).ToString("D3");
var descriptionLabel = new Label()
{
Text = $"{triggerTip} ({triggeredNodeName})",
HorizontalExpand = true,
ClipText = true,
Margin = new Thickness(8, 0, 0, 0),
};
mainContainer.AddChild(descriptionLabel);
}
// warning panel (mimics that of PowerMonitoringWindow)
(Color background, Color border, string message, string subtext)? warningDataToShow = null;
if (unlockingComponent == null)
{
if (artifactComponent.LastUnlockingSuccessful)
{
warningDataToShow = (
background: Color.Green,
border: Color.DarkGreen,
message: Loc.GetString("node-scan-warning-unlock-complete"),
subtext: Loc.GetString("node-scan-warning-unlock-complete-subtext")
);
}
else if (triggeredIndexesOrdered.Count == triggeredIndexesRelated.Count)
{
warningDataToShow = (
background: Color.Orange,
border: Color.DarkOrange,
message: Loc.GetString("node-scan-warning-partial-complete"),
subtext: Loc.GetString("node-scan-warning-partial-complete-subtext")
);
}
else
{
warningDataToShow = (
background: Color.Red,
border: Color.DarkRed,
message: Loc.GetString("node-scan-warning-failure-complete"),
subtext: Loc.GetString("node-scan-warning-failure-complete-subtext")
);
}
}
else if (triggeredIndexesOrdered.Count != triggeredIndexesRelated.Count)
{
warningDataToShow = (
background: Color.Red,
border: Color.DarkRed,
message: Loc.GetString("node-scan-warning-failure-imminent"),
subtext: Loc.GetString("node-scan-warning-failure-imminent-subtext")
);
}
if (warningDataToShow is (Color background, Color border, string message, string subtext))
{
SystemWarningPanel.Visible = true;
SystemWarningPanel.PanelOverride = new StyleBoxFlat
{
BackgroundColor = background,
BorderColor = border,
BorderThickness = new Thickness(2),
};
// warning panel pulses during unlocking phase
var lit = unlockingComponent != null && _timing.RealTime.TotalSeconds % 1f > 0.5f;
SystemWarningPanel.Modulate = lit ? Color.White : new Color(178, 178, 178);
SystemWarningLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(message));
SystemWarningSublabel.SetMessage(FormattedMessage.FromMarkupOrThrow(subtext));
}
}
}
}
@@ -0,0 +1,36 @@
using Robust.Client.Graphics;
namespace Content.Client._EE.Flight.Components;
[RegisterComponent]
public sealed partial class FlightVisualsComponent : Component
{
/// <summary>
/// The speed of the shader animation.
/// </summary>
[DataField]
public float Speed;
/// <summary>
/// How far it goes in any direction.
/// </summary>
[DataField]
public float Multiplier;
/// <summary>
/// How much the limbs (if there are any) rotate.
/// </summary>
[DataField]
public float Offset;
/// <summary>
/// Are we animating layers or the entire sprite?
/// </summary>
public bool AnimateLayer = false;
public int? TargetLayer;
[DataField]
public string AnimationKey = "default";
[ViewVariables(VVAccess.ReadWrite)]
public ShaderInstance Shader = default!;
}
+65
View File
@@ -0,0 +1,65 @@
using Robust.Client.GameObjects;
using Content.Shared._EE.Flight;
using Content.Shared._EE.Flight.Events;
using Content.Client._EE.Flight.Components;
namespace Content.Client._EE.Flight;
public sealed class FlightSystem : SharedFlightSystem
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<FlightEvent>(OnFlight);
}
private void OnFlight(FlightEvent args)
{
var uid = GetEntity(args.Uid);
if (!_entityManager.TryGetComponent(uid, out SpriteComponent? sprite)
|| !args.IsAnimated
|| !_entityManager.TryGetComponent(uid, out FlightComponent? flight))
return;
int? targetLayer = null;
if (flight.IsLayerAnimated && flight.Layer is not null)
{
targetLayer = GetAnimatedLayer(uid, flight.Layer, sprite);
if (targetLayer == null)
return;
}
if (args.IsFlying && args.IsAnimated && flight.AnimationKey != "default")
{
var comp = new FlightVisualsComponent
{
AnimateLayer = flight.IsLayerAnimated,
AnimationKey = flight.AnimationKey,
Multiplier = flight.ShaderMultiplier,
Offset = flight.ShaderOffset,
Speed = flight.ShaderSpeed,
TargetLayer = targetLayer,
};
AddComp(uid, comp);
}
if (!args.IsFlying)
RemComp<FlightVisualsComponent>(uid);
}
public int? GetAnimatedLayer(EntityUid uid, string targetLayer, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return null;
var index = 0;
foreach (var layer in sprite.AllLayers)
{
// This feels like absolute shitcode, isn't there a better way to check for it?
if (layer.Rsi?.Path.ToString() == targetLayer)
return index;
index++;
}
return null;
}
}
@@ -0,0 +1,62 @@
using Content.Client._EE.Flight.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client._EE.Flight;
/// <summary>
/// Handles offsetting an entity while flying
/// </summary>
public sealed class FlyingVisualizerSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FlightVisualsComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<FlightVisualsComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<FlightVisualsComponent, BeforePostShaderRenderEvent>(OnBeforeShaderPost);
}
private void OnStartup(EntityUid uid, FlightVisualsComponent comp, ComponentStartup args)
{
comp.Shader = _protoMan.Index<ShaderPrototype>(comp.AnimationKey).InstanceUnique();
AddShader(uid, comp.Shader, comp.AnimateLayer, comp.TargetLayer);
SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
}
private void OnShutdown(EntityUid uid, FlightVisualsComponent comp, ComponentShutdown args)
{
AddShader(uid, null, comp.AnimateLayer, comp.TargetLayer);
}
private void AddShader(Entity<SpriteComponent?> entity, ShaderInstance? shader, bool animateLayer, int? layer)
{
if (!Resolve(entity, ref entity.Comp, false))
return;
entity.Comp.PostShader = shader;
if (animateLayer && layer is not null)
entity.Comp.LayerSetShader(layer.Value, shader);
entity.Comp.GetScreenTexture = shader is not null;
entity.Comp.RaiseShaderEvent = shader is not null;
}
/// <summary>
/// This function can be used to modify the shader's values while its running.
/// </summary>
private void OnBeforeShaderPost(EntityUid uid, FlightVisualsComponent comp, ref BeforePostShaderRenderEvent args)
{
SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
}
private void SetValues(FlightVisualsComponent comp, float speed, float offset, float multiplier)
{
comp.Shader.SetParameter("Speed", speed);
comp.Shader.SetParameter("Offset", offset);
comp.Shader.SetParameter("Multiplier", multiplier);
}
}
@@ -0,0 +1,38 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-create-group-title}"
MinSize="300 150">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Group Name input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-group-name-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="GroupNameInput"
PlaceHolder="{Loc nano-chat-group-name-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="CancelButton"
Text="{Loc nano-chat-cancel}"
StyleClasses="OpenRight"
MinSize="80 0" />
<Button Name="CreateButton"
Text="{Loc nano-chat-create}"
StyleClasses="OpenLeft"
MinSize="80 0"
Disabled="True" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class CreateGroupChatPopup : DefaultWindow
{
public event Action<string>? OnGroupCreated;
public CreateGroupChatPopup(int maxFullNameLength)
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
// Button handlers
CancelButton.OnPressed += _ => Close();
CreateButton.OnPressed += _ => CreateGroup();
// Input validation
GroupNameInput.OnTextEntered += _ => CreateGroup();
GroupNameInput.OnTextChanged += args =>
{
if (args.Text.Length > maxFullNameLength)
GroupNameInput.Text = args.Text[..maxFullNameLength];
ValidateInputs();
};
}
private void ValidateInputs()
{
var isValid = !string.IsNullOrWhiteSpace(GroupNameInput.Text);
CreateButton.Disabled = !isValid;
}
private void CreateGroup()
{
if (CreateButton.Disabled)
return;
var name = GroupNameInput.Text.Trim();
OnGroupCreated?.Invoke(name);
Close();
}
public void ClearInputs()
{
GroupNameInput.Text = string.Empty;
ValidateInputs();
}
}
@@ -0,0 +1,39 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-emoji-picker}"
MinSize="300 400">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Search box -->
<LineEdit Name="SearchInput"
PlaceHolder="{Loc nano-chat-emoji-search}"
HorizontalExpand="True"
Margin="0 0 0 4" />
<!-- Emoji grid -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4"
HScroll="0"
VScroll="1">
<GridContainer Name="EmojiGrid"
Columns="6"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Close button -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="EmojiCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using Content.Shared._Funkystation.NanoChat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.ResourceManagement;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class EmojiPickerPopup : DefaultWindow
{
private List<(string name, SpriteSpecifier specifier)> _filteredEmojis;
private SpriteSystem? _spriteSystem;
public event Action<string>? OnEmojiSelected;
public EmojiPickerPopup()
{
RobustXamlLoader.Load(this);
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
ContentsContainer.Margin = new Thickness(3);
EmojiCloseButton.OnPressed += _ => Close();
SearchInput.OnTextChanged += OnSearchChanged;
OnResized += UpdateGridColumns;
PopulateEmojiGrid();
UpdateGridColumns();
}
private void UpdateGridColumns()
{
const int buttonSize = 48;
const int scrollbarWidth = 12;
var scrollContainer = EmojiGrid.Parent as ScrollContainer;
if (scrollContainer == null)
return;
var availableWidth = scrollContainer.PixelSize.X - scrollbarWidth;
if (availableWidth <= 0)
return;
var columns = Math.Max(1, (int) (availableWidth / buttonSize));
if (EmojiGrid.Columns != columns)
{
EmojiGrid.Columns = columns;
}
}
private void OnSearchChanged(LineEdit.LineEditEventArgs args)
{
var search = args.Text.ToLower().Trim();
if (string.IsNullOrEmpty(search))
{
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers.Select(kv => (kv.Key, kv.Value)).ToList();
}
else
{
_filteredEmojis = NanoChatEmojis.EmojiSpecifiers
.Where(kv => kv.Key.Contains(search, StringComparison.OrdinalIgnoreCase))
.Select(kv => (kv.Key, kv.Value))
.ToList();
}
PopulateEmojiGrid();
}
private void PopulateEmojiGrid()
{
EmojiGrid.RemoveAllChildren();
var resCache = IoCManager.Resolve<IResourceCache>();
_spriteSystem ??= IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<SpriteSystem>();
foreach (var (name, specifier) in _filteredEmojis)
{
var button = new Button
{
MinSize = new Vector2(48, 48),
MaxSize = new Vector2(48, 48),
ToolTip = $":{name}:",
};
Texture texture;
switch (specifier)
{
case SpriteSpecifier.Texture texSpecifier:
if (!resCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
continue;
texture = textureResource.Texture;
break;
case SpriteSpecifier.Rsi rsiSpecifier:
var state = _spriteSystem.GetState(rsiSpecifier);
texture = state.Frame0;
break;
default:
continue;
}
var textureRect = new TextureRect
{
Texture = texture,
Stretch = TextureRect.StretchMode.KeepAspect,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
};
button.AddChild(textureRect);
button.OnPressed += _ =>
{
OnEmojiSelected?.Invoke($":{name}:");
Close();
};
EmojiGrid.AddChild(button);
}
// Recalculate columns after populating
UpdateGridColumns();
}
public static SpriteSpecifier? GetEmojiSpecifier(string emojiName)
{
return NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out var specifier) ? specifier : null;
}
public static Dictionary<string, SpriteSpecifier> GetEmojis() => new Dictionary<string, SpriteSpecifier>(NanoChatEmojis.EmojiSpecifiers);
}
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.RichText;
using Content.Shared._Funkystation.NanoChat;
using Robust.Shared.Utility;
using Content.Client._DV.NanoChat.UI; // DeltaV - Fix emoji alignment
using Robust.Shared.Prototypes; // DeltaV - Emoji id for debugging
namespace Content.Client._Funkystation.NanoChat;
/// <summary>
/// Markup tag for rendering emoji images inline in RichTextLabel.
/// </summary>
public sealed class EmojiTag : IMarkupTagHandler
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private SpriteSystem? _spriteSystem;
public EmojiTag()
{
IoCManager.InjectDependencies(this);
}
public string Name => "emoji";
// Begin DeltaV - Render invalid emoji as text
private static ProtoId<NanoChatEmojiPrototype>? GetEmoji(MarkupNode node, out string text, out SpriteSpecifier? emoji)
{
if (!node.Value.TryGetString(out var emojiName))
{
// If we don't have a value, then render nothing
emoji = null;
text = string.Empty;
return null;
}
else if (NanoChatEmojis.EmojiSpecifiers.TryGetValue(emojiName, out emoji))
{
// If we have a valid emoji, render the emoji, but no text
text = string.Empty;
return emojiName;
}
else
{
// If we have an invalid emoji, render only text
text = $":{emojiName}:";
return emojiName;
}
}
string IMarkupTagHandler.TextBefore(MarkupNode node)
{
GetEmoji(node, out var text, out _);
return text;
}
// End DeltaV - Render invalid emoji as text
public bool TryCreateControl(MarkupNode node, out Control control)
{
control = default!;
var emojiId = GetEmoji(node, out _, out var maybeSpecifier);
if (maybeSpecifier is not { } specifier)
{
return false;
}
_spriteSystem ??= _entitySystemManager.GetEntitySystem<SpriteSystem>();
Texture texture;
switch (specifier)
{
case SpriteSpecifier.Texture texSpecifier:
if (!_resourceCache.TryGetResource<TextureResource>(texSpecifier.TexturePath, out var textureResource))
{
return false;
}
texture = textureResource.Texture;
break;
case SpriteSpecifier.Rsi rsiSpecifier:
var state = _spriteSystem.GetState(rsiSpecifier);
texture = state.Frame0;
break;
default:
return false;
}
// Begin DeltaV - Fix emoji alignment, replace TextureRect with custom EmojiControl
control = new EmojiControl
{
Texture = texture,
SetWidth = 32f,
SetHeight = 32f,
Name = $"Emoji[{emojiId}]",
// Evilly hard-coded Margin and Offset, don't ask me where they come from please
Margin = new(5f, 0f, 2f, 0),
Offset = new(5f, -5f),
};
// End DeltaV
return true;
}
}
@@ -0,0 +1,32 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc nano-chat-group-members-title}"
MinSize="400 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Member list -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4">
<BoxContainer Name="MembersList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="MembersCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -0,0 +1,201 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Client.Resources;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Robust.Client.ResourceManagement;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class GroupMembersPopup : DefaultWindow
{
private List<NanoChatRecipient> _allContacts = new();
private HashSet<uint> _members = new();
private HashSet<uint> _admins = new();
private uint _ownNumber;
private uint? _creatorId;
public event Action<uint>? OnKick;
public event Action<uint>? OnAdmin;
public event Action<uint>? OnDeadmin;
public GroupMembersPopup()
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
MembersCloseButton.OnPressed += _ => Close();
}
public void SetMembers(List<NanoChatRecipient> allContacts, HashSet<uint> members, uint ownNumber, uint? creatorId, HashSet<uint>? admins = null)
{
_allContacts = allContacts;
_members = members;
_ownNumber = ownNumber;
_creatorId = creatorId;
_admins = admins ?? new HashSet<uint>();
UpdateMembersList();
}
private void UpdateMembersList()
{
MembersList.RemoveAllChildren();
if (_members.Count == 0)
{
MembersList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-members") });
return;
}
var isCreator = _creatorId == _ownNumber;
var isAdmin = _admins.Contains(_ownNumber);
var canManage = isCreator || isAdmin;
var memberContacts = _allContacts
.Where(c => _members.Contains(c.Number))
.OrderBy(c => c.Name)
.ToList();
for (var idx = 0; idx < memberContacts.Count; idx++)
{
var contact = memberContacts[idx];
var isEvenRow = idx % 2 == 0;
var isMemberAdmin = _admins.Contains(contact.Number);
var isMemberCreator = contact.Number == _creatorId;
var isSelf = contact.Number == _ownNumber;
var canKick = false;
if (isSelf) // can always leave yourself.
{
canKick = true;
}
else if (!isSelf && canManage && !isMemberCreator)
{
if (isCreator)
canKick = true;
else if (isAdmin && !isMemberAdmin)
canKick = true;
}
var canAdmin = canManage && !isSelf && !isMemberCreator && !isMemberAdmin;
var canDeadmin = canManage && !isSelf && !isMemberCreator && isMemberAdmin;
if (!isCreator && canDeadmin)
canDeadmin = false;
var contactControl = new MemberContainer(contact, isEvenRow, isMemberCreator, isMemberAdmin, isSelf, canKick, canAdmin, canDeadmin, OnKick, OnAdmin, OnDeadmin);
MembersList.AddChild(contactControl);
}
}
private sealed class MemberContainer : PanelContainer
{
public MemberContainer(NanoChatRecipient contact, bool isEvenRow, bool isCreator, bool isAdmin, bool isSelf, bool canKick, bool canMakeAdmin, bool canDeadmin, Action<uint>? onKick, Action<uint>? onAdmin, Action<uint>? onDeadmin)
{
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
var resCache = IoCManager.Resolve<IResourceCache>();
// Show name with job title if available
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label
{
Text = displayName + (isCreator ? $" {Loc.GetString("nano-chat-owner-suffix")}" : isAdmin ? $" {Loc.GetString("nano-chat-admin-suffix")}" : ""),
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
var numberLabel = new Label
{
Text = $"#{contact.Number:D4}",
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, (canKick || canMakeAdmin || canDeadmin) ? 108 : 4, 0),
};
AddChild(nameLabel);
AddChild(numberLabel);
// Admin/Deadmin button
if (canMakeAdmin)
{
var adminButton = new Button
{
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-admin"),
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
};
adminButton.AddStyleClass("OpenBoth");
adminButton.AddChild(new TextureRect
{
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/admin.png"),
Stretch = TextureRect.StretchMode.KeepAspectCentered,
MinSize = new Vector2(28, 28),
MaxSize = new Vector2(28, 28),
});
adminButton.OnPressed += _ => onAdmin?.Invoke(contact.Number);
AddChild(adminButton);
}
else if (canDeadmin)
{
var deadminButton = new Button
{
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-deadmin"),
Margin = new Thickness(0, 0, canKick ? 72 : 36, 0),
};
deadminButton.AddStyleClass("OpenBoth");
deadminButton.AddChild(new TextureRect
{
Texture = resCache.GetTexture("/Textures/_Funkystation/Interface/NanoChat/deadmin.png"),
Stretch = TextureRect.StretchMode.KeepAspectCentered,
MinSize = new Vector2(28, 28),
MaxSize = new Vector2(28, 28),
});
deadminButton.OnPressed += _ => onDeadmin?.Invoke(contact.Number);
AddChild(deadminButton);
}
// Kick/Leave button
if (canKick)
{
var kickButton = new Button
{
Text = isSelf ? Loc.GetString("nano-chat-leave") : "X",
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(isSelf ? 64 : 32, 32),
MaxSize = new Vector2(isSelf ? 64 : 32, 32),
ToolTip = isSelf ? Loc.GetString("nano-chat-leave") : Loc.GetString("nano-chat-kick"),
Margin = new Thickness(0, 0, 36, 0),
};
kickButton.AddStyleClass("OpenBoth");
kickButton.OnPressed += _ => onKick?.Invoke(contact.Number);
AddChild(kickButton);
}
}
}
}
@@ -0,0 +1,43 @@
<!--
SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
-->
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc nano-chat-invite-to-group-title}"
MinSize="400 300">
<PanelContainer StyleClasses="AngleRect">
<BoxContainer Orientation="Vertical" Margin="4">
<!-- Search input -->
<BoxContainer Orientation="Vertical" Margin="0 4">
<Label Text="{Loc nano-chat-search-label}"
StyleClasses="LabelHeading" />
<PanelContainer StyleClasses="ButtonSquare">
<LineEdit Name="SearchInput"
PlaceHolder="{Loc nano-chat-search-placeholder}" />
</PanelContainer>
</BoxContainer>
<!-- Contact list -->
<ScrollContainer VerticalExpand="True"
HorizontalExpand="True"
Margin="0 4">
<BoxContainer Name="ContactsList"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
<!-- Action buttons -->
<BoxContainer Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0">
<Button Name="InviteCloseButton"
Text="{Loc nano-chat-close}"
MinSize="80 0" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</DefaultWindow>
@@ -0,0 +1,191 @@
// SPDX-FileCopyrightText: 2025 Evaisa <evagiacosa1@gmail.com>
// SPDX-FileCopyrightText: 2025 EvaisaDev <mail@evaisa.dev>
//
// SPDX-License-Identifier: AGPL-3.0-or-later AND MIT
using System.Linq;
using System.Numerics;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client._Funkystation.NanoChat;
[GenerateTypedNameReferences]
public sealed partial class InviteToGroupPopup : DefaultWindow
{
private List<NanoChatRecipient> _allContacts = new();
private HashSet<uint> _existingMembers = new();
private uint _ownNumber;
public event Action<uint>? OnInvite;
public InviteToGroupPopup()
{
RobustXamlLoader.Load(this);
ContentsContainer.Margin = new Thickness(3);
InviteCloseButton.OnPressed += _ => Close();
SearchInput.OnTextChanged += _ => UpdateContactList();
}
public void SetContacts(List<NanoChatRecipient> contacts, HashSet<uint> existingMembers, uint ownNumber)
{
_allContacts = contacts;
_existingMembers = existingMembers;
_ownNumber = ownNumber;
UpdateContactList();
}
private void UpdateContactList()
{
ContactsList.RemoveAllChildren();
if (_allContacts.Count == 0)
{
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-look-up-no-server") });
return;
}
var searchText = SearchInput.Text.ToLower();
var filteredContacts = _allContacts
.Where(c => c.Number != _ownNumber &&
!_existingMembers.Contains(c.Number) &&
(string.IsNullOrWhiteSpace(searchText) ||
c.Name.ToLower().Contains(searchText) ||
c.Number.ToString().Contains(searchText) ||
(!string.IsNullOrWhiteSpace(c.JobTitle) && c.JobTitle.ToLower().Contains(searchText))))
.ToList();
if (filteredContacts.Count == 0)
{
ContactsList.AddChild(new Label { Text = Loc.GetString("nano-chat-no-results") });
return;
}
// Group contacts by department like crew manifest
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var departmentGroups = new Dictionary<DepartmentPrototype, List<NanoChatRecipient>>();
var noDepartmentContacts = new List<NanoChatRecipient>();
foreach (var contact in filteredContacts)
{
if (!string.IsNullOrWhiteSpace(contact.Department) &&
prototypeManager.TryIndex<DepartmentPrototype>(contact.Department, out var dept))
{
departmentGroups.GetOrNew(dept).Add(contact);
}
else
{
noDepartmentContacts.Add(contact);
}
}
// Sort departments by weight
var sortedDepartments = departmentGroups.Keys.ToList();
sortedDepartments.Sort(DepartmentUIComparer.Instance);
var rowIndex = 0;
foreach (var department in sortedDepartments)
{
var departmentContacts = departmentGroups[department];
departmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
// Add department header
ContactsList.AddChild(new Label
{
Text = Loc.GetString(department.Name),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, rowIndex == 0 ? 0 : 8, 0, 2)
});
foreach (var contact in departmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
// Add contacts without departments at the end
if (noDepartmentContacts.Count > 0)
{
noDepartmentContacts.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name));
if (sortedDepartments.Count > 0)
{
ContactsList.AddChild(new Label
{
Text = Loc.GetString("nano-chat-other-department"),
StyleClasses = { "LabelBig" },
Margin = new Thickness(0, 8, 0, 2)
});
}
foreach (var contact in noDepartmentContacts)
{
var isEvenRow = rowIndex % 2 == 0;
var contactControl = new ContactContainer(contact, isEvenRow, OnInvite);
ContactsList.AddChild(contactControl);
rowIndex++;
}
}
}
public void ClearSearch()
{
SearchInput.Text = string.Empty;
}
private sealed class ContactContainer : PanelContainer
{
public ContactContainer(NanoChatRecipient contact, bool isEvenRow, Action<uint>? onInvite)
{
HorizontalExpand = true;
StyleClasses.Add(isEvenRow ? "PanelBackgroundBaseDark" : "PanelBackgroundLight");
// Show name with job title if available
var displayName = contact.Name;
if (!string.IsNullOrWhiteSpace(contact.JobTitle))
displayName = $"{contact.Name} ({contact.JobTitle})";
var nameLabel = new Label
{
Text = displayName,
HorizontalAlignment = HAlignment.Left,
HorizontalExpand = true
};
var numberLabel = new Label
{
Text = $"#{contact.Number:D4}",
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(0, 0, 36, 0),
};
var inviteButton = new Button
{
Text = "+",
HorizontalAlignment = HAlignment.Right,
MinSize = new Vector2(32, 32),
MaxSize = new Vector2(32, 32),
ToolTip = Loc.GetString("nano-chat-invite"),
};
inviteButton.AddStyleClass("OpenBoth");
inviteButton.OnPressed += _ => onInvite?.Invoke(contact.Number);
AddChild(nameLabel);
AddChild(numberLabel);
AddChild(inviteButton);
}
}
}
+1 -1
View File
@@ -242,7 +242,7 @@ namespace Content.IntegrationTests.Tests
"MapGrid",
"StationEvent",
"TimedDespawn",
"TimedDespawnDetailed", // DeltaV
// makes an announcement on mapInit.
"AnnounceOnSpawn",
};
@@ -76,6 +76,7 @@ namespace Content.IntegrationTests.Tests
"Hammurabi", // DeltaV
"Lighthouse", // DeltaV
"Micro", // DeltaV
"Ovni", //DeltaV
"Pebble", // DeltaV
"Shoukou", // DeltaV
"Submarine", //DeltaV
@@ -9,6 +9,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Content.Shared.Roles;
using System.Diagnostics.CodeAnalysis;
using Content.Shared._DV.Access.Components; // DeltaV
using Content.Shared._DV.NanoChat; // DeltaV
using Content.Server.Clothing.Systems;
using Content.Server.Implants;
@@ -101,11 +102,16 @@ namespace Content.Server.Access.Systems
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))
return;
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
// Begin DeltaV Additions
// Prevent certain types of IDs from being copied (i.e. Borg ID chips)
if (TryComp<PreventAgentIdComponent>(args.Target, out var preventable))
{
if (preventable.PopupText is { } popupText)
_popupSystem.PopupEntity(Loc.GetString(popupText), args.Target.Value, args.User);
return;
}
// Begin DeltaV Additions - Copy NanoChat data if available
// Copy NanoChat data if available
if (TryComp<NanoChatCardComponent>(args.Target, out var targetNanoChat) &&
TryComp<NanoChatCardComponent>(uid, out var agentNanoChat))
{
@@ -132,6 +138,10 @@ namespace Content.Server.Access.Systems
}
}
// End DeltaV Additions
var beforeLength = access.Tags.Count;
access.Tags.UnionWith(targetAccess.Tags);
var addedLength = access.Tags.Count - beforeLength;
_popupSystem.PopupEntity(Loc.GetString("agent-id-new", ("number", addedLength), ("card", args.Target)), args.Target.Value, args.User);
if (addedLength > 0)
Dirty(uid, access);
@@ -1,6 +1,7 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Guidebook;
namespace Content.Server.Atmos.Components;
@@ -82,6 +83,7 @@ public sealed partial class DeltaPressureComponent : Component
/// The minimum difference in pressure between any side required for the entity to start taking damage.
/// </summary>
[DataField]
[GuidebookData]
public float MinPressureDelta = 7500;
/// <summary>
@@ -37,4 +37,26 @@ public sealed partial class DragonRiftComponent : SharedDragonRiftComponent
[ViewVariables(VVAccess.ReadWrite), DataField("spawn", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnPrototype = "MobCarpDragon";
//Begin DeltaV - Elite spawns on dragon rifts
[DataField]
public bool SpawnElites = true;
[DataField("spawnElite")]
public EntProtoId? SpawnElitePrototype = "MobSharkminnowDragon";
/// <summary>
/// Every N-th spawn is the elite where N is Elite frequency
/// </summary>
[DataField]
public int SpawnEliteFrequency = 5;
/// <summary>
/// Accumulation of elite spawns
/// Starts at the same value as Frequency to guarantee the first spawn to be elite
/// </summary>
[DataField]
public int SpawnEliteAccumulator = 5;
//End DeltaV - Elite spawns on dragon rifts
}
+10 -1
View File
@@ -90,7 +90,16 @@ public sealed class DragonRiftSystem : EntitySystem
if (comp.SpawnAccumulator > comp.SpawnCooldown)
{
comp.SpawnAccumulator -= comp.SpawnCooldown;
var ent = Spawn(comp.SpawnPrototype, xform.Coordinates);
//Begin DeltaV - Elite spawns on dragon rifts
comp.SpawnEliteAccumulator += 1;
var entSpawnPrototype = comp.SpawnPrototype;
if (comp.SpawnElites && comp.SpawnEliteAccumulator >= comp.SpawnEliteFrequency)
{
comp.SpawnEliteAccumulator -= comp.SpawnEliteFrequency;
entSpawnPrototype = comp.SpawnElitePrototype;
}
//End DeltaV - Elite spawns on dragon rifts
var ent = Spawn(entSpawnPrototype, xform.Coordinates); //Delta-v change: comp.SpawnPrototype -> entSpawnPrototype
// Update their look to match the leader.
if (TryComp<RandomSpriteComponent>(comp.Dragon, out var randomSprite))
@@ -8,6 +8,7 @@ using Content.Server.Stack;
using Content.Shared.ActionBlocker;
using Content.Shared.Construction;
using Content.Shared.Database;
using Content.Shared._NF.Storage.Components; // Frontier
using JetBrains.Annotations;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
@@ -81,6 +82,14 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (volume <= 0 || !TryChangeMaterialAmount(uid, msg.Material, -volume))
return;
// Frontier
// If we made it this far, turn off the magnet before spawning materials
if (TryComp<MaterialStorageMagnetPickupComponent>(uid, out var magnet))
{
magnet.MagnetEnabled = false;
}
// end Frontier
var mats = SpawnMultipleFromMaterial(volume, material, Transform(uid).Coordinates, out _);
foreach (var mat in mats.Where(mat => !TerminatingOrDeleted(mat)))
{
@@ -102,14 +111,12 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (!base.TryInsertMaterialEntity(user, toInsert, receiver, storage, material, composition))
return false;
_audio.PlayPvs(storage.InsertingSound, receiver);
_popup.PopupEntity(Loc.GetString("machine-insert-item",
("user", user),
("machine", receiver),
("item", toInsert)),
receiver);
if (user != receiver) // Goobstation - for automation to not spam popups
_popup.PopupEntity(Loc.GetString("machine-insert-item", ("user", user), ("machine", receiver),
("item", toInsert)), receiver);
_popup.PopupEntity(Loc.GetString("machine-insert-item",
("user", user),
("machine", receiver),
("item", toInsert)),
receiver);
QueueDel(toInsert);
// Logging
@@ -50,7 +50,7 @@ public sealed partial class WarDeclaratorComponent : Component
/// Time allowed for declaration of war
/// </summary>
[DataField]
public float WarDeclarationDelay = 6.0f;
public float WarDeclarationDelay = 8.0f; // DeltaV - was 6.0
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan DisableAt;
@@ -51,9 +51,9 @@ public sealed partial class RadiationSystem : EntitySystem
_accumulator = 0f;
}
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time)
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time, EntityUid? origin = null)
{
var msg = new OnIrradiatedEvent(time, radsPerSecond, uid);
var msg = new OnIrradiatedEvent(time, radsPerSecond, origin);
RaiseLocalEvent(uid, msg);
}
@@ -55,6 +55,13 @@ public sealed partial class XenoArtifactSystem
var trigger = PrototypeManager.Index<XenoArchTriggerPrototype>(triggerId);
if (_entityWhitelist.IsWhitelistFail(trigger.Whitelist, ent))
continue;
// DeltaV - start of TriggerBlacklist
if (
(trigger.TriggerBlacklist != null && triggerPool.Any(otherTrigger => trigger.TriggerBlacklist.Contains(otherTrigger.ID)))
|| triggerPool.Any(otherTrigger => otherTrigger.TriggerBlacklist != null && otherTrigger.TriggerBlacklist.Contains(triggerId))
)
continue;
// DeltaV - end of TriggerBlacklist
triggerPool.Add(trigger);
}
@@ -1,10 +1,16 @@
using System; // DeltaV
using Content.Server.Research.Systems;
using Content.Server.Xenoarchaeology.Artifact;
using Content.Shared.Popups;
using Content.Shared.Psionics.Glimmer;// DeltaV
using Content.Shared._DV.Xenoarchaeology.BUI;// DeltaV
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Server.GameObjects; // DeltaV
using Robust.Shared.Audio.Systems;
namespace Content.Server.Xenoarchaeology.Equipment;
/// <inheritdoc />
@@ -14,6 +20,8 @@ public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly XenoArtifactSystem _xenoArtifact = default!;
[Dependency] private readonly GlimmerSystem _glimmerSystem = default!; // DeltaV
[Dependency] private readonly ArtifactAnalyzerSystem _analyzerSystem = default!; //DeltaV
/// <inheritdoc/>
public override void Initialize()
@@ -31,21 +39,52 @@ public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
if (!_research.TryGetClientServer(ent, out var server, out var serverComponent))
return;
// Begin DeltaV - Variables for extraction totals
var sumResearch = 0;
var sumGlimmer = 0;
if (!_analyzerSystem.TryGetAnalyzer(ent, out var analyzer))
return;
// End DeltaV
foreach (var node in _xenoArtifact.GetAllNodes(artifact.Value))
{
var research = _xenoArtifact.GetResearchValue(node);
_xenoArtifact.SetConsumedResearchValue(node, node.Comp.ConsumedResearchValue + research);
// Begin DeltaV - Only run if we have an artifact ready for extraction
if (analyzer != null)
{
sumGlimmer += (int)(research / (float)analyzer.Value.Comp.ExtractRatio);
research = (int)(research * GetGlimmerMultiplier(analyzer.Value.Comp));
}
// End DeltaV
sumResearch += research;
}
UpdateClientUI(ent, analyzer!.Value); // DeltaV
// 4-16-25: It's a sad day when a scientist makes negative 5k research
if (sumResearch <= 0)
return;
_glimmerSystem.Glimmer += sumGlimmer; // DeltaV - Add glimmer based on extracted points.
_research.ModifyServerPoints(server.Value, sumResearch, serverComponent);
_audio.PlayPvs(ent.Comp.ExtractSound, artifact.Value);
_popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"), artifact.Value, PopupType.Large);
}
// DeltaV
private void UpdateClientUI(EntityUid console, ArtifactAnalyzerComponent analyzer)
{
var uiSystem = EntityManager.System<UserInterfaceSystem>();
uiSystem.SetUiState(console, ArtifactAnalyzerUiKey.Key,
new AnalysisConsoleBoundUserInterfaceState(GetGlimmerMultiplier(analyzer), (float)analyzer.ExtractRatio));
}
// DeltaV
private float GetGlimmerMultiplier(ArtifactAnalyzerComponent comp)
{
return 1 + (MathF.Pow(_glimmerSystem.Glimmer / 1000f, 2f) * comp.PointGlimmerMultiplier);
}
}
@@ -0,0 +1,105 @@
using Content.Shared._DEN.TimedDespawn;
using Content.Shared.Examine;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
namespace Content.Server._DEN.TimedDespawn;
/// <summary>
/// This handles the <see cref="TimedDespawnDetailedComponent"/>
/// </summary>
public sealed class TimedDespawnDetailedSystem : EntitySystem
{
[Dependency] private readonly AudioSystem _audioSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
private readonly HashSet<EntityUid> _timedDespawns = new();
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<TimedDespawnDetailedComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TimedDespawnDetailedComponent, ExaminedEvent>(OnExamine);
}
public override void Update(float frameTime)
{
foreach (var entity in _timedDespawns)
{
if (!Exists(entity) || !TryComp<TimedDespawnDetailedComponent>(entity, out var timedDespawn))
continue;
TryDelete((entity, timedDespawn));
}
}
private void OnExamine(Entity<TimedDespawnDetailedComponent> ent, ref ExaminedEvent args)
{
var timeLeft = GetTimeRemaining(ent);
if (timeLeft == null || ent.Comp.ExamineLocId == null)
return;
var stringTime = double.Round(timeLeft.Value.TotalSeconds, 1);
var examineText = Loc.GetString(ent.Comp.ExamineLocId, ("remaining", stringTime));
args.PushMarkup(examineText, 1);
}
public void StartTimer(Entity<TimedDespawnDetailedComponent> ent)
{
ent.Comp.StartTime = _gameTiming.CurTime;
_timedDespawns.Add(ent);
if (ent.Comp.StartSound != null)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
_audioSystem.PlayPredicted(ent.Comp.StartSound, entCoords, null, ent.Comp.StartSoundParams);
}
}
public void StopTimer(Entity<TimedDespawnDetailedComponent> ent)
{
ent.Comp.StartTime = TimeSpan.Zero;
_timedDespawns.Remove(ent);
}
public TimeSpan? GetTimeRemaining(Entity<TimedDespawnDetailedComponent> ent)
{
if (!_timedDespawns.Contains(ent))
return null;
var despawnAfterAsSpan = TimeSpan.FromSeconds(ent.Comp.Lifetime);
var timeLeft = (ent.Comp.StartTime + despawnAfterAsSpan) - _gameTiming.CurTime;
return timeLeft;
}
public void TryDelete(Entity<TimedDespawnDetailedComponent> ent)
{
var remaining = GetTimeRemaining(ent);
if (remaining == null || remaining.Value.TotalSeconds > 0)
return;
if (ent.Comp.EndSound != null)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
_audioSystem.PlayPredicted(ent.Comp.EndSound, entCoords, null, ent.Comp.EndSoundParams);
}
StopTimer(ent);
EntityManager.QueueDeleteEntity(ent);
}
private void OnMapInit(Entity<TimedDespawnDetailedComponent> ent, ref MapInitEvent args)
{
var entCoords = _transformSystem.GetMoverCoordinates(ent);
if (ent.Comp.StartSound != null)
_audioSystem.PlayPredicted(ent.Comp.StartSound, entCoords, null, ent.Comp.StartSoundParams);
StartTimer(ent);
}
}
@@ -3,6 +3,7 @@ using Content.Server.Chat.Systems;
using Content.Server.Cloning;
using Content.Server.DoAfter;
using Content.Server.Mind;
using Content.Server.Psionics;
using Content.Server.Station.Systems;
using Content.Shared._DV.Abilities.Psionics;
using Content.Shared.Abilities.Psionics;
@@ -71,7 +72,8 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
component.NextSwap = _timing.CurTime + TimeSpan.FromSeconds(_random.Next(300, 1200));
if (HasComp<FracturedFormBodyComponent>(entity)) return; // Don't generate a new body if we're already part of a network.
AddComp<FracturedFormBodyComponent>(entity);
var bodyComp = AddComp<FracturedFormBodyComponent>(entity);
bodyComp.ControllingForm = entity.Owner;
component.Bodies.Add(entity);
GenerateForm(entity);
}
@@ -113,9 +115,9 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
}
var bodies = EntityQueryEnumerator<FracturedFormBodyComponent>();
while (bodies.MoveNext(out var uid, out var _))
while (bodies.MoveNext(out var uid, out var comp))
{
if (!HasComp<SleepingComponent>(uid) && !HasComp<FracturedFormPowerComponent>(uid))
if (!HasComp<SleepingComponent>(uid) && !_mind.GetMind(uid).HasValue)
{
_sleeping.TrySleeping(uid);
}
@@ -125,6 +127,10 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
// Ensure the body isn't forcesleep'd by the SSD system.
ssd.IsSSD = false;
}
// Cleanup the component from any out-of-network bodies. (See Mindbreaking)
if (!comp.ControllingForm.IsValid() || Deleted(comp.ControllingForm) || !HasComp<FracturedFormPowerComponent>(comp.ControllingForm))
RemCompDeferred<FracturedFormBodyComponent>(uid);
}
}
@@ -149,8 +155,18 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
var speciesPrototypes = _prototype.EnumeratePrototypes<SpeciesPrototype>();
foreach (var proto in speciesPrototypes)
{
if (proto.RoundStart)
validSpecies.Add(proto.ID);
var speciesEntityPrototype = _prototype.Index<EntityPrototype>(proto.Prototype);
if (proto.RoundStart && speciesEntityPrototype.TryGetComponent<PotentialPsionicComponent>(out var canBePsionic, Factory))
{
var chance = canBePsionic.Chance;
if (speciesEntityPrototype.TryGetComponent<PsionicBonusChanceComponent>(out var bonusChance, Factory))
chance = (chance * bonusChance.Multiplier) + bonusChance.FlatBonus;
if (chance > 0)
validSpecies.Add(proto.ID);
}
}
var species = _random.Pick(validSpecies);
var character = HumanoidCharacterProfile.RandomWithSpecies(species);
@@ -164,8 +180,9 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
if (newBody is { } body && !Deleted(body))
{
AddComp<FracturedFormBodyComponent>(body);
var bodyComp = AddComp<FracturedFormBodyComponent>(body);
original.Comp.Bodies.Add(body);
bodyComp.ControllingForm = original.Owner;
return body;
}
@@ -226,6 +243,14 @@ public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
var duplicate = AddComp<FracturedFormPowerComponent>(targetBody);
duplicate.Bodies = entity.Comp.Bodies;
// Pass the controlling form to the new component's bodies.
foreach (var body in duplicate.Bodies)
{
if (TryComp<FracturedFormBodyComponent>(body, out var bodyComp))
{
bodyComp.ControllingForm = targetBody;
}
}
RemCompDeferred(entity, entity.Comp);
return true;
}
@@ -11,6 +11,7 @@ public sealed partial class PsychokineticScreamPowerSystem : EntitySystem
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly ShatterLightsAbilitySystem _shatterLights = default!;
[Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
public override void Initialize()
{
@@ -54,6 +55,8 @@ public sealed partial class PsychokineticScreamPowerSystem : EntitySystem
SpawnAttachedTo(entity.Comp.Effect, entity.Owner.ToCoordinates());
_psionics.LogPowerUsed(entity.Owner, "psychokinetic scream", 3, 6);
args.Handled = true;
}
@@ -46,11 +46,17 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoChatCartridgeComponent, CartridgeMessageEvent>(OnMessage);
SubscribeLocalEvent<NanoChatCartridgeComponent, MapInitEvent>(OnMapInit);
Subs.CVar(_cfg, CCVars.MaxNameLength, x => _maxNameLength = x, true);
Subs.CVar(_cfg, CCVars.MaxIdJobLength, x => _maxIdJobLength = x, true);
}
private void OnMapInit(Entity<NanoChatCartridgeComponent> ent, ref MapInitEvent args)
{
UpdateUIForAllCards();
}
private void UpdateClosed(Entity<NanoChatCartridgeComponent> ent)
{
if (!TryComp<CartridgeComponent>(ent, out var cartridge) ||
@@ -124,7 +130,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
HandleCloseChat(card);
break;
case NanoChatUiMessageType.ToggleMute:
HandleToggleMute(card);
HandleToggleMute(card, msg);
break;
case NanoChatUiMessageType.ToggleMuteChat:
HandleToggleMuteChat(card, msg);
@@ -136,7 +142,22 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
HandleSendMessage(ent, card, msg);
break;
case NanoChatUiMessageType.ToggleListNumber:
HandleToggleListNumber(card);
HandleToggleListNumber(card, msg);
break;
case NanoChatUiMessageType.CreateGroupChat:
HandleCreateGroupChat(card, msg);
break;
case NanoChatUiMessageType.InviteToGroup:
HandleInviteToGroup(card, msg);
break;
case NanoChatUiMessageType.KickFromGroup:
HandleKickFromGroup(card, msg);
break;
case NanoChatUiMessageType.AdminUser:
HandleAdminUser(card, msg);
break;
case NanoChatUiMessageType.DeadminUser:
HandleDeadminUser(card, msg);
break;
}
@@ -190,16 +211,24 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
}
// Add new recipient
var recipient = new NanoChatRecipient(msg.RecipientNumber.Value,
var recipient = new NanoChatRecipient(
msg.RecipientNumber.Value,
name,
jobTitle);
jobTitle,
null,
false,
false,
null,
null,
null
);
// Initialize or update recipient
_nanoChat.SetRecipient((card, card.Comp), msg.RecipientNumber.Value, recipient);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor):user} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
$"{ToPrettyString(msg.Actor)} created new NanoChat conversation with #{msg.RecipientNumber:D4} ({name})");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
@@ -277,15 +306,65 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (msg.RecipientNumber == null || card.Comp.Number == null)
return;
var chatNumber = msg.RecipientNumber.Value;
var recipient = _nanoChat.GetRecipient((card, card.Comp), chatNumber);
if (recipient is { IsGroup: true } groupChat)
{
var members = groupChat.Members ?? new HashSet<uint>();
if (members.Remove(card.Comp.Number.Value))
{
var admins = groupChat.Admins ?? new HashSet<uint>();
admins.Remove(card.Comp.Number.Value);
// If the creator is leaving, transfer ownership
uint? newCreatorId = groupChat.CreatorId;
var isCreatorLeaving = card.Comp.Number.Value == groupChat.CreatorId;
if (isCreatorLeaving)
{
if (admins.Count > 0)
{
newCreatorId = admins.First();
}
else if (members.Count > 0)
{
newCreatorId = members.First();
}
else
{
newCreatorId = null;
}
}
// Update the group for all remaining members
var updatedRecipient = newCreatorId != null
? groupChat with { Members = members, Admins = admins, CreatorId = newCreatorId }
: groupChat with { Members = members, Admins = admins };
foreach (var memberNumber in members)
{
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var memberCardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
_nanoChat.SetRecipient((memberCardUid, memberCard), chatNumber, updatedRecipient);
UpdateUIForCard(memberCardUid);
}
}
}
}
}
// Delete chat but keep the messages
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), msg.RecipientNumber.Value, true);
var deleted = _nanoChat.TryDeleteChat((card, card.Comp), chatNumber, true);
if (!deleted)
return;
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor):user} deleted NanoChat conversation with #{msg.RecipientNumber:D4}");
$"{ToPrettyString(msg.Actor)} deleted NanoChat conversation with #{chatNumber:D4}");
UpdateUIForCard(card);
}
@@ -293,9 +372,15 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
/// <summary>
/// Handles toggling notification mute state.
/// </summary>
private void HandleToggleMute(Entity<NanoChatCardComponent> card)
private void HandleToggleMute(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
_nanoChat.SetNotificationsMuted((card, card.Comp), !_nanoChat.GetNotificationsMuted((card, card.Comp)));
var muted = _nanoChat.GetNotificationsMuted((card, card.Comp));
_nanoChat.SetNotificationsMuted((card, card.Comp), !muted);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} {(muted ? "un" : "")}muted the NanoChat on {ToPrettyString(card)}");
UpdateUIForCard(card);
}
@@ -303,13 +388,25 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
if (msg.RecipientNumber is not uint chat)
return;
_nanoChat.ToggleChatMuted((card, card.Comp), chat);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} toggled NanoChat notifcations for chat #{chat:D4} on {ToPrettyString(card)}");
UpdateUIForCard(card);
}
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card)
private void HandleToggleListNumber(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
_nanoChat.SetListNumber((card, card.Comp), !_nanoChat.GetListNumber((card, card.Comp)));
var numberListed = _nanoChat.GetListNumber((card, card.Comp));
_nanoChat.SetListNumber((card, card.Comp), !numberListed);
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} toggled the NanoChat listing of {ToPrettyString(card)} {(numberListed ? "off" : "on")}");
UpdateUIForAllCards();
}
@@ -323,7 +420,11 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
if (!EnsureRecipientExists(card, msg.RecipientNumber.Value))
var recipient = _nanoChat.GetRecipient((card, card.Comp), msg.RecipientNumber.Value);
var isGroupChat = recipient?.IsGroup ?? false;
// Only ensure recipient exists for non-group chats
if (!isGroupChat && !EnsureRecipientExists(card, msg.RecipientNumber.Value))
return;
var content = msg.Content;
@@ -341,8 +442,19 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
(uint)card.Comp.Number
);
// Attempt delivery
var (deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
List<Entity<NanoChatCardComponent>> recipients;
bool deliveryFailed;
if (isGroupChat && recipient != null)
{
// For group chats, deliver to all members
(deliveryFailed, recipients) = AttemptGroupMessageDelivery(cartridge, recipient.Value, card.Comp.Number.Value);
}
else
{
// For regular chats, deliver to single recipient
(deliveryFailed, recipients) = AttemptMessageDelivery(cartridge, msg.RecipientNumber.Value);
}
// Update delivery status
message = message with { DeliveryFailed = deliveryFailed };
@@ -357,7 +469,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
_adminLogger.Add(LogType.Chat,
LogImpact.Low,
$"{ToPrettyString(card):user} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
$"{ToPrettyString(msg.Actor)} sent NanoChat message to {recipientsText}: {content}{(deliveryFailed ? " [DELIVERY FAILED]" : "")}");
var msgEv = new NanoChatMessageReceivedEvent(card);
RaiseLocalEvent(ref msgEv);
@@ -365,9 +477,9 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (deliveryFailed)
return;
foreach (var recipient in recipients)
foreach (var recipientCard in recipients)
{
DeliverMessageToRecipient(card, recipient, message);
DeliverMessageToRecipient(card, recipientCard, message, msg.RecipientNumber.Value, recipient);
}
}
@@ -480,27 +592,88 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
/// <param name="sender">The sender's card entity</param>
/// <param name="recipient">The recipient's card entity</param>
/// <param name="message">The <see cref="NanoChatMessage" /> to deliver</param>
/// <param name="chatNumber">The chat number (for group chats, this is the group number)</param>
/// <param name="groupRecipient">Optional group recipient info if this is a group chat</param>
private void DeliverMessageToRecipient(Entity<NanoChatCardComponent> sender,
Entity<NanoChatCardComponent> recipient,
NanoChatMessage message)
NanoChatMessage message,
uint chatNumber,
NanoChatRecipient? groupRecipient = null)
{
if (sender.Comp.Number is not uint senderNumber)
return;
// Always try to get and add sender info to recipient's contacts
if (!EnsureRecipientExists(recipient, senderNumber))
// For group chats, use the group number. For regular chats, use the sender's number
var recipientNumber = (groupRecipient != null && groupRecipient.Value.IsGroup) ? chatNumber : senderNumber;
if (groupRecipient != null && groupRecipient.Value.IsGroup)
{
var existingRecipient = _nanoChat.GetRecipient((recipient, recipient.Comp), recipientNumber);
if (existingRecipient == null || !existingRecipient.Value.IsGroup)
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
else if (groupRecipient.Value.Members != null)
{
var existingMembers = existingRecipient.Value.Members ?? new HashSet<uint>();
if (!existingMembers.SetEquals(groupRecipient.Value.Members))
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
else
{
var existingAdmins = existingRecipient.Value.Admins ?? new HashSet<uint>();
var groupAdmins = groupRecipient.Value.Admins ?? new HashSet<uint>();
if (!existingAdmins.SetEquals(groupAdmins))
{
_nanoChat.SetRecipient((recipient, recipient.Comp), recipientNumber, groupRecipient.Value);
}
}
}
}
else if (!EnsureRecipientExists(recipient, recipientNumber))
{
return;
}
_nanoChat.AddMessage((recipient, recipient.Comp), senderNumber, message with { DeliveryFailed = false });
_nanoChat.AddMessage((recipient, recipient.Comp), recipientNumber, message with { DeliveryFailed = false });
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != senderNumber)
HandleUnreadNotification(recipient, message, senderNumber);
if (recipient.Comp.IsClosed || _nanoChat.GetCurrentChat((recipient, recipient.Comp)) != recipientNumber)
HandleUnreadNotification(recipient, message, recipientNumber);
var msgEv = new NanoChatMessageReceivedEvent(recipient);
RaiseLocalEvent(ref msgEv);
UpdateUIForCard(recipient);
}
/// <summary>
/// Attempts to deliver a message to all members of a group chat.
/// </summary>
private (bool failed, List<Entity<NanoChatCardComponent>> recipients) AttemptGroupMessageDelivery(
Entity<NanoChatCartridgeComponent> sender,
NanoChatRecipient groupRecipient,
uint senderNumber)
{
if (groupRecipient.Members == null)
return (true, new List<Entity<NanoChatCardComponent>>());
var deliverableRecipients = new List<Entity<NanoChatCardComponent>>();
foreach (var memberNumber in groupRecipient.Members)
{
if (memberNumber == senderNumber)
continue;
var (failed, memberCards) = AttemptMessageDelivery(sender, memberNumber);
if (!failed)
deliverableRecipients.AddRange(memberCards);
}
// Delivery failed if no recipients could receive the message
return (deliverableRecipients.Count == 0, deliverableRecipients);
}
/// <summary>
/// Handles unread message notifications and updates unread status.
/// </summary>
@@ -510,21 +683,21 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
// Get sender name from contacts or fall back to number
var recipients = _nanoChat.GetRecipients((recipient, recipient.Comp));
var senderName = recipients.TryGetValue(message.SenderId, out var senderRecipient)
var senderName = recipients.TryGetValue(senderNumber, out var senderRecipient)
? senderRecipient.Name
: $"#{message.SenderId:D4}";
: $"#{senderNumber:D4}";
var hasSelectedCurrentChat = _nanoChat.GetCurrentChat((recipient, recipient.Comp)) == senderNumber;
// Update unread status
if (!hasSelectedCurrentChat)
_nanoChat.SetRecipient((recipient, recipient.Comp),
message.SenderId,
senderNumber,
senderRecipient with { HasUnread = true });
// Temporary local to avoid trouble with read-only access; Contains doesn't modify the collection
HashSet<uint> mutedChats = recipient.Comp.MutedChats;
if (recipient.Comp.NotificationsMuted ||
mutedChats.Contains(message.SenderId) ||
mutedChats.Contains(senderNumber) ||
recipient.Comp.PdaUid is not { } pdaUid ||
!TryComp<CartridgeLoaderComponent>(pdaUid, out var loader) ||
// Don't notify if the recipient has the NanoChat program open with this chat selected.
@@ -593,14 +766,27 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
// Try to get job title from ID card if possible
string? jobTitle = null;
string? department = null;
var name = "Unknown";
if (TryComp<IdCardComponent>(uid, out var idCard))
{
jobTitle = idCard.LocalizedJobTitle;
name = idCard.FullName ?? name;
if (idCard.JobDepartments is { } depts && depts.Count > 0)
department = depts[0].ToString();
}
return new NanoChatRecipient(number, name, jobTitle);
return new NanoChatRecipient(
number,
name,
jobTitle,
department,
false,
false,
null,
null,
null
);
}
return null;
@@ -624,12 +810,34 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
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)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
}
if (!nanoChatCard.ListNumber)
continue;
if (nanoChatCard.PdaUid is null)
continue;
if (nanoChatCard.Number is not { } nanoChatNumber)
continue;
if (idCardComponent.FullName is not { } fullName)
continue;
if (_station.GetOwningStation(entityId) != station)
continue;
string? department = null;
if (idCardComponent.JobDepartments is { } depts && depts.Count > 0)
department = depts[0].ToString();
contacts.Add(new NanoChatRecipient(
nanoChatNumber,
fullName,
idCardComponent.LocalizedJobTitle,
department,
false,
false,
null,
null,
null
));
}
contacts.Sort((contactA, contactB) => string.CompareOrdinal(contactA.Name, contactB.Name));
contacts.Sort();
}
else
{
@@ -668,4 +876,314 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
listNumber);
_cartridge.UpdateCartridgeUiState(loader, state);
}
/// <summary>
/// Handles creation of a new group chat.
/// </summary>
private void HandleCreateGroupChat(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.Content == null || card.Comp.Number == null)
return;
var name = msg.Content;
if (!string.IsNullOrWhiteSpace(name))
{
name = name.Trim();
if (name.Length > _maxNameLength)
name = name[.._maxNameLength];
}
// Generate a unique group number (surely unique I actually have no idea how to generate good unique numbers.)
var groupNumber = (uint)(HashCode.Combine(card.Comp.Number.Value, _timing.CurTime.Ticks) & 0x7FFFFFFF);
// This fucking sucks (fire emoji)
while (_nanoChat.GetRecipient((card, card.Comp), groupNumber) != null)
{
groupNumber++;
}
// Create group chat recipient
var recipient = new NanoChatRecipient(
groupNumber,
name,
null,
null,
false,
true,
[card.Comp.Number.Value],
card.Comp.Number.Value,
null
);
_nanoChat.SetRecipient((card, card.Comp), groupNumber, recipient);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} created group chat '{name}' (#{groupNumber:D4})");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles inviting a user to a group chat.
/// </summary>
private void HandleInviteToGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var inviteeNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can invite
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (members.Contains(inviteeNumber))
return;
// Add member to group
members.Add(inviteeNumber);
var updatedRecipient = recipient.Value with { Members = members };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Update member lists for all members
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number != null && members.Contains(memberCard.Number.Value))
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} invited #{inviteeNumber:D4} to group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles kicking a user from a group chat.
/// </summary>
private void HandleKickFromGroup(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var kickeeNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
var isSelfKick = kickeeNumber == card.Comp.Number.Value;
if (!isSelfKick && !isCreator && !isAdmin)
return;
// Allow creator to leave the group but not be kicked by other members.
var isCreatorLeaving = kickeeNumber == recipient.Value.CreatorId && isCreator;
if (kickeeNumber == recipient.Value.CreatorId && !isCreatorLeaving)
return;
// Find all cards belonging to the kickee
var kickeeCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var kickeeCard))
{
if (kickeeCard.Number == kickeeNumber)
{
kickeeCards.Add((cardUid, kickeeCard));
}
}
foreach (var kickeeCard in kickeeCards)
{
var deleteMsg = new NanoChatUiMessageEvent(NanoChatUiMessageType.DeleteChat, groupNumber, null, null)
{
Actor = msg.Actor
};
HandleDeleteChat(kickeeCard, deleteMsg);
}
if (isCreatorLeaving)
{
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} left group chat #{groupNumber:D4}");
}
else
{
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} kicked #{kickeeNumber:D4} from group chat #{groupNumber:D4}");
}
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles promoting a user to admin in a group chat.
/// </summary>
private void HandleAdminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var targetNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can make admins
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (!members.Contains(targetNumber))
return; // how tf
if (targetNumber == recipient.Value.CreatorId)
return;
if (!isCreator && admins.Contains(targetNumber))
return;
if (!admins.Add(targetNumber))
return;
var updatedRecipient = recipient.Value with { Admins = admins };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Sync to all members
foreach (var memberNumber in members)
{
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} promoted #{targetNumber:D4} to admin in group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
/// <summary>
/// Handles demoting a user from admin in a group chat.
/// </summary>
private void HandleDeadminUser(Entity<NanoChatCardComponent> card, NanoChatUiMessageEvent msg)
{
if (msg.RecipientNumber == null || msg.Content == null || card.Comp.Number == null)
return;
var groupNumber = msg.RecipientNumber.Value;
if (!uint.TryParse(msg.Content, out var targetNumber))
return;
var recipient = _nanoChat.GetRecipient((card, card.Comp), groupNumber);
if (recipient == null || !recipient.Value.IsGroup)
return;
// Only the creator or admins can remove admins
var isCreator = recipient.Value.CreatorId == card.Comp.Number.Value;
var admins = recipient.Value.Admins ?? new HashSet<uint>();
var isAdmin = admins.Contains(card.Comp.Number.Value);
if (!isCreator && !isAdmin)
return;
var members = recipient.Value.Members ?? new HashSet<uint>();
if (!members.Contains(targetNumber))
return; // how
if (!isCreator && (targetNumber == recipient.Value.CreatorId || admins.Contains(targetNumber)))
return;
if (!admins.Remove(targetNumber))
return;
var updatedRecipient = recipient.Value with { Admins = admins };
_nanoChat.SetRecipient((card, card.Comp), groupNumber, updatedRecipient);
// Sync to all members
foreach (var memberNumber in members)
{
var memberCards = new List<Entity<NanoChatCardComponent>>();
var cardQuery = EntityQueryEnumerator<NanoChatCardComponent>();
while (cardQuery.MoveNext(out var cardUid, out var memberCard))
{
if (memberCard.Number == memberNumber)
{
memberCards.Add((cardUid, memberCard));
}
}
foreach (var memberCard in memberCards)
{
_nanoChat.SetRecipient((memberCard, memberCard.Comp), groupNumber, updatedRecipient);
UpdateUIForCard(memberCard);
}
}
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(msg.Actor)} removed admin from #{targetNumber:D4} in group chat #{groupNumber:D4}");
var recipientEv = new NanoChatRecipientUpdatedEvent(card);
RaiseLocalEvent(ref recipientEv);
UpdateUIForCard(card);
}
}
@@ -62,6 +62,7 @@ public sealed class CosmicRiftSystem : EntitySystem
var pos = Transform(uid).Coordinates;
Spawn(comp.PulseVFX, pos);
_mobs.Clear();
_lookup.GetEntitiesInRange<MobStateComponent>(pos, comp.PulseRange, _mobs);
_mobs.RemoveWhere(target => _chaplainsQuery.HasComp(target) || _cultistsQuery.HasComp(target) || _colossiQuery.HasComp(target));
foreach(var mob in _mobs)
@@ -19,6 +19,6 @@ public sealed class SiliconEmpSystem : EntitySystem
{
args.Cancel(); // Stop all the normal effects of the EMP
if (args.Damage is not { } damage) return;
_damageable.TryChangeDamage(ent, damage / 2, true); // Damage is divided by 2 because the event is raised twice (once from entity itself, and another is relayed from it's power cell) and I'm too lazy for an actual fix
_damageable.TryChangeDamage(ent, damage / 2, false); // Damage is divided by 2 because the event is raised twice (once from entity itself, and another is relayed from it's power cell) and I'm too lazy for an actual fix - NoElka | Make EMP not ignore armor.
}
}
@@ -0,0 +1,22 @@
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// DeltaV - Prohibits meteor swarms from targeting their impact near this component.
/// </summary>
[RegisterComponent]
public sealed partial class AntiMeteorZoneComponent : Component
{
/// <summary>
/// The size of the area around this entity to avoid.
/// </summary>
[DataField]
public float ZoneRadius = 5f;
/// <summary>
/// Percentage (roughly) of meteors that are BLOCKED from hitting near this component.
/// </summary>
[DataField]
public float AvoidanceRate = 0.75f;
}
@@ -0,0 +1,24 @@
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// DeltaV - Prohibits meteor swarms from targeting their impact near this component.
/// Standardized avoidance rate; cannot be changed in View Variables.
/// </summary>
[RegisterComponent]
public sealed partial class AntiMeteorZoneStandardRateComponent : Component
{
/// <summary>
/// The size of the area around this entity to avoid.
/// </summary>
[DataField]
public float ZoneRadius = 5f;
/// <summary>
/// Percentage (roughly) of meteors that are BLOCKED from hitting near this component.
/// Standardized avoidance rate; cannot be changed in View Variables.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)] // I tried to inherit AntiMeteorZoneComponent, but this VVAccess wouldn't take effect
public float AvoidanceRate = 0.75f;
}
@@ -0,0 +1,28 @@
using Content.Server.Power.Components;
using Content.Server.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// When fired, turns off power on the station for a few seconds, playing <see cref="EpsilonEventRuleComponent.PowerOffSound"/>
/// Afterwards turns power back on and sets alert level to epsilon
/// </summary>
[RegisterComponent, Access(typeof(EpsilonEventRule))]
public sealed partial class EpsilonEventRuleComponent : Component
{
/// <summary>
/// Default sound of the announcement when power turns off.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultPowerOff = new("PowerOff");
/// <summary>
/// Sound of the announcement to play when power turns off.
/// </summary>
[DataField]
public SoundSpecifier PowerOffSound = new SoundCollectionSpecifier(DefaultPowerOff, AudioParams.Default.WithVolume(-2f));
public EntityUid AffectedStation;
public readonly HashSet<Entity<ApcComponent>> ToggledAPCs = new();
}
@@ -0,0 +1,33 @@
using Content.Server._DV.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server._DV.StationEvents.Components;
[RegisterComponent, Access(typeof(MinorMassMindSwapRule))]
public sealed partial class MinorMassMindSwapRuleComponent : Component
{
/// <summary>
/// The mind swap is only temporary if true.
/// </summary>
[DataField("isTemporary")]
public bool IsTemporary = false;
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(60);
[DataField]
public SoundSpecifier AnnouncementSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
[DataField]
public SoundSpecifier SwapWarningSound = new SoundPathSpecifier("/Audio/_DV/Effects/clang2.ogg");
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SoundTime;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SwapTime;
[DataField]
public int MaxNumberOfPairs = 3;
}
@@ -0,0 +1,73 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Server.AlertLevel;
using Content.Server.Audio;
using Robust.Shared.Audio.Systems;
namespace Content.Server.StationEvents.Events
{
public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleComponent>
{
[Dependency] private readonly ApcSystem _apcSystem = default!;
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private readonly ServerGlobalSoundSystem _sound = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
protected override void Started(EntityUid uid, EpsilonEventRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// This should be checked before playing the sound
if (!TryGetRandomStation(out var chosenStation))
return;
component.AffectedStation = chosenStation.Value;
// Plays the power off sound to the station.
_sound.PlayGlobalOnStation(component.AffectedStation, _audio.ResolveSound(component.PowerOffSound));
var query = AllEntityQuery<ApcComponent, TransformComponent>();
while (query.MoveNext(out var apcUid, out var apc, out var transform))
{
if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station != chosenStation)
continue;
// Turns off APCs during the event. Basic power-outage event.
if (apc.MainBreakerEnabled)
{
_apcSystem.ApcToggleBreaker(apcUid, apc);
component.ToggledAPCs.Add((apcUid, apc));
}
}
}
protected override void Ended(EntityUid uid, EpsilonEventRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
base.Ended(uid, component, gameRule, args);
// Checks if station exists.
if (!Exists(component.AffectedStation))
return;
// Restores the power to the station.
foreach (var apc in component.ToggledAPCs)
{
if (Deleted(apc))
continue;
if (!apc.Comp.MainBreakerEnabled)
_apcSystem.ApcToggleBreaker(apc, apc);
}
component.ToggledAPCs.Clear();
// Final effect, sets the code to Epsilon
if (!HasComp<AlertLevelComponent>(component.AffectedStation))
return;
_alertLevelSystem.SetLevel(component.AffectedStation, "epsilon", true, true, true);
}
}
}
@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos.Components;
using Content.Server.GameTicking.Rules.Components;
@@ -7,6 +8,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Random;
using Robust.Shared.Spawners;
namespace Content.Server.StationEvents.Events
@@ -87,35 +89,83 @@ namespace Content.Server.StationEvents.Events
(var target, var targetArea) = GetTarget();
var targetSpread = (targetArea.TopRight - targetArea.Center).Length() * component.TargetingSpread;
var protectedAreas = new List<(MapCoordinates center, float radiusSquared, float protectionRate)>();
var protectedAreaQuery = AllEntityQuery<AntiMeteorZoneComponent, TransformComponent>();
while (protectedAreaQuery.MoveNext(out var protectedEntityUid, out var antiZone, out var transform))
{
protectedAreas.Add((
center: _transform.ToMapCoordinates(transform.Coordinates),
radiusSquared: MathF.Pow(antiZone.ZoneRadius, 2f),
protectionRate: antiZone.AvoidanceRate
));
}
// dang, I wish I could use Linq on queries
var standardRateProtectedAreaQuery = AllEntityQuery<AntiMeteorZoneStandardRateComponent, TransformComponent>();
while (standardRateProtectedAreaQuery.MoveNext(out var protectedEntityUid, out var antiZone, out var transform))
{
protectedAreas.Add((
center: _transform.ToMapCoordinates(transform.Coordinates),
radiusSquared: MathF.Pow(antiZone.ZoneRadius, 2f),
protectionRate: antiZone.AvoidanceRate
));
}
float maxImpactTime = 0;
for (var i = 0; i < component.MeteorsPerWave; i++)
{
var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
var spawnPosition = new MapCoordinates(
target.X + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.X,
target.Y + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.Y,
mapId
);
var protectedAreasThisMeteor =
protectedAreas.Where(protectedArea => RobustRandom.Prob(protectedArea.protectionRate)).ToList();
MapCoordinates spawnPosition;
Vector2 velocity;
int targetingAttempts = 0;
bool targetingSafe;
do
{
var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
spawnPosition = new MapCoordinates(
target.X + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.X,
target.Y + targetSpread * (2f * RobustRandom.NextFloat() - 1f) + offset.Y,
mapId
);
velocity = -offset.Normalized() * component.MeteorVelocity;
targetingSafe = true;
foreach (var protectedArea in protectedAreasThisMeteor)
{
var timeUntilClosestApproachToProtectedArea = TimeUntilClosestApproach(spawnPosition, velocity, protectedArea.center);
var pointOfClosestApproachToProtectedArea = spawnPosition.Position + timeUntilClosestApproachToProtectedArea * velocity;
if (
Vector2.DistanceSquared(protectedArea.center.Position, pointOfClosestApproachToProtectedArea)
< protectedArea.radiusSquared
)
{
targetingSafe = false;
break;
}
}
targetingAttempts++;
}
while (!targetingSafe && targetingAttempts <= 3); // attempt to avoid the protected areas a few times
var meteor = Spawn(proto, spawnPosition);
var physics = EntityManager.GetComponent<PhysicsComponent>(meteor);
_physics.SetBodyStatus(meteor, physics, BodyStatus.InAir);
_physics.SetLinearDamping(meteor, physics, 0f);
_physics.SetAngularDamping(meteor, physics, 0f);
var velocity = -offset.Normalized() * component.MeteorVelocity;
_physics.ApplyLinearImpulse(meteor, velocity * physics.Mass, body: physics);
_physics.SetAngularVelocity(meteor, (component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity, body: physics);
EnsureComp<TimedDespawnComponent>(meteor).Lifetime = component.MeteorLifetime;
var relativeSpawnPosition = new Vector2(target.X - spawnPosition.X, target.Y - spawnPosition.Y);
var timeUntilClosestApproach = (
(relativeSpawnPosition.X * velocity.X + relativeSpawnPosition.Y * velocity.Y) /
(MathF.Pow(velocity.X, 2f) + MathF.Pow(velocity.Y, 2f))
);
if (i == 0 || timeUntilClosestApproach > maxImpactTime)
var timeUntilClosestApproachToTarget = TimeUntilClosestApproach(spawnPosition, velocity, target);
if (i == 0 || timeUntilClosestApproachToTarget > maxImpactTime)
{
maxImpactTime = timeUntilClosestApproach;
maxImpactTime = timeUntilClosestApproachToTarget;
}
}
@@ -132,5 +182,14 @@ namespace Content.Server.StationEvents.Events
component.Cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() + component.MinimumCooldown;
}
}
private static float TimeUntilClosestApproach(MapCoordinates startPoint, Vector2 velocity, MapCoordinates approachPoint)
{
var approachPointRelativeToStartPoint = new Vector2(approachPoint.X - startPoint.X, approachPoint.Y - startPoint.Y);
return (
(approachPointRelativeToStartPoint.X * velocity.X + approachPointRelativeToStartPoint.Y * velocity.Y) /
(MathF.Pow(velocity.X, 2f) + MathF.Pow(velocity.Y, 2f))
);
}
}
}
@@ -0,0 +1,107 @@
using Content.Server.Abilities.Psionics;
using Content.Server.Chat.Systems;
using Content.Server._DV.StationEvents.Components;
using Content.Server.Psionics;
using Content.Server.StationEvents.Events;
using Content.Shared.Abilities.Psionics;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server._DV.StationEvents.Events;
/// <summary>
/// Forces a mind swap on a small amount of non-insulated psionic entities.
/// </summary>
internal sealed class MinorMassMindSwapRule : StationEventSystem<MinorMassMindSwapRuleComponent>
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MindSwapPowerSystem _mindSwap = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MobStateSystem _mobstateSystem = default!;
private TimeSpan _warningSoundLength;
private ResolvedSoundSpecifier _resolvedWarningSound = string.Empty;
protected override void Started(EntityUid uid, MinorMassMindSwapRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
_resolvedWarningSound = _audio.ResolveSound(component.SwapWarningSound);
_warningSoundLength = _audio.GetAudioLength(_resolvedWarningSound);
component.SwapTime = Timing.CurTime + component.Delay;
component.SoundTime = component.SwapTime - _warningSoundLength;
component.MaxNumberOfPairs = component.MaxNumberOfPairs < 1 ? 1 : component.MaxNumberOfPairs;
var announcement = Loc.GetString("minor-mass-mind-swap-event-announcement", ("time", component.Delay.TotalSeconds));
var sender = Loc.GetString("minor-mass-mind-swap-event-sender");
_chat.DispatchGlobalAnnouncement(announcement, sender, true, component.AnnouncementSound, Color.White);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<MinorMassMindSwapRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var comp, out var ruleComp))
{
if (comp.SwapTime == null)
continue;
if (comp.SoundTime != null && comp.SoundTime <= Timing.CurTime)
{
_audio.PlayGlobal(_resolvedWarningSound, Filter.Broadcast(), true);
comp.SoundTime = null;
continue;
}
if (comp.SwapTime > Timing.CurTime)
continue;
SwapMinds(comp);
comp.SwapTime = null;
GameTicker.EndGameRule(uid, ruleComp);
}
}
private void SwapMinds(MinorMassMindSwapRuleComponent component)
{
List<EntityUid> psionicActors = new();
var query = EntityQueryEnumerator<PotentialPsionicComponent, MobStateComponent>();
while (query.MoveNext(out var psion, out _, out _))
{
if (_mobstateSystem.IsAlive(psion) && !HasComp<PsionicInsulationComponent>(psion)
&& HasComp<ActorComponent>(psion))
// Only a list of Players
psionicActors.Add(psion);
}
// We go with 4 pairs for now
List<EntityUid> actorsToSwap = new();
var swapPairCount = _random.Next(1, component.MaxNumberOfPairs);
for (; swapPairCount > 0 && psionicActors.Count > 1; swapPairCount--)
{
var target01 = _random.PickAndTake(psionicActors);
var target02 = _random.PickAndTake(psionicActors);
_mindSwap.Swap(target01, target02);
if (!component.IsTemporary)
{
_mindSwap.GetTrapped(target01);
_mindSwap.GetTrapped(target02);
}
}
}
}
@@ -0,0 +1,5 @@
using Content.Shared._EE.Flight;
namespace Content.Server._EE.Flight;
public sealed class FlightSystem : SharedFlightSystem;
@@ -1,9 +1,9 @@
using Content.Server.Atmos.Components;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared._EE.FootPrint;
// using Content.Shared.Standing;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -21,10 +21,12 @@ public sealed class FootPrintsSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
private EntityQuery<TransformComponent> _transformQuery;
private EntityQuery<MobThresholdsComponent> _mobThresholdQuery;
private EntityQuery<AppearanceComponent> _appearanceQuery;
// private EntityQuery<LayingDownComponent> _layingQuery;
public override void Initialize()
@@ -47,6 +49,9 @@ public sealed class FootPrintsSystem : EntitySystem
private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (component.PrintsColor.A <= 0f
|| !_transformQuery.TryComp(uid, out var transform)
|| !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds)
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -12,6 +13,7 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
public override void Initialize()
{
@@ -21,6 +23,9 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args)
{
if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance)
|| !TryComp<PuddleComponent>(uid, out var puddle)
|| !TryComp<FootPrintsComponent>(args.OtherEntity, out var tripper)
@@ -69,12 +69,14 @@ public sealed class IdExaminableSystem : EntitySystem
{
var jobSuffix = string.IsNullOrWhiteSpace(id.LocalizedJobTitle) ? string.Empty : $" ({id.LocalizedJobTitle})";
var val = string.IsNullOrWhiteSpace(id.FullName)
var val = FormattedMessage.EscapeText( // DeltaV - Escape job
string.IsNullOrWhiteSpace(id.FullName)
? Loc.GetString(id.NameLocId,
("jobSuffix", jobSuffix))
: Loc.GetString(id.FullNameLocId,
("fullName", id.FullName),
("jobSuffix", jobSuffix));
("jobSuffix", jobSuffix))
);
return val;
}
@@ -425,6 +425,11 @@ public abstract class SharedAnomalySystem : EntitySystem
if (!settings.CanSpawnOnEntities)
{
// DeltaV - start of duplicate spawn fix (borrowed from upstream #37833)
// If it can't spawn on entities, ensure that maximum one entity will be spawned here this pulse.
tilerefs.Remove(tileref);
// DeltaV - end of duplicate spawn fix (borrowed from upstream #37833)
var valid = true;
foreach (var ent in _map.GetAnchoredEntities(xform.GridUid.Value, grid, tileref.GridIndices))
{
@@ -441,7 +446,7 @@ public abstract class SharedAnomalySystem : EntitySystem
}
if (!valid)
{
tilerefs.Remove(tileref);
// DeltaV - duplicate spawn fix removed: tilerefs.Remove(tileref);
continue;
}
}
@@ -80,7 +80,9 @@ public abstract class SharedTypingIndicatorSystem : EntitySystem
return;
}
SetTypingOverride(uid.Value, ev.OverrideIndicator); // DeltaV
if(ev.State != TypingIndicatorState.Idle) // DeltaV - don't remove override when transitioning to idle
SetTypingOverride(uid.Value, ev.OverrideIndicator); // DeltaV
SetTypingIndicatorState(uid.Value, ev.State);
}
@@ -65,6 +65,18 @@ public sealed partial class ChameleonClothingComponent : Component
/// </summary>
[AutoPausedField, DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextEmpChange = TimeSpan.Zero;
/// <summary>
/// Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
/// </summary>
[DataField]
public LocId? WindowTitleOverride;
/// <summary>
/// Delta-V: Override for the name of the chameleon stuff, so you can have items use the chameleon system without being illegal.
/// </summary>
[DataField]
public LocId? VerbNameOverride;
}
[Serializable, NetSerializable]
@@ -132,7 +132,7 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
args.Verbs.Add(new InteractionVerb()
{
Text = Loc.GetString("chameleon-component-verb-text"),
Text = Loc.GetString(ent.Comp.VerbNameOverride ?? "chameleon-component-verb-text"), // Delta-V: Override for the name of the Chameleon Verb
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
Act = () => UI.TryToggleUi(ent.Owner, ChameleonUiKey.Key, user)
});
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
@@ -55,6 +56,7 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy flight
public override void Initialize()
{
@@ -514,6 +516,15 @@ namespace Content.Shared.Cuffs
return false;
}
// EE - Harpy Flight
if (_flight.IsFlying(target))
{
_popup.PopupClient(Loc.GetString("handcuff-component-target-flying-error",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
return false;
}
// END EE
var cuffTime = handcuffComponent.CuffTime;
if (HasComp<StunnedComponent>(target))
@@ -45,6 +45,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] protected readonly SharedStunSystem StunSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movement = default!; // EE - Harpy Flight
/// <summary>
/// How much of a buffer is there between the stun duration and when stuns can be re-applied.
@@ -265,7 +266,8 @@ public abstract partial class SharedStaminaSystem : EntitySystem
}
public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null,
EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false)
EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false,
bool? allowsSlowdown = true) // EE - Harpy Flight
{
if (!Resolve(uid, ref component, false))
return;
@@ -283,6 +285,9 @@ public abstract partial class SharedStaminaSystem : EntitySystem
value = UniversalStaminaDamageModifier * value;
if (allowsSlowdown == true) // EE - Harpy Flight
_movement.RefreshMovementSpeedModifiers(uid);
// Have we already reached the point of max stamina damage?
if (component.Critical)
return;
@@ -362,12 +367,23 @@ public abstract partial class SharedStaminaSystem : EntitySystem
{
// Just in case we have active but not stamina we'll check and account for it.
if (!stamQuery.TryGetComponent(uid, out var comp) ||
comp.StaminaDamage <= 0f && !comp.Critical)
comp.StaminaDamage <= 0f && !comp.Critical && comp.ActiveDrains.Count == 0) // EE - Harpy Flight
{
RemComp<ActiveStaminaComponent>(uid);
continue;
}
// EE - Harpy Flight
if (comp.ActiveDrains.Count > 0)
foreach (var (source, (drainRate, modifiesSpeed)) in comp.ActiveDrains)
TakeStaminaDamage(uid,
drainRate * frameTime,
comp,
source: source,
visual: false,
allowsSlowdown: modifiesSpeed);
// End EE
// Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
var nextUpdate = comp.NextUpdate;
@@ -380,10 +396,11 @@ public abstract partial class SharedStaminaSystem : EntitySystem
comp.NextUpdate += TimeSpan.FromSeconds(1f);
TakeStaminaDamage(
uid,
comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
comp);
if (comp.ActiveDrains.Count == 0) // EE - Harpy Flight
TakeStaminaDamage(
uid,
comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
comp);
Dirty(uid, comp);
}
@@ -1,12 +1,13 @@
using System.Numerics;
using Robust.Shared.Map;
using Content.Shared._EE.Flight.Events;
namespace Content.Shared.Gravity;
/// <summary>
/// Handles offsetting a sprite when there is no gravity
/// </summary>
public abstract class SharedFloatingVisualizerSystem : EntitySystem
public abstract partial class SharedFloatingVisualizerSystem : EntitySystem // DeltaV - Made Partial for Harpy Flying
{
[Dependency] private readonly SharedGravitySystem _gravity = default!;
@@ -16,6 +17,8 @@ public abstract class SharedFloatingVisualizerSystem : EntitySystem
SubscribeLocalEvent<FloatingVisualsComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<FloatingVisualsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeNetworkEvent<FlightEvent>(OnFlight);
}
/// <summary>
@@ -1,3 +1,4 @@
using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Content.Shared.Throwing;
@@ -16,6 +17,7 @@ public abstract partial class SharedGravitySystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy Flight
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
@@ -69,6 +71,9 @@ public abstract partial class SharedGravitySystem : EntitySystem
if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic)
return false;
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
// Check if something other than the grid or map is overriding our gravity
var ev = new IsWeightlessEvent();
RaiseLocalEvent(entity, ref ev);
@@ -11,6 +11,7 @@ public abstract partial class SharedHandsSystem
private void InitializeEventListeners()
{
SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
SubscribeLocalEvent<HandsComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
}
/// <summary>
@@ -28,4 +29,27 @@ public abstract partial class SharedHandsSystem
time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count);
}
private void OnKnockedDownRefresh(Entity<HandsComponent> ent, ref KnockedDownRefreshEvent args)
{
var freeHands = CountFreeHands(ent.AsNullable());
var totalHands = GetHandCount(ent.AsNullable());
// Can't crawl around without any hands.
// Entities without the HandsComponent will always have full crawling speed.
if (totalHands == 0)
args.SpeedModifier = 0f;
// DeltaV - 1 hand free = 75% movespeed, no hands free = 50% movespeed
else if (totalHands > freeHands)
{
// For some reason, this feels a bit dirty
// 0 free hands out of 1 total hands = (1-(1-0)*.50) = 50% additional reduction to current move speed mod
// 1 free hand out of 2 total hands = (1-(2-1)*.25) = 75%
// 1 free hand out of 4 total hands = (1-(4-1)*.125) = 62.5%
// 6 free hands out of 10 total hands hands = (1-(10-6)*0.05) = 80%
var reductionPerHand = 1f / (totalHands + totalHands);
args.SpeedModifier *= (float)Math.Clamp(1f - (totalHands - freeHands) * reductionPerHand, 0.5, 1.0);
}
// END DeltaV
}
}
@@ -108,13 +108,13 @@ public sealed partial class SpeciesPrototype : IPrototype
/// Characters younger than this appear young.
/// </summary>
[DataField]
public int YoungAge = 30;
public int YoungAge = 40; // DeltaV - up from 30
/// <summary>
/// Characters older than this appear old. Characters in between young and old age appear middle aged.
/// </summary>
[DataField]
public int OldAge = 60;
public int OldAge = 70; // DeltaV - up from 60
/// <summary>
/// Characters cannot be older than this. Only used for restrictions...
@@ -60,7 +60,7 @@ public sealed partial class LabelSystem : EntitySystem
return;
label ??= EnsureComp<LabelComponent>(uid);
label.CurrentLabel = text;
label.CurrentLabel = text == null ? null : FormattedMessage.EscapeText(text);
_nameModifier.RefreshNameModifiers(uid);
Dirty(uid, label);
@@ -15,7 +15,7 @@ namespace Content.Shared.Movement.Components
// weightless
public const float DefaultWeightlessFriction = 1f;
public const float DefaultWeightlessModifier = 0.7f;
public const float DefaultWeightlessModifier = 0.70f;
public const float DefaultWeightlessAcceleration = 1f;
// friction
@@ -4,13 +4,13 @@ namespace Content.Shared.Radiation.Events;
/// Raised on entity when it was irradiated
/// by some radiation source.
/// </summary>
public readonly record struct OnIrradiatedEvent(float FrameTime, float RadsPerSecond, EntityUid Origin)
public readonly record struct OnIrradiatedEvent(float FrameTime, float RadsPerSecond, EntityUid? Origin)
{
public readonly float FrameTime = FrameTime;
public readonly float RadsPerSecond = RadsPerSecond;
public readonly EntityUid Origin = Origin;
public readonly EntityUid? Origin = Origin;
public float TotalRads => RadsPerSecond * FrameTime;
}
@@ -6,6 +6,7 @@ using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Movement.Events;
@@ -53,7 +54,7 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
// Updating movement a friction
// Updating movement and friction
SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
@@ -65,6 +66,9 @@ public abstract partial class SharedStunSystem
SubscribeLocalEvent<CrawlerComponent, KnockedDownRefreshEvent>(OnKnockdownRefresh);
SubscribeLocalEvent<CrawlerComponent, DamageChangedEvent>(OnDamaged);
SubscribeLocalEvent<KnockedDownComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
SubscribeLocalEvent<KnockedDownComponent, HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<GravityAffectedComponent, KnockDownAttemptEvent>(OnKnockdownAttempt);
SubscribeLocalEvent<GravityAffectedComponent, GetStandUpTimeEvent>(OnGetStandUpTime);
@@ -231,7 +235,14 @@ public abstract partial class SharedStunSystem
if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
return;
ToggleKnockdown(playerEnt);
// DeltaV - Double-tap Standup bind forces standup (unless hands full)
if (_standingState.IsDown(playerEnt)
&& TryComp<KnockedDownComponent>(playerEnt, out var knockedDown)
&& knockedDown.DoAfterId != null)
ForceStandUp(playerEnt);
else
ToggleKnockdown(playerEnt);
// END DeltaV
}
/// <summary>
@@ -365,7 +376,7 @@ public abstract partial class SharedStunSystem
private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} user)
if (args.SenderSession.AttachedEntity is not { } user)
return;
ForceStandUp(user);
@@ -507,6 +518,30 @@ public abstract partial class SharedStunSystem
RemCompDeferred<KnockedDownComponent>(entity);
}
private void OnHandEquipped(Entity<KnockedDownComponent> entity, ref DidEquipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandUnequipped(Entity<KnockedDownComponent> entity, ref DidUnequipHandEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnHandCountChanged(Entity<KnockedDownComponent> entity, ref HandCountChangedEvent args)
{
if (GameTiming.ApplyingState)
return; // The result of the change is already networked separately in the same game state
RefreshKnockedMovement(entity);
}
private void OnKnockdownAttempt(Entity<GravityAffectedComponent> entity, ref KnockDownAttemptEvent args)
{
// Directed, targeted moth attack.
@@ -567,6 +602,7 @@ public abstract partial class SharedStunSystem
ent.Comp.SpeedModifier = ev.SpeedModifier;
ent.Comp.FrictionModifier = ev.FrictionModifier;
Dirty(ent);
_movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
_movementSpeedModifier.RefreshFrictionModifiers(ent);
@@ -91,6 +91,31 @@ public sealed partial class XenoArtifactComponent : Component
/// </summary>
[DataField, AutoPausedField]
public TimeSpan NextUnlockTime;
/// <summary>
/// DeltaV - The end time of the last-completed XenoArtifactUnlockingComponent phase.
/// If null, no unlocking phase has occurred yet, and the other LastUnlocking... fields are also invalid.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? LastUnlockingEndTime = null;
/// <summary>
/// DeltaV - The set of related nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<int> LastUnlockingTriggeredNodeIndexesRelated = new();
/// <summary>
/// DeltaV - The list of nodes that were triggered during the last completed XenoArtifactUnlockingComponent phase.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> LastUnlockingTriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - Whether the last completed unlocking phase resulted in a newly-unlocked node.
/// </summary>
[DataField, AutoNetworkedField]
public bool LastUnlockingSuccessful = false;
#endregion
// NOTE: you should not be accessing any of these values directly. Use the methods in SharedXenoArtifactSystem.Graph
@@ -174,6 +199,18 @@ public sealed partial class XenoArtifactComponent : Component
/// </summary>
[DataField]
public EntProtoId<InstantActionComponent> SelfActivateAction = "ActionArtifactActivate";
/// <summary>
/// DeltaV - Probability for each node that its effect will be hidden in the analysis console until it is unlocked.
/// </summary>
[DataField]
public float LockedEffectHiddenProbability = 0.1f;
/// <summary>
/// DeltaV - Probability for each node that its effect will give a generic hint, rather than a specific one (until it is unlocked).
/// </summary>
[DataField]
public float LockedEffectVagueProbability = 0.67f;
}
/// <summary>
@@ -28,6 +28,30 @@ public sealed partial class XenoArtifactNodeComponent : Component
[DataField, AutoNetworkedField]
public LocId? TriggerTip;
/// <summary>
/// DeltaV - Unique effect description.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? EffectTipSpecific;
/// <summary>
/// DeltaV - Vague effect description that may be shared by other effects.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? EffectTipVague;
/// <summary>
/// DeltaV - Whether the effect of the node is hidden in the analysis console until unlocked.
/// </summary>
[DataField, AutoNetworkedField]
public bool LockedEffectTipHidden = false;
/// <summary>
/// DeltaV - Whether the effect of the node is described only vaguely in the analysis console until unlocked.
/// </summary>
[DataField, AutoNetworkedField]
public bool LockedEffectTipVague = false;
/// <summary>
/// The entity whose graph this node is a part of.
/// </summary>
@@ -15,6 +15,21 @@ public sealed partial class XenoArtifactUnlockingComponent : Component
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexes = new();
/// <summary>
/// DeltaV - Ordered list of triggered nodes.
/// </summary>
[DataField, AutoNetworkedField]
public List<int> TriggeredNodeIndexesOrdered = new();
/// <summary>
/// DeltaV - A subset of TriggeredNodeIndexes whose elements are "related" to eachother.
/// </summary>
/// <remarks>
/// See definition of relatedness in GetRelatedNodes() of SharedXenoArtifactSystem.DV.cs
/// </remarks
[DataField, AutoNetworkedField]
public HashSet<int> TriggeredNodeIndexesRelated = new();
/// <summary>
/// The time at which the unlocking state ends.
/// </summary>
@@ -25,6 +25,12 @@ public sealed partial class XenoArchTriggerPrototype : IPrototype
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// DeltaV - Triggers that CANNOT appear on the same artifact as this trigger.
/// </summary>
[DataField]
public HashSet<ProtoId<XenoArchTriggerPrototype>>? TriggerBlacklist;
/// <summary>
/// List of components that represent ways to trigger node.
/// </summary>
@@ -1,9 +1,11 @@
using System.Linq;
using Content.Shared._DV.Xenoarchaeology.Artifact.Components; // DeltaV
using Content.Shared.EntityTable;
using Content.Shared.NameIdentifier;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Content.Shared.Xenoarchaeology.Artifact.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; // DeltaV
using Robust.Shared.Utility;
namespace Content.Shared.Xenoarchaeology.Artifact;
@@ -111,6 +113,17 @@ public abstract partial class SharedXenoArtifactSystem
var nodeComponent = nodeEnt.Value.Comp;
nodeComponent.Depth = depth;
nodeComponent.TriggerTip = trigger.Tip;
// DeltaV - start of hide locked node effects
nodeComponent.LockedEffectTipHidden = RobustRandom.Prob(ent.Comp.LockedEffectHiddenProbability);
nodeComponent.LockedEffectTipVague = RobustRandom.Prob(ent.Comp.LockedEffectVagueProbability);
if (TryComp<XAEDetailsComponent>(nodeEnt.Value.Owner, out var details))
{
nodeComponent.EffectTipSpecific = details.SpecificTip;
nodeComponent.EffectTipVague = details.VagueTip;
if (!details.AllowLockedEffectHiding)
nodeComponent.LockedEffectTipHidden = false;
}
// DeltaV - end of hide locked node effects
EntityManager.AddComponents(nodeEnt.Value, trigger.Components);
Dirty(nodeEnt.Value);
@@ -82,11 +82,13 @@ public abstract partial class SharedXenoArtifactSystem
// var activated = ActivateNode((ent, artifactComponent), node.Value, null, null, Transform(ent).Coordinates, false);
// if (activated)
soundEffect = unlockingComponent.UnlockActivationSuccessfulSound;
artifactComponent.LastUnlockingSuccessful = true; // DeltaV - node scanner overhaul
}
else
{
unlockAttemptResultMsg = "artifact-unlock-state-end-failure";
soundEffect = unlockingComponent.UnlockActivationFailedSound;
artifactComponent.LastUnlockingSuccessful = false; // DeltaV - node scanner overhaul
}
if (_net.IsServer)
@@ -95,6 +97,21 @@ public abstract partial class SharedXenoArtifactSystem
_audio.PlayPvs(soundEffect, ent.Owner);
}
// DeltaV - start of node scanner overhaul
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered.Clear();
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Clear();
artifactComponent.LastUnlockingTriggeredNodeIndexesOrdered
.AddRange(unlockingComponent.TriggeredNodeIndexesOrdered);
// one day we'll get a HashSet.AddRange
foreach (var i in unlockingComponent.TriggeredNodeIndexesRelated)
artifactComponent.LastUnlockingTriggeredNodeIndexesRelated.Add(i);
artifactComponent.LastUnlockingEndTime = unlockingComponent.EndTime;
Dirty(ent, artifactComponent);
// DeltaV - end of node scanner overhaul
RemComp(ent, unlockingComponent);
RaiseUnlockingFinished(ent, node);
artifactComponent.NextUnlockTime = _timing.CurTime + artifactComponent.UnlockStateRefractory;
@@ -63,6 +63,15 @@ public abstract partial class SharedXenoArtifactSystem
if (!force && _timing.CurTime < ent.Comp.NextUnlockTime)
return;
// DeltaV - start of node scanner overhaul
(Entity<XenoArtifactNodeComponent> node, int index)? parsedNode =
(node == null)
? null
: (node.Value, GetIndex(ent, node.Value));
bool partOfRelatedTriggersSet = true;
// DeltaV - end of node scanner overhaul
if (!_unlockingQuery.TryGetComponent(ent, out var unlockingComp))
{
unlockingComp = EnsureComp<XenoArtifactUnlockingComponent>(ent);
@@ -73,24 +82,37 @@ public abstract partial class SharedXenoArtifactSystem
_popup.PopupEntity(Loc.GetString("artifact-unlock-state-begin"), ent);
Dirty(ent);
}
else if (node != null)
else if (parsedNode != null)
{
var index = GetIndex(ent, node.Value);
// DeltaV - start of node scanner overhaul
var predecessorNodeIndices = GetPredecessorNodes((ent, ent), index);
var successorNodeIndices = GetSuccessorNodes((ent, ent), index);
if (unlockingComp.TriggeredNodeIndexes.Count == 0
|| unlockingComp.TriggeredNodeIndexes.All(
x => predecessorNodeIndices.Contains(x) || successorNodeIndices.Contains(x)
)
)
var relatedNodeIndices = GetRelatedNodes((ent, ent), parsedNode.Value.index);
partOfRelatedTriggersSet = unlockingComp.TriggeredNodeIndexesRelated.All(x => relatedNodeIndices.Contains(x));
// Checking for trigger "relatedness" is a much more accurate measurement
// of "is this locking phase going to fail" than upstream's predecessor/successor check.
// Upstream's version of this check had edge-cases where time would not add, even though the
// unlocking phase ends up succeeding.
// See definition of GetRelatedNodes() for details on the concept of "relatedness".
if (
unlockingComp.TriggeredNodeIndexes.Count == unlockingComp.TriggeredNodeIndexesRelated.Count
&& partOfRelatedTriggersSet
)
// we add time on each new trigger, if it is not going to fail us
unlockingComp.EndTime += ent.Comp.UnlockStateIncrementPerNode;
// DeltaV - end of node scanner overhaul
}
if (node != null && unlockingComp.TriggeredNodeIndexes.Add(GetIndex(ent, node.Value)))
if (parsedNode != null && unlockingComp.TriggeredNodeIndexes.Add(parsedNode.Value.index))
{
// DeltaV - start of faster unlock effect
// DeltaV - start of changes
// node scanner overhaul:
unlockingComp.TriggeredNodeIndexesOrdered.Add(parsedNode.Value.index);
if (partOfRelatedTriggersSet)
unlockingComp.TriggeredNodeIndexesRelated.Add(parsedNode.Value.index);
// faster unlock effect:
if (
ent.Comp.UnlockCompleteDuration is {} completeDuration
&& TryGetNodeFromUnlockState((ent.Owner, unlockingComp, ent.Comp), out var unlockingNode)
@@ -98,7 +120,7 @@ public abstract partial class SharedXenoArtifactSystem
{
unlockingComp.EndTime = _timing.CurTime + completeDuration;
}
// DeltaV - end of faster unlock effect
// DeltaV - end of changes
Dirty(ent, unlockingComp);
}
@@ -16,6 +16,16 @@ public sealed partial class ArtifactAnalyzerComponent : Component
[DataField]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
/// <summary>
/// DeltaV - The ratio of research points per one glimmer.
/// </summary>
public int ExtractRatio = 750;
/// <summary>
/// DeltaV - The maximum added multiplier, reached at max glimmer.
/// </summary>
[DataField]
public float PointGlimmerMultiplier = 8f;
/// <summary>
/// The current artifact placed on this analyzer.
/// Can be null if none are present.
@@ -0,0 +1,34 @@
using Robust.Shared.Audio;
namespace Content.Shared._DEN.TimedDespawn;
/// <summary>
/// This is used for a more detailed timed despawn component.
/// </summary>
[RegisterComponent]
public sealed partial class TimedDespawnDetailedComponent : Component
{
[DataField]
public TimeSpan StartTime { get; set; } = TimeSpan.Zero;
/// <summary>
/// How long the entity will exist, in seconds, before despawning.
/// </summary>
[DataField]
public float Lifetime = 5f;
[DataField("examineText")]
public LocId? ExamineLocId { get; set; } = "timed-despawn-holoprojection-examine";
[DataField]
public SoundSpecifier? StartSound { get; set; } = new SoundPathSpecifier("/Audio/_DEN/Effects/holofan_sound_looped.ogg");
[DataField]
public AudioParams StartSoundParams { get; set; } = AudioParams.Default;
[DataField]
public SoundSpecifier? EndSound { get; set; }
[DataField]
public AudioParams EndSoundParams { get; set; } = AudioParams.Default;
}
@@ -3,4 +3,11 @@ using Robust.Shared.GameStates;
namespace Content.Shared._DV.Abilities.Psionics;
[RegisterComponent, NetworkedComponent]
public sealed partial class FracturedFormBodyComponent : Component;
public sealed partial class FracturedFormBodyComponent : Component
{
/// <summary>
/// The entity uid of the form that is currently being controlled.
/// </summary>
[DataField]
public EntityUid ControllingForm { get; set; } = default!;
}
@@ -0,0 +1,14 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Access.Components;
/// <summary>
/// Component to denote the ID card as one specific to a borg.
/// Used to prevent agent IDs from copying access from borg IDs chips.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PreventAgentIdComponent : Component
{
[DataField]
public LocId? PopupText = "agent-id-card-cannot-copy";
}
@@ -1,4 +1,5 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.Roles;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.CartridgeLoader.Cartridges;
@@ -57,45 +58,107 @@ public enum NanoChatUiMessageType : byte
ToggleMute,
ToggleMuteChat,
ToggleListNumber,
CreateGroupChat,
InviteToGroup,
KickFromGroup,
ViewGroupMembers,
AdminUser,
DeadminUser,
}
// putting this here because i can
/// <summary>
/// Represents an actual NanoChat Chat
/// Either a Group, if <paramref name="isGroup"/> is <see langword="true" />, else a normal user.
/// </summary>
/// <param name="number">The recipient's NanoChat number</param>
/// <param name="name">The recipient's display name</param>
/// <param name="jobTitle">Optional job title for the recipient</param>
/// <param name="department">Optional department ID for the recipient</param>
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
/// <param name="isGroup">Whether this is a group chat</param>
/// <param name="members">For group chats: list of member NanoChat numbers</param>
/// <param name="creatorId">For group chats: the creator's NanoChat number</param>
/// <param name="admins">For group chats: set of admin NanoChat numbers</param>
[Serializable, NetSerializable, DataRecord]
public struct NanoChatRecipient
public struct NanoChatRecipient(
uint number,
string name,
string? jobTitle,
string? department,
bool hasUnread,
bool isGroup,
HashSet<uint>? members,
uint? creatorId,
HashSet<uint>? admins
) : IComparable<NanoChatRecipient>
{
/// <summary>
/// The recipient's unique NanoChat number.
/// </summary>
public uint Number;
public uint Number = number;
/// <summary>
/// The recipient's display name, typically from their ID card.
/// </summary>
public string Name;
public string Name = name;
/// <summary>
/// The recipient's job title, if available.
/// </summary>
public string? JobTitle;
public string? JobTitle = jobTitle;
/// <summary>
/// The recipient's department ID, if available.
/// </summary>
public string? Department = department;
/// <summary>
/// Whether this recipient has unread messages.
/// </summary>
public bool HasUnread;
public bool HasUnread = hasUnread;
/// <summary>
/// Creates a new NanoChat recipient.
/// Whether this is a group chat.
/// </summary>
/// <param name="number">The recipient's NanoChat number</param>
/// <param name="name">The recipient's display name</param>
/// <param name="jobTitle">Optional job title for the recipient</param>
/// <param name="hasUnread">Whether there are unread messages from this recipient</param>
public NanoChatRecipient(uint number, string name, string? jobTitle = null, bool hasUnread = false)
public bool IsGroup = isGroup;
/// <summary>
/// For group chats: list of member NanoChat numbers.
/// </summary>
public HashSet<uint>? Members = members;
/// <summary>
/// For group chats: the NanoChat number of the creator.
/// </summary>
public uint? CreatorId = creatorId;
/// <summary>
/// For group chats: set of admin NanoChat numbers who can invite/kick.
/// </summary>
public HashSet<uint>? Admins = admins;
readonly int IComparable<NanoChatRecipient>.CompareTo(NanoChatRecipient other)
{
Number = number;
Name = name;
JobTitle = jobTitle;
HasUnread = hasUnread;
// Groups should go before normal users
if (IsGroup && !other.IsGroup)
return -1;
else if (!IsGroup && other.IsGroup)
return 1;
// FIXME: Order by Department?
// Order by name
var nameCompare = string.CompareOrdinal(Name, other.Name);
if (nameCompare != 0)
return nameCompare;
// Smaller number goes first
if (Number > other.Number)
return 1;
else if (Number < other.Number)
return -1;
else
return 0;
}
}

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