Compare commits

...

252 Commits

Author SHA1 Message Date
Redrover1760 ea8c844578 fixes 2026-03-30 22:01:57 -04:00
Redrover1760 a9e4468d78 Revert "Revert "Merge branch 'april1st' into main" (#3655)"
This reverts commit f3919fafc1.
2026-03-30 21:08:29 -04:00
Redrover1760 f3919fafc1 Revert "Merge branch 'april1st' into main" (#3655) 2026-03-30 21:01:19 -04:00
Redrover1760 974b78863b Merge branch 'april1st' into main 2026-03-30 20:48:47 -04:00
monolith8319 1654bd5cf8 Automated Changelog (#3650) 2026-03-30 19:42:07 +00:00
mikus d353667f97 [April 1st] Only True Noticers (#3650) 2026-03-30 15:41:35 -04:00
monolith8319 47116d8ba7 Automated Changelog (#3648) 2026-03-30 18:16:21 +00:00
Ilya246 9bd6d5f3f2 [April 1st] Asteroid Update (#3648) 2026-03-30 18:15:50 +00:00
Corrupt 1dc696deb6 AAAAA (#3646) 2026-03-30 19:34:15 +02:00
monolith8319 f2df23dc81 Automated Changelog (#3645) 2026-03-30 17:33:41 +00:00
mikus 76d6ea96e0 [April 1st] New Company (#3645)
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
2026-03-30 17:33:10 +00:00
monolith8319 93ba265e00 Automated Changelog (#3642) 2026-03-30 17:08:08 +00:00
tonotom1 16f73ec031 [April 1st] Buffs Space Pen (#3642) 2026-03-30 17:07:38 +00:00
monolith8319 9d3b0778b3 Automated Changelog (#3643) 2026-03-30 16:56:33 +00:00
lopsidedmanboobs 32b15951e2 PIrate CBT (#3643) 2026-03-30 18:56:00 +02:00
monolith8319 7a0f0d04c5 Automated Changelog (#3641) 2026-03-30 16:42:14 +00:00
mikus bcce8e67a3 [April 1st] 65 (#3641) 2026-03-30 16:41:45 +00:00
monolith8319 5afcdd9379 Automated Changelog (#3615) 2026-03-30 16:22:32 +00:00
mikus 01c7ea464a [April 1st] 65 (#3615) 2026-03-30 16:22:01 +00:00
monolith8319 55f6108312 Automated Changelog (#3514) 2026-03-30 16:16:08 +00:00
UnicornOnLSD 8be27c1658 Reworks the DIS Rig (#3514)
Co-authored-by: Corrupt <compeldominik6@gmail.com>
2026-03-30 18:15:38 +02:00
monolith8319 4df38cc9cc Automated Changelog (#3640) 2026-03-30 15:45:53 +00:00
mikus 8c0d2a50df [April 1st] Clanker Rebalance (#3640) 2026-03-30 15:45:19 +00:00
monolith8319 2d8105de13 Automated Changelog (#3611) 2026-03-30 15:43:28 +00:00
burrrkeye 01bb73bf56 [APRIL 1st] Charon Rebalance (#3611)
Co-authored-by: Corrupt <compeldominik6@gmail.com>
Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
2026-03-30 15:42:55 +00:00
monolith8319 fb09b866f2 Automated Changelog (#3639) 2026-03-30 13:57:16 +00:00
tonotom1 0458a500ff Harmonica-Fy All Handheld Instruments (#3639) 2026-03-30 09:56:46 -04:00
monolith8319 4334b07093 Automated Changelog (#3638) 2026-03-30 09:23:54 +00:00
Isratosh ad2dd99a9b Thruster Capacitor Uniformity (#3638) 2026-03-30 13:23:22 +04:00
monolith8319 f11c83a96e Automated Changelog (#3626) 2026-03-30 04:38:58 +00:00
mikus 36d1740829 [April 1st] Adjusts (#3626) 2026-03-30 08:38:28 +04:00
Ghost581 1e201bb8d2 Viper Group Whitelist Fix (#3634) 2026-03-29 23:36:29 -04:00
monolith8319 c7a34dfc22 Automated Changelog (#3627) 2026-03-29 18:38:24 +00:00
Redrover1760 e366c4d7cc [April 1st] Adds a Very Fair Drone to the Game (#3627)
Co-authored-by: Corrupt <compeldominik6@gmail.com>
Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: EckoAurum <202461799+EckoAurum@users.noreply.github.com>
Co-authored-by: ScarletLightweaver <82061820+ScarletLightweaverSS14@users.noreply.github.com>
2026-03-29 14:37:54 -04:00
monolith8319 bc0151e540 Automated Changelog (#3564) 2026-03-29 18:15:53 +00:00
UnicornOnLSD b2307660de Uniformises Shutter Sprites (#3564) 2026-03-29 20:15:23 +02:00
monolith8319 5d5f6b4654 Automated Changelog (#3622) 2026-03-29 18:10:30 +00:00
mikus 165f9950b5 {April 1st] Brings Back the Tactics of Evil Hidden TSF Ninja Warriors (#3622) 2026-03-29 14:10:01 -04:00
monolith8319 ef9858936d Automated Changelog (#3619) 2026-03-29 18:09:42 +00:00
Redrover1760 d0a95f5d8b [April 1st] Upgrades Bomb Collars (#3619)
Co-authored-by: Corrupt <compeldominik6@gmail.com>
Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: EckoAurum <202461799+EckoAurum@users.noreply.github.com>
Co-authored-by: ScarletLightweaver <82061820+ScarletLightweaverSS14@users.noreply.github.com>
2026-03-29 14:09:11 -04:00
monolith8319 ee386b0633 Automated Changelog (#3623) 2026-03-29 18:08:59 +00:00
mikus de38cc18ef [April 1st] Buffs Shortband (#3623) 2026-03-29 14:08:28 -04:00
monolith8319 ca928bdc83 Automated Changelog (#3625) 2026-03-29 17:56:37 +00:00
mikus cee6ce9566 [April 1st] Updates Ownership of the Treasury to the Bank Of America (#3625) 2026-03-29 17:56:06 +00:00
monolith8319 984a2c9b71 Automated Changelog (#3624) 2026-03-29 17:16:08 +00:00
Ilya246 548e9eccb6 [April 1st] Mobile Pois 2 (#3624) 2026-03-29 17:15:36 +00:00
monolith8319 cd817abd2c Automated Changelog (#3620) 2026-03-29 16:54:09 +00:00
NazrinNya f276df8fe9 [1 April] Add Larp (#3620)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
2026-03-29 16:53:40 +00:00
monolith8319 dd323be01d Automated Changelog (#3621) 2026-03-29 16:53:31 +00:00
mikus 0cf9b35f08 [April 1st] Adds New Forms of Energy Production (#3621) 2026-03-29 19:52:59 +03:00
monolith8319 114a09f290 Automated Changelog (#3618) 2026-03-29 16:20:15 +00:00
Redrover1760 dc4b50e71e updates VSC/build configurations (#3618) 2026-03-29 12:19:46 -04:00
monolith8319 c405c32c89 Automated Changelog (#3598) 2026-03-29 15:49:50 +00:00
monolith8319 34ff30a736 Automated Changelog (#3599) 2026-03-29 15:49:22 +00:00
ScarletLightweaver c8e162baa1 Made Bulwark Gauntlets Wideswing (#3598) 2026-03-29 11:49:12 -04:00
Ilya246 ffcbc32c43 Add Carpsuit to Xenopsychology (#3599) 2026-03-29 11:48:53 -04:00
monolith8319 351b225102 Automated Changelog (#3616) 2026-03-29 15:47:58 +00:00
gonz0 496432881e [April 1st] Detroit Borers (#3616) 2026-03-29 11:47:30 -04:00
monolith8319 9fd2a7b5af Automated Changelog (#3614) 2026-03-29 15:16:12 +00:00
EckoAurum 8972408ec1 Drone Turret Small Fix (#3614) 2026-03-29 11:15:43 -04:00
monolith8319 5798b57788 Automated Changelog (#3612) 2026-03-29 14:46:04 +00:00
mikus ec88162ac0 [April 1st] Adds New Type of Bluespace Drive (#3612) 2026-03-29 14:45:36 +00:00
monolith8319 3992aa846f Automated Changelog (#3610) 2026-03-29 14:18:14 +00:00
Ilya246 4b62f35f10 [April 1st] ilya changes round 2 (#3610) 2026-03-29 14:17:46 +00:00
Ilya246 cc1c91f39f Actually Fix Changelog (#3609) 2026-03-29 14:10:42 +00:00
mikus c5241854ce [April 1st] Piras314 Wrote This (#3606) 2026-03-29 13:57:07 +00:00
Ilya246 d02895a50f prune changelog 2026-03-29 13:55:25 +00:00
Ilya246 c41d005cf4 fix changelog workflow (#3607) 2026-03-29 13:52:43 +00:00
monolith8319 013f57a73d Automated Changelog (#3605) 2026-03-29 13:40:51 +00:00
mikus c3f687a493 [April 1st] Rebalances Gunnery Controls (#3605) 2026-03-29 13:40:19 +00:00
monolith8319 f837710be1 Automated Changelog (#3604) 2026-03-29 13:25:12 +00:00
Ilya246 7b00d623e0 [April 1st] Ilya April1st Changes Round 1 (#3603) 2026-03-29 13:24:54 +00:00
mikus dafbb5f3d8 [April 1st] Makes Borgs Balanced (#3604) 2026-03-29 13:24:41 +00:00
Corrupt d4487d9a5a RSC WL Update no.3 (#3601)
brr
2026-03-29 15:03:48 +02:00
monolith8319 7b9f602c55 Automated Changelog (#3571) 2026-03-29 06:31:08 +00:00
Antoine 97509dd3b6 Chat Message Highlighting (#3571)
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
2026-03-29 06:30:38 +00:00
monolith8319 55c2a03ea4 Automated Changelog (#3594) 2026-03-29 05:46:36 +00:00
Redrover1760 543c19645f Gigabuffs Signaller Range (#3594) 2026-03-29 01:46:05 -04:00
monolith8319 da8b19dd05 Automated Changelog (#3595) 2026-03-29 02:40:33 +00:00
Redrover1760 4fc44d3a9c Really Ensures Delayed Gamerules Don't Trigger Early (#3595) 2026-03-28 22:40:06 -04:00
Redrover1760 55688cc9e0 Nuke Collar Buff (#3593) 2026-03-28 18:29:23 -04:00
Redrover1760 5357469644 Adds Tsf/Pdv Borgs to Debug Map (#3591) 2026-03-28 17:59:46 -04:00
monolith8319 6040135534 Automated Changelog (#3590) 2026-03-28 19:05:03 +00:00
Ilya246 52649a871e Fastify Apoc (#3590) 2026-03-28 15:04:33 -04:00
monolith8319 fef33511d8 Automated Changelog (#3444) 2026-03-28 18:43:41 +00:00
EckoAurum fec5bfce0a Faction Borg Specialization | Independent Cyborg Selectable Whitelist (#3444) 2026-03-28 14:43:12 -04:00
monolith8319 11f82f95d3 Automated Changelog (#3459) 2026-03-28 18:17:06 +00:00
NazrinNya 3dbad97455 Small Mech Rebalance. (#3459)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
2026-03-28 21:16:36 +03:00
monolith8319 a193ab828e Automated Changelog (#3574) 2026-03-28 16:46:17 +00:00
Vinny Shick c75083140d Vasnyx 150mm Launcher Buff (#3574) 2026-03-28 19:45:49 +03:00
Redrover1760 106f9b6d28 Actually Removes Lancelot Wildcard Trade Crate Selling (#3577) 2026-03-28 19:45:07 +03:00
monolith8319 fe24fc22bc Automated Changelog (#3589) 2026-03-28 16:43:10 +00:00
Redrover1760 85c9636003 Buffs Thruster Speed Increase (#3589) 2026-03-28 19:42:39 +03:00
monolith8319 a1d2f4dcbc Automated Changelog (#3587) 2026-03-28 16:22:15 +00:00
NazrinNya 3381fe165c Various Patches. (#3587)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
2026-03-28 12:21:43 -04:00
monolith8319 a913c35b21 Automated Changelog (#3588) 2026-03-28 16:18:20 +00:00
Redrover1760 0706b759d4 Fixes Diagonal Collisions, Can Kill Kudzu/Chimerazu Under It (#3588) 2026-03-28 12:17:49 -04:00
monolith8319 73a91c0b0a Automated Changelog (#3585) 2026-03-28 06:42:25 +00:00
Redrover1760 9e390680bd Speciest Turrets Fix (#3585) 2026-03-28 02:41:55 -04:00
monolith8319 d3d391f5a0 Automated Changelog (#3582) 2026-03-27 21:58:06 +00:00
Redrover1760 a6506c7f46 Anti-Chimera RIP Turrets to CC and MD (#3582) 2026-03-27 17:57:35 -04:00
monolith8319 958421b6c9 Automated Changelog (#3461) 2026-03-27 19:26:53 +00:00
NazrinNya c513938ed9 Make Heavy Ballistic Turrets Less Painful. (#3461)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
2026-03-27 15:26:22 -04:00
monolith8319 a6431c9bab Automated Changelog (#3570) 2026-03-27 17:58:37 +00:00
Ilya246 f3390a81cc Gamemode Fixes and Changes (#3570)
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
2026-03-27 13:58:07 -04:00
monolith8319 6ba80381cc Automated Changelog (#3565) 2026-03-27 07:21:00 +00:00
Redrover1760 ef095aec88 Mono Claws Guidebook Update (#3565) 2026-03-27 03:20:31 -04:00
monolith8319 d02738089c Automated Changelog (#3575) 2026-03-27 07:19:51 +00:00
Redrover1760 b9f0de8576 Improves Hydrakin Head Displacement (#3575) 2026-03-27 03:19:24 -04:00
monolith8319 5a6d4b74b1 Automated Changelog (#3524) 2026-03-27 05:49:41 +00:00
significant harassment 1ac13bf597 Triple Rot Delay (#3524)
Co-authored-by: Onezero0 <onezero0@outlook.com>
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
2026-03-27 01:49:14 -04:00
monolith8319 90ee912816 Automated Changelog (#3573) 2026-03-26 22:06:02 +00:00
Redrover1760 27665829e6 Viscerators No Longer Drop Lootboxes (#3572) 2026-03-26 18:05:41 -04:00
Redrover1760 e1459b0d43 Trade Crates Can No Longer Be Sold on Helios or Lancelot (#3573) 2026-03-26 18:05:32 -04:00
monolith8319 395d334dd7 Automated Changelog (#3560) 2026-03-26 05:55:40 +00:00
UnicornOnLSD 75e166e9b3 Maintains the DIS Femur (#3560)
Merged by Corrupt, Has 1 Fakefail.
2026-03-26 06:55:12 +01:00
monolith8319 d987cfe88d Automated Changelog (#3563) 2026-03-25 22:19:01 +00:00
Redrover1760 a942f3e7b1 Reflect Fixes (#3563) 2026-03-25 18:18:31 -04:00
monolith8319 4108933834 Automated Changelog (#3542) 2026-03-25 19:54:26 +00:00
Redrover1760 6af6f3b386 Fireteam Time Requirement Tweak (#3542) 2026-03-25 15:53:53 -04:00
monolith8319 9a590069d2 Automated Changelog (#3555) 2026-03-25 18:33:51 +00:00
Redrover1760 a60321f201 Ports CMO Webbing From Goob (#3555) 2026-03-25 19:33:22 +01:00
monolith8319 dfabb71045 Automated Changelog (#3554) 2026-03-25 06:13:58 +00:00
Redrover1760 ce034c3f19 Claws Tweak, Claws-Based Chemicals (#3554) 2026-03-25 02:13:30 -04:00
monolith8319 79c9dee112 Automated Changelog (#3558) 2026-03-25 05:50:21 +00:00
lopsidedmanboobs 6a048def33 Kill Ransom (#3558) 2026-03-25 01:49:54 -04:00
monolith8319 e41ef48441 Automated Changelog (#3556) 2026-03-25 04:12:58 +00:00
Redrover1760 27e7b2f70a Research Client Fix (#3556) 2026-03-25 00:12:27 -04:00
monolith8319 2bd317dadb Automated Changelog (#3551) 2026-03-25 00:09:05 +00:00
Maxim Bychkov 1145822797 Fix Voidboots Ability To Lie In Space (#3551) 2026-03-24 20:08:36 -04:00
monolith8319 27dca32ef3 Automated Changelog (#3260) 2026-03-24 23:42:18 +00:00
Avalon 4bdad8f6ee Ullman Coat 2 Electric Boogaloo (#3260)
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
2026-03-24 19:41:50 -04:00
monolith8319 38ebc2699c Automated Changelog (#3552) 2026-03-24 19:54:37 +00:00
Ilya246 82f75ff37f Nerf Big Ame (#3552) 2026-03-24 19:54:07 +00:00
Maxim Bychkov c84cde54cb Fix Map Painter (#3544) 2026-03-24 19:28:05 +00:00
monolith8319 f0c6624762 Automated Changelog (#3550) 2026-03-24 19:01:03 +00:00
Ilya246 277ad91140 Bugfixes (#3550) 2026-03-24 19:00:31 +00:00
monolith8319 b9419b25a1 Automated Changelog (#3155) 2026-03-24 15:08:01 +00:00
gelliii e1a5488f14 New Scrap Ship - Horsefly (#3155) 2026-03-24 16:07:30 +01:00
Redrover1760 d9b46ca6dc Update README.md 2026-03-24 09:15:30 -04:00
tonotom1 a672e9bd28 Revert "Cable Destruction Lag Reduction" (#3532) 2026-03-24 08:18:13 -04:00
monolith8319 2a5fe8fe6a Automated Changelog (#3548) 2026-03-24 12:07:19 +00:00
Redrover1760 e069f2ba9d Makes Printed Contraband Worth Things Again (#3548) 2026-03-24 08:06:49 -04:00
monolith8319 db3675b3e8 Automated Changelog (#3549) 2026-03-24 11:45:08 +00:00
chuga-git 79bf0fec68 Buffs MARAUDER Projectile Speed (#3549) 2026-03-24 07:44:39 -04:00
monolith8319 41b3fb9113 Automated Changelog (#3543) 2026-03-23 17:24:06 +00:00
Redrover1760 7cd749dfa6 Datafarm Fixes (#3543) 2026-03-23 13:23:36 -04:00
monolith8319 eb44deeeda Automated Changelog (#3534) 2026-03-23 06:05:19 +00:00
Redrover1760 7035e752a9 Majorly Buffs Part Wound Tending (#3534) 2026-03-23 02:04:49 -04:00
monolith8319 2fdd1cd573 Automated Changelog (#3381) 2026-03-23 04:08:14 +00:00
Kiithnaras 6c4101e0c0 Added Roofing and Emergency Lighting to Comet (#3381) 2026-03-23 00:00:28 -04:00
monolith8319 5f90433697 Automated Changelog (#3536) 2026-03-23 03:42:43 +00:00
Redrover1760 9f54374593 Research Datafarm Fix (#3536) 2026-03-22 23:42:15 -04:00
monolith8319 5272291275 Automated Changelog (#3533) 2026-03-22 23:23:01 +00:00
Redrover1760 4a0f4cec3a Buffed Chems and Surgery Part Healing/Damage (#3533) 2026-03-22 19:22:33 -04:00
monolith8319 5d68716090 Automated Changelog (#3530) 2026-03-22 22:54:38 +00:00
Redrover1760 a4b1a5317c People No Longer Thermal Regulate While Dead, Hydrakin Do Not While Crit Either (#3530) 2026-03-22 18:54:10 -04:00
monolith8319 ee48853724 Automated Changelog (#3520) 2026-03-22 22:41:50 +00:00
tonotom1 a8651f6457 Cable Destruction Lag Reduction (#3520) 2026-03-22 18:41:10 -04:00
monolith8319 47378ac726 Automated Changelog (#3499) 2026-03-22 19:50:36 +00:00
chuga-git 9ca8d4a82a Make Directions to Purchased Shuttles Point the Right Way (#3473) 2026-03-22 22:49:33 +03:00
Redrover1760 f8e940c5f5 Faction Borg Health Decrease (#3499) 2026-03-22 22:49:14 +03:00
monolith8319 89b73cdc76 Automated Changelog (#2620) 2026-03-22 19:46:59 +00:00
significant harassment 881ee08f91 Data Farms (#2620)
Co-authored-by: Onezero0 <onezero0@outlook.com>
Co-authored-by: monolith8319 <195513600+monolith8319@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
Co-authored-by: Ilya246 <ilyukarno@gmail.com>
2026-03-22 22:46:27 +03:00
monolith8319 19d869bda1 Automated Changelog (#3201) 2026-03-22 19:44:53 +00:00
tonotom1 964b5768fd Jingle Jangle (#3201) 2026-03-22 22:44:22 +03:00
monolith8319 925933bbcb Automated Changelog (#3247) 2026-03-22 19:43:47 +00:00
Aithaed 277086116a Mech Laser Buff, The Snails Know, TSF Mech Pulse Rifle, TSF Volta Tweak (#3247)
Co-authored-by: NazrinNya <137837419+NazrinNya@users.noreply.github.com>
2026-03-22 22:43:16 +03:00
monolith8319 54412b25e4 Automated Changelog (#3371) 2026-03-22 19:40:40 +00:00
Mrrpip b75be35a1d Turns USSP Locker and Button Access to Mercenary. (#3371) 2026-03-22 22:40:11 +03:00
monolith8319 1d184cfd27 Automated Changelog (#3415) 2026-03-22 19:40:01 +00:00
Ilya246 ea8ce6d5b5 Optimise Shuttle Impacts (#3415)
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-03-22 22:39:33 +03:00
monolith8319 a9a05d0da3 Automated Changelog (#3519) 2026-03-22 16:01:37 +00:00
Redrover1760 e2c193f7c7 Edits MD/Halcyon/Drillsite Distances (#3517) 2026-03-22 12:01:34 -04:00
Kiithnaras 060a7565c5 Bedhealing Changes (#3519) 2026-03-22 12:01:07 -04:00
monolith8319 2d94504508 Automated Changelog (#3500) 2026-03-22 14:53:38 +00:00
tonotom1 9ec05018b0 Rad Hater's Initiative (#3500)
Co-authored-by: UnicornOnLSD <149102472+UnicornOnLSD@users.noreply.github.com>
2026-03-22 10:53:08 -04:00
monolith8319 9574d6fe1f Automated Changelog (#3522) 2026-03-22 13:49:56 +00:00
Ilya246 937131c2a4 Ship Htn Fix (#3522) 2026-03-22 13:49:25 +00:00
monolith8319 f2cd5a6f3a Automated Changelog (#3426) 2026-03-22 11:13:31 +00:00
Corrupt 7749344876 Wyvern Nerf (#3426) 2026-03-22 12:13:01 +01:00
monolith8319 cda0c1f802 Automated Changelog (#3469) 2026-03-22 11:05:16 +00:00
Maxim Bychkov ad37c2895a [DNM] v271.2.0 RT Update - .NET 10 (#3469)
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
2026-03-22 11:04:46 +00:00
monolith8319 7ceedb6014 Automated Changelog (#3508) 2026-03-22 11:04:17 +00:00
Ilya246 a8c0d92351 [Testmerge End] Revert Movable POIs (#3508) 2026-03-22 11:03:41 +00:00
Redrover1760 1ead85c1c5 reee (#3516) 2026-03-21 22:51:15 -04:00
monolith8319 135335f7c9 Automated Changelog (#3513) 2026-03-21 22:51:22 +00:00
Redrover1760 c1a5f4d8c5 Natural Letoferal Guidebook Again (#3513) 2026-03-21 18:50:51 -04:00
monolith8319 9f204d62d1 Automated Changelog (#3512) 2026-03-21 22:41:11 +00:00
Redrover1760 7ef79b3fda Natural Letoferol Guidebook Description Update (#3512) 2026-03-21 18:40:38 -04:00
monolith8319 3f0670b985 Automated Changelog (#3511) 2026-03-21 22:12:04 +00:00
Redrover1760 c7060a0dc3 Asakim Autopulser Burst Nerf (#3511) 2026-03-21 18:11:32 -04:00
monolith8319 6a48576e4e Automated Changelog (#3510) 2026-03-21 21:38:09 +00:00
Redrover1760 e4412cfbb6 Critical Shipgun Fixture Fix (#3510) 2026-03-21 17:37:36 -04:00
monolith8319 a119933e1a Automated Changelog (#3458) 2026-03-21 21:23:39 +00:00
significant harassment 7602c54c29 Nuke Synthalloy (#3458)
Co-authored-by: Onezero0 <onezero0@outlook.com>
2026-03-21 17:23:08 -04:00
monolith8319 f58931c915 Automated Changelog (#3468) 2026-03-21 20:04:23 +00:00
Mrrpip 686d6c8c37 Adds a Ship Based Flare Launcher. (And a Slight Spyglass Buff?) (#3468) 2026-03-21 16:03:52 -04:00
monolith8319 e8850079f0 Automated Changelog (#3482) 2026-03-21 20:00:45 +00:00
Ghost581 9cfdbd75e0 VIPER GROUP STUFF + SOME YAML TIDYING (#3482) 2026-03-21 16:00:13 -04:00
monolith8319 100800d09c Automated Changelog (#3449) 2026-03-21 19:55:43 +00:00
Mrrpip 863cd54169 Swaps the Triage's Generator. Ups It's Armor. Changes the Engine/Weapon Floors. (#3449) 2026-03-21 20:55:12 +01:00
monolith8319 b4bd44a589 Automated Changelog (#3241) 2026-03-21 19:53:23 +00:00
EldritchPineapple 8ab20f7058 New Tsf Frigate : The Altair (#3241)
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
Co-authored-by: UnicornOnLSD <149102472+UnicornOnLSD@users.noreply.github.com>
2026-03-21 20:52:52 +01:00
monolith8319 29e20df5b0 Automated Changelog (#3506) 2026-03-21 18:35:12 +00:00
Redrover1760 e7b1f19944 Increases World Loading (#3506) 2026-03-21 14:34:40 -04:00
monolith8319 615cc446e0 Automated Changelog (#3505) 2026-03-21 16:49:05 +00:00
Redrover1760 4e794d6a42 Anchor Fixes (#3505) 2026-03-21 12:48:32 -04:00
monolith8319 63ac33ebce Automated Changelog (#3504) 2026-03-21 11:39:43 +00:00
UnicornOnLSD d5d89acbb4 Port Ghost Types (#37949) and Subsequent Necessities (#3504)
Co-authored-by: Thinbug <101073555+Thinbug0@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
2026-03-21 11:39:13 +00:00
monolith8319 1cee621041 Automated Changelog (#3470) 2026-03-21 09:07:05 +00:00
NazrinNya 9dbf4629a0 Hailstorm Nerf, Exped Ore Buff. (#3470)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
2026-03-21 05:06:33 -04:00
monolith8319 67e804698e Automated Changelog (#3503) 2026-03-21 09:05:14 +00:00
Redrover1760 b63dd431e8 Mobile POIs (#3503) 2026-03-21 05:04:44 -04:00
monolith8319 849b094574 Automated Changelog (#3502) 2026-03-21 08:16:39 +00:00
Redrover1760 2dcf189143 Yet Another Shortband Buff (#3502) 2026-03-21 04:16:08 -04:00
monolith8319 9e4a84a3b2 Automated Changelog (#3501) 2026-03-21 07:44:54 +00:00
Isratosh e69a387839 Add Vanyk and Vespera Missile Magazine Recipes to Military and Ammo Techfab (#3501) 2026-03-21 03:44:24 -04:00
Corrupt 3f50e9e035 RSC WL Updates (#3496) 2026-03-20 20:44:06 -04:00
monolith8319 6b1977d0f9 Automated Changelog (#3497) 2026-03-21 00:38:12 +00:00
NazrinNya b9c02cd366 Nerf Platelets (#3497)
Co-authored-by: NazrinNya <macrohard@macrohard.localhost>
2026-03-20 20:37:38 -04:00
monolith8319 f35f5e264d Automated Changelog (#3388) 2026-03-21 00:33:19 +00:00
Ilya246 60c6741960 ADM Nerf (#3388)
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
2026-03-20 20:32:45 -04:00
monolith8319 94812b996b Automated Changelog (#3476) 2026-03-21 00:29:39 +00:00
chuga-git b291b1cfd6 Fix Charon Range (#3476) 2026-03-20 20:29:05 -04:00
significant harassment 96902972d5 Template (#3494)
Co-authored-by: Onezero0 <onezero0@outlook.com>
2026-03-20 19:26:55 -04:00
monolith8319 45bce4ed33 Automated Changelog (#3498) 2026-03-20 18:43:21 +00:00
Redrover1760 9347f67507 Draupnir Accuracy Buff (#3498) 2026-03-20 14:42:51 -04:00
monolith8319 e3635d4c91 Automated Changelog (#3495) 2026-03-20 11:39:15 +00:00
Redrover1760 a40b954978 Chimera and Other Mobs No Longer Randomly Die on Colossus (#3495) 2026-03-20 14:38:42 +03:00
monolith8319 adb4296d38 Automated Changelog (#3493) 2026-03-20 00:18:11 +00:00
significant harassment a48a235bf6 Catwalk Directional Tiles (#3493)
Co-authored-by: Onezero0 <onezero0@outlook.com>
2026-03-19 20:17:39 -04:00
monolith8319 176055630f Automated Changelog (#3492) 2026-03-19 20:31:41 +00:00
Redrover1760 ff6bf1c131 Borer Radio/Polymorph Bugfixes (#3492) 2026-03-19 16:31:07 -04:00
monolith8319 1be801abf4 Automated Changelog (#3382) 2026-03-19 20:29:27 +00:00
Kiithnaras 0f25271add Specialized Armor for Industrial Mechs (#3382) 2026-03-19 23:28:56 +03:00
monolith8319 24bc95450a Automated Changelog (#3471) 2026-03-19 20:26:28 +00:00
Nikovnik 77ebbdad20 Claws and Bows (#3471) 2026-03-19 23:25:57 +03:00
monolith8319 44a6d7eca6 Automated Changelog (#3487) 2026-03-19 19:42:52 +00:00
EldritchPineapple eb5c8ac4e9 Spica and Aldebaran Hotfix (#3487) 2026-03-19 20:42:18 +01:00
monolith8319 188070b757 Automated Changelog (#3475) 2026-03-19 19:35:28 +00:00
EldritchPineapple 3f97487c0b Nuke... 2026-03-19 20:34:56 +01:00
monolith8319 c7de9b660a Automated Changelog (#3490) 2026-03-19 19:14:06 +00:00
EldritchPineapple 4b44b891d3 A (#3490) 2026-03-19 20:13:32 +01:00
monolith8319 b895b2ba42 Automated Changelog (#3485) 2026-03-19 12:24:15 +00:00
Mrrpip 8a6b39a560 Fixes the Hades' Offsets. (#3485) 2026-03-19 08:23:42 -04:00
monolith8319 ad7633023b Automated Changelog (#3287) 2026-03-19 06:09:52 +00:00
Redrover1760 8777215b34 Bomb Collars (#3287)
Co-authored-by: Redrover1760 <gnarlysheep.personal@gmail.com>
2026-03-19 02:09:21 -04:00
monolith8319 4007b079f8 Automated Changelog (#3474) 2026-03-19 05:34:23 +00:00
chuga-git 79067884e1 Remove Dumpable From the RPED (#3474) 2026-03-19 05:33:51 +00:00
monolith8319 ae72c4b217 Automated Changelog (#3486) 2026-03-19 00:45:06 +00:00
lopsidedmanboobs 12485a5ce2 Mildly Important Safezone Clarifications Added (#3486) 2026-03-19 11:44:34 +11:00
924 changed files with 63730 additions and 54200 deletions
+1 -1
View File
@@ -348,7 +348,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,json}]
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,json,slnx}]
indent_size = 2
[nuget.config]
+7 -8
View File
@@ -1,4 +1,3 @@
<!-- Guidelines: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
## About the PR
<!-- What did you change? -->
@@ -7,23 +6,23 @@
## Why / Balance
<!-- Discuss how this would affect game balance or explain why it was changed. Link any relevant discussions or issues. -->
## How to test
<!-- Describe the way it can be tested -->
## Media
<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc).
Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. -->
## Requirements
<!-- Confirm the following by placing an X in the brackets [X]: -->
- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [ ] I have read relevant guidelines/documentation to this PR found on [our devwiki](https://monolith-station.github.io/mono-docs/).
- [ ] I have added media to this PR or it does not require an ingame showcase.
- [ ] I can confirm this PR contains no AI-generated content, and did not use any AI-generated content.
- [ ] I can confirm this PR contains either no AI-generated content, or AI-generated content that meets our guidelines.
<!-- You should understand that not following the above may get your PR closed at maintainers discretion -->
<!-- When using AI, make sure you are already proficient in creating whatever content you are attempting to generate and that you already have experience contributing to SS14. This means using AI to enhance your preexisting workflow, not vibecoding. -->
## How to test
<!-- Describe the way it can be tested -->
## Breaking changes
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them.
This will be posted in #codebase-changes. -->
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them. -->
**Changelog**
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
+5 -5
View File
@@ -1,5 +1,5 @@
name: PR Changelogs
concurrency: commit_action
concurrency: commit_action-${{ github.event.pull_request.base.ref }} # Mono
on:
pull_request_target:
types: [closed]
@@ -16,11 +16,11 @@ jobs:
permissions:
contents: write
steps:
- name: Checkout Master
- name: Checkout Base Branch # Mono
uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_TOKEN }}
ref: "${{ vars.CHANGELOG_BRANCH }}"
ref: "${{ github.event.pull_request.base.ref }}" # Mono
- name: Setup Git
run: |
@@ -47,9 +47,9 @@ jobs:
- name: Commit Changelog
run: |
git pull origin main
git pull origin "${{ github.event.pull_request.base.ref }}"
git add "${{ env.CHANGELOG_DIR }}"
git commit -m "Automated Changelog (#${{ env.PR_NUMBER }})"
git push
git push origin "${{ github.event.pull_request.base.ref }}"
shell: bash
continue-on-error: true
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Get Engine Tag
run: |
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
+3
View File
@@ -1,3 +1,6 @@
# MSbuild binlog files
*.binlog
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
+53 -7
View File
@@ -13,6 +13,26 @@
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client - Tools",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Tools",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": [],
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client - Release",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
"args": [],
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Client (Compatibility renderer)",
"type": "coreclr",
@@ -31,6 +51,26 @@
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "Server - Tools",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Tools",
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
"args": [""],
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "Server - Release",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build - Release",
"program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
"args": [""],
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "YAML Linter",
"type": "coreclr",
@@ -40,7 +80,7 @@
"cwd": "${workspaceFolder}/Content.YAMLLinter",
"console": "internalConsole",
"stopAtEntry": false
}
},
],
"compounds": [
{
@@ -51,14 +91,21 @@
],
"preLaunchTask": "build"
},
// Frontier begin
{
"name": "Server/Client (Release)",
"name": "Server/Client - Tools",
"configurations": [
"Server",
"Client"
"Server - Tools",
"Client - Tools"
],
"preLaunchTask": "build-release"
"preLaunchTask": "build - Tools"
},
{
"name": "Server/Client - Release",
"configurations": [
"Server - Release",
"Client - Release"
],
"preLaunchTask": "build - Release"
},
{
"name": "Server/Client (No Build)",
@@ -67,6 +114,5 @@
"Client"
]
}
// Frontier end
]
}
+26 -6
View File
@@ -21,26 +21,46 @@
},
"problemMatcher": "$msCompile"
},
// Frontier begin
{
"label": "build-release",
"label": "build - Tools",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"--configuration=Release", // Build in release mode. Note: --, not /. /configuration doesn't work, because Microsoft.
"--configuration",
"Tools",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:NoSummary" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build"
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "build - Release",
"command": "dotnet",
"type": "shell",
"args": [
"build",
"--configuration",
"Release",
"/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
"/consoleloggerparameters:'ForceNoAlign;NoSummary'", // Do not generate summary otherwise it leads to duplicate errors in Problems panel
],
"group": {
"kind": "build",
"isDefault": false
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
// Frontier end
{
"label": "build-yaml-linter",
"command": "dotnet",
+1 -1
View File
@@ -8,7 +8,7 @@ import shutil
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "2"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
+14 -9
View File
@@ -1,17 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
<Nullable>disable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<!-- pin transitive deps -->
<PackageReference Include="System.Management" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
@@ -19,10 +22,12 @@
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>
+3
View File
@@ -0,0 +1,3 @@
// Global usings for Content.Benchmarks
global using Robust.UnitTesting.Pool;
@@ -36,8 +36,6 @@ public sealed partial class ContentAudioSystem
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly CombatModeSystem _combatModeSystem = default!; //CLIENT ONE. WHY ARE THERE 3???
[Dependency] private readonly IPrototypeManager _protMan = default!;
[Dependency] private readonly SpaceBiomeSystem _spaceBiome = default!;
//options menu ---
private static float _volumeSliderAmbient;
@@ -8,7 +8,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Serilog;
namespace Content.Client.Cargo.UI;
@@ -32,6 +32,10 @@ public sealed class CharacterInfoSystem : EntitySystem
private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args)
{
var entity = GetEntity(msg.NetEntity);
// Mono - fix
if (TerminatingOrDeleted(entity))
return;
var data = new CharacterData(entity, msg.JobTitle, msg.Objectives, msg.Briefing, Name(entity));
OnCharacterUpdate?.Invoke(data);
+2 -2
View File
@@ -9,13 +9,13 @@
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="GeneralCategoryTexture"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="HandsCategoryTexture"/>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
@@ -5,10 +5,12 @@ using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Chat.UI;
@@ -86,6 +88,9 @@ public sealed partial class EmotesMenu : RadialMenu
continue;
AddEmoteClickAction(container);
}
GeneralCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi"), "icon"));
HandsCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi"), "icon"));
}
private void AddEmoteClickAction(RadialContainer container)
+10 -12
View File
@@ -1,26 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Client\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
<Platforms>AnyCPU</Platforms>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Nett" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="Pidgin" />
<PackageReference Include="Robust.Shared.AuthLib" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
</ItemGroup>
<ItemGroup>
+3 -2
View File
@@ -1,4 +1,5 @@
using Robust.Client.Graphics;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -34,7 +35,7 @@ namespace Content.Client.Cooldown
if (Progress >= 0f)
{
var hue = (5f / 18f) * lerp;
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f));
color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
}
else
{
+2 -1
View File
@@ -1,4 +1,5 @@
using Content.Shared.Disposal;
using System.Numerics;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -36,39 +36,37 @@ public sealed partial class DocumentParsingManager
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
.Bind(tag => _tagControlParsers[tag]);
// Frontier: comment parser
Parser<char, Unit> whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
var whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
_controlParser = OneOf(_tagParser, TryHeaderControl, TryListControl, TextControlParser)
.Before(whitespaceAndCommentParser);
// End Frontier
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
{
_tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _sandboxHelper));
}
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many()); // Frontier: SkipWhitespaces<whitespaceAndCommentParser
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many());
_sawmill = Logger.GetSawmill("Guidebook");
}
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId)
{
if (!_prototype.TryIndex(entryId, out var entry))
if (!_prototype.Resolve(entryId, out var entry))
return false;
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
return TryAddMarkup(control, file.ReadToEnd());
}
public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true)
public bool TryAddMarkup(Control control, GuideEntry entry)
{
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
return TryAddMarkup(control, file.ReadToEnd());
}
public bool TryAddMarkup(Control control, string text, bool log = true)
public bool TryAddMarkup(Control control, string text)
{
try
{
@@ -83,7 +83,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
rt.SetMessage(msg);
rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)
+1
View File
@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Holopad;
+1 -1
View File
@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
{
TitleLabel.Text = title;
if (markup)
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
else
Content.SetMessage(text);
}
+1 -1
View File
@@ -24,7 +24,7 @@ namespace Content.Client.Info
}
public void SetInfoBlob(string markup)
{
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
}
}
}
@@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Client.Items.Systems;
using Content.Shared.Clothing;
using Content.Shared.Hands;
@@ -1,6 +1,4 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
@@ -0,0 +1,7 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Vertical">
<Label Name="TitleLabel" Access="Public" />
<Label Name="ExampleLabel" Access="Public" />
<ColorSelectorSliders Name="Slider" Access="Public" HorizontalExpand="True" />
</BoxContainer>
</Control>
@@ -0,0 +1,31 @@
using Content.Client.Options.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
namespace Content.Client.Options.UI;
/// <summary>
/// Standard UI control used for color sliders in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow.AddOptionColorSlider"/>
[GenerateTypedNameReferences]
public sealed partial class OptionColorSlider : Control
{
/// <summary>
/// The text describing what this slider affects.
/// </summary>
public string? Title
{
get => TitleLabel.Text;
set => TitleLabel.Text = value;
}
/// <summary>
/// The example text showing the current color of the slider.
/// </summary>
public string? Example
{
get => ExampleLabel.Text;
set => ExampleLabel.Text = value;
}
}
@@ -143,6 +143,19 @@ public sealed partial class OptionsTabControlRow : Control
return AddOption(new OptionSliderIntCVar(this, _cfg, cVar, slider, min, max, format ?? FormatInt));
}
/// <summary>
/// Add a color slider option, backed by a simple string CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the slider.</param>
/// <param name="slider">The UI control for the option.</param>
/// <returns>The option instance backing the added option.</returns>
public OptionColorSliderCVar AddOptionColorSlider(
CVarDef<string> cVar,
OptionColorSlider slider)
{
return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider));
}
/// <summary>
/// Add a drop-down option, backed by a CVar.
/// </summary>
@@ -518,6 +531,54 @@ public sealed class OptionSliderFloatCVar : BaseOptionCVar<float>
}
}
public sealed class OptionColorSliderCVar : BaseOptionCVar<string>
{
private readonly OptionColorSlider _slider;
protected override string Value
{
get => _slider.Slider.Color.ToHex();
set
{
_slider.Slider.Color = Color.FromHex(value);
UpdateLabelColor();
}
}
/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="slider">The UI control for the option.</param>
public OptionColorSliderCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<string> cVar,
OptionColorSlider slider) : base(controller, cfg, cVar)
{
_slider = slider;
slider.Slider.OnColorChanged += _ =>
{
ValueChanged();
UpdateLabelColor();
};
}
private void UpdateLabelColor()
{
_slider.ExampleLabel.FontColorOverride = Color.FromHex(Value);
}
}
/// <summary>
/// Implementation of a CVar option that simply corresponds with an integer <see cref="OptionSlider"/>.
/// </summary>
@@ -12,6 +12,10 @@
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
<ui:OptionColorSlider Name="HighlightsColorSlider"
Title="{Loc 'ui-options-highlights-color'}"
Example="{Loc 'ui-options-highlights-color-example'}"/>
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
@@ -20,6 +20,8 @@ public sealed partial class AccessibilityTab : Control
Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);
Control.Initialize();
}
@@ -98,10 +98,13 @@ public sealed class ParallaxManager : IParallaxManager
}
else
{
layers = await Task.WhenAll(
// Explicitly allocate params array to avoid sandbox violation since C# 14.
var tasks = new[]
{
LoadParallaxLayers(parallaxPrototype.Layers, loadedLayers, cancel),
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel)
);
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel),
};
layers = await Task.WhenAll(tasks);
}
cancel.ThrowIfCancellationRequested();
@@ -11,6 +11,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using System.Numerics;
namespace Content.Client.Power.APC.UI
{
@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Content.Client.Power;
@@ -33,9 +33,22 @@ public sealed partial class BorgSelectTypeMenu : FancyWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
foreach (var borgType in _prototypeManager.EnumeratePrototypes<BorgTypePrototype>().OrderBy(PrototypeName))
// Goobstation: Customizable borgs sprites
ConfirmTypeButton.OnPressed += ConfirmButtonPressed;
HelpGuidebookIds = GuidebookEntries;
SubtypeSelection.SubtypeSelected += () =>
ConfirmTypeButton.Disabled = false;
}
// foreach (var borgType in _prototypeManager.EnumeratePrototypes<BorgTypePrototype>().OrderBy(PrototypeName))
// Mono: Selectable borg whitelist
public void Populate(IReadOnlyList<ProtoId<BorgTypePrototype>> whitelist)
{
IEnumerable<BorgTypePrototype> types = whitelist.Select(id => _prototypeManager.Index(id)).OrderBy(PrototypeName);
foreach (var borgType in types)
// Mono: Selectable borg whitelist end
{
// Goobstation-Start: Customizable borgs sprites
var chassisList = new EntityPrototypeView
{
Scale = new Vector2(2, 2),
@@ -49,13 +62,6 @@ public sealed partial class BorgSelectTypeMenu : FancyWindow
};
SelectionsContainer.AddChild(chassisList);
}
ConfirmTypeButton.OnPressed += ConfirmButtonPressed;
HelpGuidebookIds = GuidebookEntries;
SubtypeSelection.SubtypeSelected += () =>
ConfirmTypeButton.Disabled = false;
// Goobstation-End: Customizable borgs sprites
}
@@ -23,8 +23,12 @@ public sealed class BorgSelectTypeUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
//Mono: Selectable borg whitelist
EntMan.TryGetComponent<BorgSwitchableTypeComponent>(Owner, out var comp);
var whitelist = comp?.TypeWhitelist ?? [];
_menu = this.CreateWindow<BorgSelectTypeMenu>();
_menu.Populate(whitelist); //Mono
_menu.ConfirmedBorgType += (prototype, subtype) => SendPredictedMessage(new BorgSelectTypeMessage(prototype, subtype));
}
}
@@ -46,18 +46,15 @@ public sealed class BorgSwitchableTypeSystem : SharedBorgSwitchableTypeSystem
{
if (TryComp(entity, out SpriteComponent? sprite))
{
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Body, prototype.SpriteBodyState);
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Light, prototype.SpriteBodyState);
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteBodyState);
var rsiPath = SpriteSpecifierSerializer.TextureRoot / subtypePrototype.SpritePath;
if (_resourceCache.TryGetResource<RSIResource>(rsiPath, out var resource))
{
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Body, resource.RSI);
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Light, resource.RSI);
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.LightStatus, resource.RSI);
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Body, resource.RSI, prototype.SpriteBodyState);
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.Light, resource.RSI, prototype.SpriteBodyState);
_sprite.LayerSetRsi((entity, sprite), BorgVisualLayers.LightStatus, resource.RSI, prototype.SpriteToggleLightState);
}
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.Body, prototype.SpriteBodyState);
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteBodyState);
_sprite.LayerSetRsiState((entity, sprite), BorgVisualLayers.LightStatus, prototype.SpriteToggleLightState);
}
@@ -0,0 +1,38 @@
using System.Numerics;
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
namespace Content.Client.Sprite;
public sealed class ScaleVisualsSystem : SharedScaleVisualsSystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(Entity<ScaleVisualsComponent> ent, ref AppearanceChangeEvent args)
{
if (!args.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
args.Sprite == null) return;
// save the original scale
ent.Comp.OriginalScale ??= args.Sprite.Scale;
var vecScale = (Vector2)scale;
_sprite.SetScale((ent.Owner, args.Sprite), vecScale);
}
// revert to the original scale
protected override void ResetScale(Entity<ScaleVisualsComponent> ent)
{
base.ResetScale(ent);
if (ent.Comp.OriginalScale != null)
_sprite.SetScale(ent.Owner, ent.Comp.OriginalScale.Value);
}
}
@@ -0,0 +1,264 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Content.Shared.CCVar;
using Content.Client.CharacterInfo;
using static Content.Client.CharacterInfo.CharacterInfoSystem;
namespace Content.Client.UserInterface.Systems.Chat;
public sealed partial class ChatUIController : IOnSystemChanged<CharacterInfoSystem>
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
private static readonly Regex StartDoubleQuote = new("\"$");
private static readonly Regex EndDoubleQuote = new("^\"|(?<=^@)\"");
private static readonly Regex StartAtSign = new("^@");
private readonly List<string> _highlights = new();
private readonly List<string> _autoFillRawKeywords = new();
private string? _highlightsColor;
private bool _autoFillHighlightsEnabled;
private bool _charInfoIsAttach = false;
private string? _currentCharacterName;
public event Action<string>? HighlightsUpdated;
public event Action<string>? AutoFillUpdated;
private void InitializeHighlights()
{
_config.OnValueChanged(CCVars.ChatAutoFillHighlights, (value) => { _autoFillHighlightsEnabled = value; }, true);
_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }, true);
var cvar = _config.GetCVar(CCVars.ChatHighlights);
}
public void OnSystemLoaded(CharacterInfoSystem system)
{
system.OnCharacterUpdate += OnCharacterUpdated;
}
public void OnSystemUnloaded(CharacterInfoSystem system)
{
system.OnCharacterUpdate -= OnCharacterUpdated;
}
private void UpdateAutoFillHighlights()
{
_currentCharacterName = null;
_autoFillRawKeywords.Clear();
_highlights.Clear();
AutoFillUpdated?.Invoke("");
HighlightsUpdated?.Invoke("");
_charInfoIsAttach = true;
_characterInfo.RequestCharacterInfo();
}
/// <summary>
/// Saves the user's custom keywords for the given character into the CVar,
/// then rebuilds the active highlight list.
/// Called when the user submits keywords from the popup.
/// </summary>
public void UpdateHighlights(string customKeywords)
{
if (_currentCharacterName == null)
{
return;
}
SaveCustomKeywords(_currentCharacterName, customKeywords);
RebuildHighlightsList(customKeywords);
HighlightsUpdated?.Invoke(customKeywords);
}
private void OnCharacterUpdated(CharacterData data)
{
if (!_charInfoIsAttach)
{
return;
}
var (_, job, _, _, entityName) = data;
_currentCharacterName = entityName;
_autoFillRawKeywords.Clear();
var autoFillDisplay = "";
if (_autoFillHighlightsEnabled)
{
// Special rules for names can be added here
var nameHighlights = "@" + entityName;
// Split on spaces first (handles any number of space-separated words).
if (nameHighlights.Contains(' '))
nameHighlights = nameHighlights.Replace(" ", "\n@");
// Handle hyphenated tokens: single hyphen splits normally (e.g. "First-Last"),
// multiple hyphens use the lizard rule — keep only first and last part (e.g. "Eats-The-Food" → @Eats @Food).
var dashParts = nameHighlights.Split('-');
if (dashParts.Length == 2)
nameHighlights = nameHighlights.Replace("-", "\n@");
else if (dashParts.Length > 2)
nameHighlights = dashParts[0] + "\n@" + dashParts[^1];
foreach (var token in nameHighlights.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
_autoFillRawKeywords.Add(token);
}
var jobKey = job.Replace(' ', '-').ToLower();
if (_loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
{
foreach (var jobWord in jobMatches.Replace(", ", "\n").Split('\n', StringSplitOptions.RemoveEmptyEntries))
_autoFillRawKeywords.Add(jobWord);
}
autoFillDisplay = string.Join("\n", _autoFillRawKeywords);
}
var customKeywords = LoadCustomKeywords(entityName);
RebuildHighlightsList(customKeywords);
AutoFillUpdated?.Invoke(autoFillDisplay);
HighlightsUpdated?.Invoke(customKeywords);
_charInfoIsAttach = false;
}
private string LoadCustomKeywords(string characterName)
{
var cvar = _config.GetCVar(CCVars.ChatHighlights);
if (string.IsNullOrEmpty(cvar))
return "";
// Shouldn't be needed as no-one should have data by the time this rolls out but we never know
if (!cvar.Contains("||"))
{
return cvar;
}
foreach (var line in cvar.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
var sep = line.IndexOf("||", StringComparison.Ordinal);
if (sep < 0)
continue;
if (line[..sep] == characterName)
return DecodeKeywords(line[(sep + 2)..]);
}
return "";
}
private void SaveCustomKeywords(string characterName, string customKeywords)
{
var cvar = _config.GetCVar(CCVars.ChatHighlights);
var entries = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(cvar) && cvar.Contains("||"))
{
foreach (var line in cvar.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
var sep = line.IndexOf("||", StringComparison.Ordinal);
if (sep < 0)
continue;
entries[line[..sep]] = line[(sep + 2)..];
}
}
entries[characterName] = EncodeKeywords(customKeywords);
var sb = new StringBuilder();
foreach (var (name, encoded) in entries)
sb.Append(name).Append("||").Append(encoded).Append('\n');
var newValue = sb.ToString().TrimEnd('\n');
_config.SetCVar(CCVars.ChatHighlights, newValue);
_config.SaveToFile();
}
private static string EncodeKeywords(string raw)
{
return raw
.Replace(@"\", @"\\")
.Replace("|", @"\|")
.Replace("\n", @"\n");
}
private static string DecodeKeywords(string encoded)
{
var sb = new StringBuilder(encoded.Length);
for (var i = 0; i < encoded.Length; i++)
{
if (encoded[i] == '\\' && i + 1 < encoded.Length)
{
switch (encoded[i + 1])
{
case 'n': sb.Append('\n'); i++; break;
case '|': sb.Append('|'); i++; break;
case '\\': sb.Append('\\'); i++; break;
default: sb.Append(encoded[i]); break;
}
}
else
{
sb.Append(encoded[i]);
}
}
return sb.ToString();
}
private void RebuildHighlightsList(string customRaw)
{
_highlights.Clear();
foreach (var raw in _autoFillRawKeywords)
_highlights.Add(ProcessKeyword(raw));
if (!string.IsNullOrWhiteSpace(customRaw))
{
foreach (var raw in customRaw.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
_highlights.Add(ProcessKeyword(raw));
}
_highlights.Sort((x, y) => y.Length.CompareTo(x.Length));
}
private string ProcessKeyword(string raw)
{
// Replace every "\" to prevent "\n", "\0", etc. in regex.
var keyword = raw.Replace(@"\", @"\\");
// Escape special regex characters.
keyword = Regex.Escape(keyword);
// Re-escape "[" for markup-adjacent positions.
keyword = keyword.Replace(@"\[", @"\\\[");
// words in double quotes will only be hightlighted as unique, not within another word
if (keyword.Any(c => c == '"'))
{
keyword = StartDoubleQuote.Replace(keyword, "(?!\\w)");
keyword = EndDoubleQuote.Replace(keyword, "(?<!\\w)");
}
// Make it so it doesn't hightlight your own name when you are talking
keyword = StartAtSign.Replace(keyword, @"(?<=\[BubbleContent\].*)");
return keyword;
}
}
@@ -44,7 +44,7 @@ using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Chat;
public sealed class ChatUIController : UIController
public sealed partial class ChatUIController : UIController
{
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IChatManager _manager = default!;
@@ -248,6 +248,8 @@ public sealed class ChatUIController : UIController
_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
InitializeHighlights();
}
public void OnScreenLoad()
@@ -434,6 +436,7 @@ public sealed class ChatUIController : UIController
private void OnAttachedChanged(EntityUid uid)
{
UpdateChannelPermissions();
UpdateAutoFillHighlights();
}
private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
@@ -877,6 +880,11 @@ public sealed class ChatUIController : UIController
}
}
foreach (var highlight in _highlights)
{
msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor);
}
// Log all incoming chat to repopulate when filter is un-toggled
if (!msg.HideChat)
{
@@ -912,7 +920,6 @@ public sealed class ChatUIController : UIController
case ChatChannel.Dead:
if (_ghost is not {IsGhost: true})
break;
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
break;
@@ -1,10 +1,30 @@
<controls:ChannelFilterPopup
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls">
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer Name="FilterPopupPanel" StyleClasses="BorderedWindowPanel">
<BoxContainer Orientation="Horizontal">
<Control MinSize="4 0"/>
<BoxContainer Name="FilterVBox" MinWidth="110" Margin="0 10" Orientation="Vertical" SeparationOverride="4"/>
<BoxContainer Name="HighlightsVBox" MinWidth="120" Margin="0 10" Orientation="Vertical" SeparationOverride="4">
<BoxContainer Name="AutoFillVBox" Orientation="Vertical" SeparationOverride="2" Visible="False">
<Label Text="{Loc 'hud-chatbox-autofill-highlights'}"/>
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#262736"/>
</PanelContainer.PanelOverride>
<RichTextLabel Name="AutoFillLabel" Margin="5 5"/>
</PanelContainer>
</BoxContainer>
<Label Text="{Loc 'hud-chatbox-highlights'}"/>
<PanelContainer>
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#323446"/>
</PanelContainer.PanelOverride>
<TextEdit Name="HighlightEdit" MinHeight="150" Margin="5 5"/>
</PanelContainer>
<Button Name="HighlightButton" Text="{Loc 'hud-chatbox-highlights-button'}" ToolTip="{Loc 'hud-chatbox-highlights-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</controls:ChannelFilterPopup>
@@ -2,6 +2,7 @@ using Content.Shared.Chat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.UserInterface.Systems.Chat.Controls;
@@ -30,10 +31,13 @@ public sealed partial class ChannelFilterPopup : Popup
private readonly Dictionary<ChatChannel, ChannelFilterCheckbox> _filterStates = new();
public event Action<ChatChannel, bool>? OnChannelFilter;
public event Action<string>? OnNewHighlights;
public ChannelFilterPopup()
{
RobustXamlLoader.Load(this);
HighlightButton.OnPressed += HighlightsEntered;
HighlightEdit.Placeholder = new Rope.Leaf(Loc.GetString("hud-chatbox-highlights-placeholder"));
}
public bool IsActive(ChatChannel channel)
@@ -95,4 +99,36 @@ public sealed partial class ChannelFilterPopup : Popup
if (_filterStates.TryGetValue(channel, out var checkbox))
checkbox.UpdateUnreadCount(unread);
}
// Highlight
/// <summary>
/// Updates the read-only auto-fill section with name/job tokens.
/// </summary>
public void UpdateAutoFill(string autoFillKeywords)
{
AutoFillVBox.Visible = !string.IsNullOrEmpty(autoFillKeywords);
if (string.IsNullOrEmpty(autoFillKeywords))
return;
var msg = new FormattedMessage();
var lines = autoFillKeywords.Split('\n', StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
if (i > 0) msg.PushNewline();
msg.AddText(lines[i]);
}
AutoFillLabel.SetMessage(msg);
}
public void UpdateHighlights(string highlights)
{
HighlightEdit.TextRope = new Rope.Leaf(highlights);
}
private void HighlightsEntered(ButtonEventArgs _args)
{
OnNewHighlights?.Invoke(Rope.Collapse(HighlightEdit.TextRope));
}
}
@@ -47,9 +47,12 @@ public partial class ChatBox : UIWidget
ChatInput.Input.OnFocusExit += OnFocusExit;
ChatInput.ChannelSelector.OnChannelSelect += OnChannelSelect;
ChatInput.FilterButton.Popup.OnChannelFilter += OnChannelFilter;
ChatInput.FilterButton.Popup.OnNewHighlights += OnNewHighlights;
_controller = UserInterfaceManager.GetUIController<ChatUIController>();
_controller.MessageAdded += OnMessageAdded;
_controller.HighlightsUpdated += OnHighlightsUpdated;
_controller.AutoFillUpdated += OnAutoFillUpdated;
_controller.RegisterChat(this);
// WD EDIT START
@@ -133,6 +136,21 @@ public partial class ChatBox : UIWidget
}
}
private void OnNewHighlights(string highlighs)
{
_controller.UpdateHighlights(highlighs);
}
private void OnHighlightsUpdated(string highlights)
{
ChatInput.FilterButton.Popup.UpdateHighlights(highlights);
}
private void OnAutoFillUpdated(string autoFillKeywords)
{
ChatInput.FilterButton.Popup.UpdateAutoFill(autoFillKeywords);
}
public void AddLine(string message, Color color, int repeat = 0) // WD EDIT
{
var formatted = new FormattedMessage(4); // WD EDIT // specifying size beforehand smells like a useless microoptimisation, but i'll give them the benefit of doubt
@@ -148,7 +166,7 @@ public partial class ChatBox : UIWidget
("size", 8+sizeIncrease)
));
} // WD EDIT END
Contents.AddMessage(formatted);
Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.Mobs;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
// lerp
var hue = 5f / 18f * progress;
return Color.FromHsv((hue, 1f, 0.75f, 1f));
return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
}
return InterpolateColorGaussian(Plasma, progress);
@@ -614,7 +614,7 @@ public sealed class StorageWindow : BaseWindow
{
marked.Add(cell);
cell.ModulateSelfOverride = spotFree
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
: Color.FromHex("#2222CC");
}
}
+5 -1
View File
@@ -64,7 +64,11 @@ namespace Content.Client.Voting.UI
var entry = _vote.Entries[i];
if (_vote.DisplayVotes)
{
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
// Mono
if (entry.RealVotes != entry.Votes)
_voteButtons[i].Text = Loc.GetString("ui-vote-button-weighted", ("text", entry.Text), ("votes", entry.Votes), ("realvotes", entry.RealVotes));
else
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
}
else
{
+2
View File
@@ -192,6 +192,7 @@ namespace Content.Client.Voting
for (var i = 0; i < message.Options.Length; i++)
{
existingVote.Entries[i].Votes = message.Options[i].votes;
existingVote.Entries[i].RealVotes = message.Options[i].realVotes; // Mono
}
if (@new && _popupContainer != null)
@@ -259,6 +260,7 @@ namespace Content.Client.Voting
{
public string Text { get; }
public int Votes { get; set; }
public int RealVotes; // Mono
public VoteEntry(string text)
{
@@ -126,6 +126,9 @@ public sealed partial class StationPickerControl : PickerControl
foreach (var (stationEntity, stationJobInformation) in stationList)
{
var icon = stationJobInformation.StationDisplayInfo?.StationIcon;
var iconTexture = icon != null ? _spriteSystem.Frame0(icon) : null;
var viewState = new StationListItem.ViewState(
stationEntity,
stationJobInformation.GetStationNameWithJobCount(),
@@ -136,7 +139,7 @@ public sealed partial class StationPickerControl : PickerControl
? _loc.GetString(stationJobInformation.StationDisplayInfo.StationDescription)
: "",
_lastSelectedStation?.StationEntity == stationEntity,
stationJobInformation.StationDisplayInfo?.StationIcon?.CanonPath
iconTexture
);
// Always select the first station in the list if none is selected yet.
@@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -13,7 +14,7 @@ public sealed partial class StationListItem : PanelContainer
string stationSubtext,
string stationDescription,
bool selected,
string? iconPath)
Texture? icon)
{
public string StationName { get; } = stationName;
public string StationSubtext { get; } = stationSubtext;
@@ -22,7 +23,7 @@ public sealed partial class StationListItem : PanelContainer
public bool Selected { get; set; } = selected;
public string IconPath { get; } = iconPath ?? "";
public Texture? Icon { get; } = icon;
}
public StationListItem(ViewState state)
@@ -35,9 +36,9 @@ public sealed partial class StationListItem : PanelContainer
// Disallow repeated selection of same station that does UI reloads.
StationButton.Disabled = state.Selected;
if (state.IconPath.Length > 0)
if (state.Icon != null)
{
StationIcon.TexturePath = state.IconPath;
StationIcon.Texture = state.Icon;
}
}
}
@@ -22,7 +22,7 @@ public sealed class RMCCombatModeUISystem : EntitySystem
_hands.GetActiveHandEntity() is { } held &&
_rmcCombatMode.GetCrosshair(held) != null)
{
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.One);
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.Zero);
_clyde.SetCursor(_crosshairCursor);
}
else
@@ -1,12 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<OutputPath>..\bin\Content.IntegrationTests\</OutputPath>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<LangVersion>12</LangVersion>
<Nullable>disable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
@@ -17,12 +15,12 @@
<ProjectReference Include="..\Content.Server\Content.Server.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>
+1
View File
@@ -3,3 +3,4 @@
global using NUnit.Framework;
global using System;
global using System.Threading.Tasks;
global using Robust.UnitTesting.Pool;
@@ -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; }
}
@@ -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();
}
}
+30 -142
View File
@@ -1,180 +1,68 @@
#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.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.UnitTesting;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
// Contains misc helper functions to make writing tests easier.
public sealed partial class TestPair
{
public Task<TestMapData> CreateTestMap(bool initialized = true)
=> CreateTestMap(initialized, "Plating");
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// Loads a test map and returns a <see cref="TestMapData"/> representing it.
/// </summary>
[MemberNotNull(nameof(TestMap))]
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
/// <param name="testMapPath">The <see cref="ResPath"/> to the test map to load.</param>
/// <param name="initialized">Whether to initialize the map on load.</param>
/// <returns>A <see cref="TestMapData"/> representing the loaded map.</returns>
public async Task<TestMapData> LoadTestMap(ResPath testMapPath, bool initialized = true)
{
var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
TestMapData mapData = new();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = initialized };
var mapLoaderSys = Server.EntMan.System<MapLoaderSystem>();
var mapSys = Server.System<SharedMapSystem>();
TestMap = mapData;
await Server.WaitPost(() =>
// Load our test map in and assert that it exists.
await Server.WaitAssertion(() =>
{
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();
});
Assert.That(mapLoaderSys.TryLoadMap(testMapPath, out var map, out var gridSet, deserializationOptions),
$"Failed to load map {testMapPath}.");
Assert.That(gridSet, Is.Not.Empty, "There were no grids loaded from the map!");
TestMap = mapData;
if (!Settings.Connected)
return mapData;
mapData.MapUid = map!.Value.Owner;
mapData.MapId = map!.Value.Comp.MapId;
mapData.Grid = gridSet!.First();
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapSys.GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
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;
}
/// <summary>
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
{
user ??= Client.User!.Value;
if (user is not {} userId)
if (user is not { } userId)
return;
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
@@ -183,7 +71,7 @@ public sealed partial class TestPair
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
@@ -211,7 +99,7 @@ public sealed partial class TestPair
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(user);
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.
@@ -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);
}
}
+21 -156
View File
@@ -8,82 +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.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");
}
private async Task ResetModifiedPreferences()
@@ -93,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();
State = 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.");
@@ -160,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.
@@ -169,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
@@ -229,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));
@@ -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));
}
}
+88 -134
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,109 @@ 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;
}
await base.Initialize();
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)
// Prevent info log spam in some tests (particularly SpawnAndDeleteAllEntitiesOnDifferentMaps)
Server.System<SharedMapSystem>().Log.Level = LogLevel.Warning;
Client.EntMan.EntitySysManager.SystemLoaded += (_, e) =>
{
var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
if (e.System is SharedMapSystem map)
map.Log.Level = LogLevel.Warning;
};
var settings = (PoolSettings)Settings;
if (!settings.DummyTicker)
{
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;
}
}
+5 -47
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.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
@@ -29,55 +26,16 @@ 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"),
(CCVars.GameLobbyDefaultPreset.Name, "secret"), // Frontier: Adventure takes ages, default to secret
(CCVars.StaticStorageUI.Name, "true"),// Frontier: causes storage test failures
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage enabled
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage
};
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);
if (cfg.IsCVarRegistered(CCVars.GameLobbyDefaultPreset.Name) && !string.IsNullOrEmpty(settings.GameLobbyDefaultPreset)) // Frontier
cfg.SetCVar(CCVars.GameLobbyDefaultPreset, settings.GameLobbyDefaultPreset); // Frontier
});
}
private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
{
foreach (var (cvar, value) in TestCvars)
{
options.CVarOverrides[cvar] = value;
}
}
}
+37 -378
View File
@@ -1,371 +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;
if (!poolSettings.NoValidate) // Mono
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;
if (!poolSettings.NoValidate) // Mono
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)
{
return await GetServerClientPair(poolSettings ?? new PoolSettings());
}
private static string GetDefaultTestName(TestContext testContext)
{
return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
private static async Task<TestPair> GetServerClientPair(PoolSettings poolSettings)
{
if (!_initialized)
throw new InvalidOperationException($"Pool manager has not been initialized");
// Trust issues with the AsyncLocal that backs this.
var testContext = TestContext.CurrentContext;
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");
}
}
if (!poolSettings.NoValidate) // Mono
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
@@ -421,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);
}
}
+22 -106
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,94 +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; }
/// <summary>
/// Mono - skips pair validation if set. Also stops termination on error.
/// </summary>
public bool NoValidate = false;
/// <summary>
/// Frontier: the preset to run the game in.
/// Set to secret for upstream tests to mimic upstream behaviour.
/// If you need to check adventure game rule things, set this to nfadventure or nfpirate.
/// </summary>
public string GameLobbyDefaultPreset { get; set; } = "secret";
#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
&& GameLobbyDefaultPreset == nextSettings.GameLobbyDefaultPreset; // Frontier: swappable presets
return DummyTicker == next.DummyTicker
&& Map == next.Map
&& InLobby == next.InLobby;
}
}
@@ -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;
}
}
@@ -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
{
}
+97 -66
View File
@@ -1,7 +1,7 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
@@ -10,6 +10,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.IntegrationTests.Tests
{
@@ -56,7 +57,7 @@ namespace Content.IntegrationTests.Tests
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
@@ -114,7 +115,7 @@ namespace Content.IntegrationTests.Tests
SpawnEntity(entityMan, protoId, map.GridCoords); // Monolith
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
@@ -278,74 +279,104 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3);
// 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>();
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
foreach (var protoId in protoIds)
await Assert.MultipleAsync(async () =>
{
// TODO fix ninja
// Currently ninja fails to equip their own loadout.
if (protoId == "MobHumanSpaceNinja")
continue;
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
EntityUid uid = default;
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
foreach (var protoId in protoIds)
{
if (Count(server.EntMan) != count)
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
var serverEntities = new HashSet<EntityUid>(Entities(server.EntMan));
var clientEntities = new HashSet<EntityUid>(Entities(client.EntMan));
EntityUid uid = default;
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
{
Assert.Fail($"Server prototype {protoId} failed on deleting itself");
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
continue;
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
continue;
}
// Check that the number of entities has increased.
Assert.That(Count(server.EntMan), Is.GreaterThan(count), $"Server prototype {protoId} failed on spawning as entity count didn't increase\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.GreaterThan(clientCount), $"Client prototype {protoId} failed on spawning as entity count didn't increase\n" +
$"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
// Check that the number of entities has increased.
if (Count(server.EntMan) <= count)
{
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
if (Count(client.EntMan) <= clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
$"Expected at least {clientCount} and found {Count(client.EntMan)}. " +
$"Server was {count}");
// 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(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));
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
// Check that the number of entities has gone back to the original value.
if (Count(server.EntMan) != count)
{
// Frontier: add expected vs. actual
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {count} and found {server.EntMan.EntityCount}.\n");
// End Frontier
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
}
});
await pair.CleanReturnAsync();
}
private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
{
var sb = new StringBuilder();
var addedEnts = newEnts.Except(oldEnts);
var removedEnts = oldEnts.Except(newEnts);
if (addedEnts.Any())
sb.AppendLine("Listing new entities:");
foreach (var addedEnt in addedEnts)
{
sb.AppendLine(entMan.ToPrettyString(addedEnt));
}
if (removedEnts.Any())
sb.AppendLine("Listing removed entities:");
foreach (var removedEnt in removedEnts)
{
sb.AppendLine("\t" + entMan.ToPrettyString(removedEnt));
}
return sb.ToString();
}
private static bool HasRequiredDataField(Component component)
{
foreach (var field in component.GetType().GetFields())
{
foreach (var attribute in field.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
foreach (var property in component.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
return false;
}
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{
@@ -373,9 +404,6 @@ namespace Content.IntegrationTests.Tests
"GridSpawner", // mono - i wouldn't
};
// TODO TESTS
// auto ignore any components that have a "required" data field.
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
@@ -393,9 +421,12 @@ namespace Content.IntegrationTests.Tests
foreach (var type in componentFactory.AllRegisteredTypes)
{
var component = (Component) componentFactory.GetComponent(type);
var component = (Component)componentFactory.GetComponent(type);
var name = componentFactory.GetComponentName(type);
if (HasRequiredDataField(component))
continue;
// If this component is ignored
if (skipComponents.Contains(name))
{
@@ -432,9 +463,9 @@ namespace Content.IntegrationTests.Tests
private EntityUid SpawnEntity(
IEntityManager entManager,
string? protoName,
string protoName,
MapCoordinates coordinates,
ComponentRegistry? registry = null)
ComponentRegistry registry = null)
{
try
{
@@ -451,9 +482,9 @@ namespace Content.IntegrationTests.Tests
private EntityUid SpawnEntity(
IEntityManager entManager,
string? protoName,
string protoName,
EntityCoordinates coordinates,
ComponentRegistry? registry = null)
ComponentRegistry registry = null)
{
try
{
@@ -61,7 +61,7 @@ public sealed class StorageInteractionTest : InteractionTest
await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
await RunTicks(10);
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True); // Mono heisenfail - TODO: fix
// Click on the pda to pick it up and remove it from the backpack.
await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
@@ -71,7 +71,7 @@ public sealed class StorageInteractionTest : InteractionTest
// UIs should still be open
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
}
/// <summary>
@@ -14,7 +14,7 @@ public sealed class AdminTest : ToolshedTest
var toolMan = Server.ResolveDependency<ToolshedManager>();
var admin = Server.ResolveDependency<IAdminManager>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
{typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
@@ -25,6 +25,11 @@ public sealed class AdminTest : ToolshedTest
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
// Only care about content commands.
var assemblyName = cmd.Cmd.GetType().Assembly.FullName;
if (assemblyName == null || !assemblyName.StartsWith("Content."))
continue;
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
}
});
@@ -19,7 +19,7 @@ public sealed class LocTest : ToolshedTest
var locStrings = new HashSet<string>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
{typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{
+10 -2
View File
@@ -3,19 +3,27 @@
<OutputType>Exe</OutputType>
<OutputPath>..\bin\Content.MapRenderer\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NUnit" />
<PackageReference Include="Moq" />
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>
+12 -2
View File
@@ -38,8 +38,9 @@ namespace Content.MapRenderer.Painters
Fresh = true,
// Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time.
Map = map,
NoValidate = true, // Mono
});
pair.ServerLogHandler.FailureLevel = LogLevel.Fatal;
pair.ClientLogHandler.FailureLevel = LogLevel.Fatal;
await foreach (var image in RenderPair(stopwatch, pair))
yield return image;
@@ -56,8 +57,9 @@ namespace Content.MapRenderer.Painters
DummyTicker = false,
Connected = true,
Fresh = false,
NoValidate = true, // Mono
});
pair.ServerLogHandler.FailureLevel = LogLevel.Fatal;
pair.ClientLogHandler.FailureLevel = LogLevel.Fatal;
var server = pair.Server;
var client = pair.Client;
@@ -71,6 +73,14 @@ namespace Content.MapRenderer.Painters
foreach (var grid in sMapManager.GetAllGrids(mapId))
sEntityManager.QueueDeleteEntity(grid);
});
await pair.RunTicksSync(10);
await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
await server.WaitPost(() =>
{
var mapId = sEntityManager.System<GameTicker>().DefaultMap;
try
{
+4 -4
View File
@@ -2,13 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ServerGarbageCollection>True</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Robust.Packaging\Robust.Packaging.csproj" />
</ItemGroup>
<Import Project="../MSBuild/Content.props" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Packaging.props" />
</Project>
+80
View File
@@ -0,0 +1,80 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Content.Packaging;
/// <summary>
/// Helper class for working with <c>.deps.json</c> files.
/// </summary>
public sealed class DepsHandler
{
public readonly Dictionary<string, LibraryInfo> Libraries = new();
public DepsHandler(DepsData data)
{
if (data.Targets.Count != 1)
throw new Exception("Expected exactly one target");
var target = data.Targets.Single().Value;
foreach (var (libNameAndVersion, libInfo) in target)
{
var split = libNameAndVersion.Split('/', 2);
Libraries.Add(split[0], libInfo);
}
}
public static DepsHandler Load(string depsFile)
{
using var f = File.OpenRead(depsFile);
var depsData = JsonSerializer.Deserialize<DepsData>(f) ?? throw new InvalidOperationException("Deps are null!");
return new DepsHandler(depsData);
}
public HashSet<string> RecursiveGetLibrariesFrom(string start)
{
var found = new HashSet<string>();
RecursiveAddLibraries(start, found);
return found;
}
private void RecursiveAddLibraries(string start, HashSet<string> set)
{
if (!set.Add(start))
return;
var lib = Libraries[start];
if (lib.Dependencies == null)
return;
foreach (var dep in lib.Dependencies.Keys)
{
RecursiveAddLibraries(dep, set);
}
}
public sealed class DepsData
{
[JsonInclude, JsonPropertyName("targets")]
public required Dictionary<string, Dictionary<string, LibraryInfo>> Targets;
}
public sealed class LibraryInfo
{
[JsonInclude, JsonPropertyName("dependencies")]
public Dictionary<string, string>? Dependencies;
[JsonInclude, JsonPropertyName("runtime")]
public Dictionary<string, object>? Runtime;
// Paths are like lib/netstandard2.0/JetBrains.Annotations.dll
public IEnumerable<string> GetDllNames()
{
return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]);
}
}
}
+30 -33
View File
@@ -19,12 +19,15 @@ public static class ServerPackaging
new PlatformReg("osx-x64", "MacOS", true),
new PlatformReg("osx-arm64", "MacOS", true),
// Non-default platforms (i.e. for Watchdog Git)
new PlatformReg("win-x86", "Windows", false),
new PlatformReg("linux-x86", "Linux", false),
new PlatformReg("linux-arm", "Linux", false),
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static IReadOnlySet<string> ServerContentIgnoresResources { get; } = new HashSet<string>
{
"ServerInfo",
"Changelog",
};
private static List<string> PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -34,25 +37,9 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();
private static readonly List<string> ServerContentAssemblies = new()
{
"Content.Server.Database",
"Content.Server",
"Content.Shared",
"Content.Shared.Database",
};
private static readonly List<string> ServerExtraAssemblies = new()
{
// Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql",
"Microsoft",
"Discord",
};
private static readonly List<string> ServerNotExtraAssemblies = new()
{
"Microsoft.CodeAnalysis",
"JetBrains.Annotations",
};
private static readonly HashSet<string> BinSkipFolders = new()
@@ -178,23 +165,13 @@ public static class ServerPackaging
var inputPassCore = graph.InputCore;
var inputPassResources = graph.InputResources;
var contentAssemblies = new List<string>(ServerContentAssemblies);
// Additional assemblies that need to be copied such as EFCore.
var sourcePath = Path.Combine(contentDir, "bin", "Content.Server");
// Should this be an asset pass?
// For future archaeologists I just want audio rework to work and need the audio pass so
// just porting this as is from python.
foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{
var fileName = Path.GetFileNameWithoutExtension(fullPath);
var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json"));
if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o)))
{
contentAssemblies.Add(fileName);
}
}
var contentAssemblies = GetContentAssemblyNamesToCopy(deps);
await RobustSharedPackaging.DoResourceCopy(
Path.Combine("RobustToolbox", "bin", "Server",
@@ -211,7 +188,11 @@ public static class ServerPackaging
contentAssemblies,
cancel: cancel);
await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
await RobustServerPackaging.WriteServerResources(
contentDir,
inputPassResources,
ServerContentIgnoresResources.Concat(RobustSharedPackaging.SharedIgnoredResources).ToHashSet(),
cancel);
if (hybridAcz)
{
@@ -222,5 +203,21 @@ public static class ServerPackaging
inputPassResources.InjectFinished();
}
// This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql)
private static IEnumerable<string> GetContentAssemblyNamesToCopy(DepsHandler deps)
{
var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames);
var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames);
var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet();
// Remove .dll suffix and apply filtering.
var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith));
return names;
IEnumerable<string> GetLibraryNames(string library) => deps.Libraries[library].GetDllNames();
}
private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
}
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+6 -9
View File
@@ -1,26 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Replay\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Nett" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
</Project>
@@ -1,16 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Server.Database\</OutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Nullable>enable</Nullable>
<NoWarn>RA0003</NoWarn>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -25,7 +25,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
return null;
}
@@ -38,7 +38,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
if (args.Length == 1 && !bool.TryParse(args[0], out enabled))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-argument-must-be-boolean"));
return null;
}
+1 -1
View File
@@ -178,7 +178,7 @@ public sealed class AmeNodeGroup : BaseNodeGroup
// return MathF.Max(200000f * MathF.Log10(2 * fuel * MathF.Pow(cores, (float)-0.5)), 0); // Frontier: preferring old calculation for now
// return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow(0.75f, cores - 1); // Frontier: preferring old calculation for now
if (cores > 0 && fuel > 0) // Mono - default zero power unless conditions are met
return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow( 1.11111f, cores - 1); // Mono - Added positive returns from extra cores. Running AME at 2 fuel per core yields roughly the same power per core at all sizes
return 200000f * MathF.Log10(fuel * fuel) * MathF.Pow( 1.11111f, fuel / 2f - 1); // Mono - Exponential scaling at high injection
return 0;
}
@@ -61,4 +61,16 @@ public sealed partial class ThermalRegulatorComponent : Component
/// </summary>
[DataField]
public float ThermalRegulationTemperatureThreshold;
/// <summary>
/// Process regulation while they're dead if there is a MobStateComponent? - Mono Hydrakin
/// </summary>
[DataField]
public bool ProcessWhileDead = false;
/// <summary>
/// Process regulation in critical condition if there is a MobStateComponent? - Mono Hydrakin
/// </summary>
[DataField]
public bool ProcessWhileCrit = true;
}
@@ -2,6 +2,8 @@ using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Mobs; // Mono
using Content.Shared.Mobs.Components; // Mono
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
@@ -51,6 +53,14 @@ public sealed class ThermalRegulatorSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp2, logMissing: false))
return;
// mono begin
if (ent.Comp1.ProcessWhileDead == false && TryComp<MobStateComponent>(ent, out var mobComp1) && mobComp1.CurrentState == MobState.Dead)
return;
if (ent.Comp1.ProcessWhileCrit == false && TryComp<MobStateComponent>(ent, out var mobComp2) && mobComp2.CurrentState == MobState.Critical)
return;
// mono end
var totalMetabolismTempChange = ent.Comp1.MetabolismHeat - ent.Comp1.RadiatedHeat;
// implicit heat regulation
@@ -28,8 +28,6 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly ContrabandTurnInSystem _contraband = default!; // Frontier
public override void Initialize()
{
base.Initialize();
@@ -100,7 +98,6 @@ public sealed partial class BotanySystem : EntitySystem
public EntityUid SpawnSeedPacket(SeedData proto, EntityCoordinates coords, EntityUid user, float? healthOverride = null)
{
var seed = SpawnAtPosition(proto.PacketPrototype, coords); // Frontier: Spawn<SpawnAtPosition
_contraband.ClearContrabandValue(seed); // Frontier
var seedComp = EnsureComp<SeedComponent>(seed);
seedComp.Seed = proto;
seedComp.HealthOverride = healthOverride;
@@ -160,7 +157,6 @@ public sealed partial class BotanySystem : EntitySystem
var product = _robustRandom.Pick(proto.ProductPrototypes);
var entity = SpawnAtPosition(product, position); // Frontier: Spawn<SpawnAtPosition
_contraband.ClearContrabandValue(entity); // Frontier
_randomHelper.RandomOffset(entity, 0.25f);
products.Add(entity);
@@ -27,8 +27,6 @@ public sealed partial class CargoSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
@@ -310,10 +308,10 @@ public sealed partial class CargoSystem
/// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns>
public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry)
{
if (!_whitelistSys.IsValid(entry.Whitelist, entity))
if (!_whitelist.IsValid(entry.Whitelist, entity))
return false;
if (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))
if (entry.Blacklist != null && _whitelist.IsValid(entry.Blacklist, entity))
return false;
return true;
@@ -472,7 +470,7 @@ public sealed partial class CargoSystem
skipped
? CargoBountyHistoryData.BountyResult.Skipped
: CargoBountyHistoryData.BountyResult.Completed,
_gameTiming.CurTime,
_timing.CurTime,
actorName));
ent.Comp.Bounties.RemoveAt(i);
return true;
@@ -37,8 +37,6 @@ namespace Content.Server.Cargo.Systems
/// </summary>
private float _timer;
[Dependency] private readonly BankSystem _bankSystem = default!;
private void InitializeConsole()
{
SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleAddOrderMessage>(OnAddOrderMessage);
@@ -272,9 +270,9 @@ namespace Content.Server.Cargo.Systems
if (!float.IsFinite(taxCoeff) || taxCoeff <= 0.0f)
continue;
var tax = (int)Math.Floor(cost * taxCoeff);
_bankSystem.TrySectorDeposit(account, tax, LedgerEntryType.CargoTax);
_bank.TrySectorDeposit(account, tax, LedgerEntryType.CargoTax);
}
_bankSystem.TryBankWithdraw(player, cost);
_bank.TryBankWithdraw(player, cost);
// End Frontier
UpdateOrders(station.Value);
@@ -25,14 +25,10 @@ namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
[Dependency] BankSystem _bank = default!; // Mono
/*
* Handles cargo shuttle / trade mechanics.
*/
[Dependency] EntityWhitelistSystem _whitelist = default!; // Frontier
// Frontier addition:
// The maximum distance from the console to look for pallets.
private const int DefaultPalletDistance = 8;
+4 -4
View File
@@ -19,8 +19,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Random;
using Content.Server._NF.SectorServices; // Frontier
using Content.Shared._NF.Trade; // Frontier
using Content.Shared.Whitelist; // Frontier
using Content.Shared.Whitelist;
using Content.Server._NF.Bank; // Frontier
namespace Content.Server.Cargo.Systems;
@@ -44,9 +44,9 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly SectorServiceSystem _sectorService = default!; // Frontier
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; // Frontier
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!; // Frontier
[Dependency] private readonly BankSystem _bank = default!;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<CargoSellBlacklistComponent> _blacklistQuery;
+9 -10
View File
@@ -1,31 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Server\</OutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<NoWarn>1998</NoWarn>
<WarningsAsErrors>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="prometheus-net" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" />
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
<ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Packaging.props" />
</Project>
+6 -3
View File
@@ -641,7 +641,7 @@ namespace Content.Server.Database
// This allows us to semi-efficiently load all entities we need in a single DB query.
// Then we can update & insert without further round-trips to the DB.
var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
var players = updates.Select(u => u.User.UserId).Distinct().ToList();
var dbTimes = (await db.DbContext.PlayTime
.Where(p => players.Contains(p.PlayerId))
.ToArrayAsync())
@@ -875,8 +875,10 @@ namespace Content.Server.Database
{
await using var db = await GetDb();
var playerIdsList = playerIds.ToList();
var players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.Where(player => playerIdsList.Contains(player.UserId))
.ToListAsync();
var round = new Round
@@ -907,10 +909,11 @@ namespace Content.Server.Database
public async Task AddRoundPlayers(int id, Guid[] playerIds)
{
await using var db = await GetDb();
var playerIdsList = playerIds.ToList();
// ReSharper disable once SuggestVarOrType_Elsewhere
Dictionary<Guid, int> players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.Where(player => playerIdsList.Contains(player.UserId))
.ToDictionaryAsync(player => player.UserId, player => player.Id);
foreach (var player in playerIds)
@@ -171,7 +171,7 @@ namespace Content.Server.EntityEffects.Effects
interruptsDoAfters: false,
// Shitmed Change Start
targetPart: TargetBodyPart.All,
partMultiplier: 0.5f,
partMultiplier: 1.00f, // Mono, 0.5f->1.00f
canSever: false);
// Shitmed Change End
}
@@ -3,7 +3,6 @@ using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Serilog;
namespace Content.Server.EntityEffects.Effects;
@@ -12,9 +11,11 @@ namespace Content.Server.EntityEffects.Effects;
/// </summary>
public sealed partial class PlantSpeciesChange : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var log = IoCManager.Resolve<ILogManager>().GetSawmill(nameof(PlantSpeciesChange));
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
@@ -29,7 +30,7 @@ public sealed partial class PlantSpeciesChange : EntityEffect
if (protoSeed == null)
{
Log.Error($"Seed prototype could not be found: {targetProto}!");
log.Error($"Seed prototype could not be found: {targetProto}!");
return;
}
@@ -1,3 +1,4 @@
using Content.Server._Mono.Planets;
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Explosion.Components;
@@ -39,6 +40,7 @@ using Content.Shared.Projectiles; // Frontier: embed triggers
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Robust.Shared.Map.Components;
namespace Content.Server.Explosion.EntitySystems
{
@@ -83,6 +85,7 @@ namespace Content.Server.Explosion.EntitySystems
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
[Dependency] private readonly StationSystem _station = default!; // Frontier: medical insurance
[Dependency] private readonly SharedMapSystem _map = default!; // Frontier: medical insurance
public override void Initialize()
{
@@ -252,19 +255,19 @@ namespace Content.Server.Explosion.EntitySystems
var posText = $"({x}, {y})";
// Frontier: Gets station location of the implant
var station = _station.GetOwningStation(uid);
var stationText = station is null ? null : $"{Name(station.Value)} ";
var grid = ownerXform.GridUid;
var gridText = grid is null ? "" : MetaData(grid.Value).EntityName;
if (stationText == null)
stationText = "";
if (HasComp<MapComponent>(grid) && !HasComp<PlanetMapComponent>(grid))
gridText = "";
// Frontier: Gets species of the implant user
var speciesText = $"";
if (TryComp<HumanoidAppearanceComponent>(implanted.ImplantedEntity, out var species))
speciesText = $" ({species!.Species})";
speciesText = $" ({species.Species})";
var critMessage = Loc.GetString(component.CritMessage, ("user", implanted.ImplantedEntity.Value), ("specie", speciesText), ("grid", stationText!), ("position", posText));
var deathMessage = Loc.GetString(component.DeathMessage, ("user", implanted.ImplantedEntity.Value), ("specie", speciesText), ("grid", stationText!), ("position", posText));
var critMessage = Loc.GetString(component.CritMessage, ("user", implanted.ImplantedEntity.Value), ("specie", speciesText), ("grid", gridText), ("position", posText));
var deathMessage = Loc.GetString(component.DeathMessage, ("user", implanted.ImplantedEntity.Value), ("specie", speciesText), ("grid", gridText), ("position", posText));
if (!TryComp<MobStateComponent>(implanted.ImplantedEntity, out var mobstate))
return;
@@ -48,7 +48,6 @@ namespace Content.Server.Forensics
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly StackSystem _stackSystem = default!; // Frontier
[Dependency] private readonly SharedAudioSystem _audio = default!; // Frontier
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Frontier
[Dependency] private readonly RadioSystem _radio = default!; // Frontier
[Dependency] private readonly DeadDropSystem _deadDrop = default!; // Frontier
@@ -100,7 +99,7 @@ namespace Content.Server.Forensics
private void GiveReward(EntityUid uidOrigin, EntityUid target, int spesoAmount, FixedPoint2 fmcAmount, string msg)
{
SoundSpecifier confirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
_audio.PlayPvs(_audio.GetSound(confirmSound), uidOrigin);
_audioSystem.PlayPvs(_audioSystem.GetSound(confirmSound), uidOrigin);
if (spesoAmount > 0)
_bank.TrySectorDeposit(SectorBankAccount.Nfsd, spesoAmount, LedgerEntryType.AntiSmugglingBonus);
@@ -130,6 +130,10 @@ public sealed partial class GameTicker
if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up
return false;
// Mono - ensure delayed start rules don't trigger ahead of time.
if (TryComp<DelayedStartRuleComponent>(ruleEntity, out var delayComp) && delayComp.RuleStartTime > _gameTiming.CurTime)
return false;
// If we already have it, then we just skip the delay as it has already happened.
if (!RemComp<DelayedStartRuleComponent>(ruleEntity) && ruleData.Delay != null)
{
@@ -42,5 +42,11 @@ namespace Content.Server.GameTicking.Presets
/// </summary>
[DataField("supportedMaps", customTypeSerializer: typeof(PrototypeIdSerializer<GameMapPoolPrototype>))]
public string? MapPool;
/// <summary>
/// Mono - as how much votes should each player click count.
/// </summary>
[DataField]
public float Weight = 1f;
}
}
+7
View File
@@ -19,6 +19,7 @@ using Content.Shared.Eye;
using Content.Shared.FixedPoint;
using Content.Shared.Follower;
using Content.Shared.Ghost;
using Content.Shared.GhostTypes;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
@@ -71,6 +72,7 @@ namespace Content.Server.Ghost
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IAdminManager _admin = default!; // Frontier
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
[Dependency] private readonly GhostSpriteStateSystem _ghostState = default!;
private EntityQuery<GhostComponent> _ghostQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
@@ -558,6 +560,11 @@ namespace Content.Server.Ghost
var ghost = SpawnAtPosition(GameTicker.ObserverPrototypeName, spawnPosition.Value);
var ghostComponent = Comp<GhostComponent>(ghost);
if (TryComp<GhostSpriteStateComponent>(ghost, out var state)) // If more TryComps are added this should be turned into an event
{
_ghostState.SetGhostSprite((ghost, state), mind);
}
// Try setting the ghost entity name to either the character name or the player name.
// If all else fails, it'll default to the default entity prototype name, "observer".
// However, that should rarely happen.
-5
View File
@@ -59,7 +59,6 @@ namespace Content.Server.Lathe
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ContrabandTurnInSystem _contraband = default!; // Frontier
[Dependency] private readonly DeviceLinkSystem _deviceLink = default!; // Mono
/// <summary>
@@ -326,11 +325,7 @@ namespace Content.Server.Lathe
// Frontier: adjust price before merge (stack prices changed once)
if (result.Valid)
{
ModifyPrintedEntityPrice(uid, comp, result);
_contraband.ClearContrabandValue(result);
}
// End Frontier
_stack.TryMergeToContacts(result);

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