Compare commits

...

185 Commits

Author SHA1 Message Date
Ed
d73fcd53ff Update ContentLocalizationManager.cs 2025-10-06 12:38:49 +03:00
Ed
d25f979691 Update bucket.yml 2025-10-03 12:17:45 +03:00
Ed
824cddaa72 Update physical.yml 2025-10-03 12:12:46 +03:00
Ed
97ac343c6c edible 2025-10-03 12:02:03 +03:00
Ed
58b6f3d64e adapt codebase 2025-09-29 14:14:07 +03:00
Ed
d6a8c169b2 Merge remote-tracking branch 'upstream/stable' into ed-29-09-2025-upstream
# Conflicts:
#	.github/CODEOWNERS
#	Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
#	Content.Server/IdentityManagement/IdentitySystem.cs
#	Content.Server/Players/PlayTimeTracking/PlayTimeTrackingSystem.cs
2025-09-29 14:00:54 +03:00
Vasilis The Pikachu
b8ed3f9664 Empty commit 2025-09-28 17:54:18 +02:00
Jessica M
b2d09ba457 Rat King Refactor Part 0: Separate Rummaging from RatKingComponent. (#40530)
* separate rummager into its own component/system

* thing

* address review and entitytables

* reviews

* review

* warnings

* Update Content.Shared/RatKing/Systems/RummagerSystem.cs

---------

Co-authored-by: Jessica M <jessica@maybe.sh>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-09-25 03:50:24 +02:00
Hitlinemoss
d699a4e985 Xenoborg items are now highly illegal (#39856)
* Added tactical katana + tactical katana shipment (placeholder descriptions)

* Revert "Added tactical katana + tactical katana shipment (placeholder descriptions)"

This reverts commit aa1928be7f4d938df1838943781e63c47a03cc11.

Whoops, committed to master by mistake

* Xenoborg contraband is highly illegal now
2025-09-24 20:28:40 -05:00
PJBot
42786240ec Automatic changelog update 2025-09-24 23:14:22 +00:00
Keer-Sar
705e4d3aa1 Add "Lizard Visage" Snout Markings to lizards (#35294)
* initial commit

* Update attribution in meta.json

* Fix yaml formating in meta.json
2025-09-24 18:13:14 -05:00
Samuka-C
ea3c44686c Xenoborg jammer now ignores xenoborg associated frequencies (#38005)
* stop jammer from jamming radio of certain frequency

* xenoborg jammer no longer jamms xenoborg radio

* stop jammer from jamming device network signals from certain frequency

* xenoborg jammer no longer jamms xenoborg camera signal

* the old tale of the missing ;

* backwards

* fix issue with readonly

* comments to the frequencies excluded

* triple typo

* clearer summary

* add summary

* fixed 4th hidden typo
2025-09-24 17:02:46 -05:00
PJBot
4555b72608 Automatic changelog update 2025-09-24 21:49:42 +00:00
Nox
0663576c46 Descriptions for .20 Rifle (#36496)
* Initial commit - added Lecter and munitions descriptions, need to make one for the M-90GL...

* Fixed a formatting issue in the Lecter description, should be ready for review!

* Tests gaslighting me

* Third times the charm

* 99% of gamblers quit before the big win

* Update Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml

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

* Updated with roomba's changes

* Formatted descriptions, ready to go!

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

* Fixed formatting WITHOUT using spaces

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

* Updated weapon names.

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

* Added Roomba's suggestions

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

* Reverted AKMS formatting - outside the scope of this PR.

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

* Updated Estoc's description.

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

* Added AugustSun's suggestions.

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

* Slightly rephrased, im happy with it now.

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

* Fixed projectile names

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

* thats not even a bullpup dumbass

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

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2025-09-24 16:48:34 -05:00
PJBot
0678e3b468 Automatic changelog update 2025-09-24 20:34:55 +00:00
RedBookcase
f6cd8673d3 Recharger tweaks. (#38138)
* Recharger tweaks.

* Remove note.

* Fix Potato blacklist.

---------

Co-authored-by: RedBookcase <Usualmoves@gmail.com>
2025-09-24 15:33:45 -05:00
PJBot
aa828b96ab Automatic changelog update 2025-09-24 04:20:45 +00:00
Kittygyat
7c39b4595f Added diagnostic huds to the engi-vend (#40461)
Added diagnostic huds to the engivend
2025-09-23 23:19:38 -05:00
lzk
c55b41dff8 bunch of small cleanups (#40529)
* bunch of cleanups and fixes

* AND REMOVE THIS
2025-09-24 03:36:36 +02:00
PJBot
1e219aaf49 Automatic changelog update 2025-09-24 00:13:53 +00:00
āda
fabef941c2 Move circuit tiles and faux tiles to the cutter machine (#37982)
* super cutter machine

* split the big tile pack

* re-add new faux

* consistent naming

* missing category

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-23 19:12:45 -05:00
āda
7102da139b Fix dev crash when alt+clicking portals (#37540)
* Ghost portal dev crash

* ent<T>

* better comments

* refuse to reuse

* touchup comments

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-23 19:04:53 -05:00
āda
005683d074 Miscellaneous Food/Drink/Edible fixes (#40060)
* bugfixes

* Revert "bugfixes"

This reverts commit 7fe31a866fc4dc299a1667291c0744716092db19.

* Revert "bugfixes"

This reverts commit 7fe31a866fc4dc299a1667291c0744716092db19.

* more reverting

* conflicts

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-24 01:41:11 +02:00
āda
320e67a411 Predict identity (#40185)
* crossing the pond

* share some station records

* share some criminal records

* single system

* comments

* minor touchups

* I always forget this part

* requested changes

* revert predicted spawn

* requested changes

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-24 01:32:20 +02:00
DrSmugleaf
dddb6163f5 Fix SpawnAndDeleteEntityCountTest Entities and last assert being incorrect (#40511)
Fix SpawnAndDeleteEntityCountTest last assert being incorrect
2025-09-23 23:52:15 +02:00
beck-thompson
329908df92 Agent ID verbs now don't require you to pick it up (#40524)
make consitant
2025-09-23 22:55:53 +02:00
Jessica M
2f7b73e830 Weather On Trigger (#40505)
* Weather On Trigger!

* Clearing the weather

* address review, add duration option

---------

Co-authored-by: Jessica M <jessica@maybe.sh>
2025-09-23 13:36:23 -07:00
āda
eee5751a22 TriggerOnPlayerSpawnComplete and ExplosionOnTrigger (#39820) 2025-09-23 22:24:45 +02:00
Prole
3ee7d81944 Target Dummies Now Show Damage Numbers from Projectiles to User (#40101)
* Uh, guess this works for now

* Review Change
2025-09-23 10:20:46 -07:00
PJBot
04d71da982 Automatic changelog update 2025-09-23 17:04:01 +00:00
hoshizora
3f575a64f3 Fire helmets alone no longer prevent you from heating up while on fire (#40481)
* do the thing yeah

* tweaked values on regular fire helmet including cooling

* adjusted values again after further testing also to the atmos fire helmet
2025-09-23 12:02:49 -05:00
slarticodefast
8e9aa1dbb6 Merge stable into master (#40512) 2025-09-23 13:07:00 +02:00
Princess Cheeseballs
add531a434 [HOTFIX] Chameleon projector invisibility (#40509)
* GOGOGO

* Hotfix

* TODO

* Update Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-09-23 12:55:11 +02:00
LordCarve
a3ddba6f42 Cleanup - Use RemoveAllChildren() over DisposeAllChildren() (#39848)
* Content - change the (should-be-obsolete) DisposeAllChildren into the more robust RemoveAllChildren.

* Remove duplicate calls.

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-23 15:40:48 +12:00
B_Kirill
fd40888b0e Cleanup warnings: CS0067, CS8509, CS8073 (#39770)
* Cleanup

* Bonus

---------

Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
2025-09-23 15:11:29 +12:00
B_Kirill
c1a21693fa Cleanup warnings: Use TransformSystem for anchoring (#39778)
* Cleanup

* Bonus

* I hope this helps

* Revert
2025-09-23 14:52:51 +12:00
Minerva
d79fb62d8d Fixes suprise typo in the guidebook (#40501) 2025-09-22 22:07:44 +02:00
Minerva
95d91283a3 Fixes all departamental typos (both just in comments) (#40502)
Fixes all departamental typos
2025-09-22 21:44:45 +02:00
PJBot
f5cad5f12f Automatic changelog update 2025-09-22 06:41:21 +00:00
beck-thompson
a26bafacb1 Shuttle UI now properly goes into pilot mode only when using the UI (#40491)
Shuttle UI bug fix
2025-09-21 23:40:13 -07:00
PJBot
c0b1eae162 Automatic changelog update 2025-09-22 06:23:35 +00:00
Princess Cheeseballs
c7f5545a46 Vulpkanin Admin Smite (#40360)
* Cheeborger

* Vulp smite

* validate those ProtoIds

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-09-21 23:22:25 -07:00
PJBot
c70d2cfb9f Automatic changelog update 2025-09-22 02:41:04 +00:00
Samuka-C
b58bf396bc add silicon smite (#40452)
* add silicon smite

* change string to prototypes

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

* alphabetitize

* fix stuff scar broke

* clean

* make target have the silicon mindrole

* simple check

* defined a private readonly proto for the silicon mind role

* simple check

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
2025-09-22 04:39:56 +02:00
chromiumboy
2824334a1e Health increase for station AI cores (#40487)
* Initial commit

* Increased health further
2025-09-22 03:39:16 +02:00
ToastEnjoyer
83fe027964 Removed suspicion antags from antags.ftl (#40493) 2025-09-21 16:29:21 -07:00
Samuka-C
29e1f6cddf Fool players with status command (#40460) 2025-09-21 23:22:12 +02:00
PJBot
08c1b2c9be Automatic changelog update 2025-09-21 20:42:05 +00:00
Nyxilath
92f246058c bugfix - correcting poster damage resistances (#40489) 2025-09-21 13:40:58 -07:00
PJBot
0ac83937c9 Automatic changelog update 2025-09-21 19:29:01 +00:00
ToastEnjoyer
eabb00a1e2 Changed corpsman description (#40486) 2025-09-21 12:27:53 -07:00
PJBot
c7b239bcbb Automatic changelog update 2025-09-21 15:27:30 +00:00
Kowlin
2245235db1 Add date formatting to admin-notes-unbanned (#40484) 2025-09-21 17:26:22 +02:00
PJBot
d5face573d Automatic changelog update 2025-09-21 15:24:45 +00:00
Charlie Morley
818a715822 prevent repeat TriggerOnCollide triggers (#40428)
* prevent repeat TriggerOnCollide triggers

* review comment: remove TriggerOnCollide when out of triggers
2025-09-21 17:23:37 +02:00
āda
7678251ad5 Average min+max in MaterialArbitrageTest (#39578)
* feels too easy

* I guess this counts

* commit

* could have sworn I ran the test

---------

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-22 02:22:45 +12:00
Leon Friedrich
b6797afe52 Move TestPair & PoolManager to engine (#36797)
* Move TestPair & PoolManager to engine

* Add to global usings

* A

* Move ITestContextLike to engine

* Readd cvars partial class

* cleanup diff
2025-09-21 17:17:43 +12:00
PJBot
f9243dfdd7 Automatic changelog update 2025-09-21 05:17:27 +00:00
Pieter-Jan Briers
8c16b4580b Fix render target caching in overlays (#40181)
Many newer overlays use IRenderTextures that are sized to the rendered viewport. This was completely broken, because a single viewport can be rendered on multiple viewports in a single frame.

The end result of this was that in the better case, constant render targets were allocated and freed, which is  extremely inefficient. In the worse case, many of these overlays completely failed to Dispose() their render targets, leading to *extremely* swift VRAM OOMs.

This fixes all the overlays to properly cache resources per viewport. This uses new engine functionality, so it requires engine master.

This is still a pretty lousy way to do GPU resource management but, well, anything better needs a render graph, so...
2025-09-21 17:16:17 +12:00
Leon Friedrich
cc4cab5677 Fix explosion grid alignment for static grids (#40193) 2025-09-21 14:52:23 +12:00
github-actions[bot]
9893aca467 Update Credits (#40478)
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
2025-09-21 02:59:40 +02:00
2DSiggy
b85fed759a Fixing a syntax error (#40473) 2025-09-20 22:02:17 +02:00
slarticodefast
ae22c7c3d0 Fix RCD errors (#40278) 2025-09-20 12:05:57 -07:00
PJBot
85f3cc7583 Automatic changelog update 2025-09-20 19:01:30 +00:00
Absotively
a746c3cc0f Show hand labeler label text on examine (#40334) 2025-09-20 12:00:22 -07:00
PJBot
1c74e1e100 Automatic changelog update 2025-09-20 18:56:41 +00:00
Absotively
a5129c141c Don't overwrite values that are mid-edit in air alarm window (#40338) 2025-09-20 11:55:34 -07:00
PJBot
886b365099 Automatic changelog update 2025-09-20 18:39:07 +00:00
ToastEnjoyer
e6e47b599d Added AI console to amber (#40393) 2025-09-20 11:37:57 -07:00
Lordbrandon12
365d12a4e9 moves magic number from SharedMoverController to InputMoverComponent (#40411) 2025-09-20 11:35:07 -07:00
SurrealShibe
63c468d963 fixed localization text for vulp shock ear (inner) color (#40412) 2025-09-20 11:28:14 -07:00
Tiniest Shark
4796c92609 Inhand Sprites for Clear Glass (#40427) 2025-09-20 11:25:01 -07:00
Minerva
5b255d13c6 Renames the radar console computer board to "mass scanner computer board" (#40430) 2025-09-20 11:17:58 -07:00
deltanedas
7c650da7d7 fix disposal pipes deleting contents when welded (#40451) 2025-09-20 11:13:23 -07:00
PJBot
11e965cd99 Automatic changelog update 2025-09-20 18:11:31 +00:00
IProduceWidgets
0c7b1e9163 Update Oasis Teg (#40463) 2025-09-20 11:10:21 -07:00
slarticodefast
512f28458c fix chasm heisentest (#40456)
fix chasm test
2025-09-19 22:12:10 -07:00
PJBot
c2fb4a126f Automatic changelog update 2025-09-19 19:31:18 +00:00
IProduceWidgets
2b411b244e The Experimental Lecter 8 (#40372)
* XL8 files

* slap dat on the ERT lead

* oop mag indicator offset

* fix newline
2025-09-19 21:30:07 +02:00
GeneralGaws
c075c89cd0 oasis warp fix (#40454) 2025-09-19 09:55:40 -07:00
Minerva
4f311d6c44 Fixes some refuling welder typos (#40447) 2025-09-19 12:15:22 +02:00
Leon Friedrich
5c54d199a8 Update engine to v267.1.0 (#40445) 2025-09-19 14:54:15 +12:00
Lordbrandon12
ed89c0e061 adds ConveyorMask colission mask to it's fixture component (#40439) 2025-09-18 13:31:29 -07:00
PJBot
867d0f5130 Automatic changelog update 2025-09-18 20:10:52 +00:00
2DSiggy
e1da7ec9c5 Better thief objectives (#39867)
* finally some good objectives

* oopsie

* guh fuck

* more steal objectives that are actually good

* i want to die

* fucking upload to github please

* adding it to the objectiveGroups.yml

* higher weights for testing

* upload

* just need to do text shit. coding done waow

* OBJECTIVES WORK AND HAVE TEXT. FIX VIS

* I THINK ITS DONE AAAAAAAAAAAAAAA

* grammatical fix

* more formatting fixes

* i might be stupid

* forgot to fix a weight issue

* more grammar grrrrrrrrrrrrr

* made the double barrel obj have higher difficulty since it's cared about more than beer goggles

* Requested Changes

* forgot a thing oopsie
2025-09-18 13:09:44 -07:00
PJBot
393e6cbc07 Automatic changelog update 2025-09-18 20:00:37 +00:00
Kittygyat
5a67e3c26a Made all tarantulas able to drag entities (#40433)
Sent tarantulas to the gym
2025-09-18 21:59:29 +02:00
PJBot
c19cdad787 Automatic changelog update 2025-09-18 19:38:12 +00:00
Hi-Im-Shot
d95b5da7d2 Added Cutting Slicing and Executing options to the cane blade (#40311)
* Added Cutting Slicing and Executing options to the cane blade

* swaped from BaseItem to BaseSword for cleaner code

* fixed a double gap in code
2025-09-18 21:37:04 +02:00
PJBot
b2c8565a2d Automatic changelog update 2025-09-18 19:02:29 +00:00
SlamBamActionman
9d0a7b7729 Add contraband levels for several reagents (#40426)
* Initial commit

* Minor changes
2025-09-18 12:01:22 -07:00
PJBot
76b680b03b Automatic changelog update 2025-09-18 18:48:33 +00:00
rumaks
fbb9c9c524 Make ichor heal brute, burn, and toxin evenly (#39466)
* Make ichor heal brute, burn, and toxin evenly

* Nerf healing to more reasonable values
2025-09-18 11:47:25 -07:00
PJBot
8cf9da90d3 Automatic changelog update 2025-09-18 17:50:14 +00:00
PJBot
6d576fc8ce Automatic changelog update 2025-09-18 17:49:08 +00:00
Hitlinemoss
5cb0917d5f Ninja items are now highly illegal (#39855)
* Added tactical katana + tactical katana shipment (placeholder descriptions)

* Revert "Added tactical katana + tactical katana shipment (placeholder descriptions)"

This reverts commit aa1928be7f4d938df1838943781e63c47a03cc11.

Whoops, committed to master by mistake

* Made ninja items highly illegal

* Rerun checks
2025-09-18 19:49:01 +02:00
āda
128d06518e Silence mime bags (#40317)
silence!!!

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-18 10:47:55 -07:00
Mora
0e0f015422 Rename medifab implanter to implant extractor and made it's description clearer (#40375)
* Renamed regular implanter (the extractor from the medifab) to implant extractor

* wrong way
2025-09-18 19:47:38 +02:00
PJBot
e09ea850f5 Automatic changelog update 2025-09-18 17:40:43 +00:00
Kittygyat
d9d968a479 Crashed the snakeskin boots stock-market by removing their hidden no-slip properties (#40201)
Crashed the snakeskin boots stockmarket by removing their non-slip properties
2025-09-18 19:39:35 +02:00
PJBot
f13f7830d6 Automatic changelog update 2025-09-18 17:36:27 +00:00
Skye
8cf5c3f6bc Add chemical analysis goggles to ChemDrobe (#40236) 2025-09-18 10:35:19 -07:00
PJBot
c4a42e556f Automatic changelog update 2025-09-18 17:18:47 +00:00
pathetic meowmeow
940eaa4674 Bring vulpkanin in-line with other species on hugging (#40183) 2025-09-18 19:17:37 +02:00
PJBot
eb1bd0a565 Automatic changelog update 2025-09-18 07:16:18 +00:00
Alex
2349898dcc Plasma: add tropico to atmos (#40436) 2025-09-18 00:15:09 -07:00
PJBot
3844f1e7a5 Automatic changelog update 2025-09-18 02:38:47 +00:00
Pixel8-dev
b41ce9cce6 Stun rune Fix (#40432)
Added a single line of code
2025-09-18 04:37:40 +02:00
Minerva
4a815c006f Renames the "Integrated GPS" to "integrated GPS" (#40431)
Renames the Integrated GPS to not use title case
2025-09-17 18:42:35 -07:00
PJBot
27b86bcca8 Automatic changelog update 2025-09-18 00:26:39 +00:00
Kittygyat
e59bc06c25 Updated the cyborg weapon module's uplink description to be accurate (#40429) 2025-09-17 17:25:31 -07:00
PJBot
0a61f2a583 Automatic changelog update 2025-09-18 00:16:25 +00:00
PicklOH
1a92ada5bd Adds Nukie IDs and PDAs, makes Nukie IDs able to copy accesses. (#37304)
* Adds Nukie IDs and PDAs, makes IDs able to copy access.

* Fixed PDA and ID parenting

* Meta.json spacing

* PDA parenting

* retest

* Forgot a comma OOPS

* Spacing

* Minor meff fix

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-09-17 17:15:18 -07:00
PJBot
9c98f5f9f4 Automatic changelog update 2025-09-17 23:35:33 +00:00
SurrealShibe
857ae2a088 Turn the Satanic Bible's pentagram around, fix left inhand (#40234)
change sprites, update meta
2025-09-17 19:34:25 -04:00
PJBot
ffb5bd7325 Automatic changelog update 2025-09-17 22:00:15 +00:00
Errant
b692b6e33e Antag Rolebans (#35966)
Co-authored-by: beck-thompson <beck314159@hotmail.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
2025-09-17 23:59:07 +02:00
PJBot
e1ba33814b Automatic changelog update 2025-09-17 21:50:58 +00:00
IProduceWidgets
933da32da5 Remove Misgendering (#40425)
fix misgendering
2025-09-17 23:49:50 +02:00
PJBot
684a4a382d Automatic changelog update 2025-09-17 17:01:33 +00:00
Minemoder5000
1dd977effd Remove drone lawset from ion storms (#40374) 2025-09-17 10:00:22 -07:00
Winkarst-cpu
ca47e59e43 Update `DoorComponent` to use TimeSpans and fix comments (#40420)
Cleanup
2025-09-17 15:28:11 +02:00
Winkarst-cpu
0dd1733998 Change `GetPryTimeModifierEvent.BaseTime` to the TimeSpan (#40419)
* Cleanup

* Update
2025-09-17 15:03:51 +02:00
ScarKy0
599b962234 Localize vulp emotes (#40418)
init
2025-09-17 14:52:29 +02:00
āda
09eee5074d Use an alias in job icons yml (#40415)
commit

Co-authored-by: iaada <iaada@users.noreply.github.com>
2025-09-17 12:29:08 +02:00
PJBot
f21c6f2030 Automatic changelog update 2025-09-17 04:48:25 +00:00
Alex
ce05248428 Fland: on evac fix delta pressure destroying the air storage cell (#40413) 2025-09-16 21:47:17 -07:00
slarticodefast
a4368264f0 Add chasm integration tests (#40286)
* add chasm integration test

* fix assert

* fix

* more fixes

* review
2025-09-17 14:19:46 +10:00
Princess Cheeseballs
fc89f231a5 Mothership Core Prototype Cleanup (#40410)
* I cannot escape bodysystem no matter how hard I try

* Move 2 things

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-09-17 00:25:53 +02:00
Kyle Tyo
7ff98dd94f Readyall and Toggleready commands to LEC. Fix an issue with ready button desync. (#38706)
* commit

* commit

* change lobby shell string.
2025-09-16 23:25:17 +02:00
PJBot
bf18b5e26b Automatic changelog update 2025-09-16 19:05:57 +00:00
ScarKy0
dfc7d183ad Intellicards rename to AI stored on them (#40402)
* intellicard name changing

* review
2025-09-16 21:04:50 +02:00
Winkarst-cpu
972adcee21 `NarcolepsySystem` refactor (#40305)
* Refactor

* Update

* Update
2025-09-16 20:29:48 +02:00
PJBot
138ea68076 Automatic changelog update 2025-09-16 15:36:59 +00:00
ScarKy0
377dd6b36c Add intellicards to AI crates (#40401)
init
2025-09-16 10:35:51 -05:00
chromiumboy
bc0691822a Bug fix for Station AI damaged accent (#40399)
Initial commit
2025-09-16 16:09:38 +02:00
PJBot
d80f53bb48 Automatic changelog update 2025-09-16 08:00:22 +00:00
slarticodefast
d8ab007c50 Add missing admin changelog entry (#40395) 2025-09-16 00:59:11 -07:00
PJBot
731d6ff53c Automatic changelog update 2025-09-15 23:32:58 +00:00
SlamBamActionman
09a197eb91 Detunes Ninja Stun To Actually Have Some Counterplay (#39707) 2025-09-16 01:31:50 +02:00
Myra
584f0aaa7b Clerify salamander description (#40379)
* Clerify salamander description

Had someone in the help channel get confused with the previous wording. Thought this may be better.

* "Review"
2025-09-16 00:34:50 +02:00
PJBot
fb71020889 Automatic changelog update 2025-09-15 14:19:40 +00:00
chromiumboy
7444c8ea4a The station AI can be destroyed (#39588)
* Initial commit

* Fixing merge conflict

* Merge conflict fixed

* Anchorable entities can now be marked as 'unanchorable'

* Revert "Anchorable entities can now be marked as 'unanchorable'"

This reverts commit 6a502e62a703cf06bd36ed3bdefe655fc074cfc5

This functionality will be made into a separate PR

* Error sprite

* Update AI core appearance with sustained damage, spawn scrap on destroyed

* Added intellicard sprite

* AI damage overlays

* Added fixtures

* AI core accent changes when damaged or low on power

* Bug fix and pop up messages for inserting AIs into inoperable cores

* Updated 'dead' sprite

* Destroying the AI core reduces the number of AI job slots available

* AI battery duration set to 10 minutes

* Initial commit

* Allow MMIs used in the construction of AI cores to take them over

* Initial resources commit

* Initial code commit

* Sprite update

* Bug fixes and updates

* Basic console UI

* Code refactor

* Added lock screen

* Added all outstanding UI features

* Added purge sprites

* Better appearance handling

* Fixed issue with purge sprite

* Finalized UI design

* Major components finalized

* Bit of clean up

* Removed some code that was used for testing

* Tweaked some text

* Removed extra space

* Added the circuitboard to the RD's locker

* Addressed reviewer comments plus tweaks

* Addressed reviewer comments plus tweaks

* Removed instances of granular damage

* Various improvements

* Removed testing code

* Fixed issue with disabled buttons

* Finalized code

* Addressed review comments

* Added a spare Station AI core electronics to the research director's locker

* Fixing build failure

* Addressed review comments

* Addressed review comments

* Added reverse path for construction graph

* Removed unneeded reference

* Parts can be purchased through cargo

* Fixing merge conflict

* Merge conflict resolved

* Fixing merge conflict

* Code update

* Code updates

* Increased AI core health and gave it a sell price to fix test fail

* Added screen static sprite

* Added better support for ghosted AI players plus code tweaks

* Various improvements and clean up

* Increased purge duration to 60 seconds

* Fixed needless complication

* Addressed reviewer comments part 1

* Addressed reviewer comments part 2

* Further fixes

* Trying lower battery values to see if it fixes the test fail

* Adjusted power values again

* Addressed review comments

* Addressed review comments

* Fixed test fail

* Fixed bug with endless rebooting. Using rejuvenation on an AI core revives the AI inside.

* Added pop up text

* Bug fix

* Tweaks and fixes

* Fixed restoration console not updating when the AI finishes rebooting

* Update SharedStationAiSystem.Held.cs

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
2025-09-15 16:18:32 +02:00
slarticodefast
e0fd44da66 merge stable into master (#40369) 2025-09-15 13:45:40 +02:00
Errant
9b5f9c3fd6 [Hotfix] Remove pull-escape trick (#40368)
* make HandleStopPull byref

* we get signal
2025-09-15 13:31:22 +02:00
PJBot
e27576929f Automatic changelog update 2025-09-15 07:20:35 +00:00
chromiumboy
02061592dd Devices with access restrictions list those restrictions in their examination description (#37712) 2025-09-15 10:19:25 +03:00
ToastEnjoyer
ff94d3e7ad Added spanky to mapping codeowners (#40362) 2025-09-14 18:42:46 -07:00
PJBot
ea89711029 Automatic changelog update 2025-09-15 01:31:19 +00:00
SharkSnake98
fd20cc2a00 Dark/Light Grass & Desert Astrotiles (#37867)
* Added Waterjug, a low-mid pop map with a tropical theme and custom evac shuttle

* Fixed postmapinittest issues (Hopefully)

* Actually fixed the afformentioned issue.

* Added Warden Spawnpoint which I forgot

* Named APCs, Substations, & Cameras, added some more decals

* Decorated some more, notably the bar.

* Minor adjustments, added cans, slightly reworked salv and maints bar

* Fixed some small issues, notably weird closed doors, added a few small things (shutters mostly)

* Added 2 new astrotiles, dark grass and desert sand.

* Removed map. Fixing issue. Please hold.

* Forgot to remove a comma, please god forgive me maptainers. I blame Rider IDE for it's autoaddition of all changes made even on seperate branches.

* Added localization for stacks.

* Actually fixed the loc. issue. Maybe. Please.

* Hopefully fixed the last localization issue.

* Added Light Astro-tiles, and edited the names of the inhand png's for the dark grass astrotiles to be more internally consistant

* Fixed some issues caused by another PR I made, added more maints stuff

* Made some small decorative and practical changes

* Fixed, changed, and added a ton of stuff. I don't think I can list it all, honestly.

* Removed shields to try to fix an issue with the test

* Hopefully fixed issues relating to a failed test.

* Replaced grass/flora decals with randomized ones, readded shields to armory

* Fixed some YML issues, whitelisted files for flora decal spawners

* Added a bridge-beach, added some misc. items and objects.

* Small changes to buttons, fixed wires and flooring

* Fixed AME-Holopad issue.

* Added a Custom Waterjug Parallax, made it so the parallaxes actually work, and made some minor adjustments to the map

* Fixed an accidental adjustment to CoreStation's parallax prototype YML

* Changed some Salvage and Cargo stuff

* Fixed some merge issues, updated Adriatic with a locker and added some little details to Waterjug

* Fixed some stuff, added docking arm near evac

* meta json fix tiles

* fixed again

* fixed once more

* Removed all the waterjug stuff.

* fix spacing

* fix unnecessary formatting

---------

Co-authored-by: SharkSnake98 <sharksnake87@gmail.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2025-09-14 18:30:12 -07:00
PJBot
31d30f24f9 Automatic changelog update 2025-09-15 01:14:21 +00:00
ToastEnjoyer
9313c07924 Replaced incendiary AK ammo with normal AK ammo, bagel. (#40359) 2025-09-14 18:13:12 -07:00
PJBot
e05d9e944b Automatic changelog update 2025-09-15 00:37:56 +00:00
Nox
f1d52e0c13 Plasma Armory Restock (#39763)
* made some tweaks and fixes to the equipment in armory, security, and genpop.

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

* implemented minor fixes

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

* Added changes from #40012

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

* Removed warden gun, tweaked some things, fixed camera coverage and atmos devices etc

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

* Got comp to sign off on changes, finished everything up, good to merge!

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

* Tested for issues and fixed

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

---------

Signed-off-by: Nox38 <nebulousnox38@gmail.com>
2025-09-14 18:37:41 -06:00
SlamBamActionman
97d4153d84 Add jetpacks to the Nukie Infiltrator (#39887) 2025-09-14 18:36:47 -06:00
PJBot
b4f4d6e295 Automatic changelog update 2025-09-14 23:47:14 +00:00
ToastEnjoyer
2ffe0db61f Linked radiation shields on bagel (#40358) 2025-09-14 16:46:03 -07:00
Princess Cheeseballs
11d434818e Merge staging into master (#40356) 2025-09-14 13:57:50 -07:00
PJBot
cc6aa626da Automatic changelog update 2025-09-14 19:27:49 +00:00
Winkarst-cpu
9c3af67cd1 Fix wizard's recharge spell not adding charges to wands that use LimitedChargesComponent (#40347)
* Fix

* Update
2025-09-14 21:26:42 +02:00
Stefano Pigozzi
8c67c5b5a2 Add myself to credits (#40345)
Co-authored-by: Steffo99 <1540885+Steffo99@users.noreply.github.com>
2025-09-14 20:27:10 +02:00
PJBot
3732885713 Automatic changelog update 2025-09-14 15:19:56 +00:00
Huaqas
7616b9aa1c Fix Heterochromia for Vulpkanin (#40320)
* Add More Holy Books

* Revert "Add More Holy Books"

This reverts commit 665eb0de10fe0784d634f9eaf672e60f18a62995.

* Fix eyes

* Add the undergarments and ftl designations.

* Missed something.

* second edited line didnt save for some reason

* Im tired dont judge me

* Fix eyes

* Reverting stuff

* fix markings

* Its joever

* Update meta.json

* small tweak

---------

Co-authored-by: ScarKy0 <scarky0@onet.eu>
2025-09-14 11:18:48 -04:00
PJBot
1908317e3c Automatic changelog update 2025-09-14 07:40:46 +00:00
ScarKy0
fdd4789d32 Give vulps correct undergarments (#40341)
init
2025-09-14 00:39:38 -07:00
PJBot
29da03b4e4 Automatic changelog update 2025-09-14 05:45:42 +00:00
MissKay1994
c317fa9840 Massively reduce how lethal Man-O-War shuttle is (#40339)
no longer nukies in disguise
2025-09-13 23:44:32 -06:00
github-actions[bot]
e7dc6ae990 Update Credits (#40342)
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
2025-09-14 03:18:18 +02:00
Winkarst-cpu
3aece8d46c Fix throwing objects causing pushback on the player who threw them in a not weightless environment (#40335)
* Fix

* Update Content.Shared/Gravity/SharedGravitySystem.cs

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

* Update

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2025-09-13 21:51:13 +02:00
PJBot
52a4e95651 Automatic changelog update 2025-09-13 17:37:36 +00:00
Huaqas
f1ae8ecdfe Add Undergarments to Vulpkanin (#40321)
Putting underwear on dogs.
2025-09-13 13:36:28 -04:00
PJBot
6768ff1e91 Automatic changelog update 2025-09-13 15:54:55 +00:00
slarticodefast
d17182c162 Change listplayers command permissions to require the PII flag (#40324) 2025-09-13 17:53:48 +02:00
IProduceWidgets
a4b7cd73c5 +1 Spam mail (#40310)
* 2nd edition

* remove OOC

* oop

* double oops
2025-09-13 09:15:10 +02:00
PJBot
659648b03d Automatic changelog update 2025-09-13 07:02:21 +00:00
FungiFellow
bcc30813e9 Cockroach Gib when Stepped on (#40103)
* Cockroach Gib

* Prevent Cockroaches From Gibbing Eachother

* Added - type: RandomChanceTriggerCondition

* Update animals.yml

* Named SuccessChance Datafield

* successChance

* Revert Change

* Uncapitalize C

* RECAPITALIZE THE C
2025-09-13 00:01:13 -07:00
PJBot
0ba1a7c4dd Automatic changelog update 2025-09-12 23:25:05 +00:00
Princess Cheeseballs
ab40b1ab73 Chameleon Projector Physics Fix (#37960)
* One commit

* Move files

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-09-13 01:23:57 +02:00
PJBot
79a34556e5 Automatic changelog update 2025-09-12 22:48:32 +00:00
SurrealShibe
71bcda1fec Toilet fixes: Exception when constructing, proper seat layering (#40313)
they call me a plumber the
the way i fix da toiler
2025-09-12 15:47:21 -07:00
slarticodefast
82e7cb020c Delete DrinkComponent, migrate prototypes to EdibleComponent (#40308) 2025-09-12 15:26:56 -07:00
Princess Cheeseballs
928e6c8079 Edible Sound Specifier Override (#40312)
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2025-09-12 23:49:12 +02:00
Admiral-Obvious-001
c6fc95e32b Addressed Requested Changes 2025-08-17 13:41:39 -07:00
Admiral-Obvious-001
5e84fae772 Commit 2 2025-08-17 12:47:57 -07:00
Admiral-Obvious-001
2f3db89ca2 Test First commit 2025-08-17 12:42:19 -07:00
511 changed files with 22880 additions and 10226 deletions

View File

@@ -0,0 +1,3 @@
// Global usings for Content.Benchmarks
global using Robust.UnitTesting.Pool;

View File

@@ -25,7 +25,7 @@ namespace Content.Client.Access.UI
public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
{
_accessButtons.Clear();
AccessLevelGrid.DisposeAllChildren();
AccessLevelGrid.RemoveAllChildren();
foreach (var access in accessLevels)
{

View File

@@ -41,7 +41,7 @@ namespace Content.Client.Access.UI
public void SetAllowedIcons(string currentJobIconId)
{
IconGrid.DisposeAllChildren();
IconGrid.RemoveAllChildren();
var jobIconButtonGroup = new ButtonGroup();
var i = 0;

View File

@@ -99,8 +99,8 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
private bool TryRebuildAccessGroupControls()
{
AccessGroupList.DisposeAllChildren();
AccessLevelChecklist.DisposeAllChildren();
AccessGroupList.RemoveAllChildren();
AccessLevelChecklist.RemoveAllChildren();
// No access level prototypes were assigned to any of the access level groups.
// Either the turret controller has no assigned access levels or their names were invalid.
@@ -165,7 +165,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
/// </summary>
public void RebuildAccessLevelsControls()
{
AccessLevelChecklist.DisposeAllChildren();
AccessLevelChecklist.RemoveAllChildren();
_accessLevelEntries.Clear();
// No access level prototypes were assigned to any of the access level groups

View File

@@ -33,6 +33,7 @@ namespace Content.Client.Actions
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
public event Action<EntityUid>? OnActionAdded;
public event Action<EntityUid>? OnActionRemoved;
@@ -286,8 +287,27 @@ namespace Content.Client.Actions
continue;
}
if (assignmentNode is SequenceDataNode sequenceAssignments)
{
try
{
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(sequenceAssignments, notNullableOverride: true);
foreach (var index in nodeAssignments)
{
assignments.Add(new SlotAssignment(index.Hotbar, index.Slot, actionId));
}
}
catch (Exception ex)
{
Log.Error($"Failed to parse action assignments: {ex}");
}
}
AddActionDirect((user, actions), actionId);
}
AssignSlot?.Invoke(assignments);
}
private void OnWorldTargetAttempt(Entity<WorldTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
@@ -309,10 +329,10 @@ namespace Content.Client.Actions
// this is the actual entity-world targeting magic
EntityUid? targetEnt = null;
if (TryComp<EntityTargetActionComponent>(ent, out var entity) &&
args.Input.EntityUid != null &&
ValidateEntityTarget(user, args.Input.EntityUid, (uid, entity)))
args.Input.EntityUid is { Valid: true } entityUid &&
ValidateEntityTarget(user, entityUid, (uid, entity)))
{
targetEnt = args.Input.EntityUid;
targetEnt = entityUid;
}
if (action.ClientExclusive)

View File

@@ -1,6 +1,5 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc admin-camera-window-title-placeholder}"
SetSize="425 550"
MinSize="200 225"
Name="Window">
MinSize="200 225">
</DefaultWindow>

View File

@@ -24,7 +24,7 @@ namespace Content.Client.Administration.UI.BanPanel;
[GenerateTypedNameReferences]
public sealed partial class BanPanel : DefaultWindow
{
public event Action<string?, (IPAddress, int)?, bool, ImmutableTypedHwid?, bool, uint, string, NoteSeverity, string[]?, bool>? BanSubmitted;
public event Action<Ban>? BanSubmitted;
public event Action<string>? PlayerChanged;
private string? PlayerUsername { get; set; }
private (IPAddress, int)? IpAddress { get; set; }
@@ -37,8 +37,8 @@ public sealed partial class BanPanel : DefaultWindow
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
// Role group name -> the role buttons themselves.
private readonly Dictionary<string, List<Button>> _roleCheckboxes = new();
private readonly ISawmill _banpanelSawmill;
private readonly Dictionary<string, List<(Button, IPrototype)>> _roleCheckboxes = new();
private readonly ISawmill _banPanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
@@ -79,7 +79,7 @@ public sealed partial class BanPanel : DefaultWindow
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
_banPanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ =>
@@ -110,7 +110,7 @@ public sealed partial class BanPanel : DefaultWindow
TypeOption.SelectId(args.Id);
OnTypeChanged();
};
LastConnCheckbox.OnPressed += args =>
LastConnCheckbox.OnPressed += _ =>
{
IpLine.ModulateSelfOverride = null;
HwidLine.ModulateSelfOverride = null;
@@ -164,7 +164,7 @@ public sealed partial class BanPanel : DefaultWindow
var antagRoles = _protoMan.EnumeratePrototypes<AntagPrototype>()
.OrderBy(x => x.ID);
CreateRoleGroup("Antagonist", Color.Red, antagRoles);
CreateRoleGroup(AntagPrototype.GroupName, AntagPrototype.GroupColor, antagRoles);
}
/// <summary>
@@ -236,14 +236,14 @@ public sealed partial class BanPanel : DefaultWindow
{
foreach (var role in _roleCheckboxes[groupName])
{
role.Pressed = args.Pressed;
role.Item1.Pressed = args.Pressed;
}
if (args.Pressed)
{
if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
_banpanelSawmill
_banPanelSawmill
.Warning("Departmental role ban severity could not be parsed from config!");
return;
}
@@ -255,14 +255,14 @@ public sealed partial class BanPanel : DefaultWindow
{
foreach (var button in roleButtons)
{
if (button.Pressed)
if (button.Item1.Pressed)
return;
}
}
if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
{
_banpanelSawmill
_banPanelSawmill
.Warning("Role ban severity could not be parsed from config!");
return;
}
@@ -294,7 +294,7 @@ public sealed partial class BanPanel : DefaultWindow
}
/// <summary>
/// Adds a checkbutton specifically for one "role" in a "group"
/// Adds a check button specifically for one "role" in a "group"
/// E.g. it would add the Chief Medical Officer "role" into the "Medical" group.
/// </summary>
private void AddRoleCheckbox(string group, string role, GridContainer roleGroupInnerContainer, Button roleGroupCheckbox)
@@ -302,23 +302,36 @@ public sealed partial class BanPanel : DefaultWindow
var roleCheckboxContainer = new BoxContainer();
var roleCheckButton = new Button
{
Name = $"{role}RoleCheckbox",
Name = role,
Text = role,
ToggleMode = true,
};
roleCheckButton.OnToggled += args =>
{
// Checks the role group checkbox if all the children are pressed
if (args.Pressed && _roleCheckboxes[group].All(e => e.Pressed))
if (args.Pressed && _roleCheckboxes[group].All(e => e.Item1.Pressed))
roleGroupCheckbox.Pressed = args.Pressed;
else
roleGroupCheckbox.Pressed = false;
};
IPrototype rolePrototype;
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype))
rolePrototype = jobPrototype;
else if (_protoMan.TryIndex<AntagPrototype>(role, out var antagPrototype))
rolePrototype = antagPrototype;
else
{
_banPanelSawmill.Error($"Adding a role checkbox for role {role}: role is not a JobPrototype or AntagPrototype.");
return;
}
// This is adding the icon before the role name
// TODO: This should not be using raw strings for prototypes as it means it won't be validated at all.
// I know the ban manager is doing the same thing, but that should not leak into UI code.
if (_protoMan.TryIndex<JobPrototype>(role, out var jobPrototype) && _protoMan.Resolve(jobPrototype.Icon, out var iconProto))
// // I know the ban manager is doing the same thing, but that should not leak into UI code.
if (jobPrototype is not null && _protoMan.TryIndex(jobPrototype.Icon, out var iconProto))
{
var jobIconTexture = new TextureRect
{
@@ -335,7 +348,7 @@ public sealed partial class BanPanel : DefaultWindow
roleGroupInnerContainer.AddChild(roleCheckboxContainer);
_roleCheckboxes.TryAdd(group, []);
_roleCheckboxes[group].Add(roleCheckButton);
_roleCheckboxes[group].Add((roleCheckButton, rolePrototype));
}
public void UpdateBanFlag(bool newFlag)
@@ -488,7 +501,7 @@ public sealed partial class BanPanel : DefaultWindow
newSeverity = serverSeverity;
else
{
_banpanelSawmill
_banPanelSawmill
.Warning("Server ban severity could not be parsed from config!");
}
@@ -501,7 +514,7 @@ public sealed partial class BanPanel : DefaultWindow
}
else
{
_banpanelSawmill
_banPanelSawmill
.Warning("Role ban severity could not be parsed from config!");
}
break;
@@ -546,34 +559,51 @@ public sealed partial class BanPanel : DefaultWindow
private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
{
string[]? roles = null;
ProtoId<JobPrototype>[]? jobs = null;
ProtoId<AntagPrototype>[]? antags = null;
if (TypeOption.SelectedId == (int) Types.Role)
{
var rolesList = new List<string>();
var jobList = new List<ProtoId<JobPrototype>>();
var antagList = new List<ProtoId<AntagPrototype>>();
if (_roleCheckboxes.Count == 0)
throw new DebugAssertException("RoleCheckboxes was empty");
foreach (var button in _roleCheckboxes.Values.SelectMany(departmentButtons => departmentButtons))
{
if (button is { Pressed: true, Text: not null })
if (button.Item1 is { Pressed: true, Name: not null })
{
rolesList.Add(button.Text);
switch (button.Item2)
{
case JobPrototype:
jobList.Add(button.Item2.ID);
break;
case AntagPrototype:
antagList.Add(button.Item2.ID);
break;
}
}
}
if (rolesList.Count == 0)
if (jobList.Count + antagList.Count == 0)
{
Tabs.CurrentTab = (int) TabNumbers.Roles;
return;
}
roles = rolesList.ToArray();
jobs = jobList.ToArray();
antags = antagList.ToArray();
}
if (TypeOption.SelectedId == (int) Types.None)
{
TypeOption.ModulateSelfOverride = Color.Red;
Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
return;
}
@@ -585,6 +615,7 @@ public sealed partial class BanPanel : DefaultWindow
ReasonTextEdit.GrabKeyboardFocus();
ReasonTextEdit.ModulateSelfOverride = Color.Red;
ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
return;
}
@@ -593,6 +624,7 @@ public sealed partial class BanPanel : DefaultWindow
ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
SubmitButton.ModulateSelfOverride = Color.Red;
SubmitButton.Text = Loc.GetString("ban-panel-confirm");
return;
}
@@ -601,7 +633,22 @@ public sealed partial class BanPanel : DefaultWindow
var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
var severity = (NoteSeverity) SeverityOption.SelectedId;
var erase = EraseCheckbox.Pressed;
BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles, erase);
var ban = new Ban(
player,
IpAddress,
useLastIp,
Hwid,
useLastHwid,
(uint)(TimeEntered * Multiplier),
reason,
severity,
jobs,
antags,
erase
);
BanSubmitted?.Invoke(ban);
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -14,8 +14,7 @@ public sealed class BanPanelEui : BaseEui
{
BanPanel = new BanPanel();
BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase)
=> SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles, erase));
BanPanel.BanSubmitted += ban => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(ban));
BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
}

View File

@@ -67,7 +67,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// </summary>
public void UpdateReagents()
{
ReagentList.DisposeAllChildren();
ReagentList.RemoveAllChildren();
if (_selectedSolution == null || _solutions == null)
return;
@@ -92,7 +92,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// <param name="solution">The selected solution.</param>
private void UpdateVolumeBox(Solution solution)
{
VolumeBox.DisposeAllChildren();
VolumeBox.RemoveAllChildren();
var volumeLabel = new Label();
volumeLabel.HorizontalExpand = true;
@@ -131,7 +131,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
/// <param name="solution">The selected solution.</param>
private void UpdateThermalBox(Solution solution)
{
ThermalBox.DisposeAllChildren();
ThermalBox.RemoveAllChildren();
var heatCap = solution.GetHeatCapacity(null);
var specificHeatLabel = new Label();
specificHeatLabel.HorizontalExpand = true;

View File

@@ -82,7 +82,11 @@ public sealed partial class AdminNotesLine : BoxContainer
if (Note.UnbannedTime is not null)
{
ExtraLabel.Text = Loc.GetString("admin-notes-unbanned", ("admin", Note.UnbannedByName ?? "[error]"), ("date", Note.UnbannedTime));
ExtraLabel.Text = Loc.GetString(
"admin-notes-unbanned",
("admin", Note.UnbannedByName ?? "[error]"),
("date", Note.UnbannedTime.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))
);
ExtraLabel.Visible = true;
}
else if (Note.ExpiryTime is not null)
@@ -139,7 +143,7 @@ public sealed partial class AdminNotesLine : BoxContainer
private string FormatRoleBanMessage()
{
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
return FormatBanMessageCommon(banMessage);
}

View File

@@ -30,7 +30,10 @@ public sealed partial class ThresholdBoundControl : BoxContainer
public void SetValue(float value)
{
_value = value;
CSpinner.Value = ScaledValue;
if (!CSpinner.HasKeyboardFocus())
{
CSpinner.Value = ScaledValue;
}
}
public void SetEnabled(bool enabled)

View File

@@ -206,7 +206,7 @@ namespace Content.Client.Cargo.UI
if (!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
return;
Requests.DisposeAllChildren();
Requests.RemoveAllChildren();
foreach (var order in orders)
{

View File

@@ -30,7 +30,7 @@ namespace Content.Client.Cargo.UI
public void SetOrders(SpriteSystem sprites, IPrototypeManager protoManager, List<CargoOrderData> orders)
{
Orders.DisposeAllChildren();
Orders.RemoveAllChildren();
foreach (var order in orders)
{

View File

@@ -1,4 +1,4 @@
using Content.Client.CrewManifest.UI;
using Content.Client.CrewManifest.UI;
using Content.Shared.CrewManifest;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
@@ -21,7 +21,6 @@ public sealed partial class CrewManifestUiFragment : BoxContainer
public void UpdateState(string stationName, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;

View File

@@ -55,7 +55,7 @@ namespace Content.Client.Changelog
// Changelog is not kept in memory so load it again.
var changelogs = await _changelog.LoadChangelog();
Tabs.DisposeAllChildren();
Tabs.RemoveAllChildren();
var i = 0;
foreach (var changelog in changelogs)

View File

@@ -95,7 +95,7 @@ namespace Content.Client.ContextMenu.UI
/// </summary>
public void Close()
{
RootMenu.MenuBody.DisposeAllChildren();
RootMenu.MenuBody.RemoveAllChildren();
CancelOpen?.Cancel();
CancelClose?.Cancel();
OnContextClosed?.Invoke();

View File

@@ -293,7 +293,7 @@ namespace Content.Client.ContextMenu.UI
var element = new EntityMenuElement(entity);
element.SubMenu = new ContextMenuPopup(_context, element);
element.SubMenu.OnPopupOpen += () => _verb.OpenVerbMenu(entity, popup: element.SubMenu);
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.DisposeAllChildren;
element.SubMenu.OnPopupHide += element.SubMenu.MenuBody.RemoveAllChildren;
_context.AddElement(menu, element);
Elements.TryAdd(entity, element);
}

View File

@@ -53,7 +53,7 @@ namespace Content.Client.Crayon.UI
private void RefreshList()
{
// Clear
Grids.DisposeAllChildren();
Grids.RemoveAllChildren();
if (_decals == null || _allDecals == null)
return;

View File

@@ -18,7 +18,6 @@ public sealed partial class CrewManifestUi : DefaultWindow
public void Populate(string name, CrewManifestEntries? entries)
{
CrewManifestListing.DisposeAllChildren();
CrewManifestListing.RemoveAllChildren();
StationNameContainer.Visible = entries != null;

View File

@@ -31,7 +31,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.OpeningAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
Length = comp.OpeningAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -47,7 +47,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.ClosingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
Length = comp.ClosingAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -63,7 +63,7 @@ public sealed class DoorSystem : SharedDoorSystem
comp.EmaggingAnimation = new Animation
{
Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
Length = comp.EmaggingAnimationTime,
AnimationTracks =
{
new AnimationTrackSpriteFlick
@@ -116,14 +116,14 @@ public sealed class DoorSystem : SharedDoorSystem
return;
case DoorState.Opening:
if (entity.Comp.OpeningAnimationTime == 0.0)
if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
return;
case DoorState.Closing:
if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero || entity.Comp.CurrentlyCrushing.Count != 0)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);

View File

@@ -72,7 +72,7 @@ public sealed partial class GatewayWindow : FancyWindow,
_isUnlockPending = _nextUnlock >= _timing.CurTime;
_isCooldownPending = _nextReady >= _timing.CurTime;
Container.DisposeAllChildren();
Container.RemoveAllChildren();
if (_destinations.Count == 0)
{

View File

@@ -0,0 +1,90 @@
using Robust.Client.Graphics;
namespace Content.Client.Graphics;
/// <summary>
/// A cache for <see cref="Overlay"/>s to store per-viewport render resources, such as render targets.
/// </summary>
/// <typeparam name="T">The type of data stored in the cache.</typeparam>
public sealed class OverlayResourceCache<T> : IDisposable where T : class, IDisposable
{
private readonly Dictionary<long, CacheEntry> _cache = new();
/// <summary>
/// Get the data for a specific viewport, creating a new entry if necessary.
/// </summary>
/// <remarks>
/// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
/// </remarks>
/// <param name="viewport">The viewport for which to retrieve cached data.</param>
/// <param name="factory">A delegate used to create the cached data, if necessary.</param>
public T GetForViewport(IClydeViewport viewport, Func<IClydeViewport, T> factory)
{
return GetForViewport(viewport, out _, factory);
}
/// <summary>
/// Get the data for a specific viewport, creating a new entry if necessary.
/// </summary>
/// <remarks>
/// The cached data may be cleared at any time if <see cref="IClydeViewport.ClearCachedResources"/> gets invoked.
/// </remarks>
/// <param name="viewport">The viewport for which to retrieve cached data.</param>
/// <param name="wasCached">True if the data was pulled from cache, false if it was created anew.</param>
/// <param name="factory">A delegate used to create the cached data, if necessary.</param>
public T GetForViewport(IClydeViewport viewport, out bool wasCached, Func<IClydeViewport, T> factory)
{
if (_cache.TryGetValue(viewport.Id, out var entry))
{
wasCached = true;
return entry.Data;
}
wasCached = false;
entry = new CacheEntry
{
Data = factory(viewport),
Viewport = new WeakReference<IClydeViewport>(viewport),
};
_cache.Add(viewport.Id, entry);
viewport.ClearCachedResources += ViewportOnClearCachedResources;
return entry.Data;
}
private void ViewportOnClearCachedResources(ClearCachedViewportResourcesEvent ev)
{
if (!_cache.Remove(ev.ViewportId, out var entry))
{
// I think this could theoretically happen if you manually dispose the cache *after* a leaked viewport got
// GC'd, but before its ClearCachedResources got invoked.
return;
}
entry.Data.Dispose();
if (ev.Viewport != null)
ev.Viewport.ClearCachedResources -= ViewportOnClearCachedResources;
}
public void Dispose()
{
foreach (var entry in _cache)
{
if (entry.Value.Viewport.TryGetTarget(out var viewport))
viewport.ClearCachedResources -= ViewportOnClearCachedResources;
entry.Value.Data.Dispose();
}
_cache.Clear();
}
private struct CacheEntry
{
public T Data;
public WeakReference<IClydeViewport> Viewport;
}
}

View File

@@ -116,7 +116,7 @@ namespace Content.Client.HealthAnalyzer.UI
AlertsContainer.Visible = showAlerts;
if (showAlerts)
AlertsContainer.DisposeAllChildren();
AlertsContainer.RemoveAllChildren();
if (msg.Unrevivable == true)
AlertsContainer.AddChild(new RichTextLabel

View File

@@ -404,7 +404,7 @@ public sealed partial class MarkingPicker : Control
var stateNames = GetMarkingStateNames(prototype);
_currentMarkingColors.Clear();
CMarkingColors.DisposeAllChildren();
CMarkingColors.RemoveAllChildren();
List<ColorSelectorSliders> colorSliders = new();
for (int i = 0; i < prototype.Sprites.Count; i++)
{

View File

@@ -216,7 +216,6 @@ public sealed partial class SingleMarkingPicker : BoxContainer
var marking = _markings[Slot];
ColorSelectorContainer.DisposeAllChildren();
ColorSelectorContainer.RemoveAllChildren();
if (marking.MarkingColors.Count != proto.Sprites.Count)

View File

@@ -1,7 +0,0 @@
using Content.Shared.IdentityManagement;
namespace Content.Client.IdentityManagement;
public sealed class IdentitySystem : SharedIdentitySystem
{
}

View File

@@ -30,6 +30,7 @@ public sealed class AfterLightTargetOverlay : Overlay
return;
var lightOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var lightRes = lightOverlay.GetCachedForViewport(args.Viewport);
var bounds = args.WorldBounds;
// at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
@@ -38,7 +39,7 @@ public sealed class AfterLightTargetOverlay : Overlay
var localMatrix =
viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
var diff = (lightRes.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
var halfDiff = diff / 2;
// Pixels -> Metres -> Half distance.
@@ -53,7 +54,7 @@ public sealed class AfterLightTargetOverlay : Overlay
viewport.LightRenderTarget.Size.Y + halfDiff.Y);
worldHandle.SetTransform(localMatrix);
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
worldHandle.DrawTextureRectRegion(lightRes.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
}, Color.Transparent);
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Client.Graphics;
@@ -27,11 +28,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private IRenderTexture? _aoTarget;
private IRenderTexture? _aoBlurBuffer;
// Couldn't figure out a way to avoid this so if you can then please do.
private IRenderTexture? _aoStencilTarget;
private readonly OverlayResourceCache<CachedResources> _resources = new ();
public AmbientOcclusionOverlay()
{
@@ -69,30 +66,32 @@ public sealed class AmbientOcclusionOverlay : Overlay
var turfSystem = _entManager.System<TurfSystem>();
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
if (_aoTarget?.Texture.Size != target.Size)
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.AOTarget?.Texture.Size != target.Size)
{
_aoTarget?.Dispose();
_aoTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
res.AOTarget?.Dispose();
res.AOTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-target");
}
if (_aoBlurBuffer?.Texture.Size != target.Size)
if (res.AOBlurBuffer?.Texture.Size != target.Size)
{
_aoBlurBuffer?.Dispose();
_aoBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
res.AOBlurBuffer?.Dispose();
res.AOBlurBuffer = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-blur-target");
}
if (_aoStencilTarget?.Texture.Size != target.Size)
if (res.AOStencilTarget?.Texture.Size != target.Size)
{
_aoStencilTarget?.Dispose();
_aoStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
res.AOStencilTarget?.Dispose();
res.AOStencilTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "ambient-occlusion-stencil-target");
}
// Draw the texture data to the texture.
args.WorldHandle.RenderInRenderTarget(_aoTarget,
args.WorldHandle.RenderInRenderTarget(res.AOTarget,
() =>
{
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
var invMatrix = res.AOTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
foreach (var entry in query.QueryAabb(mapId, worldBounds))
{
@@ -106,11 +105,11 @@ public sealed class AmbientOcclusionOverlay : Overlay
}
}, Color.Transparent);
_clyde.BlurRenderTarget(viewport, _aoTarget, _aoBlurBuffer, viewport.Eye!, 14f);
_clyde.BlurRenderTarget(viewport, res.AOTarget, res.AOBlurBuffer, viewport.Eye!, 14f);
// Need to do stencilling after blur as it will nuke it.
// Draw stencil for the grid so we don't draw in space.
args.WorldHandle.RenderInRenderTarget(_aoStencilTarget,
args.WorldHandle.RenderInRenderTarget(res.AOStencilTarget,
() =>
{
// Don't want lighting affecting it.
@@ -136,13 +135,36 @@ public sealed class AmbientOcclusionOverlay : Overlay
// Draw the stencil texture to depth buffer.
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.AOStencilTarget!.Texture, worldBounds);
// Draw the Blurred AO texture finally.
worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance());
worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
worldHandle.DrawTextureRect(res.AOTarget!.Texture, worldBounds, color);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
args.WorldHandle.UseShader(null);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTexture? AOTarget;
public IRenderTexture? AOBlurBuffer;
// Couldn't figure out a way to avoid this so if you can then please do.
public IRenderTexture? AOStencilTarget;
public void Dispose()
{
AOTarget?.Dispose();
AOBlurBuffer?.Dispose();
AOStencilTarget?.Dispose();
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using Content.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -13,7 +13,8 @@ public sealed class BeforeLightTargetOverlay : Overlay
[Dependency] private readonly IClyde _clyde = default!;
public IRenderTexture EnlargedLightTarget = default!;
private readonly OverlayResourceCache<CachedResources> _resources = new();
public Box2Rotated EnlargedBounds;
/// <summary>
@@ -36,16 +37,42 @@ public sealed class BeforeLightTargetOverlay : Overlay
var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
// This just exists to copy the lightrendertarget and write back to it.
if (EnlargedLightTarget?.Size != size)
if (res.EnlargedLightTarget?.Size != size)
{
EnlargedLightTarget = _clyde
res.EnlargedLightTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
}
args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
args.WorldHandle.RenderInRenderTarget(res.EnlargedLightTarget,
() =>
{
}, _clyde.GetClearColor(args.MapUid));
}
internal CachedResources GetCachedForViewport(IClydeViewport viewport)
{
return _resources.GetForViewport(viewport,
static _ => throw new InvalidOperationException(
"Expected BeforeLightTargetOverlay to have created its resources"));
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
internal sealed class CachedResources : IDisposable
{
public IRenderTexture EnlargedLightTarget = default!;
public void Dispose()
{
EnlargedLightTarget?.Dispose();
}
}
}

View File

@@ -1,3 +1,4 @@
using Content.Client.Graphics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -15,7 +16,7 @@ public sealed class LightBlurOverlay : Overlay
public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
private IRenderTarget? _blurTarget;
private readonly OverlayResourceCache<CachedResources> _resources = new();
public LightBlurOverlay()
{
@@ -29,16 +30,36 @@ public sealed class LightBlurOverlay : Overlay
return;
var beforeOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var size = beforeOverlay.EnlargedLightTarget.Size;
var beforeLightRes = beforeOverlay.GetCachedForViewport(args.Viewport);
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (_blurTarget?.Size != size)
var size = beforeLightRes.EnlargedLightTarget.Size;
if (res.BlurTarget?.Size != size)
{
_blurTarget = _clyde
res.BlurTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
}
var target = beforeOverlay.EnlargedLightTarget;
var target = beforeLightRes.EnlargedLightTarget;
// Yeah that's all this does keep walkin.
_clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 5f);
_clyde.BlurRenderTarget(args.Viewport, target, res.BlurTarget, args.Viewport.Eye, 14f * 5f);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTarget? BlurTarget;
public void Dispose()
{
BlurTarget?.Dispose();
}
}
}

View File

@@ -51,8 +51,9 @@ public sealed class RoofOverlay : Overlay
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var lightRes = lightoverlay.GetCachedForViewport(args.Viewport);
var bounds = lightoverlay.EnlargedBounds;
var target = lightoverlay.EnlargedLightTarget;
var target = lightRes.EnlargedLightTarget;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids, approx: true, includeMap: true);

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Shared.Light.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
@@ -24,8 +25,7 @@ public sealed class SunShadowOverlay : Overlay
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
private IRenderTexture? _blurTarget;
private IRenderTexture? _target;
private readonly OverlayResourceCache<CachedResources> _resources = new();
public SunShadowOverlay()
{
@@ -55,16 +55,18 @@ public sealed class SunShadowOverlay : Overlay
var worldBounds = args.WorldBounds;
var targetSize = viewport.LightRenderTarget.Size;
if (_target?.Size != targetSize)
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.Target?.Size != targetSize)
{
_target = _clyde
res.Target = _clyde
.CreateRenderTarget(targetSize,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: "sun-shadow-target");
if (_blurTarget?.Size != targetSize)
if (res.BlurTarget?.Size != targetSize)
{
_blurTarget = _clyde
res.BlurTarget = _clyde
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
}
}
@@ -93,11 +95,11 @@ public sealed class SunShadowOverlay : Overlay
_shadows.Clear();
// Draw shadow polys to stencil
args.WorldHandle.RenderInRenderTarget(_target,
args.WorldHandle.RenderInRenderTarget(res.Target,
() =>
{
var invMatrix =
_target.GetWorldToLocalMatrix(eye, scale);
res.Target.GetWorldToLocalMatrix(eye, scale);
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
// Go through shadows in range.
@@ -142,7 +144,7 @@ public sealed class SunShadowOverlay : Overlay
Color.Transparent);
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
_clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
_clyde.BlurRenderTarget(viewport, res.Target, res.BlurTarget!, eye, 1f);
// Draw stencil (see roofoverlay).
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
@@ -155,8 +157,27 @@ public sealed class SunShadowOverlay : Overlay
var maskShader = _protoManager.Index(MixShader).Instance();
worldHandle.UseShader(maskShader);
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
worldHandle.DrawTextureRect(res.Target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
}, null);
}
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTexture? BlurTarget;
public IRenderTexture? Target;
public void Dispose()
{
BlurTarget?.Dispose();
Target?.Dispose();
}
}
}

View File

@@ -47,7 +47,7 @@ public sealed class TileEmissionOverlay : Overlay
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var bounds = lightoverlay.EnlargedBounds;
var target = lightoverlay.EnlargedLightTarget;
var target = lightoverlay.GetCachedForViewport(args.Viewport).EnlargedLightTarget;
var viewport = args.Viewport;
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);

View File

@@ -186,10 +186,10 @@ namespace Content.Client.Lobby
else
{
Lobby!.StartTime.Text = string.Empty;
Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
Lobby!.ReadyButton.ToggleMode = true;
Lobby!.ReadyButton.Disabled = false;
Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
Lobby!.ObserveButton.Disabled = true;
}

View File

@@ -72,7 +72,7 @@ namespace Content.Client.Lobby.UI
public void ReloadCharacterPickers()
{
_createNewCharacterButton.Orphan();
Characters.DisposeAllChildren();
Characters.RemoveAllChildren();
var numberOfFullSlots = 0;
var characterButtonsGroup = new ButtonGroup();

View File

@@ -491,7 +491,7 @@ namespace Content.Client.Lobby.UI
/// </summary>
public void RefreshTraits()
{
TraitsList.DisposeAllChildren();
TraitsList.RemoveAllChildren();
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
@@ -632,7 +632,7 @@ namespace Content.Client.Lobby.UI
public void RefreshAntags()
{
AntagList.DisposeAllChildren();
AntagList.RemoveAllChildren();
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
@@ -660,8 +660,10 @@ namespace Content.Client.Lobby.UI
selector.Setup(items, title, 250, description, guides: antag.Guides);
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
if (!_requirements.IsAllowed(
antag,
(HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter,
out var reason))
{
selector.LockRequirements(reason);
Profile = Profile?.WithAntagPreference(antag.ID, false);
@@ -824,7 +826,7 @@ namespace Content.Client.Lobby.UI
/// </summary>
public void RefreshJobs()
{
JobList.DisposeAllChildren();
JobList.RemoveAllChildren();
_jobCategories.Clear();
_jobPriorities.Clear();
var firstCategory = true;

View File

@@ -42,7 +42,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
{
var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
RestrictionsContainer.DisposeAllChildren();
RestrictionsContainer.RemoveAllChildren();
if (_groupProto.MinLimit > 0)
{
@@ -71,7 +71,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
});
}
LoadoutsContainer.DisposeAllChildren();
LoadoutsContainer.RemoveAllChildren();
// Get all loadout prototypes for this group.
var validProtos = _groupProto.Loadouts.Select(id => protoMan.Index(id));

View File

@@ -43,7 +43,7 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
_previewDummy = uid;
ViewBox.DisposeAllChildren();
ViewBox.RemoveAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -37,7 +37,7 @@ public sealed partial class MappingPrototypeList : Control
{
_prototypes.Clear();
PrototypeList.DisposeAllChildren();
PrototypeList.RemoveAllChildren();
_prototypes.AddRange(prototypes);
@@ -99,7 +99,7 @@ public sealed partial class MappingPrototypeList : Control
public void Search(List<MappingPrototype> prototypes)
{
_search.Clear();
SearchList.DisposeAllChildren();
SearchList.RemoveAllChildren();
_lastIndices = (0, -1);
_search.AddRange(prototypes);

View File

@@ -861,7 +861,7 @@ public sealed class MappingState : GameplayStateBase
}
else
{
button.ChildrenPrototypes.DisposeAllChildren();
button.ChildrenPrototypes.RemoveAllChildren();
button.CollapseButton.Label.Text = "▶";
}
}

View File

@@ -1,7 +0,0 @@
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Client.Nutrition.EntitySystems;
public sealed class DrinkSystem : SharedDrinkSystem
{
}

View File

@@ -7,7 +7,11 @@ namespace Content.Client.Overlays;
public sealed partial class StencilOverlay
{
private void DrawRestrictedRange(in OverlayDrawArgs args, RestrictedRangeComponent rangeComp, Matrix3x2 invMatrix)
private void DrawRestrictedRange(
in OverlayDrawArgs args,
CachedResources res,
RestrictedRangeComponent rangeComp,
Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var renderScale = args.Viewport.RenderScale.X;
@@ -38,7 +42,7 @@ public sealed partial class StencilOverlay
// Cut out the irrelevant bits via stencil
// This is why we don't just use parallax; we might want specific tiles to get drawn over
// particularly for planet maps or stations.
worldHandle.RenderInRenderTarget(_blep!, () =>
worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
worldHandle.UseShader(_shader);
worldHandle.DrawRect(localAABB, Color.White);
@@ -46,7 +50,7 @@ public sealed partial class StencilOverlay
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);

View File

@@ -11,7 +11,12 @@ public sealed partial class StencilOverlay
{
private List<Entity<MapGridComponent>> _grids = new();
private void DrawWeather(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha, Matrix3x2 invMatrix)
private void DrawWeather(
in OverlayDrawArgs args,
CachedResources res,
WeatherPrototype weatherProto,
float alpha,
Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var mapId = args.MapId;
@@ -23,7 +28,7 @@ public sealed partial class StencilOverlay
// Cut out the irrelevant bits via stencil
// This is why we don't just use parallax; we might want specific tiles to get drawn over
// particularly for planet maps or stations.
worldHandle.RenderInRenderTarget(_blep!, () =>
worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
@@ -64,7 +69,7 @@ public sealed partial class StencilOverlay
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Client.Parallax;
using Content.Client.Weather;
using Content.Shared._CP14.CloudShadow;
@@ -35,7 +36,7 @@ public sealed partial class StencilOverlay : Overlay
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
private IRenderTexture? _blep;
private readonly OverlayResourceCache<CachedResources> _resources = new();
private readonly ShaderInstance _shader;
@@ -56,10 +57,12 @@ public sealed partial class StencilOverlay : Overlay
var mapUid = _map.GetMapOrInvalid(args.MapId);
var invMatrix = args.Viewport.GetWorldToLocalMatrix();
if (_blep?.Texture.Size != args.Viewport.Size)
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.Blep?.Texture.Size != args.Viewport.Size)
{
_blep?.Dispose();
_blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
res.Blep?.Dispose();
res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
}
if (_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
@@ -70,24 +73,41 @@ public sealed partial class StencilOverlay : Overlay
continue;
var alpha = _weather.GetPercent(weather, mapUid);
DrawWeather(args, weatherProto, alpha, invMatrix);
DrawWeather(args, res, weatherProto, alpha, invMatrix);
}
}
if (_entManager.TryGetComponent<RestrictedRangeComponent>(mapUid, out var restrictedRangeComponent))
{
DrawRestrictedRange(args, restrictedRangeComponent, invMatrix);
DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);
}
//CP14 Overlays
if (_entManager.TryGetComponent<CP14CloudShadowsComponent>(mapUid, out var shadows))
{
DrawCloudShadows(args, shadows, invMatrix);
DrawCloudShadows(args, res, shadows, invMatrix);
}
//CP14 Overlays end
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTexture? Blep;
public void Dispose()
{
Blep?.Dispose();
}
}
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Content.Client.Lobby;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
@@ -26,7 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
[Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary<string, TimeSpan> _roles = new();
private readonly List<string> _roleBans = new();
private readonly List<string> _jobBans = new();
private readonly List<string> _antagBans = new();
private readonly List<string> _jobWhitelists = new();
private ISawmill _sawmill = default!;
@@ -52,16 +52,19 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
// Reset on disconnect, just in case.
_roles.Clear();
_jobWhitelists.Clear();
_roleBans.Clear();
_jobBans.Clear();
_antagBans.Clear();
}
}
private void RxRoleBans(MsgRoleBans message)
{
_sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries.");
_sawmill.Debug($"Received role ban info: {message.JobBans.Count} job ban entries and {message.AntagBans.Count} antag ban entries.");
_roleBans.Clear();
_roleBans.AddRange(message.Bans);
_jobBans.Clear();
_jobBans.AddRange(message.JobBans);
_antagBans.Clear();
_antagBans.AddRange(message.AntagBans);
Updated?.Invoke();
}
@@ -90,33 +93,97 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
Updated?.Invoke();
}
public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
/// <summary>
/// Check a list of job- and antag prototypes against the current player, for requirements and bans.
/// </summary>
/// <returns>
/// False if any of the prototypes are banned or have unmet requirements.
/// </returns>>
public bool IsAllowed(
List<ProtoId<JobPrototype>>? jobs,
List<ProtoId<AntagPrototype>>? antags,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
if (_roleBans.Contains($"Job:{job.ID}"))
if (antags is not null)
{
foreach (var proto in antags)
{
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
return false;
}
}
if (jobs is not null)
{
foreach (var proto in jobs)
{
if (!IsAllowed(_prototypes.Index(proto), profile, out reason))
return false;
}
}
return true;
}
/// <summary>
/// Check the job prototype against the current player, for requirements and bans
/// </summary>
public bool IsAllowed(
JobPrototype job,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{
// Check the player's bans
if (_jobBans.Contains(job.ID))
{
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
return false;
}
// Check whitelist requirements
if (!CheckWhitelist(job, out reason))
return false;
var player = _playerManager.LocalSession;
if (player == null)
return true;
// Check other role requirements
var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(job);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
return CheckRoleRequirements(job, profile, out reason);
return true;
}
public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
/// <summary>
/// Check the antag prototype against the current player, for requirements and bans
/// </summary>
public bool IsAllowed(
AntagPrototype antag,
HumanoidCharacterProfile? profile,
[NotNullWhen(false)] out FormattedMessage? reason)
{
var reqs = _entManager.System<SharedRoleSystem>().GetJobRequirement(job);
return CheckRoleRequirements(reqs, profile, out reason);
// Check the player's bans
if (_antagBans.Contains(antag.ID))
{
reason = FormattedMessage.FromUnformatted(Loc.GetString("role-ban"));
return false;
}
// Check whitelist requirements
if (!CheckWhitelist(antag, out reason))
return false;
// Check other role requirements
var reqs = _entManager.System<SharedRoleSystem>().GetRoleRequirements(antag);
if (!CheckRoleRequirements(reqs, profile, out reason))
return false;
return true;
}
public bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
// This must be private so code paths can't accidentally skip requirement overrides. Call this through IsAllowed()
private bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
@@ -152,6 +219,15 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
return true;
}
public bool CheckWhitelist(AntagPrototype antag, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = default;
// TODO: Implement antag whitelisting.
return true;
}
public TimeSpan FetchOverallPlaytime()
{
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;

View File

@@ -53,8 +53,8 @@ public sealed partial class PowerMonitoringWindow
// Selection action
windowEntry.Button.OnButtonUp += args =>
{
windowEntry.SourcesContainer.DisposeAllChildren();
windowEntry.LoadsContainer.DisposeAllChildren();
windowEntry.SourcesContainer.RemoveAllChildren();
windowEntry.LoadsContainer.RemoveAllChildren();
ButtonAction(windowEntry, masterContainer);
};
}

View File

@@ -125,7 +125,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
_prototypeManager.Resolve(proto.Prototype, out var entProto))
_prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
{
name = entProto.Name;
}
@@ -144,7 +144,7 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
&& proto.Prototype != null
&& _prototypeManager.Resolve(proto.Prototype, out var entProto))
&& _prototypeManager.TryIndex(proto.Prototype, out var entProto)) // don't use Resolve because this can be a tile
{
tooltip = Loc.GetString(entProto.Name);
}

View File

@@ -70,7 +70,7 @@ public sealed partial class OfferingWindow : FancyWindow,
public void ClearOptions()
{
Container.DisposeAllChildren();
Container.RemoveAllChildren();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -67,8 +67,8 @@ public sealed partial class DockingScreen : BoxContainer
{
DockingControl.BuildDocks(shuttle);
var currentDock = DockingControl.ViewedDock;
// DockedWith.DisposeAllChildren();
DockPorts.DisposeAllChildren();
// DockedWith.RemoveAllChildren();
DockPorts.RemoveAllChildren();
_ourDockButtons.Clear();
if (shuttle == null)

View File

@@ -59,7 +59,7 @@ public sealed partial class EmergencyConsoleWindow : FancyWindow,
// TODO: Loc and cvar for this.
_earlyLaunchTime = scc.EarlyLaunchTime;
AuthorizationsContainer.DisposeAllChildren();
AuthorizationsContainer.RemoveAllChildren();
var remainingAuths = scc.AuthorizationsRequired - scc.Authorizations.Count;
AuthorizationCount.Text = Loc.GetString("emergency-shuttle-ui-remaining", ("remaining", remainingAuths));

View File

@@ -237,7 +237,7 @@ public sealed partial class MapScreen : BoxContainer
private void ClearMapObjects()
{
_mapObjectControls.Clear();
HyperspaceDestinations.DisposeAllChildren();
HyperspaceDestinations.RemoveAllChildren();
_pendingMapObjects.Clear();
_mapObjects.Clear();
_mapHeadings.Clear();

View File

@@ -0,0 +1,42 @@
using Content.Shared.Silicons.StationAi;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.StationAi;
public sealed class StationAiFixerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private StationAiFixerConsoleWindow? _window;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<StationAiFixerConsoleWindow>();
_window.SetOwner(Owner);
_window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
_window.OpenConfirmationDialogAction += OpenConfirmationDialog;
}
public override void Update()
{
base.Update();
_window?.UpdateState();
}
private void OpenConfirmationDialog()
{
if (_window == null)
return;
_window.ConfirmationDialog?.Close();
_window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog();
_window.ConfirmationDialog.OpenCentered();
_window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage;
}
private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendPredictedMessage(new StationAiFixerConsoleMessage(action));
}
}

View File

@@ -0,0 +1,22 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'station-ai-fixer-console-window-purge-warning-title'}"
Resizable="False">
<BoxContainer Orientation="Vertical" VerticalExpand="True" SetWidth="400">
<RichTextLabel Name="PurgeWarningLabel1" Margin="20 10 20 0"/>
<RichTextLabel Name="PurgeWarningLabel2" Margin="20 10 20 0"/>
<RichTextLabel Name="PurgeWarningLabel3" Margin="20 10 20 10"/>
<BoxContainer HorizontalExpand="True">
<Button Name="CancelPurge"
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}"
SetWidth="150"
Margin="20 10 0 10"/>
<Control HorizontalExpand="True"/>
<Button Name="ContinuePurge"
Text="{Loc 'station-ai-fixer-console-window-continue-action'}"
SetWidth="150"
Margin="0 10 20 10"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,30 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Silicons.StationAi;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Silicons.StationAi;
[GenerateTypedNameReferences]
public sealed partial class StationAiFixerConsoleConfirmationDialog : FancyWindow
{
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
public StationAiFixerConsoleConfirmationDialog()
{
RobustXamlLoader.Load(this);
PurgeWarningLabel1.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-1"));
PurgeWarningLabel2.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-2"));
PurgeWarningLabel3.SetMessage(Loc.GetString($"station-ai-fixer-console-window-purge-warning-3"));
CancelPurge.OnButtonDown += _ => Close();
ContinuePurge.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Purge);
}
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendStationAiFixerConsoleMessageAction?.Invoke(action);
Close();
}
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Silicons.StationAi;
using Robust.Client.GameObjects;
namespace Content.Client.Silicons.StationAi;
public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiFixerConsoleComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(Entity<StationAiFixerConsoleComponent> ent, ref AppearanceChangeEvent args)
{
if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui))
{
bui?.Update<StationAiFixerConsoleBoundUserInterfaceState>();
}
}
}

View File

@@ -0,0 +1,172 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'station-ai-fixer-console-window'}"
Resizable="False">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Horizontal">
<!-- Left side - AI display -->
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="20 15 20 20">
<!-- AI panel -->
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Vertical">
<!-- AI name -->
<Label Name="StationAiNameLabel"
HorizontalAlignment="Center"
Margin="0 5 0 0"
Text="{Loc 'station-ai-fixer-console-window-no-station-ai'}"/>
<!-- AI portrait -->
<AnimatedTextureRect Name="StationAiPortraitTexture" VerticalAlignment="Center" SetSize="128 128" />
</BoxContainer>
</PanelContainer>
<!-- AI status panel-->
<PanelContainer Name="StationAiStatus">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#757575" />
</PanelContainer.PanelOverride>
<!-- AI name -->
<Label Name="StationAiStatusLabel"
HorizontalAlignment="Center"
Text="{Loc 'station-ai-fixer-console-window-no-station-ai-status'}"/>
</PanelContainer>
</BoxContainer>
<!-- Central divider -->
<PanelContainer StyleClasses="LowDivider" VerticalExpand="True" Margin="0 0 0 0" SetWidth="2"/>
<!-- Right side - control panel -->
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical" MinWidth="225" Margin="10 10 10 10">
<!-- Locked controls -->
<BoxContainer Name="LockScreen"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False">
<controls:StripeBack VerticalExpand="True" HorizontalExpand="True" Margin="0 0 0 5">
<PanelContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer VerticalExpand="True" HorizontalExpand="True" Orientation="Vertical">
<Control VerticalExpand="True"/>
<TextureRect VerticalAlignment="Center"
HorizontalAlignment="Center"
SetSize="64 64"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png">
</TextureRect>
<Label Text="{Loc 'station-ai-fixer-console-window-controls-locked'}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
<Control VerticalExpand="True"/>
</BoxContainer>
</PanelContainer>
</controls:StripeBack>
</BoxContainer>
<!-- Action progress screen -->
<BoxContainer Name="ActionProgressScreen"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False"
Visible="False">
<Control VerticalExpand="True" Margin="0 0 0 0"/>
<Label Name="ActionInProgressLabel" Text="???" HorizontalAlignment="Center"/>
<ProgressBar Name="ActionProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20"
Margin="5 10 5 10">
</ProgressBar>
<Label Name="ActionProgressEtaLabel" Text="???" HorizontalAlignment="Center"/>
<!-- Cancel button -->
<Button Name="CancelButton" HorizontalExpand="True" Margin="0 20 0 10" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-cancel-action'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="24 24"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/Nano/cross.svg.png">
</TextureRect>
</Button>
</BoxContainer>
<!-- Visible controls -->
<BoxContainer Name="MainControls"
VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
ReservesSpace="False"
Visible="False">
<controls:StripeBack>
<PanelContainer>
<Label Text="{Loc 'Controls'}"
HorizontalExpand="True"
HorizontalAlignment="Center"/>
</PanelContainer>
</controls:StripeBack>
<!-- Eject button -->
<Button Name="EjectButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-eject'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/eject.svg.192dpi.png">
</TextureRect>
</Button>
<!-- Repair button -->
<Button Name="RepairButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-repair'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/hammer_scaled.svg.192dpi.png">
</TextureRect>
</Button>
<!-- Purge button -->
<Button Name="PurgeButton" HorizontalExpand="True" Margin="0 10 0 0" SetHeight="40"
Text="{Loc 'station-ai-fixer-console-window-station-ai-purge'}">
<TextureRect HorizontalAlignment="Left"
VerticalAlignment="Center"
SetSize="32 32"
Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/VerbIcons/delete_transparent.svg.192dpi.png">
</TextureRect>
</Button>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-left'}" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'station-ai-fixer-console-window-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,198 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Lock;
using Content.Shared.Silicons.StationAi;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
namespace Content.Client.Silicons.StationAi;
[GenerateTypedNameReferences]
public sealed partial class StationAiFixerConsoleWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly StationAiFixerConsoleSystem _stationAiFixerConsole;
private readonly SharedStationAiSystem _stationAi;
private EntityUid? _owner;
private readonly SpriteSpecifier.Rsi _emptyPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_empty");
private readonly SpriteSpecifier.Rsi _rebootingPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_fuzz");
private SpriteSpecifier? _currentPortrait;
public event Action<StationAiFixerConsoleAction>? SendStationAiFixerConsoleMessageAction;
public event Action? OpenConfirmationDialogAction;
public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog;
private readonly Dictionary<StationAiState, Color> _statusColors = new()
{
[StationAiState.Empty] = Color.FromHex("#464966"),
[StationAiState.Occupied] = Color.FromHex("#3E6C45"),
[StationAiState.Rebooting] = Color.FromHex("#A5762F"),
[StationAiState.Dead] = Color.FromHex("#BB3232"),
};
public StationAiFixerConsoleWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_stationAiFixerConsole = _entManager.System<StationAiFixerConsoleSystem>();
_stationAi = _entManager.System<StationAiSystem>();
StationAiPortraitTexture.DisplayRect.TextureScale = new Vector2(4f, 4f);
CancelButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Cancel);
EjectButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Eject);
RepairButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Repair);
PurgeButton.OnButtonDown += _ => OnOpenConfirmationDialog();
CancelButton.Label.HorizontalAlignment = HAlignment.Left;
EjectButton.Label.HorizontalAlignment = HAlignment.Left;
RepairButton.Label.HorizontalAlignment = HAlignment.Left;
PurgeButton.Label.HorizontalAlignment = HAlignment.Left;
CancelButton.Label.Margin = new Thickness(40, 0, 0, 0);
EjectButton.Label.Margin = new Thickness(40, 0, 0, 0);
RepairButton.Label.Margin = new Thickness(40, 0, 0, 0);
PurgeButton.Label.Margin = new Thickness(40, 0, 0, 0);
}
public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action)
{
SendStationAiFixerConsoleMessageAction?.Invoke(action);
}
public void OnOpenConfirmationDialog()
{
OpenConfirmationDialogAction?.Invoke();
}
public override void Close()
{
base.Close();
ConfirmationDialog?.Close();
}
public void SetOwner(EntityUid owner)
{
_owner = owner;
UpdateState();
}
public void UpdateState()
{
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
return;
var ent = (_owner.Value, stationAiFixerConsole);
var isLocked = _entManager.TryGetComponent<LockComponent>(_owner, out var lockable) && lockable.Locked;
var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole));
var stationAi = stationAiFixerConsole.ActionTarget;
var stationAiState = StationAiState.Empty;
if (_entManager.TryGetComponent<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization))
{
stationAiState = stationAiCustomization.State;
}
// Set subscreen visibility
LockScreen.Visible = isLocked;
MainControls.Visible = !isLocked && !_stationAiFixerConsole.IsActionInProgress(ent);
ActionProgressScreen.Visible = !isLocked && _stationAiFixerConsole.IsActionInProgress(ent);
// Update station AI name
StationAiNameLabel.Text = GetStationAiName(stationAi);
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-no-station-ai-status");
// Update station AI portrait
var portrait = _emptyPortrait;
var statusColor = _statusColors[StationAiState.Empty];
if (stationAiState == StationAiState.Rebooting)
{
portrait = _rebootingPortrait;
StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-station-ai-rebooting");
_statusColors.TryGetValue(StationAiState.Rebooting, out statusColor);
}
else if (stationAi != null &&
stationAiCustomization != null &&
_stationAi.TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData))
{
StationAiStatusLabel.Text = stationAiState == StationAiState.Occupied ?
Loc.GetString("station-ai-fixer-console-window-station-ai-online") :
Loc.GetString("station-ai-fixer-console-window-station-ai-offline");
if (layerData.TryGetValue(stationAiState.ToString(), out var stateData) && stateData is { RsiPath: not null, State: not null })
{
portrait = new SpriteSpecifier.Rsi(new ResPath(stateData.RsiPath), stateData.State);
}
_statusColors.TryGetValue(stationAiState, out statusColor);
}
if (_currentPortrait == null || !_currentPortrait.Equals(portrait))
{
StationAiPortraitTexture.SetFromSpriteSpecifier(portrait);
_currentPortrait = portrait;
}
StationAiStatus.PanelOverride = new StyleBoxFlat
{
BackgroundColor = statusColor,
};
// Update buttons
EjectButton.Disabled = !stationAiHolderInserted;
RepairButton.Disabled = !stationAiHolderInserted || stationAiState != StationAiState.Dead;
PurgeButton.Disabled = !stationAiHolderInserted || stationAiState == StationAiState.Empty;
// Update progress bar
if (ActionProgressScreen.Visible)
UpdateProgressBar(ent);
}
public void UpdateProgressBar(Entity<StationAiFixerConsoleComponent> ent)
{
ActionInProgressLabel.Text = ent.Comp.ActionType == StationAiFixerConsoleAction.Repair ?
Loc.GetString("station-ai-fixer-console-window-action-progress-repair") :
Loc.GetString("station-ai-fixer-console-window-action-progress-purge");
var fullTimeSpan = ent.Comp.ActionEndTime - ent.Comp.ActionStartTime;
var remainingTimeSpan = ent.Comp.ActionEndTime - _timing.CurTime;
var time = remainingTimeSpan.TotalSeconds > 60 ? remainingTimeSpan.TotalMinutes : remainingTimeSpan.TotalSeconds;
var units = remainingTimeSpan.TotalSeconds > 60 ? Loc.GetString("generic-minutes") : Loc.GetString("generic-seconds");
ActionProgressEtaLabel.Text = Loc.GetString("station-ai-fixer-console-window-action-progress-eta", ("time", (int)time), ("units", units));
ActionProgressBar.Value = 1f - (float)remainingTimeSpan.Divide(fullTimeSpan);
}
private string GetStationAiName(EntityUid? uid)
{
if (_entManager.TryGetComponent<MetaDataComponent>(uid, out var metadata))
{
return metadata.EntityName;
}
return Loc.GetString("station-ai-fixer-console-window-no-station-ai");
}
protected override void FrameUpdate(FrameEventArgs args)
{
if (!ActionProgressScreen.Visible)
return;
if (!_entManager.TryGetComponent<StationAiFixerConsoleComponent>(_owner, out var stationAiFixerConsole))
return;
UpdateProgressBar((_owner.Value, stationAiFixerConsole));
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.Graphics;
using Content.Shared.Silicons.StationAi;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -26,8 +27,7 @@ public sealed class StationAiOverlay : Overlay
private readonly HashSet<Vector2i> _visibleTiles = new();
private IRenderTexture? _staticTexture;
private IRenderTexture? _stencilTexture;
private readonly OverlayResourceCache<CachedResources> _resources = new();
private float _updateRate = 1f / 30f;
private float _accumulator;
@@ -39,12 +39,14 @@ public sealed class StationAiOverlay : Overlay
protected override void Draw(in OverlayDrawArgs args)
{
if (_stencilTexture?.Texture.Size != args.Viewport.Size)
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (res.StencilTexture?.Texture.Size != args.Viewport.Size)
{
_staticTexture?.Dispose();
_stencilTexture?.Dispose();
_stencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
_staticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
res.StaticTexture?.Dispose();
res.StencilTexture?.Dispose();
res.StencilTexture = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "station-ai-stencil");
res.StaticTexture = _clyde.CreateRenderTarget(args.Viewport.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: "station-ai-static");
}
@@ -78,7 +80,7 @@ public sealed class StationAiOverlay : Overlay
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
// Draw visible tiles to stencil
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
{
worldHandle.SetTransform(matty);
@@ -91,7 +93,7 @@ public sealed class StationAiOverlay : Overlay
Color.Transparent);
// Once this is gucci optimise rendering.
worldHandle.RenderInRenderTarget(_staticTexture!,
worldHandle.RenderInRenderTarget(res.StaticTexture!,
() =>
{
worldHandle.SetTransform(invMatrix);
@@ -104,12 +106,12 @@ public sealed class StationAiOverlay : Overlay
// Not on a grid
else
{
worldHandle.RenderInRenderTarget(_stencilTexture!, () =>
worldHandle.RenderInRenderTarget(res.StencilTexture!, () =>
{
},
Color.Transparent);
worldHandle.RenderInRenderTarget(_staticTexture!,
worldHandle.RenderInRenderTarget(res.StaticTexture!,
() =>
{
worldHandle.SetTransform(Matrix3x2.Identity);
@@ -119,14 +121,33 @@ public sealed class StationAiOverlay : Overlay
// Use the lighting as a mask
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.StencilTexture!.Texture, worldBounds);
// Draw the static
worldHandle.UseShader(_proto.Index(StencilDrawShader).Instance());
worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.StaticTexture!.Texture, worldBounds);
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(null);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
base.DisposeBehavior();
}
private sealed class CachedResources : IDisposable
{
public IRenderTexture? StaticTexture;
public IRenderTexture? StencilTexture;
public void Dispose()
{
StaticTexture?.Dispose();
StencilTexture?.Dispose();
}
}
}

View File

@@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
if (args.Sprite == null)
return;
if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component))
_sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData);
if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component))
_sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData);
_sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData != null);
_sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData != null);
}
public override void Shutdown()

View File

@@ -25,9 +25,9 @@ namespace Content.Client.Strip
public void ClearButtons()
{
InventoryContainer.DisposeAllChildren();
HandsContainer.DisposeAllChildren();
SnareContainer.DisposeAllChildren();
InventoryContainer.RemoveAllChildren();
HandsContainer.RemoveAllChildren();
SnareContainer.RemoveAllChildren();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -29,7 +29,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
public void UpdateState(ThiefBackpackBoundUserInterfaceState state)
{
SetsGrid.DisposeAllChildren();
SetsGrid.RemoveAllChildren();
var selectedNumber = 0;
foreach (var (set, info) in state.Sets)
{

View File

@@ -66,7 +66,8 @@ namespace Content.Client.UserInterface.Controls
Viewport.StretchMode = filterMode switch
{
"nearest" => ScalingViewportStretchMode.Nearest,
"bilinear" => ScalingViewportStretchMode.Bilinear
"bilinear" => ScalingViewportStretchMode.Bilinear,
_ => ScalingViewportStretchMode.Nearest
};
Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -19,7 +19,7 @@ namespace Content.Client.UserInterface.Controls
public void Clear()
{
DisposeAllChildren();
RemoveAllChildren();
}
public void AddEntry(float amount, Color color, string? tooltip = null)

View File

@@ -17,7 +17,7 @@ namespace Content.Client.UserInterface
public void UpdateValues(List<string> headers, List<string[]> values)
{
Values.DisposeAllChildren();
Values.RemoveAllChildren();
Values.Columns = headers.Count;
for (var i = 0; i < headers.Count; i++)

View File

@@ -45,7 +45,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls
public void Populate()
{
ButtonContainer.DisposeAllChildren();
ButtonContainer.RemoveAllChildren();
AddButtons();
}

View File

@@ -90,23 +90,25 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
// Grouping roles
var groupedRoles = ghostState.GhostRoles.GroupBy(
role => (role.Name, role.Description, role.Requirements));
role => (
role.Name,
role.Description,
// Check the prototypes for role requirements and bans
requirementsManager.IsAllowed(role.RolePrototypes.Item1, role.RolePrototypes.Item2, null, out var reason),
reason));
// Add a new entry for each role group
foreach (var group in groupedRoles)
{
var reason = group.Key.reason;
var name = group.Key.Name;
var description = group.Key.Description;
var hasAccess = requirementsManager.CheckRoleRequirements(
group.Key.Requirements,
null,
out var reason);
var prototypesAllowed = group.Key.Item3;
// Adding a new role
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
_window.AddEntry(name, description, prototypesAllowed, reason, group, spriteSystem);
}
// Restore the Collapsible box state if it is saved

View File

@@ -26,7 +26,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
public void ClearEntries()
{
NoRolesMessage.Visible = true;
EntryContainer.DisposeAllChildren();
EntryContainer.RemoveAllChildren();
_collapsibleBoxes.Clear();
}

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using Content.Client.UserInterface.Systems.Inventory.Controls;
using Robust.Client.UserInterface.Controls;
@@ -74,7 +74,7 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
public void Clear()
{
ClearButtons();
_grid.DisposeAllChildren();
_grid.RemoveAllChildren();
}
public IEnumerable<HandButton> GetButtons()

View File

@@ -109,7 +109,7 @@ namespace Content.Client.Verbs.UI
Close();
var menu = popup ?? _context.RootMenu;
menu.MenuBody.DisposeAllChildren();
menu.MenuBody.RemoveAllChildren();
CurrentTarget = target;
CurrentVerbs = _verbSystem.GetVerbs(target, user, Verb.VerbTypes, out ExtraCategories, force);
@@ -207,7 +207,7 @@ namespace Content.Client.Verbs.UI
/// </summary>
public void AddServerVerbs(List<Verb>? verbs, ContextMenuPopup popup)
{
popup.MenuBody.DisposeAllChildren();
popup.MenuBody.RemoveAllChildren();
// Verbs may be null if the server does not think we can see the target entity. This **should** not happen.
if (verbs == null)

View File

@@ -7,7 +7,11 @@ namespace Content.Client.Overlays;
public sealed partial class StencilOverlay
{
private void DrawCloudShadows(in OverlayDrawArgs args, CP14CloudShadowsComponent cloudComp, Matrix3x2 invMatrix)
private void DrawCloudShadows(
in OverlayDrawArgs args,
CachedResources res,
CP14CloudShadowsComponent cloudComp,
Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
var mapId = args.MapId;
@@ -18,7 +22,7 @@ public sealed partial class StencilOverlay
// Cut out the irrelevant bits via stencil
// This is why we don't just use parallax; we might want specific tiles to get drawn over
// particularly for planet maps or stations.
worldHandle.RenderInRenderTarget(_blep!, () =>
worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
@@ -51,7 +55,7 @@ public sealed partial class StencilOverlay
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(cloudComp.ParallaxPath), curTime);

View File

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

View File

@@ -3,3 +3,4 @@
global using NUnit.Framework;
global using System;
global using System.Threading.Tasks;
global using Robust.UnitTesting.Pool;

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.IntegrationTests.Pair;
/// <summary>
/// Simple data class that stored information about a map being used by a test.
/// </summary>
public sealed class TestMapData
{
public EntityUid MapUid { get; set; }
public Entity<MapGridComponent> Grid;
public MapId MapId;
public EntityCoordinates GridCoords { get; set; }
public MapCoordinates MapCoords { get; set; }
public TileRef Tile { get; set; }
// Client-side uids
public EntityUid CMapUid { get; set; }
public EntityUid CGridUid { get; set; }
public EntityCoordinates CGridCoords { get; set; }
}

View File

@@ -1,69 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
public sealed partial class TestPair
{
private readonly Dictionary<string, object> _modifiedClientCvars = new();
private readonly Dictionary<string, object> _modifiedServerCvars = new();
private void OnServerCvarChanged(CVarChangeInfo args)
{
_modifiedServerCvars.TryAdd(args.Name, args.OldValue);
}
private void OnClientCvarChanged(CVarChangeInfo args)
{
_modifiedClientCvars.TryAdd(args.Name, args.OldValue);
}
internal void ClearModifiedCvars()
{
_modifiedClientCvars.Clear();
_modifiedServerCvars.Clear();
}
/// <summary>
/// Reverts any cvars that were modified during a test back to their original values.
/// </summary>
public async Task RevertModifiedCvars()
{
await Server.WaitPost(() =>
{
foreach (var (name, value) in _modifiedServerCvars)
{
if (Server.CfgMan.GetCVar(name).Equals(value))
continue;
Server.Log.Info($"Resetting cvar {name} to {value}");
Server.CfgMan.SetCVar(name, value);
}
// I just love order dependent cvars
if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
});
await Client.WaitPost(() =>
{
foreach (var (name, value) in _modifiedClientCvars)
{
if (Client.CfgMan.GetCVar(name).Equals(value))
continue;
var flags = Client.CfgMan.GetCVarFlags(name);
if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
continue;
Client.Log.Info($"Resetting cvar {name} to {value}");
Client.CfgMan.SetCVar(name, value);
}
});
ClearModifiedCvars();
}
}

View File

@@ -1,172 +1,19 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
// Contains misc helper functions to make writing tests easier.
public sealed partial class TestPair
{
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// </summary>
[MemberNotNull(nameof(TestMap))]
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
{
var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
TestMap = mapData;
await Server.WaitPost(() =>
{
mapData.MapUid = Server.System<SharedMapSystem>().CreateMap(out mapData.MapId, runMapInit: initialized);
mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
TestMap = mapData;
if (!Settings.Connected)
return mapData;
await RunTicksSync(10);
mapData.CMapUid = ToClientUid(mapData.MapUid);
mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
TestMap = mapData;
return mapData;
}
/// <summary>
/// Convert a client-side uid into a server-side uid
/// </summary>
public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
/// <summary>
/// Convert a server-side uid into a client-side uid
/// </summary>
public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
private static EntityUid ConvertUid(
EntityUid uid,
RobustIntegrationTest.IntegrationInstance source,
RobustIntegrationTest.IntegrationInstance destination)
{
if (!uid.IsValid())
return EntityUid.Invalid;
if (!source.EntMan.TryGetComponent<MetaDataComponent>(uid, out var meta))
{
Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
return EntityUid.Invalid;
}
if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
{
Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
return EntityUid.Invalid;
}
return otherUid.Value;
}
/// <summary>
/// Execute a command on the server and wait some number of ticks.
/// </summary>
public async Task WaitCommand(string cmd, int numTicks = 10)
{
await Server.ExecuteCommand(cmd);
await RunTicksSync(numTicks);
}
/// <summary>
/// Execute a command on the client and wait some number of ticks.
/// </summary>
public async Task WaitClientCommand(string cmd, int numTicks = 10)
{
await Client.ExecuteCommand(cmd);
await RunTicksSync(numTicks);
}
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
where T : IComponent, new()
{
if (!Server.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out var reg)
&& !Client.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out reg))
{
Assert.Fail($"Unknown component: {typeof(T).Name}");
return new();
}
var id = reg.Name;
var list = new List<(EntityPrototype, T)>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
if (ignored != null && ignored.Contains(proto.ID))
continue;
if (ignoreAbstract && proto.Abstract)
continue;
if (ignoreTestPrototypes && IsTestPrototype(proto))
continue;
if (proto.Components.TryGetComponent(id, out var cmp))
list.Add((proto, (T)cmp));
}
return list;
}
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
{
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
var list = new List<EntityPrototype>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
if (ignored != null && ignored.Contains(proto.ID))
continue;
if (ignoreAbstract && proto.Abstract)
continue;
if (ignoreTestPrototypes && IsTestPrototype(proto))
continue;
if (proto.Components.ContainsKey(id))
list.Add((proto));
}
return list;
}
public Task<TestMapData> CreateTestMap(bool initialized = true)
=> CreateTestMap(initialized, "Plating");
/// <summary>
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.

View File

@@ -1,64 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
// This partial class contains helper methods to deal with yaml prototypes.
public sealed partial class TestPair
{
private Dictionary<Type, HashSet<string>> _loadedPrototypes = new();
private HashSet<string> _loadedEntityPrototypes = new();
public async Task LoadPrototypes(List<string> prototypes)
{
await LoadPrototypes(Server, prototypes);
await LoadPrototypes(Client, prototypes);
}
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
{
var changed = new Dictionary<Type, HashSet<string>>();
foreach (var file in prototypes)
{
instance.ProtoMan.LoadString(file, changed: changed);
}
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
foreach (var (kind, ids) in changed)
{
_loadedPrototypes.GetOrNew(kind).UnionWith(ids);
}
if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
_loadedEntityPrototypes.UnionWith(entIds);
}
public bool IsTestPrototype(EntityPrototype proto)
{
return _loadedEntityPrototypes.Contains(proto.ID);
}
public bool IsTestEntityPrototype(string id)
{
return _loadedEntityPrototypes.Contains(id);
}
public bool IsTestPrototype<TPrototype>(string id) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), id);
}
public bool IsTestPrototype<TPrototype>(TPrototype proto) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), proto.ID);
}
public bool IsTestPrototype(Type kind, string id)
{
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
}
}

View File

@@ -8,84 +8,17 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Preferences;
using Robust.Client;
using Robust.Server.Player;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Utility;
using Robust.Shared.Player;
namespace Content.IntegrationTests.Pair;
// This partial class contains logic related to recycling & disposing test pairs.
public sealed partial class TestPair : IAsyncDisposable
public sealed partial class TestPair
{
public PairState State { get; private set; } = PairState.Ready;
private async Task OnDirtyDispose()
protected override async Task Cleanup()
{
var usageTime = Watch.Elapsed;
Watch.Restart();
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
Kill();
var disposeTime = Watch.Elapsed;
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
// because someone forgot to clean-return the pair.
Assert.Warn("Test was dirty-disposed.");
}
private async Task OnCleanDispose()
{
await Server.WaitIdleAsync();
await Client.WaitIdleAsync();
await base.Cleanup();
await ResetModifiedPreferences();
await Server.RemoveAllDummySessions();
if (TestMap != null)
{
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
TestMap = null;
}
await RevertModifiedCvars();
var usageTime = Watch.Elapsed;
Watch.Restart();
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
// Let any last minute failures the test cause happen.
await ReallyBeIdle();
if (!Settings.Destructive)
{
if (Client.IsAlive == false)
{
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
}
if (Server.IsAlive == false)
{
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
}
}
if (Settings.MustNotBeReused)
{
Kill();
await ReallyBeIdle();
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
return;
}
var sRuntimeLog = Server.ResolveDependency<IRuntimeLog>();
if (sRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Client.ResolveDependency<IRuntimeLog>();
if (cRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
var returnTime = Watch.Elapsed;
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
State = PairState.Ready;
}
private async Task ResetModifiedPreferences()
@@ -95,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
_modifiedProfiles.Clear();
}
public async ValueTask CleanReturnAsync()
protected override async Task Recycle(PairSettings next, TextWriter testOut)
{
if (State != PairState.InUse)
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
State = PairState.CleanDisposed;
await OnCleanDispose();
DebugTools.Assert(State is PairState.Dead or PairState.Ready);
PoolManager.NoCheckReturn(this);
ClearContext();
}
public async ValueTask DisposeAsync()
{
switch (State)
{
case PairState.Dead:
case PairState.Ready:
break;
case PairState.InUse:
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
await OnDirtyDispose();
PoolManager.NoCheckReturn(this);
ClearContext();
break;
default:
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
}
}
public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
{
Settings = default!;
Watch.Restart();
await testOut.WriteLineAsync($"Recycling...");
var gameTicker = Server.System<GameTicker>();
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
await RunTicksSync(1);
// Disconnect the client if they are connected.
if (cNetMgr.IsConnected)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
await RunTicksSync(1);
}
Assert.That(cNetMgr.IsConnected, Is.False);
// Move to pre-round lobby. Required to toggle dummy ticker on and off
var gameTicker = Server.System<GameTicker>();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
@@ -162,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
//Apply Cvars
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
await PoolManager.SetupCVars(Client, settings);
await PoolManager.SetupCVars(Server, settings);
await ApplySettings(next);
await RunTicksSync(1);
// Restart server.
@@ -171,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);
// Connect client
if (settings.ShouldBeConnected)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
Client.SetConnectTarget(Server);
await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
}
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
await ReallyBeIdle();
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
}
public void ValidateSettings(PoolSettings settings)
public override void ValidateSettings(PairSettings s)
{
base.ValidateSettings(s);
var settings = (PoolSettings) s;
var cfg = Server.CfgMan;
Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
var entMan = Server.ResolveDependency<EntityManager>();
var ticker = entMan.System<GameTicker>();
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
var ticker = Server.System<GameTicker>();
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
var expectPreRound = settings.InLobby | settings.DummyTicker;
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
var baseClient = Client.ResolveDependency<IBaseClient>();
var netMan = Client.ResolveDependency<INetManager>();
Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
if (!settings.ShouldBeConnected)
if (ticker.DummyTicker || !settings.Connected)
return;
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
var cPlayer = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
var sPlayer = Server.ResolveDependency<IPlayerManager>();
Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
var sPlayer = Server.ResolveDependency<ISharedPlayerManager>();
var session = sPlayer.Sessions.Single();
Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
if (ticker.DummyTicker)
return;
var status = ticker.PlayerGameStatuses[session.UserId];
var expected = settings.InLobby
? PlayerGameStatus.NotReadyToPlay
@@ -231,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
}
Assert.That(session.AttachedEntity, Is.Not.Null);
Assert.That(entMan.EntityExists(session.AttachedEntity));
Assert.That(entMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
var mindCont = entMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
Assert.That(Server.EntMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
var mindCont = Server.EntMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
Assert.That(mindCont.Mind, Is.Not.Null);
Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(mind!.VisitingEntity, Is.Null);
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
Assert.That(mind.UserId, Is.EqualTo(session.UserId));

View File

@@ -1,77 +0,0 @@
#nullable enable
namespace Content.IntegrationTests.Pair;
// This partial class contains methods for running the server/client pairs for some number of ticks
public sealed partial class TestPair
{
/// <summary>
/// Runs the server-client pair in sync
/// </summary>
/// <param name="ticks">How many ticks to run them for</param>
public async Task RunTicksSync(int ticks)
{
for (var i = 0; i < ticks; i++)
{
await Server.WaitRunTicks(1);
await Client.WaitRunTicks(1);
}
}
/// <summary>
/// Convert a time interval to some number of ticks.
/// </summary>
public int SecondsToTicks(float seconds)
{
return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
}
/// <summary>
/// Run the server & client in sync for some amount of time
/// </summary>
public async Task RunSeconds(float seconds)
{
await RunTicksSync(SecondsToTicks(seconds));
}
/// <summary>
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
/// </summary>
/// <param name="runTicks">How many ticks to run</param>
public async Task ReallyBeIdle(int runTicks = 25)
{
for (var i = 0; i < runTicks; i++)
{
await Client.WaitRunTicks(1);
await Server.WaitRunTicks(1);
for (var idleCycles = 0; idleCycles < 4; idleCycles++)
{
await Client.WaitIdleAsync();
await Server.WaitIdleAsync();
}
}
}
/// <summary>
/// Run the server/clients until the ticks are synchronized.
/// By default the client will be one tick ahead of the server.
/// </summary>
public async Task SyncTicks(int targetDelta = 1)
{
var sTick = (int)Server.Timing.CurTick.Value;
var cTick = (int)Client.Timing.CurTick.Value;
var delta = cTick - sTick;
if (delta == targetDelta)
return;
if (delta > targetDelta)
await Server.WaitRunTicks(delta - targetDelta);
else
await Client.WaitRunTicks(targetDelta - delta);
sTick = (int)Server.Timing.CurTick.Value;
cTick = (int)Client.Timing.CurTick.Value;
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
}

View File

@@ -1,16 +1,17 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Content.Client.IoC;
using Content.Client.Parallax.Managers;
using Content.IntegrationTests.Tests.Destructible;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.GameTicking;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
@@ -18,156 +19,99 @@ namespace Content.IntegrationTests.Pair;
/// <summary>
/// This object wraps a pooled server+client pair.
/// </summary>
public sealed partial class TestPair
public sealed partial class TestPair : RobustIntegrationTest.TestPair
{
public readonly int Id;
private bool _initialized;
private TextWriter _testOut = default!;
public readonly Stopwatch Watch = new();
public readonly List<string> TestHistory = new();
public PoolSettings Settings = default!;
public TestMapData? TestMap;
private List<NetUserId> _modifiedProfiles = new();
private int _nextServerSeed;
private int _nextClientSeed;
public int ServerSeed;
public int ClientSeed;
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
public void Deconstruct(
out RobustIntegrationTest.ServerIntegrationInstance server,
out RobustIntegrationTest.ClientIntegrationInstance client)
{
server = Server;
client = Client;
}
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
public TestPair(int id)
protected override async Task Initialize()
{
Id = id;
}
public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
{
if (_initialized)
throw new InvalidOperationException("Already initialized");
_initialized = true;
Settings = settings;
(Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
ActivateContext(testOut);
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
if (!settings.NoLoadTestPrototypes)
await LoadPrototypes(testPrototypes!);
if (!settings.UseDummyTicker)
var settings = (PoolSettings)Settings;
if (!settings.DummyTicker)
{
var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
var gameTicker = Server.System<GameTicker>();
await Server.WaitPost(() => gameTicker.RestartRound());
}
}
// Always initially connect clients to generate an initial random set of preferences/profiles.
// This is to try and prevent issues where if the first test that connects the client is consistently some test
// that uses a fixed seed, it would effectively prevent it from beingrandomized.
public override async Task RevertModifiedCvars()
{
// I just love order dependent cvars
// I.e., cvars that when changed automatically cause others to also change.
var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
Client.SetConnectTarget(Server);
await Client.WaitIdleAsync();
var netMgr = Client.ResolveDependency<IClientNetManager>();
await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
await ReallyBeIdle(10);
await Client.WaitRunTicks(1);
await base.RevertModifiedCvars();
if (!settings.ShouldBeConnected)
if (!modified)
return;
await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
ClearModifiedCvars();
}
protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
{
var next = (PoolSettings)n;
await base.ApplySettings(instance, next);
var cfg = instance.CfgMan;
await instance.WaitPost(() =>
{
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
await ReallyBeIdle(10);
}
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
var cRand = Client.ResolveDependency<IRobustRandom>();
var sRand = Server.ResolveDependency<IRobustRandom>();
_nextClientSeed = cRand.Next();
_nextServerSeed = sRand.Next();
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
cfg.SetCVar(CCVars.GameMap, next.Map);
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
});
}
public void Kill()
protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
{
State = PairState.Dead;
ServerLogHandler.ShuttingDown = true;
ClientLogHandler.ShuttingDown = true;
Server.Dispose();
Client.Dispose();
}
var opts = base.ClientOptions();
private void ClearContext()
{
_testOut = default!;
ServerLogHandler.ClearContext();
ClientLogHandler.ClearContext();
}
public void ActivateContext(TextWriter testOut)
{
_testOut = testOut;
ServerLogHandler.ActivateContext(testOut);
ClientLogHandler.ActivateContext(testOut);
}
public void Use()
{
if (State != PairState.Ready)
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
State = PairState.InUse;
}
public enum PairState : byte
{
Ready = 0,
InUse = 1,
CleanDisposed = 2,
Dead = 3,
}
public void SetupSeed()
{
var sRand = Server.ResolveDependency<IRobustRandom>();
if (Settings.ServerSeed is { } severSeed)
opts.LoadTestAssembly = false;
opts.ContentStart = true;
opts.FailureLogLevel = LogLevel.Warning;
opts.Options = new()
{
ServerSeed = severSeed;
sRand.SetSeed(ServerSeed);
}
else
{
ServerSeed = _nextServerSeed;
sRand.SetSeed(ServerSeed);
_nextServerSeed = sRand.Next();
}
LoadConfigAndUserData = false,
};
var cRand = Client.ResolveDependency<IRobustRandom>();
if (Settings.ClientSeed is { } clientSeed)
opts.BeforeStart += () =>
{
ClientSeed = clientSeed;
cRand.SetSeed(ClientSeed);
}
else
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
{
ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
});
};
return opts;
}
protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
{
var opts = base.ServerOptions();
opts.LoadTestAssembly = false;
opts.ContentStart = true;
opts.Options = new()
{
ClientSeed = _nextClientSeed;
cRand.SetSeed(ClientSeed);
_nextClientSeed = cRand.Next();
}
LoadConfigAndUserData = false,
};
opts.BeforeStart += () =>
{
// Server-only systems (i.e., systems that subscribe to events with server-only components)
// There's probably a better way to do this.
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
};
return opts;
}
}

View File

@@ -1,15 +1,14 @@
#nullable enable
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
// Partial class containing cvar logic
// Partial class containing test cvars
// This could probably be merged into the main file, but I'm keeping it separate to reduce
// conflicts for forks.
public static partial class PoolManager
{
private static readonly (string cvar, string value)[] TestCvars =
public static readonly (string cvar, string value)[] TestCvars =
{
// @formatter:off
(CCVars.DatabaseSynchronous.Name, "true"),
@@ -17,9 +16,7 @@ public static partial class PoolManager
(CCVars.HolidaysEnabled.Name, "false"),
(CCVars.GameMap.Name, TestMap),
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
(CVars.NetPVS.Name, "false"),
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleLoadoutTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
@@ -30,49 +27,13 @@ public static partial class PoolManager
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
(CVars.ReplayClientRecordingEnabled.Name, "false"),
(CVars.ReplayServerRecordingEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"),
(CCVars.GameLobbyEnabled.Name, "false"),
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
(CCVars.MovementMobPushing.Name, "false"),
};
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
{
var cfg = instance.ResolveDependency<IConfigurationManager>();
await instance.WaitPost(() =>
{
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
cfg.SetCVar(CCVars.GameMap, settings.Map);
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
});
}
private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
{
foreach (var (cvar, value) in TestCvars)
{
options.CVarOverrides[cvar] = value;
}
}
}

View File

@@ -1,35 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Reflection;
using Robust.Shared.Utility;
namespace Content.IntegrationTests;
// Partial class for handling the discovering and storing test prototypes.
public static partial class PoolManager
{
private static List<string> _testPrototypes = new();
private const BindingFlags Flags = BindingFlags.Static
| BindingFlags.NonPublic
| BindingFlags.Public
| BindingFlags.DeclaredOnly;
private static void DiscoverTestPrototypes(Assembly assembly)
{
foreach (var type in assembly.GetTypes())
{
foreach (var field in type.GetFields(Flags))
{
if (!field.HasCustomAttribute<TestPrototypesAttribute>())
continue;
var val = field.GetValue(null);
if (val is not string str)
throw new Exception($"TestPrototypeAttribute is only valid on non-null string fields");
_testPrototypes.Add(str);
}
}
}
}

View File

@@ -1,373 +1,17 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using Content.Client.IoC;
using Content.Client.Parallax.Managers;
using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Tests;
using Content.IntegrationTests.Tests.Destructible;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.IntegrationTests.Tests.Interaction.Click;
using Robust.Client;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.CCVar;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
/// <summary>
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
/// </summary>
// The static class exist to avoid breaking changes
public static partial class PoolManager
{
public static readonly ContentPoolManager Instance = new();
public const string TestMap = "Empty";
private static int _pairId;
private static readonly object PairLock = new();
private static bool _initialized;
// Pair, IsBorrowed
private static readonly Dictionary<TestPair, bool> Pairs = new();
private static bool _dead;
private static Exception? _poolFailureReason;
private static HashSet<Assembly> _contentAssemblies = default!;
public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
PoolSettings poolSettings,
TextWriter testOut)
{
var options = new RobustIntegrationTest.ServerIntegrationOptions
{
ContentStart = true,
Options = new ServerOptions()
{
LoadConfigAndUserData = false,
LoadContentResources = !poolSettings.NoLoadContent,
},
ContentAssemblies = _contentAssemblies.ToArray()
};
var logHandler = new PoolTestLogHandler("SERVER");
logHandler.ActivateContext(testOut);
options.OverrideLogHandler = () => logHandler;
options.BeforeStart += () =>
{
// Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
};
SetDefaultCVars(options);
var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
await server.WaitIdleAsync();
await SetupCVars(server, poolSettings);
return (server, logHandler);
}
/// <summary>
/// This shuts down the pool, and disposes all the server/client pairs.
/// This is a one time operation to be used when the testing program is exiting.
/// </summary>
public static void Shutdown()
{
List<TestPair> localPairs;
lock (PairLock)
{
if (_dead)
return;
_dead = true;
localPairs = Pairs.Keys.ToList();
}
foreach (var pair in localPairs)
{
pair.Kill();
}
_initialized = false;
}
public static string DeathReport()
{
lock (PairLock)
{
var builder = new StringBuilder();
var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
foreach (var pair in pairs)
{
var borrowed = Pairs[pair];
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
}
}
return builder.ToString();
}
}
public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
PoolSettings poolSettings,
TextWriter testOut)
{
var options = new RobustIntegrationTest.ClientIntegrationOptions
{
FailureLogLevel = LogLevel.Warning,
ContentStart = true,
ContentAssemblies = new[]
{
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Client.Entry.EntryPoint).Assembly,
typeof(PoolManager).Assembly,
}
};
if (poolSettings.NoLoadContent)
{
Assert.Warn("NoLoadContent does not work on the client, ignoring");
}
options.Options = new GameControllerOptions()
{
LoadConfigAndUserData = false,
// LoadContentResources = !poolSettings.NoLoadContent
};
var logHandler = new PoolTestLogHandler("CLIENT");
logHandler.ActivateContext(testOut);
options.OverrideLogHandler = () => logHandler;
options.BeforeStart += () =>
{
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
{
ClientBeforeIoC = () =>
{
// do not register extra systems or components here -- they will get cleared when the client is
// disconnected. just use reflection.
IoCManager.Register<IParallaxManager, DummyParallaxManager>(true);
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
}
});
};
SetDefaultCVars(options);
var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
await client.WaitIdleAsync();
await SetupCVars(client, poolSettings);
return (client, logHandler);
}
/// <summary>
/// Gets a <see cref="Pair.TestPair"/>, which can be used to get access to a server, and client <see cref="Pair.TestPair"/>
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
public static async Task<TestPair> GetServerClient(
PoolSettings? poolSettings = null,
ITestContextLike? testContext = null)
{
return await GetServerClientPair(
poolSettings ?? new PoolSettings(),
testContext ?? new NUnitTestContextWrap(TestContext.CurrentContext, TestContext.Out));
}
private static string GetDefaultTestName(ITestContextLike testContext)
{
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
private static async Task<TestPair> GetServerClientPair(
PoolSettings poolSettings,
ITestContextLike testContext)
{
if (!_initialized)
throw new InvalidOperationException($"Pool manager has not been initialized");
// Trust issues with the AsyncLocal that backs this.
var testOut = testContext.Out;
DieIfPoolFailure();
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
var poolRetrieveTimeWatch = new Stopwatch();
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
TestPair? pair = null;
try
{
poolRetrieveTimeWatch.Start();
if (poolSettings.MustBeNew)
{
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
pair = await CreateServerClientPair(poolSettings, testOut);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
pair = GrabOptimalPair(poolSettings);
if (pair != null)
{
pair.ActivateContext(testOut);
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
if (canSkip)
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
await SetupCVars(pair.Client, poolSettings);
await SetupCVars(pair.Server, poolSettings);
await pair.RunTicksSync(1);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
await pair.CleanPooledPair(poolSettings, testOut);
}
await pair.RunTicksSync(5);
await pair.SyncTicks(targetDelta: 1);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
pair = await CreateServerClientPair(poolSettings, testOut);
}
}
}
finally
{
if (pair != null && pair.TestHistory.Count > 0)
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
}
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
}
}
pair.ValidateSettings(poolSettings);
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
pair.ClearModifiedCvars();
pair.Settings = poolSettings;
pair.TestHistory.Add(currentTestName);
pair.SetupSeed();
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
pair.Watch.Restart();
return pair;
}
private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
{
lock (PairLock)
{
TestPair? fallback = null;
foreach (var pair in Pairs.Keys)
{
if (Pairs[pair])
continue;
if (!pair.Settings.CanFastRecycle(poolSettings))
{
fallback = pair;
continue;
}
pair.Use();
Pairs[pair] = true;
return pair;
}
if (fallback != null)
{
fallback.Use();
Pairs[fallback!] = true;
}
return fallback;
}
}
/// <summary>
/// Used by TestPair after checking the server/client pair, Don't use this.
/// </summary>
public static void NoCheckReturn(TestPair pair)
{
lock (PairLock)
{
if (pair.State == TestPair.PairState.Dead)
Pairs.Remove(pair);
else if (pair.State == TestPair.PairState.Ready)
Pairs[pair] = false;
else
throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
}
}
private static void DieIfPoolFailure()
{
if (_poolFailureReason != null)
{
// If the _poolFailureReason is not null, we can assume at least one test failed.
// So we say inconclusive so we don't add more failed tests to search through.
Assert.Inconclusive(@$"
In a different test, the pool manager had an exception when trying to create a server/client pair.
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
}
if (_dead)
{
// If Pairs is null, we ran out of time, we can't assume a test failed.
// So we are going to tell it all future tests are a failure.
Assert.Fail("The pool was shut down");
}
}
private static async Task<TestPair> CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
{
try
{
var id = Interlocked.Increment(ref _pairId);
var pair = new TestPair(id);
await pair.Initialize(poolSettings, testOut, _testPrototypes);
pair.Use();
await pair.RunTicksSync(5);
await pair.SyncTicks(targetDelta: 1);
return pair;
}
catch (Exception ex)
{
_poolFailureReason = ex;
throw;
}
}
/// <summary>
/// Runs a server, or a client until a condition is true
@@ -423,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
Assert.That(passed);
}
/// <summary>
/// Initialize the pool manager.
/// </summary>
/// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
public static void Startup(params Assembly[] extraAssemblies)
public static async Task<TestPair> GetServerClient(
PoolSettings? settings = null,
ITestContextLike? testContext = null)
{
if (_initialized)
throw new InvalidOperationException("Already initialized");
return await Instance.GetPair(settings, testContext);
}
_initialized = true;
_contentAssemblies =
[
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Server.Entry.EntryPoint).Assembly,
typeof(PoolManager).Assembly
];
_contentAssemblies.UnionWith(extraAssemblies);
public static void Startup(params Assembly[] extra)
=> Instance.Startup(extra);
_testPrototypes.Clear();
DiscoverTestPrototypes(typeof(PoolManager).Assembly);
foreach (var assembly in extraAssemblies)
{
DiscoverTestPrototypes(assembly);
}
public static void Shutdown() => Instance.Shutdown();
public static string DeathReport() => Instance.DeathReport();
}
/// <summary>
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
/// </summary>
public sealed class ContentPoolManager : PoolManager<TestPair>
{
public override PairSettings DefaultSettings => new PoolSettings();
protected override string GetDefaultTestName(ITestContextLike testContext)
{
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
public override void Startup(params Assembly[] extraAssemblies)
{
DefaultCvars.AddRange(PoolManager.TestCvars);
var shared = extraAssemblies
.Append(typeof(Shared.Entry.EntryPoint).Assembly)
.Append(typeof(PoolManager).Assembly)
.ToArray();
Startup([typeof(Client.Entry.EntryPoint).Assembly],
[typeof(Server.Entry.EntryPoint).Assembly],
shared);
}
}

View File

@@ -1,43 +1,31 @@
#nullable enable
namespace Content.IntegrationTests;
using Robust.Shared.Random;
namespace Content.IntegrationTests;
/// <summary>
/// Settings for the pooled server, and client pair.
/// Some options are for changing the pair, and others are
/// so the pool can properly clean up what you borrowed.
/// </summary>
public sealed class PoolSettings
/// <inheritdoc/>
public sealed class PoolSettings : PairSettings
{
/// <summary>
/// Set to true if the test will ruin the server/client pair.
/// </summary>
public bool Destructive { get; init; }
public override bool Connected
{
get => _connected || InLobby;
init => _connected = value;
}
/// <summary>
/// Set to true if the given server/client pair should be created fresh.
/// </summary>
public bool Fresh { get; init; }
private readonly bool _dummyTicker = true;
private readonly bool _connected;
/// <summary>
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
/// </summary>
public bool DummyTicker { get; init; } = true;
public bool DummyTicker
{
get => _dummyTicker && !InLobby;
init => _dummyTicker = value;
}
/// <summary>
/// If true, this enables the creation of admin logs during the test.
/// </summary>
public bool AdminLogsEnabled { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be connected from each other.
/// Defaults to disconnected as it makes dirty recycling slightly faster.
/// If <see cref="InLobby"/> is true, this option is ignored.
/// </summary>
public bool Connected { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be in the lobby.
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
@@ -53,81 +41,22 @@ public sealed class PoolSettings
/// </summary>
public bool NoLoadContent { get; init; }
/// <summary>
/// This will return a server-client pair that has not loaded test prototypes.
/// Try avoiding this whenever possible, as this will always create & destroy a new pair.
/// Use <see cref="Pair.TestPair.IsTestPrototype(Robust.Shared.Prototypes.EntityPrototype)"/> if you need to exclude test prototypees.
/// </summary>
public bool NoLoadTestPrototypes { get; init; }
/// <summary>
/// Set this to true to disable the NetInterp CVar on the given server/client pair
/// </summary>
public bool DisableInterpolate { get; init; }
/// <summary>
/// Set this to true to always clean up the server/client pair before giving it to another borrower
/// </summary>
public bool Dirty { get; init; }
/// <summary>
/// Set this to the path of a map to have the given server/client pair load the map.
/// </summary>
public string Map { get; init; } = PoolManager.TestMap;
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
public string? TestName { get; set; }
/// <summary>
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
/// </summary>
public int? ServerSeed { get; set; }
/// <summary>
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
/// </summary>
public int? ClientSeed { get; set; }
#region Inferred Properties
/// <summary>
/// If the returned pair must not be reused
/// </summary>
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// If the given pair must be brand new
/// </summary>
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
public bool UseDummyTicker => !InLobby && DummyTicker;
public bool ShouldBeConnected => InLobby || Connected;
#endregion
/// <summary>
/// Tries to guess if we can skip recycling the server/client pair.
/// </summary>
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
/// <returns>If we can skip cleaning it up</returns>
public bool CanFastRecycle(PoolSettings nextSettings)
public override bool CanFastRecycle(PairSettings nextSettings)
{
if (MustNotBeReused)
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
if (!base.CanFastRecycle(nextSettings))
return false;
if (nextSettings.MustBeNew)
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
if (Dirty)
if (nextSettings is not PoolSettings next)
return false;
// Check that certain settings match.
return !ShouldBeConnected == !nextSettings.ShouldBeConnected
&& UseDummyTicker == nextSettings.UseDummyTicker
&& Map == nextSettings.Map
&& InLobby == nextSettings.InLobby;
return DummyTicker == next.DummyTicker
&& Map == next.Map
&& InLobby == next.InLobby;
}
}

View File

@@ -1,79 +0,0 @@
using System.IO;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Serilog.Events;
namespace Content.IntegrationTests;
#nullable enable
/// <summary>
/// Log handler intended for pooled integration tests.
/// </summary>
/// <remarks>
/// <para>
/// This class logs to two places: an NUnit <see cref="TestContext"/>
/// (so it nicely gets attributed to a test in your IDE),
/// and an in-memory ring buffer for diagnostic purposes.
/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
/// </para>
/// <para>
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
/// </para>
/// </remarks>
public sealed class PoolTestLogHandler : ILogHandler
{
private readonly string? _prefix;
private RStopwatch _stopwatch;
public TextWriter? ActiveContext { get; private set; }
public LogLevel? FailureLevel { get; set; }
public PoolTestLogHandler(string? prefix)
{
_prefix = prefix != null ? $"{prefix}: " : "";
}
public bool ShuttingDown;
public void Log(string sawmillName, LogEvent message)
{
var level = message.Level.ToRobust();
if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
return;
if (ActiveContext is not { } testContext)
{
// If this gets hit it means something is logging to this instance while it's "between" tests.
// This is a bug in either the game or the testing system, and must always be investigated.
throw new InvalidOperationException("Log to pool test log handler without active test context");
}
var name = LogMessage.LogLevelToName(level);
var seconds = _stopwatch.Elapsed.TotalSeconds;
var rendered = message.RenderMessage();
var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
testContext.WriteLine(line);
if (FailureLevel == null || level < FailureLevel)
return;
testContext.Flush();
Assert.Fail($"{line} Exception: {message.Exception}");
}
public void ClearContext()
{
ActiveContext = null;
}
public void ActivateContext(TextWriter context)
{
_stopwatch.Restart();
ActiveContext = context;
}
}

View File

@@ -1,12 +0,0 @@
using JetBrains.Annotations;
namespace Content.IntegrationTests;
/// <summary>
/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
[MeansImplicitUse]
public sealed class TestPrototypesAttribute : Attribute
{
}

View File

@@ -55,7 +55,7 @@ namespace Content.IntegrationTests.Tests.Access
system.ClearDenyTags(reader);
// test one list
system.AddAccess(reader, "A");
system.TryAddAccess(reader, "A");
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -63,10 +63,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.TryClearAccesses(reader);
// test one list - two items
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A", "B" });
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.False);
@@ -74,14 +74,14 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.TryClearAccesses(reader);
// test two list
var accesses = new List<HashSet<ProtoId<AccessLevelPrototype>>>() {
new HashSet<ProtoId<AccessLevelPrototype>> () { "A" },
new HashSet<ProtoId<AccessLevelPrototype>> () { "B", "C" }
};
system.AddAccesses(reader, accesses);
system.TryAddAccesses(reader, accesses);
Assert.Multiple(() =>
{
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A" }, reader), Is.True);
@@ -91,10 +91,10 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "C", "B", "A" }, reader), Is.True);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.TryClearAccesses(reader);
// test deny list
system.AddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.TryAddAccess(reader, new HashSet<ProtoId<AccessLevelPrototype>> { "A" });
system.AddDenyTag(reader, "B");
Assert.Multiple(() =>
{
@@ -103,7 +103,7 @@ namespace Content.IntegrationTests.Tests.Access
Assert.That(system.AreAccessTagsAllowed(new List<ProtoId<AccessLevelPrototype>> { "A", "B" }, reader), Is.False);
Assert.That(system.AreAccessTagsAllowed(Array.Empty<ProtoId<AccessLevelPrototype>>(), reader), Is.False);
});
system.ClearAccesses(reader);
system.TryClearAccesses(reader);
system.ClearDenyTags(reader);
});
await pair.CleanReturnAsync();

View File

@@ -0,0 +1,135 @@
using Content.IntegrationTests.Tests.Movement;
using Content.Shared.Chasm;
using Content.Shared.Projectiles;
using Content.Shared.Weapons.Misc;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Chasm;
/// <summary>
/// A test for chasms, which delete entities when a player walks over them.
/// </summary>
[TestOf(typeof(ChasmComponent))]
public sealed class ChasmTest : MovementTest
{
private readonly EntProtoId _chasmProto = "FloorChasmEntity";
private readonly EntProtoId _catWalkProto = "Catwalk";
private readonly EntProtoId _grapplingGunProto = "WeaponGrapplingGun";
/// <summary>
/// Test that a player falls into the chasm when walking over it.
/// </summary>
[Test]
public async Task ChasmFallTest()
{
// Spawn a chasm.
await SpawnTarget(_chasmProto);
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
// Attempt (and fail) to walk past the chasm.
// If you are modifying the default value of ChasmFallingComponent.DeletionTime this time might need to be adjusted.
await Move(DirectionFlag.East, 0.5f);
// We should be falling right now.
Assert.That(TryComp<ChasmFallingComponent>(Player, out var falling), "Player is not falling after walking over a chasm.");
var fallTime = (float)falling.DeletionTime.TotalSeconds;
// Wait until we get deleted.
await Pair.RunSeconds(fallTime);
// Check that the player was deleted.
AssertDeleted(Player);
}
/// <summary>
/// Test that a catwalk placed over a chasm will protect a player from falling.
/// </summary>
[Test]
public async Task ChasmCatwalkTest()
{
// Spawn a chasm.
await SpawnTarget(_chasmProto);
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
// Spawn a catwalk over the chasm.
var catwalk = await Spawn(_catWalkProto);
// Attempt to walk past the chasm.
await Move(DirectionFlag.East, 1f);
// We should be on the other side.
Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a catwalk.");
// Check that the player is not deleted.
AssertExists(Player);
// Make sure the player is not falling right now.
Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after walking over a catwalk.");
// Delete the catwalk.
await Delete(catwalk);
// Attempt (and fail) to walk past the chasm.
await Move(DirectionFlag.West, 1f);
// Wait until we get deleted.
await Pair.RunSeconds(5f);
// Check that the player was deleted
AssertDeleted(Player);
}
/// <summary>
/// Tests that a player is able to cross a chasm by using a grappling gun.
/// </summary>
[Test]
public async Task ChasmGrappleTest()
{
// Spawn a chasm.
await SpawnTarget(_chasmProto);
Assert.That(Delta(), Is.GreaterThan(0.5), "Player did not spawn left of the chasm.");
// Give the player a grappling gun.
var grapplingGun = await PlaceInHands(_grapplingGunProto);
await Pair.RunSeconds(2f); // guns have a cooldown when picking them up
// Shoot at the wall to the right.
Assert.That(WallRight, Is.Not.Null, "No wall to shoot at!");
await AttemptShoot(WallRight);
await Pair.RunSeconds(2f);
// Check that the grappling hook is embedded into the wall.
Assert.That(TryComp<GrapplingGunComponent>(grapplingGun, out var grapplingGunComp), "Grappling gun did not have GrapplingGunComponent.");
Assert.That(grapplingGunComp.Projectile, Is.Not.Null, "Grappling gun projectile does not exist.");
Assert.That(SEntMan.TryGetComponent<EmbeddableProjectileComponent>(grapplingGunComp.Projectile, out var embeddable), "Grappling hook was not embeddable.");
Assert.That(embeddable.EmbeddedIntoUid, Is.EqualTo(ToServer(WallRight)), "Grappling hook was not embedded into the wall.");
// Check that the player is hooked.
var grapplingSystem = SEntMan.System<SharedGrapplingGunSystem>();
Assert.That(grapplingSystem.IsEntityHooked(SPlayer), "Player is not hooked to the wall.");
Assert.That(HasComp<JointRelayTargetComponent>(Player), "Player does not have the JointRelayTargetComponent after using a grappling gun.");
// Attempt to walk past the chasm.
await Move(DirectionFlag.East, 1f);
// We should be on the other side.
Assert.That(Delta(), Is.LessThan(-0.5), "Player was unable to walk over a chasm with a grappling gun.");
// Check that the player is not deleted.
AssertExists(Player);
// Make sure the player is not falling right now.
Assert.That(HasComp<ChasmFallingComponent>(Player), Is.False, "Player has ChasmFallingComponent after moving over a chasm with a grappling gun.");
// Drop the grappling gun.
await Drop();
// Check that the player no longer hooked.
Assert.That(grapplingSystem.IsEntityHooked(SPlayer), Is.False, "Player still hooked after dropping the grappling gun.");
Assert.That(HasComp<JointRelayTargetComponent>(Player), Is.False, "Player still has the JointRelayTargetComponent after dropping the grappling gun.");
}
}

View File

@@ -285,7 +285,7 @@ namespace Content.IntegrationTests.Tests
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
await Assert.MultipleAsync(async () =>
{
@@ -325,8 +325,8 @@ namespace Content.IntegrationTests.Tests
// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(client.EntMan.EntityCount, Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
}

View File

@@ -10,6 +10,7 @@ using Content.Server.Construction.Components;
using Content.Server.Gravity;
using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.CombatMode;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Gravity;
using Content.Shared.Item;
@@ -85,7 +86,7 @@ public abstract partial class InteractionTest
}
/// <summary>
/// Spawn an entity entity and set it as the target.
/// Spawn an entity at the target coordinates and set it as the target.
/// </summary>
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
@@ -103,6 +104,22 @@ public abstract partial class InteractionTest
}
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
/// <summary>
/// Spawn an entity entity at the target coordinates without setting it as the target.
/// </summary>
protected async Task<NetEntity> Spawn(string prototype)
{
var entity = NetEntity.Invalid;
await Server.WaitPost(() =>
{
entity = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
});
await RunTicks(5);
AssertPrototype(prototype, entity);
return entity;
}
/// <summary>
/// Spawn an entity in preparation for deconstruction
/// </summary>
@@ -386,6 +403,119 @@ public abstract partial class InteractionTest
#endregion
# region Combat
/// <summary>
/// Returns if the player is currently in combat mode.
/// </summary>
protected bool IsInCombatMode()
{
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
{
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
return false;
}
return combat.IsInCombatMode;
}
/// <summary>
/// Set the combat mode for the player.
/// </summary>
protected async Task SetCombatMode(bool enabled)
{
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
{
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
return;
}
await Server.WaitPost(() => SCombatMode.SetInCombatMode(SPlayer, enabled, combat));
await RunTicks(1);
Assert.That(combat.IsInCombatMode, Is.EqualTo(enabled), $"Player could not set combate mode to {enabled}");
}
/// <summary>
/// Make the player shoot with their currently held gun.
/// The player needs to be able to enter combat mode for this.
/// This does not pass a target entity into the GunSystem, meaning that targets that
/// need to be aimed at directly won't be hit.
/// </summary>
/// <remarks>
/// Guns have a cooldown when picking them up.
/// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
/// </remarks>
/// <param name="target">The target coordinates to shoot at. Defaults to the current <see cref="TargetCoords"/>.</param>
/// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
protected async Task AttemptShoot(NetCoordinates? target = null, bool assert = true)
{
var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
{
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
return;
}
// Enter combat mode before shooting.
var wasInCombatMode = IsInCombatMode();
await SetCombatMode(true);
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
await Server.WaitAssertion(() =>
{
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, actualTarget);
if (assert)
Assert.That(success, "Gun failed to shoot.");
});
await RunTicks(1);
// If the player was not in combat mode before then disable it again.
await SetCombatMode(wasInCombatMode);
}
/// <summary>
/// Make the player shoot with their currently held gun.
/// The player needs to be able to enter combat mode for this.
/// </summary>
/// <remarks>
/// Guns have a cooldown when picking them up.
/// So make sure to wait a little after spawning a gun in the player's hand or this will fail.
/// </remarks>
/// <param name="target">The target entity to shoot at. Defaults to the current <see cref="Target"/> entity.</param>
/// <param name="assert">If true this method will assert that the gun was successfully fired.</param>
protected async Task AttemptShoot(NetEntity? target = null, bool assert = true)
{
var actualTarget = target ?? Target;
Assert.That(actualTarget, Is.Not.Null, "No target to shoot at!");
if (!SEntMan.TryGetComponent(SPlayer, out CombatModeComponent? combat))
{
Assert.Fail($"Entity {SEntMan.ToPrettyString(SPlayer)} does not have a CombatModeComponent");
return;
}
// Enter combat mode before shooting.
var wasInCombatMode = IsInCombatMode();
await SetCombatMode(true);
Assert.That(SGun.TryGetGun(SPlayer, out var gunUid, out var gunComp), "Player was not holding a gun!");
await Server.WaitAssertion(() =>
{
var success = SGun.AttemptShoot(SPlayer, gunUid, gunComp!, Position(actualTarget!.Value), ToServer(actualTarget));
if (assert)
Assert.That(success, "Gun failed to shoot.");
});
await RunTicks(1);
// If the player was not in combat mode before then disable it again.
await SetCombatMode(wasInCombatMode);
}
#endregion
/// <summary>
/// Wait for any currently active DoAfters to finish.
/// </summary>
@@ -746,6 +876,18 @@ public abstract partial class InteractionTest
return SEntMan.GetComponent<T>(ToServer(target!.Value));
}
/// <summary>
/// Convenience method to check if the target has a component on the server.
/// </summary>
protected bool HasComp<T>(NetEntity? target = null) where T : IComponent
{
target ??= Target;
if (target == null)
Assert.Fail("No target specified");
return SEntMan.HasComponent<T>(ToServer(target));
}
/// <inheritdoc cref="Comp{T}"/>
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
{
@@ -1013,7 +1155,7 @@ public abstract partial class InteractionTest
}
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
return (TControl) control;
return (TControl)control;
}
/// <summary>
@@ -1177,8 +1319,8 @@ public abstract partial class InteractionTest
{
var atmosSystem = SEntMan.System<AtmosphereSystem>();
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;
moles[(int)Gas.Oxygen] = 21.824779f;
moles[(int)Gas.Nitrogen] = 82.10312f;
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
});
}

View File

@@ -7,12 +7,16 @@ using Content.IntegrationTests.Pair;
using Content.Server.Hands.Systems;
using Content.Server.Stack;
using Content.Server.Tools;
using Content.Shared.CombatMode;
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Input;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
@@ -21,8 +25,6 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.UnitTesting;
using Content.Shared.Item.ItemToggle;
using Robust.Client.State;
namespace Content.IntegrationTests.Tests.Interaction;
@@ -107,6 +109,8 @@ public abstract partial class InteractionTest
protected SharedMapSystem MapSystem = default!;
protected ISawmill SLogger = default!;
protected SharedUserInterfaceSystem SUiSys = default!;
protected SharedCombatModeSystem SCombatMode = default!;
protected SharedGunSystem SGun = default!;
// CLIENT dependencies
protected IEntityManager CEntMan = default!;
@@ -124,7 +128,7 @@ public abstract partial class InteractionTest
protected HandsComponent Hands = default!;
protected DoAfterComponent DoAfters = default!;
public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
public float TickPeriod => (float)STiming.TickPeriod.TotalSeconds;
// Simple mob that has one hand and can perform misc interactions.
[TestPrototypes]
@@ -149,6 +153,7 @@ public abstract partial class InteractionTest
tags:
- CanPilot
- type: UserInterface
- type: CombatMode
";
[SetUp]
@@ -163,6 +168,7 @@ public abstract partial class InteractionTest
ProtoMan = Server.ResolveDependency<IPrototypeManager>();
Factory = Server.ResolveDependency<IComponentFactory>();
STiming = Server.ResolveDependency<IGameTiming>();
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
HandSys = SEntMan.System<HandsSystem>();
InteractSys = SEntMan.System<SharedInteractionSystem>();
ToolSys = SEntMan.System<ToolSystem>();
@@ -173,20 +179,21 @@ public abstract partial class InteractionTest
SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
STestSystem = SEntMan.System<InteractionTestSystem>();
Stack = SEntMan.System<StackSystem>();
SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
SUiSys = Client.System<SharedUserInterfaceSystem>();
SUiSys = SEntMan.System<SharedUserInterfaceSystem>();
SCombatMode = SEntMan.System<SharedCombatModeSystem>();
SGun = SEntMan.System<SharedGunSystem>();
// client dependencies
CEntMan = Client.ResolveDependency<IEntityManager>();
UiMan = Client.ResolveDependency<IUserInterfaceManager>();
CTiming = Client.ResolveDependency<IGameTiming>();
InputManager = Client.ResolveDependency<IInputManager>();
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
InputSystem = CEntMan.System<Robust.Client.GameObjects.InputSystem>();
CTestSystem = CEntMan.System<InteractionTestSystem>();
CConSys = CEntMan.System<ConstructionSystem>();
ExamineSys = CEntMan.System<ExamineSystem>();
CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
CUiSys = Client.System<SharedUserInterfaceSystem>();
CUiSys = CEntMan.System<SharedUserInterfaceSystem>();
// Setup map.
await Pair.CreateTestMap();

View File

@@ -145,7 +145,7 @@ public sealed class MaterialArbitrageTest
Dictionary<string, double> priceCache = new();
Dictionary<string, (Dictionary<string, int> Ents, Dictionary<string, int> Mats)> spawnedOnDestroy = new();
Dictionary<string, (Dictionary<string, float> Ents, Dictionary<string, float> Mats)> spawnedOnDestroy = new();
// cache the compositions of entities
// If the entity is refineable (i.e. glass shared can be turned into glass, we take the greater of the two compositions.
@@ -217,8 +217,8 @@ public sealed class MaterialArbitrageTest
var comp = (DestructibleComponent) destructible.Component;
var spawnedEnts = new Dictionary<string, int>();
var spawnedMats = new Dictionary<string, int>();
var spawnedEnts = new Dictionary<string, float>();
var spawnedMats = new Dictionary<string, float>();
// This test just blindly assumes that ALL spawn entity behaviors get triggered. In reality, some entities
// might only trigger a subset. If that starts being a problem, this test either needs fixing or needs to
@@ -233,14 +233,14 @@ public sealed class MaterialArbitrageTest
foreach (var (key, value) in spawn.Spawn)
{
spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + value.Max;
spawnedEnts[key] = spawnedEnts.GetValueOrDefault(key) + (float)(value.Min + value.Max) / 2;
if (!compositions.TryGetValue(key, out var composition))
continue;
foreach (var (matId, amount) in composition)
{
spawnedMats[matId] = value.Max * amount + spawnedMats.GetValueOrDefault(matId);
spawnedMats[matId] = (float)(value.Min + value.Max) / 2 * amount + spawnedMats.GetValueOrDefault(matId);
}
}
}
@@ -451,7 +451,7 @@ public sealed class MaterialArbitrageTest
await server.WaitPost(() => mapSystem.DeleteMap(testMap.MapId));
await pair.CleanReturnAsync();
async Task<double> GetSpawnedPrice(Dictionary<string, int> ents)
async Task<double> GetSpawnedPrice(Dictionary<string, float> ents)
{
double price = 0;
foreach (var (id, num) in ents)

View File

@@ -24,6 +24,15 @@ public abstract class MovementTest : InteractionTest
/// </summary>
protected virtual bool AddWalls => true;
/// <summary>
/// The wall entity on the left side.
/// </summary>
protected NetEntity? WallLeft;
/// <summary>
/// The wall entity on the right side.
/// </summary>
protected NetEntity? WallRight;
[SetUp]
public override async Task Setup()
{
@@ -38,8 +47,11 @@ public abstract class MovementTest : InteractionTest
if (AddWalls)
{
await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0)));
var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0)));
WallLeft = SEntMan.GetNetEntity(sWallLeft);
WallRight = SEntMan.GetNetEntity(sWallRight);
}
await AddGravity();

View File

@@ -20,6 +20,7 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

View File

@@ -9,6 +9,7 @@ using Content.IntegrationTests;
using Content.MapRenderer.Painters;
using Content.Server.Maps;
using Robust.Shared.Prototypes;
using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;

View File

@@ -229,7 +229,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
_adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
_accessReader.SetAccesses(accessReaderEnt.Value, newAccessList);
_accessReader.TrySetAccesses(accessReaderEnt.Value, newAccessList);
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);

View File

@@ -7,9 +7,7 @@ using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Eui;
using Content.Shared.Roles;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration;
@@ -21,7 +19,6 @@ public sealed class BanPanelEui : BaseEui
[Dependency] private readonly IPlayerLocator _playerLocator = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IAdminManager _admins = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly ISawmill _sawmill;
@@ -52,7 +49,7 @@ public sealed class BanPanelEui : BaseEui
switch (msg)
{
case BanPanelEuiStateMsg.CreateBanRequest r:
BanPlayer(r.Player, r.IpAddress, r.UseLastIp, r.Hwid, r.UseLastHwid, r.Minutes, r.Severity, r.Reason, r.Roles, r.Erase);
BanPlayer(r.Ban);
break;
case BanPanelEuiStateMsg.GetPlayerInfoRequest r:
ChangePlayer(r.PlayerUsername);
@@ -60,29 +57,26 @@ public sealed class BanPanelEui : BaseEui
}
}
private async void BanPlayer(string? target, string? ipAddressString, bool useLastIp, ImmutableTypedHwid? hwid, bool useLastHwid, uint minutes, NoteSeverity severity, string reason, IReadOnlyCollection<string>? roles, bool erase)
private async void BanPlayer(Ban ban)
{
if (!_admins.HasAdminFlag(Player, AdminFlags.Ban))
{
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to create a ban with no ban flag");
return;
}
if (target == null && string.IsNullOrWhiteSpace(ipAddressString) && hwid == null)
if (ban.Target == null && string.IsNullOrWhiteSpace(ban.IpAddress) && ban.Hwid == null)
{
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-no-data"));
return;
}
(IPAddress, int)? addressRange = null;
if (ipAddressString is not null)
if (ban.IpAddress is not null)
{
var hid = "0";
var split = ipAddressString.Split('/', 2);
ipAddressString = split[0];
if (split.Length > 1)
hid = split[1];
if (!IPAddress.TryParse(ipAddressString, out var ipAddress) || !uint.TryParse(hid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
if (!IPAddress.TryParse(ban.IpAddress, out var ipAddress) || !uint.TryParse(ban.IpAddressHid, out var hidInt) || hidInt > Ipv6_CIDR || hidInt > Ipv4_CIDR && ipAddress.AddressFamily == AddressFamily.InterNetwork)
{
_chat.DispatchServerMessage(Player, Loc.GetString("ban-panel-invalid-ip"));
return;
@@ -94,12 +88,12 @@ public sealed class BanPanelEui : BaseEui
addressRange = (ipAddress, (int) hidInt);
}
var targetUid = target is not null ? PlayerId : null;
addressRange = useLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
var targetHWid = useLastHwid ? LastHwid : hwid;
if (target != null && target != PlayerName || Guid.TryParse(target, out var parsed) && parsed != PlayerId)
var targetUid = ban.Target is not null ? PlayerId : null;
addressRange = ban.UseLastIp && LastAddress is not null ? (LastAddress, LastAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR) : addressRange;
var targetHWid = ban.UseLastHwid ? LastHwid : ban.Hwid;
if (ban.Target != null && ban.Target != PlayerName || Guid.TryParse(ban.Target, out var parsed) && parsed != PlayerId)
{
var located = await _playerLocator.LookupIdByNameOrIdAsync(target);
var located = await _playerLocator.LookupIdByNameOrIdAsync(ban.Target);
if (located == null)
{
_chat.DispatchServerMessage(Player, Loc.GetString("cmd-ban-player"));
@@ -107,7 +101,7 @@ public sealed class BanPanelEui : BaseEui
}
targetUid = located.UserId;
var targetAddress = located.LastAddress;
if (useLastIp && targetAddress != null)
if (ban.UseLastIp && targetAddress != null)
{
if (targetAddress.IsIPv4MappedToIPv6)
targetAddress = targetAddress.MapToIPv4();
@@ -116,30 +110,50 @@ public sealed class BanPanelEui : BaseEui
var hid = targetAddress.AddressFamily == AddressFamily.InterNetworkV6 ? Ipv6_CIDR : Ipv4_CIDR;
addressRange = (targetAddress, hid);
}
targetHWid = useLastHwid ? located.LastHWId : hwid;
targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid;
}
if (roles?.Count > 0)
if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0)
{
var now = DateTimeOffset.UtcNow;
foreach (var role in roles)
foreach (var role in ban.BannedJobs ?? [])
{
if (_prototypeManager.HasIndex<JobPrototype>(role))
{
_banManager.CreateRoleBan(targetUid, target, Player.UserId, addressRange, targetHWid, role, minutes, severity, reason, now);
}
else
{
_sawmill.Warning($"{Player.Name} ({Player.UserId}) tried to issue a job ban with an invalid job: {role}");
}
_banManager.CreateRoleBan(
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
}
foreach (var role in ban.BannedAntags ?? [])
{
_banManager.CreateRoleBan(
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
role,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason,
now
);
}
Close();
return;
}
if (erase &&
targetUid != null)
if (ban.Erase && targetUid is not null)
{
try
{
@@ -152,7 +166,16 @@ public sealed class BanPanelEui : BaseEui
}
}
_banManager.CreateServerBan(targetUid, target, Player.UserId, addressRange, targetHWid, minutes, severity, reason);
_banManager.CreateServerBan(
targetUid,
ban.Target,
Player.UserId,
addressRange,
targetHWid,
ban.BanDurationMinutes,
ban.Severity,
ban.Reason
);
Close();
}

View File

@@ -1,37 +0,0 @@
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands
{
[AdminCommand(AdminFlags.Round)]
public sealed class ReadyAll : IConsoleCommand
{
[Dependency] private readonly IEntityManager _e = default!;
public string Command => "readyall";
public string Description => "Readies up all players in the lobby, except for observers.";
public string Help => $"{Command} | ̣{Command} <ready>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ready = true;
if (args.Length > 0)
{
ready = bool.Parse(args[0]);
}
var gameTicker = _e.System<GameTicker>();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.WriteLine("This command can only be ran while in the lobby!");
return;
}
gameTicker.ToggleReadyAll(ready);
}
}
}

View File

@@ -0,0 +1,32 @@
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Round)]
public sealed class ReadyAllCommand : LocalizedEntityCommands
{
[Dependency] private readonly GameTicker _gameTicker = default!;
public override string Command => "readyall";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var ready = true;
if (_gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
shell.WriteError(Loc.GetString("shell-can-only-run-from-pre-round-lobby"));
return;
}
if (args.Length > 0 && !bool.TryParse(args[0], out ready))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
return;
}
_gameTicker.ToggleReadyAll(ready);
}
}

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