Upstream sync (#1035)
* Add new implants to deimplant list (#35563) Initial commit * Doxarubixadone Description Fix (#35568) Changed medicine.ftl for Doxa. * Reptilians Can Eat Chicken Nuggets (#35569) Added meat tag to misc.yml for chicken nuggets. * Automatic changelog update * Unheck Admin Smites (#35348) * Fix admin verb names Fixed admin verb names. * Add antag verb names * Adjust antag verb icons * Amber Station - A Couple Changes (#35548) * [ADMIN] Minor Refactor AdminNameOverlay (#35520) * refactor(src): Minor refactor of Draw in "AdminNameOverlay. And new info about playtime player * fix(src): Add configure classic admin owerlay * fix * tweak(src): Use _antagLabelClassic and tweak style * tweak(src): Add config display overlay for startingJob and playTime * tweak(src): Vector2 is replaced by var * tweak(src): return to the end of the list * Automatic changelog update * Wizard PDA (#35572) * wizard PDA * colour change to brown * Automatic changelog update * Increase line spacing of the admin overlay (#35591) line spacing * make slime hair less transparent (#35158) * blabl blump or something * +0.3 * blimpuf * Automatic changelog update * Fix being able to write on/stamp/fax paper scrap (#35596) * init * item * requested changes * Apply suggestions from code review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Changed Pride to Hubris in ion_storm.yml (#35602) Update ion_storm.yml * Sentry turrets - Part 3: Turret AI (#35058) * Initial commit * Updated Access/command.yml * Fix for Access/AccessLevelPrototype.cs * Added silicon access levels to admin items * Included self-recharging battery changes * Revert "Included self-recharging battery changes" * Addressed reviewers comments * Additional reviewer comments * DetGadget Hat Revitalization (#35438) * DetGadget Hat * uh... half-assed item description * Reduce hat range to one tile, you have to stand on someone to steal their hat items * Fix Integration Errors * Only the wearer can access voice commands * init work - handscomp is unable to be pulled * second bit of progress * basic working implementation * nuke storageslots and add adminlogging * disallow trolling nukies or hiding objective items * remove unnecessary tags additions * finish nuking unused tags * death to yamllinter * int tests be damned * milon is a furry * address review * upd desc * address reviews part 2 * address more reviews * remove unused refs * fix order of dependencies * add ShowVerb to SharedStorageSystem.cs This will allow or disallow showing the "Open Storage" verb if defined on the component. * orks is a nerd * add proper locale, fix adminlogging * orks is a nerd 2 --------- Co-authored-by: Coenx-flex <coengmurray@gmail.com> * Automatic changelog update * Fingerprint Reader System (#35600) * init * public api * stuff * weh * Remove cellular resistance for slimes (#35583) * Remove cellular resistance for slimes * Update guidebook * Automatic changelog update * Give the station map inhand sprites (#35605) map has inhands * Reagent guidebook reactions UI dividers (#35608) * Update GuideReagentReaction.xaml * Update Content.Client/Guidebook/Controls/GuideReagentReaction.xaml Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> * Update Content.Client/Guidebook/Controls/GuideReagentReaction.xaml Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> --------- Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> * fix cluwne pda pen slot (#35611) Co-authored-by: deltanedas <@deltanedas:kde.org> * Revert "Make radioactive material radioactive" (#35330) * Automatic changelog update * Predict vending machine UI (#33412) * Automatic changelog update * #32209 changelog (#35619) Since it was merged into staging no changelog was made, but we should at least have it for next release. (And vulture) * Automatic changelog update * Cloning Refactor and bugfixes (#35555) * cloning refactor * cleanup and fixes * don't pick from 0 * give dwarves the correct species * fix dna and bloodstream reagent data cloning * don't copy helmets * be less redundant * Automatic changelog update * centcomm update (#35627) * Better Insectoid Glasses (#31812) Sprites and file changes Adds the variants for arachnid and moth glasses, adds the code for those in the meta.json files, and adds the speciesID tag in both arachnid and moth prototype files. * Automatic changelog update * Add GetBaseName method to NameModifierSystem (#35633) * Add GetBaseName method to NameModifierSystem * Use Name * Save Space Station 14 from the Toilet Gibber Forever (#35587) * The evil is defeated * Tag body bags * uwu, cwush me cwusher-chan * absolute 18+ sloggery * botos binted? 👽 * Automatic changelog update * Changed Damage Overlay to check Burn Damage (#34535) * updated BruteLevel to be PainLevel with burn damage checks in DamageOverlayUiController.cs * dehardcoded pain level by adding damage groups to paindamagegroups to affect * re-added the name for painDamageGroups * fixed overlay default and added minimum limit to component check first * renamed to PainDamageGroups and removed obsolete tag * Automatic changelog update * Wizard's Magical Pen (#35623) * wizard pen * description change * Automatic changelog update * Added decelerator percentage drain (#35643) * Added variable PercentageDrain to SinguloFoodComponent * Set percentageDrain to 0.03 (3%) for anti particles * Added percentageDrain logic in public OnConsumed * Simplify SinguloFoodComponent and set percentageDrain to negative * EnergyFactor now applies to positive values too * Better commenting on EnergyFactor * Update Content.Server/Singularity/Components/SinguloFoodComponent.cs * Documentation of EnergyFactor * Fixing spelling mistake --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Made butter require less milk (#35650) made butter take less milk * Automatic changelog update * Delete SolutionContainerVisualsComponent.InitialName (#35654) * Fix name of cotton dough rope (#35657) changed in-game name of cotton dough rope to differentiate from normal dough rope * CVar - Toggle display of round-end greentext (#35651) * hide greentext if cvar false * change IFs around a lil * reviews * Open State for cowtools (#35666) Open State * Make implants unshielded (#35667) * Add undergarments & "Censor Nudity" toggle to options (#33185) * Initial commit * Attribution * Review changes * Added comment for upstream * Automatic changelog update * centcomm update (#35674) * More scars! (#35644) * :3 * whitespace, stomach scar * Automatic changelog update * Lathe menu UI displays a count of available recipes (#35570) * commit * jumped the gun * changes * Players with unknown playtimes now are tagged as new players, take 2 (#35648) * your commit? our commit. * skreee * show joined players before lobby players; comments * comments * playerinfo retains playtime data after disconnect * new connection status symbols * Automatic changelog update * Add firelocks and locked external airlocks to ATS (#35516) * Add firelocks and locked airlocks to ATS * Add fire alarms * Elkridge Tesla and TEG Improvements + Other stuff (#35684) * better tesla, better TEG, better sci maints, chef gets chef closet * added storage room for tesla parts, added captain bathroom, changed vault so nuke can be armed * ran fixgridatmos and added some vacuum markers * unflatpacked containment shit * Cargo Mail System (#35429) * shitcode init * biocoding, SpawnTableOnUse, Moving shit to shared * server :( * fixes * ok works * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * Discard changes to Content.Shared/Forensics/Components/FingerprintMaskComponent.cs * Discard changes to Content.Shared/Forensics/Components/FingerprintComponent.cs * Discard changes to Content.Server/Forensics/Systems/ForensicsSystem.cs * Discard changes to Content.Server/StationRecords/Systems/StationRecordsSystem.cs * Discard changes to Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * big stuff * preperation * temperory spawning thing for testing * Update CargoDeliveryDataComponent.cs * kinda proper spawning idk god save me * cleanup (kinda) * preparation 2.0 * stuff i think * entity table work * renames * spawn ratio based on players * comment * letter tables * more spam * package tables * comment * biocodedn't * builds correctly * cleaning * Update deliveries_tables.yml * labels * package sprites * mail teleporter * revert testing value * fix test * fix other test * i love tests * mail teleporter enabled by default * random cooldowns * fixtures * Discard changes to Content.Shared/FingerprintReader/FingerprintReaderComponent.cs * Discard changes to Content.Shared/FingerprintReader/FingerprintReaderSystem.cs * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * Discard changes to Resources/Locale/en-US/fingerprint-reader/fingerprint-reader.ftl * clean * fuck paper scrap * oops * fuck SpawnTableOnUse * mail teleporter board in QM locker + addressed review * oops * clean * sound on delivery spawn * address review * partial review address * partial review addressing * addressing partial review * pratarial revivew address * misprediction hell * stuff * more stuff * unrelated * TODO * link * partial review * DirtyField --------- Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * Add AssertMultiple to ContrabandTest (#35662) * add AssertMultiple to ContrabandTest * do the same for magazine visuals test * :trollface: --------- Co-authored-by: deltanedas <@deltanedas:kde.org> * add forceghost admin command (#35518) * add forceghost admin command * sweep linq under the rug * braces * ûse LocalizedEntityCommands * Automatic changelog update * Text related keybinds can now be changed in Controls (#35630) * Add ability to rebind text related keybinds * fix placement of locales * Automatic changelog update * Update b2dynamictree (#30630) * Update submodule to 248.0.0 (#35720) * Add sun shadows (planet lighting stage 2) (#35145) * Implements a Dynamic Lighting System on maps. * Edit: the night should be a little bit brighter and blue now. * Major edit: everything must be done on the client side now, with certain datafield replicated. Changes were outlined in the salvage to accommodate the new lighting system. * Edit: The offset is now serverside, this makes the time accurate in all situations. * Removing ununsed import * Minor tweaks * Tweak in time precision * Minor tweak + Unused import removed * Edit: apparently RealTime is better for what I'm looking for * Fix: Now the time is calculated correctly. * Minor tweaks * Adds condition for when the light should be updated * Add planet lighting * she * close-ish * c * bittersweat * Fixes * Revert "Merge branch '22719' into 2024-09-29-planet-lighting" This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86. * Europa and day-night * weh * rooves working * Clean * Remove Europa * Fixes * fix * Update * Fix caves * Update for engine * Add sun shadows (planet lighting v2) For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish. * Documentation * a * Fixes * Move blur to an overlay * Slughands * Fixes * Apply RoofOverlay per-grid not per-map * Fix light render scales * sangas * Juice it a bit * Better angle * Fixes * Add color support * Rounding bandaid * Wehs * Better * Remember I forgot to do this when writing docs --------- Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com> * Automatic changelog update * Omega Mail Teleporter (#35705) Mail! * Packed Mail Teleporter (#35706) Mail! * Box Mail Teleporter (#35707) Mail! * Oasis Mail Teleporter (#35708) Mail! * Meta Mail Teleporter (#35709) Mail! * Marathon Mail Teleporter (#35710) Mail! * Fland Mail Teleporter (#35711) Mail! * Plasma fixes 4 (#35716) Fixes 15 Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com> * Aroace pride pin, scarf, and cloak (#35718) cloak, pin, and scarf added yayyyy * Automatic changelog update * [Part of #32893] Localize silicon dataset names (#33352) * Localize ai names * Apply requested changes * Localize autoborg * Localize borg names * Localize atv names * Correct prototypes ids to follow naming conventions * Remove AI localization (Moved to another PR) * Weh * [Part of #32893] Localize arachnid dataset names (#33353) * Localize arachnid dataset names * Correct prototype ids to follow naming conventions * Combine arachnid_first.yml and arachnid_last.yml * Upstream names * [Part of #32893] Localize summonable creatures dataset names (#33392) * Localize clown names * Localize golem names * Localize hologram names * Correct prototype ids to follow naming conventions * Update Resources/Locale/en-US/datasets/names/golem.ftl --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize antagonists dataset names (#33393) * Localize fake human names * Localize ninja names * Localize operation names * Localize regalrat names * Localize revenant names * Localize syndicate names * Localize wizard names * Correct prototype ids to follow naming conventions * Combine fake_human_first.yml and fake_human_last.yml * Move contents of ninja_title.yml into ninja.yml * Combine Operation_prefix.yml and Operation_suffix.yml * Combine wizard_first.yml and wizard_last.yml * Upstream names * fix wizard --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize humanoid species dataset names (#33395) * Localize diona names * Localize moth names * Localize mushman names * Localize reptilian names * Localize skeleton names * Upstream diona names * names-moth-male/female-first-dataset -> names-moth-first-male/female-dataset * Correct prototype ids to follow naming conventions * NamesSkeletonFirst -> NamesSkeleton * Combine moth_first_female.yml, moth_first_male.yml and moth_last.yml * Forgot about skeleton prototype * Upstream names * Update Resources/Locale/en-US/datasets/names/diona_last.ftl * Update Resources/Locale/en-US/datasets/names/diona_last.ftl * keep first name for skeleton --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize vox dataset names (#33396) * Localize vox names * Correct prototype id to follow naming conventions * Upstream names * [Part of #32893] Localize first & last dataset names (#33401) * Localize first names * Localize last names * Correct prototype ids to follow naming conventions * Combine first.yml and last.yml into base.yml * Forgot about = in last * [Part of #32893] Localize first male & female dataset names (#33402) * Localize first_name * Localize first_female * names-male/female-first-dataset -> names-first-male/female-dataset * Correct prototype ids to follow naming conventions * Combine first_male.yml and first_female.yml into base_gendered.yml * [Part of #32893] Localize misc dataset names (#33404) * Localize cargo_shuttle names * Localize death_commando names * Localize fortunes * Localize military names * Localize rollie names * fortunes.ftl -> cookie_fortune.ftl * Correct prototype ids to follow naming conventions * Localize all dataset names (#32893) * Use `LocalizedDatasetPrototype` instead of `DatasetPrototype` in `RoleLoadoutPrototype` * Localize ai names * Replace to `LocalizedDatasetPrototype` in `NamingSystem` * Localize arachnid first and last names * Localize atv names * Localize autoborg names * Forgot to change type to localizedDataset * Localize borer names * Localize borg names * Localize cargo shuttle names * Localize clown names * Localize death_commando names * Localize diona names * Localize fake_human names * Localize first and last names * Localize first male and female names * Localize fortunes descriptions * Forgot about equal sign * Localize golem names * Localize hologram names * Localize military names * Localize moth first male and female names * Localize moth last names * Fix autoborg name error * Localize mushman first and last names * Localize ninja names * Localize operation names * Localize regalrat names * Fix mushman_first * Forgot about `Loc.GetString` * Move comments into comment section & fix names * Localize reptilian male and female names * Localize revenant names * Fix locale word order in operation * Localize rollie (btw it was never used and was added as "for the futuгe" long time ago) * Localize skeleton_first names * Localize syndicate names * Localize vox names * Localize wizard first and last names * `{owner}-name-dataset` -> `names-{owner}-dataset` * Change `DatasetPrototype` to `LocalizedDatasetPrototype` and make sure it works properly GetFTLName is no more the static method, we need it to be able to use `Loc.GetString` * I hate those mothname comments * Combine name datasets prototypes * Move every ftl from` /en-US/names` to ` /en-US/datasets/names` * Remove ftl files * Get every dataset yml back * Remove changes for planets. Move it in another PR * Revert these changes (Moved to another PR) * How * Apply suggested changes * Fix integration tests (#35727) * test * fix names * fix more * Initial delivery balance changes (#35728) * init * small balance * guess not * Update Content.Server/Delivery/CargoDeliveryDataComponent.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fixed delivery popups (#35724) * :) * cool stuff * Remove a bonus Loc.GetString (#35731) oops * Bagel Engineering Improvements (#35717) * woe, better engineering be upon ye * im going to lose it * radical plan * oopsie * Revert "oopsie" This reverts commit 45ab057f55b46acd795e58257c3cc5967e5cb946. * Revert "radical plan" This reverts commit 57b1ae081725a47aef3ae03111cecbc91b4f47a8. * Revert "im going to lose it" This reverts commit e7b4afaf5d9a10a42e89831ffc9294d3b9bd96d4. * Revert "woe, better engineering be upon ye" This reverts commit 471dc3716b58a39631aa8bee00de79e981391d63. * complete revamp * revision * oops 2 electric boogaloo * another one * every time i push to fix a minor mistake i found in walking around i get closer to my limit * Update Credits (#35733) Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> * Loop mail teleporter (#35729) * latejoin * youve got mail * Core mail update (#35719) * core mail update * empty * derotate core (#35740) Update default.yml * Elkridge Mail Update (#35738) add mail teleporter and mailing unit system * Automatic changelog update * Plasma Mail Teleporter (#35741) Mail! * Convex Mail Teleporter (#35742) Mail! * Remove unneeded Loc.GetString (#35739) * Steal the mail thieving objective (#35746) * mail theft * networked * Automatic changelog update * fix UpdateBankAccount (#35749) * trolled * fun * fuck me * Slightly better letter loot table (#35748) * init * review --------- Co-authored-by: Milon <milonpl.git@proton.me> * Python Suit Storage Visual (#35593) * Python-SUITSTORAGE-Visuals Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * REVised Sprite Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Copyright Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Update Resources/Textures/Objects/Weapons/Guns/Revolvers/python.rsi/meta.json Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * fix nukeops commander name (#35753) * bagel update (#35754) * Predict some power PowerReceiver stuff (#33834) * Predict some power PowerReceiver stuff Need it for some atmos device prediction. * Also this * Localize traitor codeverbs datasets (#35737) * Localize verbs dataset * Localize adjectives dataset * Localize corporations dataset * Update TraitorRuleSystem to use LocalizedDatasetPrototype instead of DatasetPrototype * Fix sun shadows in ANGLE (#35757) I think I fat-fingered a ctrl-Z on this at some point but the intermediate blur is necessary. * Automatic changelog update * Tweak sun shadow rotations (#35758) Won't use the entity's rotation for the matrix, I just forgot to do this. Means shadows will always point in the same direction and the points will correctly adjust as the entity rotates. * Automatic changelog update * Fix Ahelp window playerlist resize (#35747) reorganize bwoink window layout * Automatic changelog update * Ensure speech bubble cap is always respected (#32223) Ensure speech bubble cap is respected, even when messages are sent very fast * Cleanup: Fix ``PaperWriteEvent`` in ``PaperSystem`` (#35763) * Cleanup + fix * Revert * Cleanup: Add missing locale ``cmd-planet-map-prototype`` (#35766) Cleanup * Added New Cocktails and new fill level sprites to existing drinks. (#33570) * Added New Cocktails and new fill level sprites to existing drinks * Updated copyright info and fixed recipies for Caipirinha/Mojito. --------- Co-authored-by: RedBookcase <Usualmoves@gmail.com> * Automatic changelog update * Performer's Wig (#35764) * miku wig * fix to correct json convention Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> --------- Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> * Automatic changelog update * Merge showsubfloorforever into showsubfloor (#33682) * convex fix * Removable mindshields and revolutionary tweaks. (#35769) * I fucking hate revs * Update preset-revolutionary.ftl * fixy fix * Automatic changelog update * Mail Resprite (#35776) * init commit * init commit * delete those * added github to copyright info * Fix Chameleon PDAs renaming the user in station records (#35782) * Automatic changelog update * Restore the order of admin overlay elements (#35783) admin overlay order fix * Automatic changelog update * Fixes and refactoring to discord changelog script (#33859) * Fixes and refactoring to discord changelog script Upstreamed from https://github.com/impstation/imp-station-14/pull/1023 * Add some prints back in * Update to borg ion storms (#35751) * Updates ion storms for borgs. * Remove additional ion laws into future PR * Automatic changelog update * TriggerSystem improvements (#35762) * desynchronizer real * yaml stuff from slarti branch * C# stuff * oops * fix triggers * atomize PR --------- Co-authored-by: Flareguy <woaj9999@outlook.com> * Roleban command error handling (#35784) roleban command jobid fail handling * Localize news dataset (#35774) * Localize news dataset * Remove the `"` * Localize rat king commands datasets (#35780) * Added mail room * Update submodule to 248.0.2 (#35787) * Update Space Law to reflect Implant changes (#35701) * Change implanter Space Law * Add clarification regarding unidentified implanter vs. unidentified implant sentensing * Add support for antag-before-job selection (#35789) * Add support for antag-before-job selection * Include logging * forensics cleanup (#35795) * polymorph popup fixes (#35796) polymorph fixes * fix more syndicate names (#35788) * New Feature: Warden job rolls before security officer/cadet/detective (#35313) Commit * Automatic changelog update * Fix anomaly doublelogging (#34297) cull doublelogging * Add wallmount N2 closets, Revived (#34042) * Add standard, wallmount and improvised N2 closets, Revived * remove improvised locker * Parent>ID * Undo sprite replacement * Update meta.json --------- Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Milon <milonpl.git@proton.me> * Cryo and grinder cleanup (#34842) * cryopod and grinder cleanup * lower case name * 4 spaces * prototype clean * looks like there is some kind of test that prevents removing this * grinder too * both should be empty * cleanup * Add Gold and Coal Rock Anomalies (#34809) * This commit adds 2 new Rock Anomaly types, Coal and Gold. It also adds Resource Crabs, colored crystals, and lights for both. * Added crafting recipes for yellow and black light tubes. Somehow I forgot that the first time. * Sorted tags.yml alphabetically this time instead of not doing that. * Updated Texture Copyright information * Attempted to fix Merge Conflict * Added bulb light variants for both yellow and black crystals. * Automatic changelog update * Tools/Devices: In-hand Sprites (#33689) * Adds in-hand sprites to the barber scissors. * adds in-hand sprites to the floodlight. * adds in-hand sprites to the gas analyzer. * adds in-hand sprites to the gps. * Update copyright wording, linting * resprite gps inhand sprites. * adds in-hand sprites to the mass scanner. * adds in-hand sprites to the spray_painter. * resprite in-hand sprites to the mass_scanner. * fix in-hand sprites to the mass_scanner. * Resprite mass_scanner in-hand sprites. * Automatic changelog update * IconSmooth additional smoothing keys (#35790) * additionalKeys * Update lava.yml * Update Content.Client/IconSmoothing/IconSmoothComponent.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Locks nitrous oxide canisters (#35785) lock nitrous oxide canisters * Automatic changelog update * Cleanup Objective files, add PickSpecificPersonComponent (#35802) * cleanup objectives * remove unrelated access restriction * review * Adds popup when firing gun while gun has no ammo (#34816) * Adds popup when firing gun while gun has no ammo * simplify --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Add the ability to pet the mail teleporter (#35819) good mailbox * Automatic changelog update * Whitehole/Singularity grenade price adjustments + whitehole grenade fix (#35821) * prices + adjustments * teehee * Automatic changelog update * Update Lobby Music Attribtions (#35817) Biggest change is updating the attributions and links for Sunbeamstress' to reflect the changes in their online profile as the previous link is now a dead link. Updated Comet Haley's link to go directly to Stellardrone's Bandcamp instead of diverting to Free Music Archive Fixed a double the in the comment for Space Asshole * Paradox Clone (#35794) * polymorph fixes * paradox clone * forensics cleanup * bump doors * 4 * attribution * polymorphn't * clean up objectives * Update Resources/ServerInfo/Guidebook/Antagonist/MinorAntagonists.xml * review * add virtual items to blacklist * allow them to roll sleeper agent * Automatic changelog update * Improvements to antag-before-job selection system (#35822) * Fix the latejoin-antag-deficit bug, add datafield, add logging * Fix multiple roles being made for single-role defs, * HOTFIX: Fix paradox clone event (#35858) fix paradox clone event * Update CP14TownSendConditionSystem.cs --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: Smith <182301147+AgentSmithRadio@users.noreply.github.com> Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Pancake <Pangogie@users.noreply.github.com> Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> Co-authored-by: Schrödinger <132720404+Schrodinger71@users.noreply.github.com> Co-authored-by: Velken <8467292+Velken@users.noreply.github.com> Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com> Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Coenx-flex <coengmurray@gmail.com> Co-authored-by: hivehum <ketchupfaced@gmail.com> Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: Myra <vasilis@pikachu.systems> Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com> Co-authored-by: HTML/Crystal <152909599+HTMLSystem@users.noreply.github.com> Co-authored-by: Tayrtahn <tayrtahn@gmail.com> Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com> Co-authored-by: Coolsurf6 <coolsurf24@yahoo.com.au> Co-authored-by: rokudara-sen <160833839+rokudara-sen@users.noreply.github.com> Co-authored-by: DuckManZach <144298822+DuckManZach@users.noreply.github.com> Co-authored-by: MisterImp <101299120+MisterImp@users.noreply.github.com> Co-authored-by: Killerqu00 <47712032+Killerqu00@users.noreply.github.com> Co-authored-by: Ps3Moira <113228053+ps3moira@users.noreply.github.com> Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com> Co-authored-by: āda <ss.adasts@gmail.com> Co-authored-by: War Pigeon <54217755+minus1over12@users.noreply.github.com> Co-authored-by: Deerstop <edainturner@gmail.com> Co-authored-by: Milon <milonpl.git@proton.me> Co-authored-by: Łukasz Mędrek <lukasz@lukaszm.xyz> Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com> Co-authored-by: compilatron <40789662+Compilatron144@users.noreply.github.com> Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com> Co-authored-by: Momo <Rsnesrud@gmail.com> Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com> Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: Evelyn Gordon <evelyn.gordon20@gmail.com> Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> Co-authored-by: RedBookcase <crazykid1590@gmail.com> Co-authored-by: RedBookcase <Usualmoves@gmail.com> Co-authored-by: SpaceManiac <tad@platymuus.com> Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: imcb <irismessage@protonmail.com> Co-authored-by: valquaint <57813693+valquaint@users.noreply.github.com> Co-authored-by: Flareguy <woaj9999@outlook.com> Co-authored-by: ninruB <ninrub@tuta.io> Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com> Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Łukasz Lindert <lukasz.lindert@protonmail.com> Co-authored-by: Firewars763 <35506916+Firewars763@users.noreply.github.com> Co-authored-by: onesch <118821520+onesch@users.noreply.github.com> Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com> Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com> Co-authored-by: Crude Oil <124208219+CroilBird@users.noreply.github.com> Co-authored-by: Lusatia <ultimate_doge@outlook.com>
This commit is contained in:
@@ -44,7 +44,7 @@ namespace Content.Benchmarks
|
|||||||
for (var i = 0; i < Aabbs1.Length; i++)
|
for (var i = 0; i < Aabbs1.Length; i++)
|
||||||
{
|
{
|
||||||
var aabb = Aabbs1[i];
|
var aabb = Aabbs1[i];
|
||||||
_b2Tree.CreateProxy(aabb, i);
|
_b2Tree.CreateProxy(aabb, uint.MaxValue, i);
|
||||||
_tree.Add(i);
|
_tree.Add(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ internal sealed class AdminNameOverlay : Overlay
|
|||||||
|
|
||||||
//TODO make this adjustable via GUI
|
//TODO make this adjustable via GUI
|
||||||
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
|
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
|
||||||
|
var playTime = _config.GetCVar(CCVars.AdminOverlayPlaytime);
|
||||||
|
var startingJob = _config.GetCVar(CCVars.AdminOverlayStartingJob);
|
||||||
|
|
||||||
foreach (var playerInfo in _system.PlayerList)
|
foreach (var playerInfo in _system.PlayerList)
|
||||||
{
|
{
|
||||||
@@ -76,25 +78,44 @@ internal sealed class AdminNameOverlay : Overlay
|
|||||||
}
|
}
|
||||||
|
|
||||||
var uiScale = _userInterfaceManager.RootControl.UIScale;
|
var uiScale = _userInterfaceManager.RootControl.UIScale;
|
||||||
var lineoffset = new Vector2(0f, 11f) * uiScale;
|
var lineoffset = new Vector2(0f, 14f) * uiScale;
|
||||||
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
||||||
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
||||||
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
||||||
|
|
||||||
|
var currentOffset = Vector2.Zero;
|
||||||
|
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||||
|
currentOffset += lineoffset;
|
||||||
|
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||||
|
currentOffset += lineoffset;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
|
||||||
|
{
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
|
||||||
|
currentOffset += lineoffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && startingJob)
|
||||||
|
{
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
|
||||||
|
currentOffset += lineoffset;
|
||||||
|
}
|
||||||
|
|
||||||
if (classic && playerInfo.Antag)
|
if (classic && playerInfo.Antag)
|
||||||
{
|
{
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, _antagLabelClassic, uiScale, Color.OrangeRed);
|
||||||
|
currentOffset += lineoffset;
|
||||||
}
|
}
|
||||||
else if (!classic && _filter.Contains(playerInfo.RoleProto))
|
else if (!classic && _filter.Contains(playerInfo.RoleProto))
|
||||||
{
|
{
|
||||||
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
|
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
|
||||||
var color = playerInfo.RoleProto.Color;
|
var color = playerInfo.RoleProto.Color;
|
||||||
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
|
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, label, uiScale, color);
|
||||||
|
currentOffset += lineoffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,26 @@
|
|||||||
xmlns="https://spacestation14.io"
|
xmlns="https://spacestation14.io"
|
||||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
|
||||||
<PanelContainer StyleClasses="BackgroundDark">
|
<PanelContainer StyleClasses="BackgroundDark">
|
||||||
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
|
<SplitContainer Orientation="Vertical">
|
||||||
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="1" />
|
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
|
||||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
|
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="2" />
|
||||||
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
|
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
|
||||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
|
||||||
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
|
|
||||||
<Control HorizontalExpand="True" MinWidth="5" />
|
|
||||||
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
|
|
||||||
<Control HorizontalExpand="True" MinWidth="5" />
|
|
||||||
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
|
|
||||||
<Control HorizontalExpand="True" />
|
|
||||||
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
|
|
||||||
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
|
|
||||||
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
|
|
||||||
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
|
|
||||||
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
|
|
||||||
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
|
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
</SplitContainer>
|
||||||
|
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
|
||||||
|
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
|
||||||
|
<Control HorizontalExpand="True" MinWidth="5" />
|
||||||
|
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
|
||||||
|
<Control HorizontalExpand="True" MinWidth="5" />
|
||||||
|
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
|
||||||
|
<Control HorizontalExpand="True" />
|
||||||
|
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
|
||||||
|
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
|
||||||
|
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
|
||||||
|
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
|
||||||
|
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
|
||||||
|
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</SplitContainer>
|
</SplitContainer>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
RobustXamlLoader.Load(this);
|
RobustXamlLoader.Load(this);
|
||||||
IoCManager.InjectDependencies(this);
|
IoCManager.InjectDependencies(this);
|
||||||
|
|
||||||
|
var newPlayerThreshold = 0;
|
||||||
|
_cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
|
||||||
|
|
||||||
var uiController = _ui.GetUIController<AHelpUIController>();
|
var uiController = _ui.GetUIController<AHelpUIController>();
|
||||||
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
|
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
|
||||||
return;
|
return;
|
||||||
@@ -59,9 +62,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
if (info.Connected)
|
if (info.Connected)
|
||||||
sb.Append('●');
|
sb.Append(info.ActiveThisRound ? '⚫' : '◐');
|
||||||
else
|
else
|
||||||
sb.Append(info.ActiveThisRound ? '○' : '·');
|
sb.Append(info.ActiveThisRound ? '⭘' : '·');
|
||||||
|
|
||||||
sb.Append(' ');
|
sb.Append(' ');
|
||||||
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||||
@@ -73,10 +76,12 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
sb.Append(' ');
|
sb.Append(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark antagonists with symbol
|
||||||
if (info.Antag && info.ActiveThisRound)
|
if (info.Antag && info.ActiveThisRound)
|
||||||
sb.Append(new Rune(0x1F5E1)); // 🗡
|
sb.Append(new Rune(0x1F5E1)); // 🗡
|
||||||
|
|
||||||
if (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
|
// Mark new players with symbol
|
||||||
|
if (IsNewPlayer(info))
|
||||||
sb.Append(new Rune(0x23F2)); // ⏲
|
sb.Append(new Rune(0x23F2)); // ⏲
|
||||||
|
|
||||||
sb.AppendFormat("\"{0}\"", text);
|
sb.AppendFormat("\"{0}\"", text);
|
||||||
@@ -84,6 +89,19 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// <summary>
|
||||||
|
// Returns true if the player's overall playtime is under the set threshold
|
||||||
|
// </summary>
|
||||||
|
bool IsNewPlayer(PlayerInfo info)
|
||||||
|
{
|
||||||
|
// Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
|
||||||
|
if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return (info.OverallPlaytime is null
|
||||||
|
|| info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
|
||||||
|
}
|
||||||
|
|
||||||
ChannelSelector.Comparison = (a, b) =>
|
ChannelSelector.Comparison = (a, b) =>
|
||||||
{
|
{
|
||||||
var ach = AHelpHelper.EnsurePanel(a.SessionId);
|
var ach = AHelpHelper.EnsurePanel(a.SessionId);
|
||||||
@@ -93,31 +111,37 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
if (a.IsPinned != b.IsPinned)
|
if (a.IsPinned != b.IsPinned)
|
||||||
return a.IsPinned ? -1 : 1;
|
return a.IsPinned ? -1 : 1;
|
||||||
|
|
||||||
// First, sort by unread. Any chat with unread messages appears first.
|
// Then, any chat with unread messages.
|
||||||
var aUnread = ach.Unread > 0;
|
var aUnread = ach.Unread > 0;
|
||||||
var bUnread = bch.Unread > 0;
|
var bUnread = bch.Unread > 0;
|
||||||
if (aUnread != bUnread)
|
if (aUnread != bUnread)
|
||||||
return aUnread ? -1 : 1;
|
return aUnread ? -1 : 1;
|
||||||
|
|
||||||
// Sort by recent messages during the current round.
|
// Then, any chat with recent messages from the current round
|
||||||
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
|
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
|
||||||
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
|
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
|
||||||
if (aRecent != bRecent)
|
if (aRecent != bRecent)
|
||||||
return aRecent ? -1 : 1;
|
return aRecent ? -1 : 1;
|
||||||
|
|
||||||
// Next, sort by connection status. Any disconnected players are grouped towards the end.
|
// Sort by connection status. Disconnected players will be last.
|
||||||
if (a.Connected != b.Connected)
|
if (a.Connected != b.Connected)
|
||||||
return a.Connected ? -1 : 1;
|
return a.Connected ? -1 : 1;
|
||||||
|
|
||||||
// Sort connected players by New Player status, then by Antag status
|
// Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
|
||||||
if (a.Connected && b.Connected)
|
if (a.Connected && b.Connected)
|
||||||
{
|
{
|
||||||
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
|
var aNewPlayer = IsNewPlayer(a);
|
||||||
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
|
var bNewPlayer = IsNewPlayer(b);
|
||||||
|
|
||||||
|
// Players who have joined the round will be listed before players in the lobby
|
||||||
|
if (a.ActiveThisRound != b.ActiveThisRound)
|
||||||
|
return a.ActiveThisRound ? -1 : 1;
|
||||||
|
|
||||||
|
// Within both the joined group and lobby group, new players will be grouped and listed first
|
||||||
if (aNewPlayer != bNewPlayer)
|
if (aNewPlayer != bNewPlayer)
|
||||||
return aNewPlayer ? -1 : 1;
|
return aNewPlayer ? -1 : 1;
|
||||||
|
|
||||||
|
// Within all four previous groups, antagonists will be listed first.
|
||||||
if (a.Antag != b.Antag)
|
if (a.Antag != b.Antag)
|
||||||
return a.Antag ? -1 : 1;
|
return a.Antag ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,9 @@ namespace Content.Client.Administration.UI.Bwoink
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Title = $"{sel.CharacterName} / {sel.Username}";
|
Title = $"{sel.CharacterName} / {sel.Username} | {Loc.GetString("generic-playtime-title")}: ";
|
||||||
|
|
||||||
if (sel.OverallPlaytime != null)
|
Title += sel.OverallPlaytime != null ? sel.PlaytimeString : Loc.GetString("generic-unknown-title");
|
||||||
{
|
|
||||||
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
OnOpen += () =>
|
OnOpen += () =>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using Content.Shared.Advertise.Systems;
|
||||||
|
|
||||||
|
namespace Content.Client.Advertise.Systems;
|
||||||
|
|
||||||
|
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
|
||||||
@@ -28,7 +28,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
|
|||||||
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
|
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
|
||||||
{
|
{
|
||||||
var meta = MetaData(uid);
|
var meta = MetaData(uid);
|
||||||
component.InitialName = meta.EntityName;
|
|
||||||
component.InitialDescription = meta.EntityDescription;
|
component.InitialDescription = meta.EntityDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,29 +36,6 @@ internal sealed class ShowSubFloor : LocalizedCommands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ShowSubFloorForever : LocalizedCommands
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
|
||||||
|
|
||||||
public const string CommandName = "showsubfloorforever";
|
|
||||||
public override string Command => CommandName;
|
|
||||||
|
|
||||||
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
|
|
||||||
|
|
||||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
|
||||||
{
|
|
||||||
_entitySystemManager.GetEntitySystem<SubFloorHideSystem>().ShowAll = true;
|
|
||||||
|
|
||||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
|
||||||
var components = entMan.EntityQuery<SubFloorHideComponent, SpriteComponent>(true);
|
|
||||||
|
|
||||||
foreach (var (_, sprite) in components)
|
|
||||||
{
|
|
||||||
sprite.DrawDepth = (int) DrawDepth.Overlays;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class NotifyCommand : LocalizedCommands
|
internal sealed class NotifyCommand : LocalizedCommands
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
|
|||||||
{
|
{
|
||||||
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
|
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
|
||||||
_lightManager.Enabled = false;
|
_lightManager.Enabled = false;
|
||||||
shell.ExecuteCommand("showsubfloorforever");
|
shell.ExecuteCommand("showsubfloor");
|
||||||
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false);
|
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
Content.Client/Delivery/DeliverySystem.cs
Normal file
5
Content.Client/Delivery/DeliverySystem.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using Content.Shared.Delivery;
|
||||||
|
|
||||||
|
namespace Content.Client.Delivery;
|
||||||
|
|
||||||
|
public sealed class DeliverySystem : SharedDeliverySystem;
|
||||||
45
Content.Client/Delivery/DeliveryVisualizerSystem.cs
Normal file
45
Content.Client/Delivery/DeliveryVisualizerSystem.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using Content.Shared.Delivery;
|
||||||
|
using Content.Shared.StatusIcon;
|
||||||
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Client.Delivery;
|
||||||
|
|
||||||
|
public sealed class DeliveryVisualizerSystem : VisualizerSystem<DeliveryComponent>
|
||||||
|
{
|
||||||
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
|
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||||
|
|
||||||
|
private static readonly ProtoId<JobIconPrototype> UnknownIcon = "JobIconUnknown";
|
||||||
|
|
||||||
|
protected override void OnAppearanceChange(EntityUid uid, DeliveryComponent component, ref AppearanceChangeEvent args)
|
||||||
|
{
|
||||||
|
if (args.Sprite == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_appearance.TryGetData(uid, DeliveryVisuals.JobIcon, out string job, args.Component);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(job))
|
||||||
|
job = UnknownIcon;
|
||||||
|
|
||||||
|
if (!_prototype.TryIndex<JobIconPrototype>(job, out var icon))
|
||||||
|
{
|
||||||
|
args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(_prototype.Index("JobIconUnknown")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(icon.Icon));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DeliveryVisualLayers : byte
|
||||||
|
{
|
||||||
|
Icon,
|
||||||
|
Lock,
|
||||||
|
FragileStamp,
|
||||||
|
JobStamp,
|
||||||
|
PriorityTape,
|
||||||
|
Breakage,
|
||||||
|
Trash,
|
||||||
|
}
|
||||||
@@ -4,8 +4,7 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalExpand="True"
|
HorizontalExpand="True"
|
||||||
Margin="0 0 0 5">
|
Margin="0 0 0 5">
|
||||||
<BoxContainer
|
<BoxContainer Orientation="Horizontal">
|
||||||
Orientation="Horizontal">
|
|
||||||
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
|
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<RichTextLabel Name="ReactantsLabel"
|
<RichTextLabel Name="ReactantsLabel"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Humanoid.Markings;
|
using Content.Shared.Humanoid.Markings;
|
||||||
using Content.Shared.Humanoid.Prototypes;
|
using Content.Shared.Humanoid.Prototypes;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
@@ -12,12 +14,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||||
|
Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
|
||||||
|
Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
|
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
|
||||||
@@ -25,6 +30,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||||||
UpdateSprite(component, Comp<SpriteComponent>(uid));
|
UpdateSprite(component, Comp<SpriteComponent>(uid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnCvarChanged(bool value)
|
||||||
|
{
|
||||||
|
var humanoidQuery = EntityManager.AllEntityQueryEnumerator<HumanoidAppearanceComponent, SpriteComponent>();
|
||||||
|
while (humanoidQuery.MoveNext(out var _, out var humanoidComp, out var spriteComp))
|
||||||
|
{
|
||||||
|
UpdateSprite(humanoidComp, spriteComp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
|
private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
|
||||||
{
|
{
|
||||||
UpdateLayers(component, sprite);
|
UpdateLayers(component, sprite);
|
||||||
@@ -218,16 +232,30 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||||||
// Really, markings should probably be a separate component altogether.
|
// Really, markings should probably be a separate component altogether.
|
||||||
ClearAllMarkings(humanoid, sprite);
|
ClearAllMarkings(humanoid, sprite);
|
||||||
|
|
||||||
|
var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
|
||||||
|
_configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
|
||||||
|
// The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
|
||||||
|
var applyUndergarmentTop = censorNudity;
|
||||||
|
var applyUndergarmentBottom = censorNudity;
|
||||||
|
|
||||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||||
{
|
{
|
||||||
foreach (var marking in markingList)
|
foreach (var marking in markingList)
|
||||||
{
|
{
|
||||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
|
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
|
||||||
|
{
|
||||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
|
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
|
||||||
|
if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
|
||||||
|
applyUndergarmentTop = false;
|
||||||
|
else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
|
||||||
|
applyUndergarmentBottom = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
|
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
|
||||||
|
|
||||||
|
AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
|
private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
|
||||||
@@ -275,6 +303,31 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
|||||||
spriteComp.RemoveLayer(index);
|
spriteComp.RemoveLayer(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
|
||||||
|
{
|
||||||
|
if (undergarmentTop && humanoid.UndergarmentTop != null)
|
||||||
|
{
|
||||||
|
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
|
||||||
|
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||||
|
{
|
||||||
|
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
|
||||||
|
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking>{ marking });
|
||||||
|
ApplyMarking(prototype, null, true, humanoid, sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
|
||||||
|
{
|
||||||
|
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
|
||||||
|
if (_markingManager.TryGetMarking(marking, out var prototype))
|
||||||
|
{
|
||||||
|
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking>{ marking });
|
||||||
|
ApplyMarking(prototype, null, true, humanoid, sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ApplyMarking(MarkingPrototype markingPrototype,
|
private void ApplyMarking(MarkingPrototype markingPrototype,
|
||||||
IReadOnlyList<Color>? colors,
|
IReadOnlyList<Color>? colors,
|
||||||
bool visible,
|
bool visible,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Content.Client.IconSmoothing
|
|||||||
/// Additional keys to smooth with.
|
/// Additional keys to smooth with.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public List<string> AdditionalKeys { get; private set; } = new();
|
public List<string> AdditionalKeys = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepended to the RSI state.
|
/// Prepended to the RSI state.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
PlaceHolder="0"
|
PlaceHolder="0"
|
||||||
Text="1"
|
Text="1"
|
||||||
HorizontalExpand="True" />
|
HorizontalExpand="True" />
|
||||||
|
<Label Name="RecipeCount" Margin="8 0 8 0" MinWidth="90" Align="Right" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ public sealed partial class LatheMenu : DefaultWindow
|
|||||||
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
|
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
|
||||||
quantity = 1;
|
quantity = 1;
|
||||||
|
|
||||||
|
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
|
||||||
|
|
||||||
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
|
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
|
||||||
RecipeList.Children.Clear();
|
RecipeList.Children.Clear();
|
||||||
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
|
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
|
|||||||
|
|
||||||
worldHandle.SetTransform(localMatrix);
|
worldHandle.SetTransform(localMatrix);
|
||||||
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||||
}, null);
|
}, Color.Transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
|||||||
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
|
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
|
||||||
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
|
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
|
||||||
_overlayMan.AddOverlay(new LightBlurOverlay());
|
_overlayMan.AddOverlay(new LightBlurOverlay());
|
||||||
|
_overlayMan.AddOverlay(new SunShadowOverlay());
|
||||||
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
|
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
|
|||||||
_overlayMan.RemoveOverlay<RoofOverlay>();
|
_overlayMan.RemoveOverlay<RoofOverlay>();
|
||||||
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
|
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
|
||||||
_overlayMan.RemoveOverlay<LightBlurOverlay>();
|
_overlayMan.RemoveOverlay<LightBlurOverlay>();
|
||||||
|
_overlayMan.RemoveOverlay<SunShadowOverlay>();
|
||||||
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
|
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal file
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Numerics;
|
||||||
|
using Content.Client.GameTicking.Managers;
|
||||||
|
using Content.Shared.Light.Components;
|
||||||
|
using Content.Shared.Light.EntitySystems;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
|
namespace Content.Client.Light.EntitySystems;
|
||||||
|
|
||||||
|
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!_timing.IsFirstTimePredicted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mapQuery = AllEntityQuery<SunShadowCycleComponent, SunShadowComponent>();
|
||||||
|
while (mapQuery.MoveNext(out var uid, out var cycle, out var shadow))
|
||||||
|
{
|
||||||
|
if (!cycle.Running || cycle.Directions.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var pausedTime = _metadata.GetPauseTime(uid);
|
||||||
|
|
||||||
|
var time = (float)(_timing.CurTime
|
||||||
|
.Add(cycle.Offset)
|
||||||
|
.Subtract(_ticker.RoundStartTimeSpan)
|
||||||
|
.Subtract(pausedTime)
|
||||||
|
.TotalSeconds % cycle.Duration.TotalSeconds);
|
||||||
|
|
||||||
|
var (direction, alpha) = GetShadow((uid, cycle), time);
|
||||||
|
shadow.Direction = direction;
|
||||||
|
shadow.Alpha = alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public (Vector2 Direction, float Alpha) GetShadow(Entity<SunShadowCycleComponent> entity, float time)
|
||||||
|
{
|
||||||
|
// So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
|
||||||
|
// dynamically and we don't have to manually handle it.
|
||||||
|
// It will lerp from each value to the next one with angle and length handled separately
|
||||||
|
var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
|
||||||
|
|
||||||
|
for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var dir = entity.Comp.Directions[i];
|
||||||
|
|
||||||
|
if (ratio > dir.Ratio)
|
||||||
|
{
|
||||||
|
var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
|
||||||
|
float nextRatio;
|
||||||
|
|
||||||
|
// Last entry
|
||||||
|
if (i == entity.Comp.Directions.Count - 1)
|
||||||
|
{
|
||||||
|
nextRatio = next.Ratio + 1f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextRatio = next.Ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
var range = nextRatio - dir.Ratio;
|
||||||
|
var diff = (ratio - dir.Ratio) / range;
|
||||||
|
DebugTools.Assert(diff is >= 0f and <= 1f);
|
||||||
|
|
||||||
|
// We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
|
||||||
|
var currentAngle = dir.Direction.ToAngle();
|
||||||
|
var nextAngle = next.Direction.ToAngle();
|
||||||
|
|
||||||
|
var angle = Angle.Lerp(currentAngle, nextAngle, diff);
|
||||||
|
// This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
|
||||||
|
var lengthDiff = MathF.Pow(diff, 1f / 2f);
|
||||||
|
var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
|
||||||
|
|
||||||
|
var vector = angle.ToVec() * length;
|
||||||
|
var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
|
||||||
|
return (vector, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Content.Client.GameTicking.Managers;
|
using Content.Client.GameTicking.Managers;
|
||||||
using Content.Shared;
|
using Content.Shared;
|
||||||
using Content.Shared.Light.Components;
|
using Content.Shared.Light.Components;
|
||||||
|
using Content.Shared.Light.EntitySystems;
|
||||||
using Robust.Shared.Map.Components;
|
using Robust.Shared.Map.Components;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
[Dependency] private readonly ClientGameTicker _ticker = default!;
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
public override void Update(float frameTime)
|
||||||
{
|
{
|
||||||
base.Update(frameTime);
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
if (!_timing.IsFirstTimePredicted)
|
||||||
|
return;
|
||||||
|
|
||||||
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
|
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
|
||||||
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
|
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
|
||||||
{
|
{
|
||||||
if (!cycle.Running)
|
if (!cycle.Running)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// We still iterate paused entities as we still want to override the lighting color and not have
|
||||||
|
// it apply the server state
|
||||||
|
var pausedTime = _metadata.GetPauseTime(uid);
|
||||||
|
|
||||||
var time = (float) _timing.CurTime
|
var time = (float) _timing.CurTime
|
||||||
.Add(cycle.Offset)
|
.Add(cycle.Offset)
|
||||||
.Subtract(_ticker.RoundStartTimeSpan)
|
.Subtract(_ticker.RoundStartTimeSpan)
|
||||||
|
.Subtract(pausedTime)
|
||||||
.TotalSeconds;
|
.TotalSeconds;
|
||||||
|
|
||||||
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
|
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
|
||||||
|
|||||||
@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
|
|||||||
// Due to stencilling we essentially draw on unrooved tiles
|
// Due to stencilling we essentially draw on unrooved tiles
|
||||||
while (tileEnumerator.MoveNext(out var tileRef))
|
while (tileEnumerator.MoveNext(out var tileRef))
|
||||||
{
|
{
|
||||||
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
|
var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
|
||||||
|
|
||||||
|
if (color == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
|
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
|
||||||
worldHandle.DrawRect(local, roof.Color);
|
worldHandle.DrawRect(local, color.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
|
|||||||
160
Content.Client/Light/SunShadowOverlay.cs
Normal file
160
Content.Client/Light/SunShadowOverlay.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using Content.Shared.Light.Components;
|
||||||
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Shared.Enums;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
using Robust.Shared.Map.Components;
|
||||||
|
using Robust.Shared.Physics;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Client.Light;
|
||||||
|
|
||||||
|
public sealed class SunShadowOverlay : Overlay
|
||||||
|
{
|
||||||
|
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
|
||||||
|
|
||||||
|
[Dependency] private readonly IClyde _clyde = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||||
|
private readonly EntityLookupSystem _lookup;
|
||||||
|
private readonly SharedTransformSystem _xformSys;
|
||||||
|
|
||||||
|
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
|
||||||
|
|
||||||
|
private IRenderTexture? _blurTarget;
|
||||||
|
private IRenderTexture? _target;
|
||||||
|
|
||||||
|
public SunShadowOverlay()
|
||||||
|
{
|
||||||
|
IoCManager.InjectDependencies(this);
|
||||||
|
_xformSys = _entManager.System<SharedTransformSystem>();
|
||||||
|
_lookup = _entManager.System<EntityLookupSystem>();
|
||||||
|
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Entity<MapGridComponent>> _grids = new();
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var viewport = args.Viewport;
|
||||||
|
var eye = viewport.Eye;
|
||||||
|
|
||||||
|
if (eye == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_grids.Clear();
|
||||||
|
_mapManager.FindGridsIntersecting(args.MapId,
|
||||||
|
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
|
||||||
|
ref _grids);
|
||||||
|
|
||||||
|
var worldHandle = args.WorldHandle;
|
||||||
|
var mapId = args.MapId;
|
||||||
|
var worldBounds = args.WorldBounds;
|
||||||
|
var targetSize = viewport.LightRenderTarget.Size;
|
||||||
|
|
||||||
|
if (_target?.Size != targetSize)
|
||||||
|
{
|
||||||
|
_target = _clyde
|
||||||
|
.CreateRenderTarget(targetSize,
|
||||||
|
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
|
||||||
|
name: "sun-shadow-target");
|
||||||
|
|
||||||
|
if (_blurTarget?.Size != targetSize)
|
||||||
|
{
|
||||||
|
_blurTarget = _clyde
|
||||||
|
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
|
||||||
|
var scale = viewport.RenderScale / (Vector2.One / lightScale);
|
||||||
|
|
||||||
|
foreach (var grid in _grids)
|
||||||
|
{
|
||||||
|
if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction = sun.Direction;
|
||||||
|
var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
|
||||||
|
|
||||||
|
// Nowhere to cast to so ignore it.
|
||||||
|
if (direction.Equals(Vector2.Zero) || alpha == 0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
|
||||||
|
// TODO: Jittering still not quite perfect
|
||||||
|
|
||||||
|
var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
|
||||||
|
_shadows.Clear();
|
||||||
|
|
||||||
|
// Draw shadow polys to stencil
|
||||||
|
args.WorldHandle.RenderInRenderTarget(_target,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var invMatrix =
|
||||||
|
_target.GetWorldToLocalMatrix(eye, scale);
|
||||||
|
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
|
||||||
|
|
||||||
|
// Go through shadows in range.
|
||||||
|
|
||||||
|
// For each one we:
|
||||||
|
// - Get the original vertices.
|
||||||
|
// - Extrapolate these along the sun direction.
|
||||||
|
// - Combine the above into 1 single polygon to draw.
|
||||||
|
|
||||||
|
// Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
|
||||||
|
// This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
|
||||||
|
// Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
|
||||||
|
// You might need to batch verts or the likes as this could get expensive.
|
||||||
|
_lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
|
||||||
|
|
||||||
|
foreach (var ent in _shadows)
|
||||||
|
{
|
||||||
|
var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
|
||||||
|
var (worldPos, worldRot) = _xformSys.GetWorldPositionRotation(xform);
|
||||||
|
// Need no rotation on matrix as sun shadow direction doesn't care.
|
||||||
|
var worldMatrix = Matrix3x2.CreateTranslation(worldPos);
|
||||||
|
var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
|
||||||
|
var pointCount = ent.Comp.Points.Length;
|
||||||
|
|
||||||
|
Array.Copy(ent.Comp.Points, indices, pointCount);
|
||||||
|
|
||||||
|
for (var i = 0; i < pointCount; i++)
|
||||||
|
{
|
||||||
|
// Update point based on entity rotation.
|
||||||
|
indices[i] = worldRot.RotateVec(indices[i]);
|
||||||
|
|
||||||
|
// Add the offset point by the sun shadow direction.
|
||||||
|
indices[pointCount + i] = indices[i] + direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
|
||||||
|
worldHandle.SetTransform(renderMatrix);
|
||||||
|
|
||||||
|
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Color.Transparent);
|
||||||
|
|
||||||
|
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
|
||||||
|
_clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
|
||||||
|
|
||||||
|
// Draw stencil (see roofoverlay).
|
||||||
|
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var invMatrix =
|
||||||
|
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
|
||||||
|
worldHandle.SetTransform(invMatrix);
|
||||||
|
|
||||||
|
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
|
||||||
|
worldHandle.UseShader(maskShader);
|
||||||
|
|
||||||
|
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
<BoxContainer Orientation="Vertical">
|
<BoxContainer Orientation="Vertical">
|
||||||
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
|
||||||
<BoxContainer Orientation="Vertical" Margin="8">
|
<BoxContainer Orientation="Vertical" Margin="8">
|
||||||
|
<Label Text="{Loc 'ui-options-accessability-header-visuals'}"
|
||||||
|
StyleClasses="LabelKeyText"/>
|
||||||
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
|
||||||
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
|
||||||
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
|
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
|
||||||
@@ -12,6 +14,9 @@
|
|||||||
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
|
<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="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
|
||||||
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
|
||||||
|
<Label Text="{Loc 'ui-options-accessability-header-content'}"
|
||||||
|
StyleClasses="LabelKeyText"/>
|
||||||
|
<CheckBox Name="CensorNudityCheckBox" Text="{Loc 'ui-options-censor-nudity'}" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public sealed partial class AccessibilityTab : Control
|
|||||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
|
||||||
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
|
||||||
|
|
||||||
|
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
|
||||||
|
|
||||||
Control.Initialize();
|
Control.Initialize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,51 @@ namespace Content.Client.Options.UI.Tabs
|
|||||||
AddButton(EngineKeyFunctions.HideUI);
|
AddButton(EngineKeyFunctions.HideUI);
|
||||||
AddButton(ContentKeyFunctions.InspectEntity);
|
AddButton(ContentKeyFunctions.InspectEntity);
|
||||||
|
|
||||||
|
AddHeader("ui-options-header-text-cursor");
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorLeft);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorRight);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorUp);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorDown);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorWordLeft);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorWordRight);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorBegin);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorEnd);
|
||||||
|
|
||||||
|
AddHeader("ui-options-header-text-cursor-select");
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelect);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectLeft);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectRight);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectUp);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectDown);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectWordLeft);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectWordRight);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectBegin);
|
||||||
|
AddButton(EngineKeyFunctions.TextCursorSelectEnd);
|
||||||
|
|
||||||
|
AddHeader("ui-options-header-text-edit");
|
||||||
|
AddButton(EngineKeyFunctions.TextBackspace);
|
||||||
|
AddButton(EngineKeyFunctions.TextDelete);
|
||||||
|
AddButton(EngineKeyFunctions.TextWordBackspace);
|
||||||
|
AddButton(EngineKeyFunctions.TextWordDelete);
|
||||||
|
AddButton(EngineKeyFunctions.TextNewline);
|
||||||
|
AddButton(EngineKeyFunctions.TextSubmit);
|
||||||
|
AddButton(EngineKeyFunctions.MultilineTextSubmit);
|
||||||
|
AddButton(EngineKeyFunctions.TextSelectAll);
|
||||||
|
AddButton(EngineKeyFunctions.TextCopy);
|
||||||
|
AddButton(EngineKeyFunctions.TextCut);
|
||||||
|
AddButton(EngineKeyFunctions.TextPaste);
|
||||||
|
|
||||||
|
AddHeader("ui-options-header-text-chat");
|
||||||
|
AddButton(EngineKeyFunctions.TextHistoryPrev);
|
||||||
|
AddButton(EngineKeyFunctions.TextHistoryNext);
|
||||||
|
AddButton(EngineKeyFunctions.TextReleaseFocus);
|
||||||
|
AddButton(EngineKeyFunctions.TextScrollToBottom);
|
||||||
|
|
||||||
|
AddHeader("ui-options-header-text-other");
|
||||||
|
AddButton(EngineKeyFunctions.TextTabComplete);
|
||||||
|
AddButton(EngineKeyFunctions.TextCompleteNext);
|
||||||
|
AddButton(EngineKeyFunctions.TextCompletePrev);
|
||||||
|
|
||||||
//CP14
|
//CP14
|
||||||
AddHeader("ui-options-header-cp14");
|
AddHeader("ui-options-header-cp14");
|
||||||
AddButton(ContentKeyFunctions.CP14OpenKnowledgeMenu);
|
AddButton(ContentKeyFunctions.CP14OpenKnowledgeMenu);
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
component.Powered = state.Powered;
|
component.Powered = state.Powered;
|
||||||
|
component.NeedsPower = state.NeedsPower;
|
||||||
|
component.PowerDisabled = state.PowerDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
|
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
|
||||||
|
|||||||
@@ -64,9 +64,15 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
|
|||||||
|
|
||||||
args.Sprite.Visible = hasVisibleLayer || revealed;
|
args.Sprite.Visible = hasVisibleLayer || revealed;
|
||||||
|
|
||||||
// allows a t-ray to show wires/pipes above carpets/puddles
|
if (ShowAll)
|
||||||
if (scannerRevealed)
|
|
||||||
{
|
{
|
||||||
|
// Allows sandbox mode to make wires visible over other stuff.
|
||||||
|
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
|
||||||
|
args.Sprite.DrawDepth = (int)Shared.DrawDepth.DrawDepth.Overdoors;
|
||||||
|
}
|
||||||
|
else if (scannerRevealed)
|
||||||
|
{
|
||||||
|
// Allows a t-ray to show wires/pipes above carpets/puddles.
|
||||||
if (component.OriginalDrawDepth is not null)
|
if (component.OriginalDrawDepth is not null)
|
||||||
return;
|
return;
|
||||||
component.OriginalDrawDepth = args.Sprite.DrawDepth;
|
component.OriginalDrawDepth = args.Sprite.DrawDepth;
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ public class ListContainer : Control
|
|||||||
{
|
{
|
||||||
ListContainerButton control = new(data[0], 0);
|
ListContainerButton control = new(data[0], 0);
|
||||||
GenerateItem?.Invoke(data[0], control);
|
GenerateItem?.Invoke(data[0], control);
|
||||||
|
// Yes this AddChild is necessary for reasons (get proper style or whatever?)
|
||||||
|
// without it the DesiredSize may be different to the final DesiredSize.
|
||||||
|
AddChild(control);
|
||||||
control.Measure(Vector2Helpers.Infinity);
|
control.Measure(Vector2Helpers.Infinity);
|
||||||
_itemHeight = control.DesiredSize.Y;
|
_itemHeight = control.DesiredSize.Y;
|
||||||
control.Dispose();
|
control.Orphan();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure buttons are re-generated.
|
// Ensure buttons are re-generated.
|
||||||
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
|
|||||||
|
|
||||||
public ListContainerButton(ListData data, int index)
|
public ListContainerButton(ListData data, int index)
|
||||||
{
|
{
|
||||||
|
AddStyleClass(StyleClassButton);
|
||||||
Data = data;
|
Data = data;
|
||||||
Index = index;
|
Index = index;
|
||||||
// AddChild(Background = new PanelContainer
|
// AddChild(Background = new PanelContainer
|
||||||
|
|||||||
@@ -467,8 +467,9 @@ public sealed class ChatUIController : UIController
|
|||||||
|
|
||||||
if (existing.Count > SpeechBubbleCap)
|
if (existing.Count > SpeechBubbleCap)
|
||||||
{
|
{
|
||||||
// Get the oldest to start fading fast.
|
// Get the next speech bubble to fade
|
||||||
var last = existing[0];
|
// Any speech bubbles before it are already fading
|
||||||
|
var last = existing[^(SpeechBubbleCap + 1)];
|
||||||
last.FadeNow();
|
last.FadeNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public sealed class DamageOverlayUiController : UIController
|
|||||||
{
|
{
|
||||||
_overlay.DeadLevel = 0f;
|
_overlay.DeadLevel = 0f;
|
||||||
_overlay.CritLevel = 0f;
|
_overlay.CritLevel = 0f;
|
||||||
_overlay.BruteLevel = 0f;
|
_overlay.PainLevel = 0f;
|
||||||
_overlay.OxygenLevel = 0f;
|
_overlay.OxygenLevel = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,13 +95,22 @@ public sealed class DamageOverlayUiController : UIController
|
|||||||
{
|
{
|
||||||
case MobState.Alive:
|
case MobState.Alive:
|
||||||
{
|
{
|
||||||
if (EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
FixedPoint2 painLevel = 0;
|
||||||
|
_overlay.PainLevel = 0;
|
||||||
|
|
||||||
|
if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||||
{
|
{
|
||||||
_overlay.BruteLevel = 0;
|
foreach (var painDamageType in damageable.PainDamageGroups)
|
||||||
}
|
{
|
||||||
else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
|
damageable.DamagePerGroup.TryGetValue(painDamageType, out var painDamage);
|
||||||
{
|
painLevel += painDamage;
|
||||||
_overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
|
}
|
||||||
|
_overlay.PainLevel = FixedPoint2.Min(1f, painLevel / critThreshold).Float();
|
||||||
|
|
||||||
|
if (_overlay.PainLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
|
||||||
|
{
|
||||||
|
_overlay.PainLevel = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage))
|
if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage))
|
||||||
@@ -109,11 +118,6 @@ public sealed class DamageOverlayUiController : UIController
|
|||||||
_overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float();
|
_overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_overlay.BruteLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
|
|
||||||
{
|
|
||||||
_overlay.BruteLevel = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_overlay.CritLevel = 0;
|
_overlay.CritLevel = 0;
|
||||||
_overlay.DeadLevel = 0;
|
_overlay.DeadLevel = 0;
|
||||||
break;
|
break;
|
||||||
@@ -125,13 +129,13 @@ public sealed class DamageOverlayUiController : UIController
|
|||||||
return;
|
return;
|
||||||
_overlay.CritLevel = critLevel.Value.Float();
|
_overlay.CritLevel = critLevel.Value.Float();
|
||||||
|
|
||||||
_overlay.BruteLevel = 0;
|
_overlay.PainLevel = 0;
|
||||||
_overlay.DeadLevel = 0;
|
_overlay.DeadLevel = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MobState.Dead:
|
case MobState.Dead:
|
||||||
{
|
{
|
||||||
_overlay.BruteLevel = 0;
|
_overlay.PainLevel = 0;
|
||||||
_overlay.CritLevel = 0;
|
_overlay.CritLevel = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ public sealed class DamageOverlay : Overlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the red pulsing overlay
|
/// Handles the red pulsing overlay
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float BruteLevel = 0f;
|
public float PainLevel = 0f;
|
||||||
|
|
||||||
private float _oldBruteLevel = 0f;
|
private float _oldPainLevel = 0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the darkening overlay.
|
/// Handles the darkening overlay.
|
||||||
@@ -92,14 +92,14 @@ public sealed class DamageOverlay : Overlay
|
|||||||
DeadLevel = 0f;
|
DeadLevel = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
|
if (!MathHelper.CloseTo(_oldPainLevel, PainLevel, 0.001f))
|
||||||
{
|
{
|
||||||
var diff = BruteLevel - _oldBruteLevel;
|
var diff = PainLevel - _oldPainLevel;
|
||||||
_oldBruteLevel += GetDiff(diff, lastFrameTime);
|
_oldPainLevel += GetDiff(diff, lastFrameTime);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_oldBruteLevel = BruteLevel;
|
_oldPainLevel = PainLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
|
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
|
||||||
@@ -135,7 +135,7 @@ public sealed class DamageOverlay : Overlay
|
|||||||
|
|
||||||
// Makes debugging easier don't @ me
|
// Makes debugging easier don't @ me
|
||||||
float level = 0f;
|
float level = 0f;
|
||||||
level = _oldBruteLevel;
|
level = _oldPainLevel;
|
||||||
|
|
||||||
// TODO: Lerping
|
// TODO: Lerping
|
||||||
if (level > 0f && _oldCritLevel <= 0f)
|
if (level > 0f && _oldCritLevel <= 0f)
|
||||||
@@ -165,7 +165,7 @@ public sealed class DamageOverlay : Overlay
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_oldBruteLevel = BruteLevel;
|
_oldPainLevel = PainLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
level = State != MobState.Critical ? _oldOxygenLevel : 1f;
|
level = State != MobState.Critical ? _oldOxygenLevel : 1f;
|
||||||
|
|||||||
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
|
|||||||
|
|
||||||
NameLabel.Text = text;
|
NameLabel.Text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetText(string text)
|
||||||
|
{
|
||||||
|
NameLabel.Text = text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Content.Shared.VendingMachines;
|
using Content.Shared.VendingMachines;
|
||||||
using Robust.Client.AutoGenerated;
|
using Robust.Client.AutoGenerated;
|
||||||
@@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
|
|||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
|
||||||
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
|
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
|
||||||
|
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
|
||||||
|
private readonly Dictionary<EntProtoId, uint> _amounts = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vending machine is able to be interacted with or not.
|
||||||
|
/// </summary>
|
||||||
|
private bool _enabled;
|
||||||
|
|
||||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||||
|
|
||||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
|
||||||
|
|
||||||
public VendingMachineMenu()
|
public VendingMachineMenu()
|
||||||
{
|
{
|
||||||
MinSize = SetSize = new Vector2(250, 150);
|
MinSize = SetSize = new Vector2(250, 150);
|
||||||
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
|
|||||||
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
|
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
|
||||||
return;
|
return;
|
||||||
|
|
||||||
button.AddChild(new VendingMachineItem(protoID, text));
|
var item = new VendingMachineItem(protoID, text);
|
||||||
|
_listItems[protoID] = (button, item);
|
||||||
button.ToolTip = text;
|
button.AddChild(item);
|
||||||
button.StyleBoxOverride = _styleBox;
|
button.AddStyleClass("ButtonSquare");
|
||||||
|
button.Disabled = !_enabled || _amounts[protoID] == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populates the list of available items on the vending machine interface
|
/// Populates the list of available items on the vending machine interface
|
||||||
/// and sets icons based on their prototypes
|
/// and sets icons based on their prototypes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Populate(List<VendingMachineInventoryEntry> inventory)
|
public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
|
||||||
{
|
{
|
||||||
|
_enabled = enabled;
|
||||||
|
_listItems.Clear();
|
||||||
|
_amounts.Clear();
|
||||||
|
|
||||||
if (inventory.Count == 0 && VendingContents.Visible)
|
if (inventory.Count == 0 && VendingContents.Visible)
|
||||||
{
|
{
|
||||||
SearchBar.Visible = false;
|
SearchBar.Visible = false;
|
||||||
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
|
|||||||
var entry = inventory[i];
|
var entry = inventory[i];
|
||||||
|
|
||||||
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
|
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
|
||||||
|
{
|
||||||
|
_amounts[entry.ID] = 0;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_dummies.TryGetValue(entry.ID, out var dummy))
|
if (!_dummies.TryGetValue(entry.ID, out var dummy))
|
||||||
{
|
{
|
||||||
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
|
|||||||
|
|
||||||
var itemName = Identity.Name(dummy, _entityManager);
|
var itemName = Identity.Name(dummy, _entityManager);
|
||||||
var itemText = $"{itemName} [{entry.Amount}]";
|
var itemText = $"{itemName} [{entry.Amount}]";
|
||||||
|
_amounts[entry.ID] = entry.Amount;
|
||||||
|
|
||||||
if (itemText.Length > longestEntry.Length)
|
if (itemText.Length > longestEntry.Length)
|
||||||
longestEntry = itemText;
|
longestEntry = itemText;
|
||||||
|
|
||||||
listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
|
listData.Add(new VendorItemsListData(prototype.ID, i)
|
||||||
|
{
|
||||||
|
ItemText = itemText,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
VendingContents.PopulateList(listData);
|
VendingContents.PopulateList(listData);
|
||||||
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
|
|||||||
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
|
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates text entries for vending data in place without modifying the list controls.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
|
||||||
|
{
|
||||||
|
_enabled = enabled;
|
||||||
|
|
||||||
|
foreach (var proto in _dummies.Keys)
|
||||||
|
{
|
||||||
|
if (!_listItems.TryGetValue(proto, out var button))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dummy = _dummies[proto];
|
||||||
|
var amount = cachedInventory.First(o => o.ID == proto).Amount;
|
||||||
|
// Could be better? Problem is all inventory entries get squashed.
|
||||||
|
var text = GetItemText(dummy, amount);
|
||||||
|
|
||||||
|
button.Item.SetText(text);
|
||||||
|
button.Button.Disabled = !enabled || amount == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetItemText(EntityUid dummy, uint amount)
|
||||||
|
{
|
||||||
|
var itemName = Identity.Name(dummy, _entityManager);
|
||||||
|
return $"{itemName} [{amount}]";
|
||||||
|
}
|
||||||
|
|
||||||
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
|
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
|
||||||
{
|
{
|
||||||
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
|
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
|
||||||
Math.Clamp(contentCount * 50, 150, 350));
|
Math.Clamp(contentCount * 50, 150, 350));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
|
public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
|
||||||
|
{
|
||||||
|
public string ItemText = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
|
|||||||
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
|
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||||
|
|
||||||
var system = EntMan.System<VendingMachineSystem>();
|
var system = EntMan.System<VendingMachineSystem>();
|
||||||
_cachedInventory = system.GetAllInventory(Owner);
|
_cachedInventory = system.GetAllInventory(Owner);
|
||||||
|
|
||||||
_menu?.Populate(_cachedInventory);
|
_menu?.Populate(_cachedInventory, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateAmounts()
|
||||||
|
{
|
||||||
|
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
|
||||||
|
|
||||||
|
var system = EntMan.System<VendingMachineSystem>();
|
||||||
|
_cachedInventory = system.GetAllInventory(Owner);
|
||||||
|
_menu?.UpdateAmounts(_cachedInventory, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
||||||
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
|
|||||||
if (selectedItem == null)
|
if (selectedItem == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Shared.VendingMachines;
|
using Content.Shared.VendingMachines;
|
||||||
using Robust.Client.Animations;
|
using Robust.Client.Animations;
|
||||||
using Robust.Client.GameObjects;
|
using Robust.Client.GameObjects;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
namespace Content.Client.VendingMachines;
|
namespace Content.Client.VendingMachines;
|
||||||
|
|
||||||
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||||||
{
|
{
|
||||||
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
||||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||||||
|
|
||||||
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||||
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||||
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
|
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
|
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
|
||||||
{
|
{
|
||||||
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
if (args.Current is not VendingMachineComponentState state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var uid = entity.Owner;
|
||||||
|
var component = entity.Comp;
|
||||||
|
|
||||||
|
component.Contraband = state.Contraband;
|
||||||
|
component.EjectEnd = state.EjectEnd;
|
||||||
|
component.DenyEnd = state.DenyEnd;
|
||||||
|
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
|
||||||
|
|
||||||
|
// If all we did was update amounts then we can leave BUI buttons in place.
|
||||||
|
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
|
||||||
|
!component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
|
||||||
|
!component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
|
||||||
|
|
||||||
|
component.Inventory.Clear();
|
||||||
|
component.EmaggedInventory.Clear();
|
||||||
|
component.ContrabandInventory.Clear();
|
||||||
|
|
||||||
|
foreach (var entry in state.Inventory)
|
||||||
{
|
{
|
||||||
bui.Refresh();
|
component.Inventory.Add(entry.Key, new(entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in state.EmaggedInventory)
|
||||||
|
{
|
||||||
|
component.EmaggedInventory.Add(entry.Key, new(entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in state.ContrabandInventory)
|
||||||
|
{
|
||||||
|
component.ContrabandInventory.Add(entry.Key, new(entry.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
|
||||||
|
{
|
||||||
|
if (fullUiUpdate)
|
||||||
|
{
|
||||||
|
bui.Refresh();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bui.UpdateAmounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
|
||||||
|
{
|
||||||
|
if (!Resolve(entity, ref entity.Comp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
|
||||||
|
VendingMachineUiKey.Key,
|
||||||
|
out var bui))
|
||||||
|
{
|
||||||
|
bui.UpdateAmounts();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
|
|||||||
if (component.LoopDenyAnimation)
|
if (component.LoopDenyAnimation)
|
||||||
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
|
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
|
||||||
else
|
else
|
||||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
|
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
|
||||||
|
|
||||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case VendingMachineVisualState.Eject:
|
case VendingMachineVisualState.Eject:
|
||||||
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
|
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
|
||||||
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ public sealed class CraftingTests : InteractionTest
|
|||||||
await CraftItem(Spear);
|
await CraftItem(Spear);
|
||||||
await FindEntity(Spear);
|
await FindEntity(Spear);
|
||||||
|
|
||||||
|
// Reset target because entitylookup will dump this.
|
||||||
|
Target = null;
|
||||||
|
|
||||||
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
|
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
|
||||||
// Spear and left over stacks should be on the floor.
|
// Spear and left over stacks should be on the floor.
|
||||||
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
|
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
|
||||||
|
|||||||
@@ -17,23 +17,26 @@ public sealed class ContrabandTest
|
|||||||
|
|
||||||
await client.WaitAssertion(() =>
|
await client.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||||
continue;
|
{
|
||||||
|
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
|
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
|
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
|
||||||
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
|
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
|
||||||
|
|
||||||
if (!severity.ShowDepartmentsAndJobs)
|
if (!severity.ShowDepartmentsAndJobs)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
|
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
|
||||||
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
|
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await pair.CleanReturnAsync();
|
await pair.CleanReturnAsync();
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ public abstract partial class InteractionTest
|
|||||||
|
|
||||||
// Please someone purge async construction code
|
// Please someone purge async construction code
|
||||||
Task<bool> task = default!;
|
Task<bool> task = default!;
|
||||||
await Server.WaitPost(() => task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player)));
|
await Server.WaitPost(() =>
|
||||||
|
{
|
||||||
|
task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
|
||||||
|
});
|
||||||
|
|
||||||
Task? tickTask = null;
|
Task? tickTask = null;
|
||||||
while (!task.IsCompleted)
|
while (!task.IsCompleted)
|
||||||
|
|||||||
@@ -23,46 +23,49 @@ public sealed class MagazineVisualsSpriteTest
|
|||||||
|
|
||||||
await client.WaitAssertion(() =>
|
await client.WaitAssertion(() =>
|
||||||
{
|
{
|
||||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
|
|
||||||
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
|
|
||||||
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
|
|
||||||
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
|
|
||||||
|
|
||||||
var toTest = new List<(int, string)>();
|
|
||||||
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
|
|
||||||
toTest.Add((magLayerId, ""));
|
|
||||||
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
|
|
||||||
toTest.Add((magUnshadedLayerId, "-unshaded"));
|
|
||||||
|
|
||||||
Assert.That(toTest, Is.Not.Empty,
|
|
||||||
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
|
|
||||||
|
|
||||||
var start = visuals.ZeroVisible ? 0 : 1;
|
|
||||||
foreach (var (id, midfix) in toTest)
|
|
||||||
{
|
{
|
||||||
Assert.That(sprite.TryGetLayer(id, out var layer));
|
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||||
var rsi = layer.ActualRsi;
|
continue;
|
||||||
for (var i = start; i < visuals.MagSteps; i++)
|
|
||||||
{
|
|
||||||
var state = $"{visuals.MagState}{midfix}-{i}";
|
|
||||||
Assert.That(rsi.TryGetState(state, out _),
|
|
||||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// MagSteps includes the 0th step, so sometimes people are off by one.
|
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
|
||||||
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
|
continue;
|
||||||
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
|
|
||||||
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
|
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
|
||||||
|
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
|
||||||
|
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
|
||||||
|
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
|
||||||
|
|
||||||
|
var toTest = new List<(int, string)>();
|
||||||
|
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
|
||||||
|
toTest.Add((magLayerId, ""));
|
||||||
|
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
|
||||||
|
toTest.Add((magUnshadedLayerId, "-unshaded"));
|
||||||
|
|
||||||
|
Assert.That(toTest, Is.Not.Empty,
|
||||||
|
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
|
||||||
|
|
||||||
|
var start = visuals.ZeroVisible ? 0 : 1;
|
||||||
|
foreach (var (id, midfix) in toTest)
|
||||||
|
{
|
||||||
|
Assert.That(sprite.TryGetLayer(id, out var layer));
|
||||||
|
var rsi = layer.ActualRsi;
|
||||||
|
for (var i = start; i < visuals.MagSteps; i++)
|
||||||
|
{
|
||||||
|
var state = $"{visuals.MagState}{midfix}-{i}";
|
||||||
|
Assert.That(rsi.TryGetState(state, out _),
|
||||||
|
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// MagSteps includes the 0th step, so sometimes people are off by one.
|
||||||
|
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
|
||||||
|
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
|
||||||
|
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await pair.CleanReturnAsync();
|
await pair.CleanReturnAsync();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
|||||||
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
|
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
|
||||||
{
|
{
|
||||||
if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
|
if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (TryComp<AccessComponent>(uid, out var access))
|
if (TryComp<AccessComponent>(uid, out var access))
|
||||||
{
|
{
|
||||||
@@ -78,7 +78,12 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Give them a wonderful new access to compensate for everything
|
// Give them a wonderful new access to compensate for everything
|
||||||
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
|
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
|
||||||
|
|
||||||
|
if (ids.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var random = _random.Pick(ids);
|
||||||
|
|
||||||
access.Tags.Add(random.ID);
|
access.Tags.Add(random.ID);
|
||||||
Dirty(uid, access);
|
Dirty(uid, access);
|
||||||
|
|||||||
61
Content.Server/Administration/Commands/ForceGhostCommand.cs
Normal file
61
Content.Server/Administration/Commands/ForceGhostCommand.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.Ghost;
|
||||||
|
using Content.Shared.Administration;
|
||||||
|
using Content.Shared.GameTicking;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Console;
|
||||||
|
|
||||||
|
namespace Content.Server.Administration.Commands;
|
||||||
|
|
||||||
|
[AdminCommand(AdminFlags.Admin)]
|
||||||
|
public sealed class ForceGhostCommand : LocalizedEntityCommands
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
|
[Dependency] private readonly GhostSystem _ghost = default!;
|
||||||
|
|
||||||
|
public override string Command => "forceghost";
|
||||||
|
|
||||||
|
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 0 || args.Length > 1)
|
||||||
|
{
|
||||||
|
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_playerManager.TryGetSessionByUsername(args[0], out var player))
|
||||||
|
{
|
||||||
|
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
|
||||||
|
playerStatus is not PlayerGameStatus.JoinedGame)
|
||||||
|
{
|
||||||
|
shell.WriteLine(Loc.GetString("cmd-forceghost-error-lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_mind.TryGetMind(player, out var mindId, out var mind))
|
||||||
|
(mindId, mind) = _mind.CreateMind(player.UserId);
|
||||||
|
|
||||||
|
if (!_ghost.OnGhostAttempt(mindId, false, true, true, mind))
|
||||||
|
shell.WriteLine(Loc.GetString("cmd-forceghost-denied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 1)
|
||||||
|
{
|
||||||
|
return CompletionResult.FromHintOptions(
|
||||||
|
CompletionHelper.SessionNames(players: _playerManager),
|
||||||
|
Loc.GetString("cmd-forceghost-hint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletionResult.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ using Content.Shared.Database;
|
|||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
namespace Content.Server.Administration.Commands;
|
namespace Content.Server.Administration.Commands;
|
||||||
|
|
||||||
[AdminCommand(AdminFlags.Ban)]
|
[AdminCommand(AdminFlags.Ban)]
|
||||||
@@ -15,6 +17,7 @@ public sealed class RoleBanCommand : IConsoleCommand
|
|||||||
[Dependency] private readonly IPlayerLocator _locator = default!;
|
[Dependency] private readonly IPlayerLocator _locator = default!;
|
||||||
[Dependency] private readonly IBanManager _bans = default!;
|
[Dependency] private readonly IBanManager _bans = default!;
|
||||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||||
|
|
||||||
public string Command => "roleban";
|
public string Command => "roleban";
|
||||||
public string Description => Loc.GetString("cmd-roleban-desc");
|
public string Description => Loc.GetString("cmd-roleban-desc");
|
||||||
@@ -76,6 +79,12 @@ public sealed class RoleBanCommand : IConsoleCommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_proto.HasIndex<JobPrototype>(job))
|
||||||
|
{
|
||||||
|
shell.WriteError(Loc.GetString("cmd-roleban-job-parse",("job", job)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
||||||
if (located == null)
|
if (located == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ public sealed class AdminSystem : EntitySystem
|
|||||||
var entityName = string.Empty;
|
var entityName = string.Empty;
|
||||||
var identityName = string.Empty;
|
var identityName = string.Empty;
|
||||||
|
|
||||||
|
// Visible (identity) name can be different from real name
|
||||||
if (session?.AttachedEntity != null)
|
if (session?.AttachedEntity != null)
|
||||||
{
|
{
|
||||||
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
||||||
@@ -230,6 +231,7 @@ public sealed class AdminSystem : EntitySystem
|
|||||||
|
|
||||||
var antag = false;
|
var antag = false;
|
||||||
|
|
||||||
|
// Starting role, antagonist status and role type
|
||||||
RoleTypePrototype roleType = new();
|
RoleTypePrototype roleType = new();
|
||||||
var startingRole = string.Empty;
|
var startingRole = string.Empty;
|
||||||
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
|
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
|
||||||
@@ -243,8 +245,13 @@ public sealed class AdminSystem : EntitySystem
|
|||||||
startingRole = _jobs.MindTryGetJobName(mindId);
|
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection status and playtime
|
||||||
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
||||||
TimeSpan? overallPlaytime = null;
|
|
||||||
|
// Start with the last available playtime data
|
||||||
|
var cachedInfo = GetCachedPlayerInfo(data.UserId);
|
||||||
|
var overallPlaytime = cachedInfo?.OverallPlaytime;
|
||||||
|
// Overwrite with current playtime data, unless it's null (such as if the player just disconnected)
|
||||||
if (session != null &&
|
if (session != null &&
|
||||||
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
||||||
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
||||||
|
|||||||
@@ -74,23 +74,25 @@ public sealed partial class AdminVerbSystem
|
|||||||
args.Verbs.Add(vampire);
|
args.Verbs.Add(vampire);
|
||||||
|
|
||||||
/* CP14 disable default antags
|
/* CP14 disable default antags
|
||||||
|
var traitorName = Loc.GetString("admin-verb-text-make-traitor");
|
||||||
Verb traitor = new()
|
Verb traitor = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-traitor"),
|
Text = traitorName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
|
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "Syndicate"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_antag.ForceMakeAntag<TraitorRuleComponent>(targetPlayer, DefaultTraitorRule);
|
_antag.ForceMakeAntag<TraitorRuleComponent>(targetPlayer, DefaultTraitorRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-traitor"),
|
Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(traitor);
|
args.Verbs.Add(traitor);
|
||||||
|
|
||||||
|
var initialInfectedName = Loc.GetString("admin-verb-text-make-initial-infected");
|
||||||
Verb initialInfected = new()
|
Verb initialInfected = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-initial-infected"),
|
Text = initialInfectedName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -98,42 +100,44 @@ public sealed partial class AdminVerbSystem
|
|||||||
_antag.ForceMakeAntag<ZombieRuleComponent>(targetPlayer, DefaultInitialInfectedRule);
|
_antag.ForceMakeAntag<ZombieRuleComponent>(targetPlayer, DefaultInitialInfectedRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-initial-infected"),
|
Message = string.Join(": ", initialInfectedName, Loc.GetString("admin-verb-make-initial-infected")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(initialInfected);
|
args.Verbs.Add(initialInfected);
|
||||||
|
|
||||||
|
var zombieName = Loc.GetString("admin-verb-text-make-zombie");
|
||||||
Verb zombie = new()
|
Verb zombie = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-zombie"),
|
Text = zombieName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Zombie"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_zombie.ZombifyEntity(args.Target);
|
_zombie.ZombifyEntity(args.Target);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-zombie"),
|
Message = string.Join(": ", zombieName, Loc.GetString("admin-verb-make-zombie")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(zombie);
|
args.Verbs.Add(zombie);
|
||||||
|
|
||||||
|
var nukeOpName = Loc.GetString("admin-verb-text-make-nuclear-operative");
|
||||||
Verb nukeOp = new()
|
Verb nukeOp = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-nuclear-operative"),
|
Text = nukeOpName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hardsuits/syndicate.rsi"), "icon"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
{
|
{
|
||||||
_antag.ForceMakeAntag<NukeopsRuleComponent>(targetPlayer, DefaultNukeOpRule);
|
_antag.ForceMakeAntag<NukeopsRuleComponent>(targetPlayer, DefaultNukeOpRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
|
Message = string.Join(": ", nukeOpName, Loc.GetString("admin-verb-make-nuclear-operative")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(nukeOp);
|
args.Verbs.Add(nukeOp);
|
||||||
|
|
||||||
|
var pirateName = Loc.GetString("admin-verb-text-make-pirate");
|
||||||
Verb pirate = new()
|
Verb pirate = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-pirate"),
|
Text = pirateName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -142,13 +146,14 @@ public sealed partial class AdminVerbSystem
|
|||||||
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
|
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-pirate"),
|
Message = string.Join(": ", pirateName, Loc.GetString("admin-verb-make-pirate")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(pirate);
|
args.Verbs.Add(pirate);
|
||||||
|
|
||||||
|
var headRevName = Loc.GetString("admin-verb-text-make-head-rev");
|
||||||
Verb headRev = new()
|
Verb headRev = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-head-rev"),
|
Text = headRevName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
|
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -156,13 +161,14 @@ public sealed partial class AdminVerbSystem
|
|||||||
_antag.ForceMakeAntag<RevolutionaryRuleComponent>(targetPlayer, DefaultRevsRule);
|
_antag.ForceMakeAntag<RevolutionaryRuleComponent>(targetPlayer, DefaultRevsRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-head-rev"),
|
Message = string.Join(": ", headRevName, Loc.GetString("admin-verb-make-head-rev")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(headRev);
|
args.Verbs.Add(headRev);
|
||||||
|
|
||||||
|
var thiefName = Loc.GetString("admin-verb-text-make-thief");
|
||||||
Verb thief = new()
|
Verb thief = new()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("admin-verb-text-make-thief"),
|
Text = thiefName,
|
||||||
Category = VerbCategory.Antag,
|
Category = VerbCategory.Antag,
|
||||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
|
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -170,7 +176,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
_antag.ForceMakeAntag<ThiefRuleComponent>(targetPlayer, DefaultThiefRule);
|
_antag.ForceMakeAntag<ThiefRuleComponent>(targetPlayer, DefaultThiefRule);
|
||||||
},
|
},
|
||||||
Impact = LogImpact.High,
|
Impact = LogImpact.High,
|
||||||
Message = Loc.GetString("admin-verb-make-thief"),
|
Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
|
||||||
};
|
};
|
||||||
args.Verbs.Add(thief);
|
args.Verbs.Add(thief);
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
|
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
|
||||||
Verb flames = new()
|
Verb flames = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-set-alight-name",
|
Text = flamesName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
|
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -481,7 +481,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
|
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
|
||||||
Verb bread = new()
|
Verb bread = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-kill-sign-name",
|
Text = breadName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
|
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -496,7 +496,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
|
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
|
||||||
Verb mouse = new()
|
Verb mouse = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-cluwne-name",
|
Text = mouseName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
|
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -650,7 +650,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
|
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
|
||||||
Verb instrumentation = new()
|
Verb instrumentation = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-become-mouse-name",
|
Text = instrumentationName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
|
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -721,7 +721,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
|
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
|
||||||
Verb headstand = new()
|
Verb headstand = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-run-walk-swap-name",
|
Text = headstandName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
|
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -819,7 +819,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
|
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
|
||||||
Verb superSpeed = new()
|
Verb superSpeed = new()
|
||||||
{
|
{
|
||||||
Text = "admin-smite-garbage-can-name",
|
Text = superSpeedName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
|
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
|
||||||
Act = () =>
|
Act = () =>
|
||||||
@@ -852,7 +852,7 @@ public sealed partial class AdminVerbSystem
|
|||||||
args.Verbs.Add(superBonkLite);
|
args.Verbs.Add(superBonkLite);
|
||||||
|
|
||||||
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
|
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
|
||||||
Verb superBonk= new()
|
Verb superBonk = new()
|
||||||
{
|
{
|
||||||
Text = superBonkName,
|
Text = superBonkName,
|
||||||
Category = VerbCategory.Smite,
|
Category = VerbCategory.Smite,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using Content.Server.Advertise.Components;
|
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Dataset;
|
using Content.Shared.Advertise.Components;
|
||||||
|
using Content.Shared.Advertise.Systems;
|
||||||
|
using Content.Shared.UserInterface;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
|
|
||||||
|
|
||||||
namespace Content.Server.Advertise;
|
namespace Content.Server.Advertise.EntitySystems;
|
||||||
|
|
||||||
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
|
|||||||
entity.Comp.Flag = false;
|
entity.Comp.Flag = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
|
|
||||||
{
|
|
||||||
if (!Resolve(entity, ref entity.Comp))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
entity.Comp.Flag = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ using System.Linq;
|
|||||||
using Content.Server.Antag.Components;
|
using Content.Server.Antag.Components;
|
||||||
using Content.Server.GameTicking.Rules.Components;
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
using Content.Server.Objectives;
|
using Content.Server.Objectives;
|
||||||
|
using Content.Shared.Antag;
|
||||||
using Content.Shared.Chat;
|
using Content.Shared.Chat;
|
||||||
|
using Content.Shared.GameTicking.Components;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Preferences;
|
using Content.Shared.Preferences;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@@ -25,7 +27,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
definition = null;
|
definition = null;
|
||||||
|
|
||||||
var totalTargetCount = GetTargetAntagCount(ent, players);
|
var totalTargetCount = GetTargetAntagCount(ent, players);
|
||||||
var mindCount = ent.Comp.SelectedMinds.Count;
|
var mindCount = ent.Comp.AssignedMinds.Count;
|
||||||
if (mindCount >= totalTargetCount)
|
if (mindCount >= totalTargetCount)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -95,7 +97,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
var countOffset = 0;
|
var countOffset = 0;
|
||||||
foreach (var otherDef in ent.Comp.Definitions)
|
foreach (var otherDef in ent.Comp.Definitions)
|
||||||
{
|
{
|
||||||
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
|
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; // Note: Is the PlayerRatio necessary here? Seems like it can cause issues for defs with varied PlayerRatio.
|
||||||
}
|
}
|
||||||
// make sure we don't double-count the current selection
|
// make sure we don't double-count the current selection
|
||||||
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||||
@@ -115,7 +117,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
return new List<(EntityUid, SessionData, string)>();
|
return new List<(EntityUid, SessionData, string)>();
|
||||||
|
|
||||||
var output = new List<(EntityUid, SessionData, string)>();
|
var output = new List<(EntityUid, SessionData, string)>();
|
||||||
foreach (var (mind, name) in ent.Comp.SelectedMinds)
|
foreach (var (mind, name) in ent.Comp.AssignedMinds)
|
||||||
{
|
{
|
||||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -137,7 +139,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
return new();
|
return new();
|
||||||
|
|
||||||
var output = new List<Entity<MindComponent>>();
|
var output = new List<Entity<MindComponent>>();
|
||||||
foreach (var (mind, _) in ent.Comp.SelectedMinds)
|
foreach (var (mind, _) in ent.Comp.AssignedMinds)
|
||||||
{
|
{
|
||||||
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -155,7 +157,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
if (!Resolve(ent, ref ent.Comp, false))
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
return new();
|
return new();
|
||||||
|
|
||||||
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
|
return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -247,7 +249,7 @@ public sealed partial class AntagSelectionSystem
|
|||||||
if (!Resolve(ent, ref ent.Comp, false))
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
|
return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -352,8 +354,66 @@ public sealed partial class AntagSelectionSystem
|
|||||||
var ruleEnt = GameTicker.AddGameRule(id);
|
var ruleEnt = GameTicker.AddGameRule(id);
|
||||||
RemComp<LoadMapRuleComponent>(ruleEnt);
|
RemComp<LoadMapRuleComponent>(ruleEnt);
|
||||||
var antag = Comp<AntagSelectionComponent>(ruleEnt);
|
var antag = Comp<AntagSelectionComponent>(ruleEnt);
|
||||||
antag.SelectionsComplete = true; // don't do normal selection.
|
antag.AssignmentComplete = true; // don't do normal selection.
|
||||||
GameTicker.StartGameRule(ruleEnt);
|
GameTicker.StartGameRule(ruleEnt);
|
||||||
return (ruleEnt, antag);
|
return (ruleEnt, antag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all sessions that have been preselected for antag.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||||
|
public HashSet<ICommonSession> GetPreSelectedAntagSessions(AntagSelectionDefinition? except = null)
|
||||||
|
{
|
||||||
|
var result = new HashSet<ICommonSession>();
|
||||||
|
var query = QueryAllRules();
|
||||||
|
while (query.MoveNext(out var uid, out var comp, out _))
|
||||||
|
{
|
||||||
|
if (HasComp<EndedGameRuleComponent>(uid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var def in comp.Definitions)
|
||||||
|
{
|
||||||
|
if (def.Equals(except))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
result.UnionWith(set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="except">A specific definition to be excluded from the check.</param>
|
||||||
|
// Note: This is a bit iffy since technically this exclusive definition is defined via the MultiAntagSetting, while there's a separately tracked antagExclusive variable in the mindrole.
|
||||||
|
// We can't query that however since there's no guarantee the mindrole has been given out yet when checking pre-selected antags.
|
||||||
|
// I don't think there's any instance where they differ, but it's something to be aware of for a potential future refactor.
|
||||||
|
public HashSet<ICommonSession> GetPreSelectedExclusiveAntagSessions(AntagSelectionDefinition? except = null)
|
||||||
|
{
|
||||||
|
var result = new HashSet<ICommonSession>();
|
||||||
|
var query = QueryAllRules();
|
||||||
|
while (query.MoveNext(out var uid, out var comp, out _))
|
||||||
|
{
|
||||||
|
if (HasComp<EndedGameRuleComponent>(uid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var def in comp.Definitions)
|
||||||
|
{
|
||||||
|
if (def.Equals(except))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (def.MultiAntagSetting == AntagAcceptability.None && comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
{
|
||||||
|
result.UnionWith(set);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ using Content.Server.Preferences.Managers;
|
|||||||
using Content.Server.Roles;
|
using Content.Server.Roles;
|
||||||
using Content.Server.Roles.Jobs;
|
using Content.Server.Roles.Jobs;
|
||||||
using Content.Server.Shuttles.Components;
|
using Content.Server.Shuttles.Components;
|
||||||
|
using Content.Server.Station.Events;
|
||||||
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Antag;
|
using Content.Shared.Antag;
|
||||||
using Content.Shared.Clothing;
|
using Content.Shared.Clothing;
|
||||||
|
using Content.Shared.Database;
|
||||||
using Content.Shared.GameTicking;
|
using Content.Shared.GameTicking;
|
||||||
using Content.Shared.GameTicking.Components;
|
using Content.Shared.GameTicking.Components;
|
||||||
using Content.Shared.Ghost;
|
using Content.Shared.Ghost;
|
||||||
@@ -46,6 +49,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
[Dependency] private readonly RoleSystem _role = default!;
|
[Dependency] private readonly RoleSystem _role = default!;
|
||||||
[Dependency] private readonly TransformSystem _transform = default!;
|
[Dependency] private readonly TransformSystem _transform = default!;
|
||||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||||
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||||
|
|
||||||
// arbitrary random number to give late joining some mild interest.
|
// arbitrary random number to give late joining some mild interest.
|
||||||
public const float LateJoinRandomChance = 0.5f;
|
public const float LateJoinRandomChance = 0.5f;
|
||||||
@@ -89,19 +93,33 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
var query = QueryActiveRules();
|
var query = QueryActiveRules();
|
||||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||||
{
|
{
|
||||||
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
|
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (comp.SelectionsComplete)
|
if (comp.AssignmentComplete)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ChooseAntags((uid, comp), pool); // We choose the antags here...
|
||||||
|
|
||||||
|
if (comp.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
|
||||||
|
{
|
||||||
|
AssignPreSelectedSessions((uid, comp)); // ...But only assign them if PrePlayerSpawn
|
||||||
|
foreach (var session in comp.AssignedSessions)
|
||||||
|
{
|
||||||
|
args.PlayerPool.Remove(session);
|
||||||
|
GameTicker.PlayerJoinGame(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If IntraPlayerSpawn is selected, delayed rules should choose at this point too.
|
||||||
|
var queryDelayed = QueryDelayedRules();
|
||||||
|
while (queryDelayed.MoveNext(out var uid, out _, out var comp, out _))
|
||||||
|
{
|
||||||
|
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ChooseAntags((uid, comp), pool);
|
ChooseAntags((uid, comp), pool);
|
||||||
|
|
||||||
foreach (var session in comp.SelectedSessions)
|
|
||||||
{
|
|
||||||
args.PlayerPool.Remove(session);
|
|
||||||
GameTicker.PlayerJoinGame(session);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +128,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
var query = QueryActiveRules();
|
var query = QueryActiveRules();
|
||||||
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
while (query.MoveNext(out var uid, out _, out var comp, out _))
|
||||||
{
|
{
|
||||||
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
|
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ChooseAntags((uid, comp), args.Players);
|
ChooseAntags((uid, comp), args.Players);
|
||||||
|
AssignPreSelectedSessions((uid, comp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +145,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
// eventually this should probably store the players per definition with some kind of unique identifier.
|
// eventually this should probably store the players per definition with some kind of unique identifier.
|
||||||
// something to figure out later.
|
// something to figure out later.
|
||||||
|
|
||||||
var query = QueryActiveRules();
|
var query = QueryAllRules();
|
||||||
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
var rules = new List<(EntityUid, AntagSelectionComponent)>();
|
||||||
while (query.MoveNext(out var uid, out _, out var antag, out _))
|
while (query.MoveNext(out var uid, out var antag, out _))
|
||||||
{
|
{
|
||||||
rules.Add((uid, antag));
|
if (HasComp<ActiveGameRuleComponent>(uid) ||
|
||||||
|
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
|
||||||
|
rules.Add((uid, antag));
|
||||||
}
|
}
|
||||||
RobustRandom.Shuffle(rules);
|
RobustRandom.Shuffle(rules);
|
||||||
|
|
||||||
@@ -142,7 +163,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
|
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
|
DebugTools.AssertNotEqual(antag.SelectionTime, AntagSelectionTime.PrePlayerSpawn);
|
||||||
|
|
||||||
// do not count players in the lobby for the antag ratio
|
// do not count players in the lobby for the antag ratio
|
||||||
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
|
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
|
||||||
@@ -150,7 +171,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (TryMakeAntag((uid, antag), args.Player, def.Value))
|
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
|
||||||
|
|
||||||
|
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,14 +206,20 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (GameTicker.RunLevel != GameRunLevel.InRound)
|
if (GameTicker.RunLevel != GameRunLevel.InRound)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (component.SelectionsComplete)
|
if (component.AssignmentComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var players = _playerManager.Sessions
|
if (!component.PreSelectionsComplete)
|
||||||
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
|
{
|
||||||
.ToList();
|
var players = _playerManager.Sessions
|
||||||
|
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
|
||||||
|
status == PlayerGameStatus.JoinedGame)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
ChooseAntags((uid, component), players, midround: true);
|
ChooseAntags((uid, component), players, midround: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
AssignPreSelectedSessions((uid, component));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -201,7 +230,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
||||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
||||||
{
|
{
|
||||||
if (ent.Comp.SelectionsComplete)
|
if (ent.Comp.PreSelectionsComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var def in ent.Comp.Definitions)
|
foreach (var def in ent.Comp.Definitions)
|
||||||
@@ -209,7 +238,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
ChooseAntags(ent, pool, def, midround: midround);
|
ChooseAntags(ent, pool, def, midround: midround);
|
||||||
}
|
}
|
||||||
|
|
||||||
ent.Comp.SelectionsComplete = true;
|
ent.Comp.PreSelectionsComplete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -250,21 +279,53 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session != null && ent.Comp.SelectedSessions.Contains(session))
|
if (session != null && ent.Comp.PreSelectedSessions.Values.Any(x => x.Contains(session)))
|
||||||
{
|
{
|
||||||
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
|
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MakeAntag(ent, session, def);
|
if (session == null)
|
||||||
|
MakeAntag(ent, null, def); // This is for spawner antags
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||||
|
set.Add(session); // Selection done!
|
||||||
|
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||||
|
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assigns antag roles to sessions selected for it.
|
||||||
|
/// </summary>
|
||||||
|
public void AssignPreSelectedSessions(Entity<AntagSelectionComponent> ent)
|
||||||
|
{
|
||||||
|
// Only assign if there's been a pre-selection, and the selection hasn't already been made
|
||||||
|
if (!ent.Comp.PreSelectionsComplete || ent.Comp.AssignmentComplete)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var def in ent.Comp.Definitions)
|
||||||
|
{
|
||||||
|
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var session in set)
|
||||||
|
{
|
||||||
|
TryMakeAntag(ent, session, def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.Comp.AssignmentComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to makes a given player into the specified antagonist.
|
/// Tries to makes a given player into the specified antagonist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true)
|
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
|
||||||
{
|
{
|
||||||
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
if (checkPref && !HasPrimaryAntagPreference(session, def))
|
||||||
return false;
|
return false;
|
||||||
@@ -272,7 +333,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
|
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
MakeAntag(ent, session, def, ignoreSpawner);
|
if (onlyPreSelect && session != null)
|
||||||
|
{
|
||||||
|
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||||
|
set.Add(session);
|
||||||
|
Log.Debug($"Pre-selected {session!.Name} as antagonist: {ToPrettyString(ent)}");
|
||||||
|
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MakeAntag(ent, session, def, ignoreSpawner);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +359,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
|
|
||||||
if (session != null)
|
if (session != null)
|
||||||
{
|
{
|
||||||
ent.Comp.SelectedSessions.Add(session);
|
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
|
||||||
|
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
|
||||||
|
set.Add(session);
|
||||||
|
ent.Comp.AssignedSessions.Add(session);
|
||||||
|
|
||||||
// we shouldn't be blocking the entity if they're just a ghost or smth.
|
// we shouldn't be blocking the entity if they're just a ghost or smth.
|
||||||
if (!HasComp<GhostComponent>(session.AttachedEntity))
|
if (!HasComp<GhostComponent>(session.AttachedEntity))
|
||||||
@@ -309,7 +385,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
{
|
{
|
||||||
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
|
||||||
if (session != null)
|
if (session != null)
|
||||||
ent.Comp.SelectedSessions.Remove(session);
|
{
|
||||||
|
ent.Comp.AssignedSessions.Remove(session);
|
||||||
|
ent.Comp.PreSelectedSessions[def].Remove(session);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +410,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
{
|
{
|
||||||
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
||||||
if (session != null)
|
if (session != null)
|
||||||
ent.Comp.SelectedSessions.Remove(session);
|
{
|
||||||
|
ent.Comp.AssignedSessions.Remove(session);
|
||||||
|
ent.Comp.PreSelectedSessions[def].Remove(session);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,10 +447,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
|
|
||||||
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
||||||
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
|
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
|
||||||
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
|
ent.Comp.AssignedMinds.Add((curMind.Value, Name(player)));
|
||||||
SendBriefing(session, def.Briefing);
|
SendBriefing(session, def.Briefing);
|
||||||
|
|
||||||
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
Log.Debug($"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
||||||
|
_adminLogger.Add(LogType.AntagSelection, $"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
|
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
|
||||||
@@ -412,15 +497,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
|
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (ent.Comp.SelectedSessions.Contains(session))
|
if (ent.Comp.AssignedSessions.Contains(session))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mind ??= session.GetMind();
|
mind ??= session.GetMind();
|
||||||
|
|
||||||
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
|
|
||||||
if (mind == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
|
//todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds)
|
||||||
|
|
||||||
switch (def.MultiAntagSetting)
|
switch (def.MultiAntagSetting)
|
||||||
@@ -429,12 +510,16 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
{
|
{
|
||||||
if (_role.MindIsAntagonist(mind))
|
if (_role.MindIsAntagonist(mind))
|
||||||
return false;
|
return false;
|
||||||
|
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
|
||||||
|
return false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AntagAcceptability.NotExclusive:
|
case AntagAcceptability.NotExclusive:
|
||||||
{
|
{
|
||||||
if (_role.MindIsExclusiveAntagonist(mind))
|
if (_role.MindIsExclusiveAntagonist(mind))
|
||||||
return false;
|
return false;
|
||||||
|
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
|
||||||
|
return false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,7 +566,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
|||||||
if (ent.Comp.AgentName is not { } name)
|
if (ent.Comp.AgentName is not { } name)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
args.Minds = ent.Comp.SelectedMinds;
|
args.Minds = ent.Comp.AssignedMinds;
|
||||||
args.AgentName = Loc.GetString(name);
|
args.AgentName = Loc.GetString(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ namespace Content.Server.Antag.Components;
|
|||||||
public sealed partial class AntagSelectionComponent : Component
|
public sealed partial class AntagSelectionComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has the primary selection of antagonists finished yet?
|
/// Has the primary assignment of antagonists finished yet?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public bool SelectionsComplete;
|
public bool AssignmentComplete;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the antagonists been preselected but yet to be fully assigned?
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool PreSelectionsComplete;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The definitions for the antagonists
|
/// The definitions for the antagonists
|
||||||
@@ -26,10 +32,10 @@ public sealed partial class AntagSelectionComponent : Component
|
|||||||
public List<AntagSelectionDefinition> Definitions = new();
|
public List<AntagSelectionDefinition> Definitions = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The minds and original names of the players selected to be antagonists.
|
/// The minds and original names of the players assigned to be antagonists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField]
|
||||||
public List<(EntityUid, string)> SelectedMinds = new();
|
public List<(EntityUid, string)> AssignedMinds = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When the antag selection will occur.
|
/// When the antag selection will occur.
|
||||||
@@ -37,11 +43,17 @@ public sealed partial class AntagSelectionComponent : Component
|
|||||||
[DataField]
|
[DataField]
|
||||||
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
|
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached sessions of antag definitions and selected players. Players in this dict are not guaranteed to have been assigned the role yet.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public Dictionary<AntagSelectionDefinition, HashSet<ICommonSession>>PreSelectedSessions = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
|
/// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick.
|
||||||
/// Is not serialized.
|
/// Is not serialized.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<ICommonSession> SelectedSessions = new();
|
public HashSet<ICommonSession> AssignedSessions = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Locale id for the name of the antag.
|
/// Locale id for the name of the antag.
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
using Content.Server.Power.Components;
|
|
||||||
using Content.Shared.UserInterface;
|
using Content.Shared.UserInterface;
|
||||||
using Content.Server.Advertise;
|
using Content.Server.Advertise.EntitySystems;
|
||||||
using Content.Server.Advertise.Components;
|
using Content.Shared.Advertise.Components;
|
||||||
using Content.Shared.Arcade;
|
using Content.Shared.Arcade;
|
||||||
using Content.Shared.Power;
|
using Content.Shared.Power;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Player;
|
|
||||||
|
|
||||||
namespace Content.Server.Arcade.BlockGame;
|
namespace Content.Server.Arcade.BlockGame;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using Content.Server.Power.Components;
|
using Content.Server.Power.Components;
|
||||||
using Content.Shared.UserInterface;
|
using Content.Shared.UserInterface;
|
||||||
using Content.Server.Advertise;
|
using Content.Server.Advertise.EntitySystems;
|
||||||
using Content.Server.Advertise.Components;
|
using Content.Shared.Advertise.Components;
|
||||||
|
using Content.Shared.Arcade;
|
||||||
using Content.Shared.Power;
|
using Content.Shared.Power;
|
||||||
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
|
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Audio.Systems;
|
using Robust.Shared.Audio.Systems;
|
||||||
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||||||
|
|
||||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
|
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
|
||||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
|
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
|
||||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
|
||||||
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
|
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||||||
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
|
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
|
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
|
||||||
{
|
{
|
||||||
if (component.Game == null)
|
if (component.Game == null)
|
||||||
return;
|
return;
|
||||||
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||||||
|
|
||||||
switch (msg.PlayerAction)
|
switch (msg.PlayerAction)
|
||||||
{
|
{
|
||||||
case PlayerAction.Attack:
|
case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
|
||||||
case PlayerAction.Heal:
|
case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
|
||||||
case PlayerAction.Recharge:
|
case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
|
||||||
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
|
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
|
||||||
// Any sort of gameplay action counts
|
// Any sort of gameplay action counts
|
||||||
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
|
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
|
||||||
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
|
||||||
break;
|
break;
|
||||||
case PlayerAction.NewGame:
|
case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
|
||||||
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
|
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
|
||||||
|
|
||||||
component.Game = new SpaceVillainGame(uid, component, this);
|
component.Game = new SpaceVillainGame(uid, component, this);
|
||||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||||
break;
|
break;
|
||||||
case PlayerAction.RequestData:
|
case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
|
||||||
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
|
|||||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
|
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
|
_uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Content.Server.Body.Components;
|
using Content.Server.Body.Components;
|
||||||
using Content.Server.EntityEffects.Effects;
|
using Content.Server.EntityEffects.Effects;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Server.Forensics;
|
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
||||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||||
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
||||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||||
|
|
||||||
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
|
|
||||||
|
|
||||||
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
|
|
||||||
{
|
|
||||||
donorComp.DNA = _forensicsSystem.GenerateDNA();
|
|
||||||
|
|
||||||
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
|
|
||||||
RaiseLocalEvent(entity.Owner, ref ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill blood solution with BLOOD
|
// Fill blood solution with BLOOD
|
||||||
|
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
var bloodData = new List<ReagentData>();
|
var bloodData = new List<ReagentData>();
|
||||||
var dnaData = new DnaData();
|
var dnaData = new DnaData();
|
||||||
|
|
||||||
if (TryComp<DnaComponent>(uid, out var donorComp))
|
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
|
||||||
{
|
|
||||||
dnaData.DNA = donorComp.DNA;
|
dnaData.DNA = donorComp.DNA;
|
||||||
} else
|
else
|
||||||
{
|
|
||||||
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
||||||
}
|
|
||||||
|
|
||||||
bloodData.Add(dnaData);
|
bloodData.Add(dnaData);
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace Content.Server.Cargo.Systems
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_audio.PlayPvs(component.ConfirmSound, uid);
|
_audio.PlayPvs(component.ConfirmSound, uid);
|
||||||
UpdateBankAccount(stationUid.Value, bank, (int) price);
|
UpdateBankAccount((stationUid.Value, bank), (int) price);
|
||||||
QueueDel(args.Used);
|
QueueDel(args.Used);
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ namespace Content.Server.Cargo.Systems
|
|||||||
while (stationQuery.MoveNext(out var uid, out var bank))
|
while (stationQuery.MoveNext(out var uid, out var bank))
|
||||||
{
|
{
|
||||||
var balanceToAdd = bank.IncreasePerSecond * Delay;
|
var balanceToAdd = bank.IncreasePerSecond * Delay;
|
||||||
UpdateBankAccount(uid, bank, balanceToAdd);
|
UpdateBankAccount((uid, bank), balanceToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
|
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
|
||||||
@@ -229,7 +229,7 @@ namespace Content.Server.Cargo.Systems
|
|||||||
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
|
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
|
||||||
|
|
||||||
orderDatabase.Orders.Remove(order);
|
orderDatabase.Orders.Remove(order);
|
||||||
UpdateBankAccount(station.Value, bank, -cost);
|
UpdateBankAccount((station.Value, bank), -cost);
|
||||||
UpdateOrders(station.Value);
|
UpdateOrders(station.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,19 +76,23 @@ public sealed partial class CargoSystem : SharedCargoSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public void UpdateBankAccount(EntityUid uid, StationBankAccountComponent component, int balanceAdded)
|
public void UpdateBankAccount(Entity<StationBankAccountComponent?> ent, int balanceAdded)
|
||||||
{
|
{
|
||||||
component.Balance += balanceAdded;
|
if (!Resolve(ent, ref ent.Comp))
|
||||||
var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>();
|
return;
|
||||||
|
|
||||||
var ev = new BankBalanceUpdatedEvent(uid, component.Balance);
|
ent.Comp.Balance += balanceAdded;
|
||||||
|
|
||||||
|
var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Balance);
|
||||||
|
|
||||||
|
var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>();
|
||||||
while (query.MoveNext(out var client, out var comp, out var xform))
|
while (query.MoveNext(out var client, out var comp, out var xform))
|
||||||
{
|
{
|
||||||
var station = _station.GetOwningStation(client, xform);
|
var station = _station.GetOwningStation(client, xform);
|
||||||
if (station != uid)
|
if (station != ent)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
comp.Balance = component.Balance;
|
comp.Balance = ent.Comp.Balance;
|
||||||
Dirty(client, comp);
|
Dirty(client, comp);
|
||||||
RaiseLocalEvent(client, ref ev);
|
RaiseLocalEvent(client, ref ev);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
|
|||||||
{
|
{
|
||||||
private readonly EntityUid _mindId;
|
private readonly EntityUid _mindId;
|
||||||
private readonly MindComponent _mind;
|
private readonly MindComponent _mind;
|
||||||
private readonly CloningSystem _cloningSystem;
|
private readonly CloningPodSystem _cloningPodSystem;
|
||||||
|
|
||||||
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
|
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
|
||||||
{
|
{
|
||||||
_mindId = mindId;
|
_mindId = mindId;
|
||||||
_mind = mind;
|
_mind = mind;
|
||||||
_cloningSystem = cloningSys;
|
_cloningPodSystem = cloningPodSys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void HandleMessage(EuiMessageBase msg)
|
public override void HandleMessage(EuiMessageBase msg)
|
||||||
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cloningSystem.TransferMindToClone(_mindId, _mind);
|
_cloningPodSystem.TransferMindToClone(_mindId, _mind);
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
|
|||||||
using Content.Server.Cloning.Components;
|
using Content.Server.Cloning.Components;
|
||||||
using Content.Server.DeviceLinking.Systems;
|
using Content.Server.DeviceLinking.Systems;
|
||||||
using Content.Server.Medical.Components;
|
using Content.Server.Medical.Components;
|
||||||
using Content.Server.Power.Components;
|
|
||||||
using Content.Server.Power.EntitySystems;
|
using Content.Server.Power.EntitySystems;
|
||||||
using Content.Shared.UserInterface;
|
using Content.Shared.UserInterface;
|
||||||
using Content.Shared.Cloning;
|
using Content.Shared.Cloning;
|
||||||
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
|
|||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Power;
|
using Content.Shared.Power;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
|
|
||||||
namespace Content.Server.Cloning
|
namespace Content.Server.Cloning
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class CloningConsoleSystem : EntitySystem
|
public sealed class CloningConsoleSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
[Dependency] private readonly CloningSystem _cloningSystem = default!;
|
[Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||||
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
|
|||||||
if (mind.UserId.HasValue == false || mind.Session == null)
|
if (mind.UserId.HasValue == false || mind.Session == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
|
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.Cloning.Components;
|
||||||
|
using Content.Server.DeviceLinking.Systems;
|
||||||
|
using Content.Server.EUI;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
using Content.Server.Materials;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Server.Power.EntitySystems;
|
||||||
|
using Content.Shared.Atmos;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
|
using Content.Shared.Chemistry.Components;
|
||||||
|
using Content.Shared.Cloning;
|
||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.DeviceLinking.Events;
|
||||||
|
using Content.Shared.Emag.Components;
|
||||||
|
using Content.Shared.Emag.Systems;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.GameTicking;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Mind.Components;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Robust.Server.Containers;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Audio.Systems;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
public sealed class CloningPodSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||||
|
[Dependency] private readonly EuiManager _euiManager = null!;
|
||||||
|
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
|
||||||
|
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||||
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||||
|
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
|
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||||
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||||
|
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||||
|
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||||
|
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||||
|
[Dependency] private readonly EmagSystem _emag = default!;
|
||||||
|
|
||||||
|
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||||
|
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
|
||||||
|
public const float EasyModeCloningCost = 0.7f;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||||
|
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
|
||||||
|
{
|
||||||
|
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
|
||||||
|
_signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
||||||
|
{
|
||||||
|
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
||||||
|
!EntityManager.EntityExists(entity) ||
|
||||||
|
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
|
||||||
|
mindComp.Mind != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
|
||||||
|
_mindSystem.UnVisit(mindId, mind);
|
||||||
|
ClonesWaitingForMind.Remove(mind);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
|
||||||
|
{
|
||||||
|
if (clonedComponent.Parent == EntityUid.Invalid ||
|
||||||
|
!EntityManager.EntityExists(clonedComponent.Parent) ||
|
||||||
|
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
||||||
|
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
||||||
|
{
|
||||||
|
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
||||||
|
}
|
||||||
|
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
|
||||||
|
{
|
||||||
|
ent.Comp.ConnectedConsole = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
|
||||||
|
{
|
||||||
|
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Anchored)
|
||||||
|
{
|
||||||
|
_cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
|
||||||
|
{
|
||||||
|
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
|
||||||
|
return;
|
||||||
|
|
||||||
|
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref clonePod))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (HasComp<ActiveCloningPodComponent>(uid))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var mind = mindEnt.Comp;
|
||||||
|
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
||||||
|
{
|
||||||
|
if (EntityManager.EntityExists(clone) &&
|
||||||
|
!_mobStateSystem.IsDead(clone) &&
|
||||||
|
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
||||||
|
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
||||||
|
return false; // Mind already has clone
|
||||||
|
|
||||||
|
ClonesWaitingForMind.Remove(mind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
||||||
|
return false; // Body controlled by mind is not dead
|
||||||
|
|
||||||
|
// Yes, we still need to track down the client because we need to open the Eui
|
||||||
|
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
|
||||||
|
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
|
||||||
|
|
||||||
|
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cloningCost = (int)Math.Round(physics.FixturesMass);
|
||||||
|
|
||||||
|
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
||||||
|
cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
|
||||||
|
|
||||||
|
// biomass checks
|
||||||
|
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
||||||
|
|
||||||
|
if (biomassAmount < cloningCost)
|
||||||
|
{
|
||||||
|
if (clonePod.ConnectedConsole != null)
|
||||||
|
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of biomass checks
|
||||||
|
|
||||||
|
// genetic damage checks
|
||||||
|
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
||||||
|
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
||||||
|
{
|
||||||
|
var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
|
||||||
|
chance *= failChanceModifier;
|
||||||
|
|
||||||
|
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
||||||
|
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
||||||
|
|
||||||
|
if (_robustRandom.Prob(chance))
|
||||||
|
{
|
||||||
|
clonePod.FailedClone = true;
|
||||||
|
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
||||||
|
AddComp<ActiveCloningPodComponent>(uid);
|
||||||
|
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||||
|
clonePod.UsedBiomass = cloningCost;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end of genetic damage checks
|
||||||
|
|
||||||
|
if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
|
||||||
|
{
|
||||||
|
if (clonePod.ConnectedConsole != null)
|
||||||
|
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
|
||||||
|
cloneMindReturn.Mind = mind;
|
||||||
|
cloneMindReturn.Parent = uid;
|
||||||
|
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
|
||||||
|
ClonesWaitingForMind.Add(mind, mob.Value);
|
||||||
|
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
||||||
|
|
||||||
|
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
||||||
|
AddComp<ActiveCloningPodComponent>(uid);
|
||||||
|
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||||
|
clonePod.UsedBiomass = cloningCost;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
||||||
|
{
|
||||||
|
cloningPod.Status = status;
|
||||||
|
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
||||||
|
while (query.MoveNext(out var uid, out var _, out var cloning))
|
||||||
|
{
|
||||||
|
if (!_powerReceiverSystem.IsPowered(uid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cloning.CloningProgress += frameTime;
|
||||||
|
if (cloning.CloningProgress < cloning.CloningTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cloning.FailedClone)
|
||||||
|
EndFailedCloning(uid, cloning);
|
||||||
|
else
|
||||||
|
Eject(uid, cloning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
||||||
|
/// </summary>
|
||||||
|
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
|
||||||
|
{
|
||||||
|
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.IsPowered(ent.Owner, EntityManager))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref clonePod))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
||||||
|
return;
|
||||||
|
|
||||||
|
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
||||||
|
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
||||||
|
clonePod.CloningProgress = 0f;
|
||||||
|
clonePod.UsedBiomass = 0;
|
||||||
|
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||||
|
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
|
||||||
|
{
|
||||||
|
clonePod.FailedClone = false;
|
||||||
|
clonePod.CloningProgress = 0f;
|
||||||
|
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||||
|
var transform = Transform(uid);
|
||||||
|
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
||||||
|
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
||||||
|
|
||||||
|
if (HasComp<EmaggedComponent>(uid))
|
||||||
|
{
|
||||||
|
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
||||||
|
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
Solution bloodSolution = new();
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (i < 1)
|
||||||
|
{
|
||||||
|
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
|
||||||
|
bloodSolution.AddReagent("Blood", 50);
|
||||||
|
if (_robustRandom.Prob(0.2f))
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
||||||
|
|
||||||
|
if (!HasComp<EmaggedComponent>(uid))
|
||||||
|
{
|
||||||
|
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
clonePod.UsedBiomass = 0;
|
||||||
|
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset(RoundRestartCleanupEvent ev)
|
||||||
|
{
|
||||||
|
ClonesWaitingForMind.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,350 +1,123 @@
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
|
||||||
using Content.Server.Chat.Systems;
|
|
||||||
using Content.Server.Cloning.Components;
|
|
||||||
using Content.Server.DeviceLinking.Systems;
|
|
||||||
using Content.Server.EUI;
|
|
||||||
using Content.Server.Fluids.EntitySystems;
|
|
||||||
using Content.Server.Humanoid;
|
using Content.Server.Humanoid;
|
||||||
using Content.Server.Jobs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Server.Materials;
|
|
||||||
using Content.Server.Popups;
|
|
||||||
using Content.Server.Power.EntitySystems;
|
|
||||||
using Content.Shared.Atmos;
|
|
||||||
using Content.Shared.CCVar;
|
|
||||||
using Content.Shared.Chemistry.Components;
|
|
||||||
using Content.Shared.Cloning;
|
using Content.Shared.Cloning;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Cloning.Events;
|
||||||
using Content.Shared.DeviceLinking.Events;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Emag.Components;
|
|
||||||
using Content.Shared.Emag.Systems;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.GameTicking;
|
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Mind.Components;
|
using Content.Shared.NameModifier.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.StatusEffect;
|
||||||
using Content.Shared.Roles.Jobs;
|
using Content.Shared.Whitelist;
|
||||||
using Robust.Server.Containers;
|
using Robust.Shared.Map;
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Audio.Systems;
|
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Physics.Components;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Content.Server.Cloning
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System responsible for making a copy of a humanoid's body.
|
||||||
|
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CloningSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public sealed class CloningSystem : EntitySystem
|
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||||
|
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||||
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||||
|
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
|
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||||
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
|
||||||
{
|
{
|
||||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
clone = null;
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
if (!_prototype.TryIndex(settingsId, out var settings))
|
||||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
return false; // invalid settings
|
||||||
[Dependency] private readonly EuiManager _euiManager = null!;
|
|
||||||
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
|
|
||||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
|
|
||||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
|
||||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
|
||||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
|
||||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
|
||||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
|
||||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
||||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
|
||||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
|
||||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
|
||||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
|
||||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
|
||||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
|
||||||
[Dependency] private readonly EmagSystem _emag = default!;
|
|
||||||
|
|
||||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
|
||||||
public const float EasyModeCloningCost = 0.7f;
|
return false; // whatever body was to be cloned, was not a humanoid
|
||||||
|
|
||||||
public override void Initialize()
|
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
||||||
|
return false; // invalid species
|
||||||
|
|
||||||
|
var attemptEv = new CloningAttemptEvent(settings);
|
||||||
|
RaiseLocalEvent(original, ref attemptEv);
|
||||||
|
if (attemptEv.Cancelled && !settings.ForceCloning)
|
||||||
|
return false; // cannot clone, for example due to the unrevivable trait
|
||||||
|
|
||||||
|
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
|
||||||
|
_humanoidSystem.CloneAppearance(original, clone.Value);
|
||||||
|
|
||||||
|
var componentsToCopy = settings.Components;
|
||||||
|
|
||||||
|
// don't make status effects permanent
|
||||||
|
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
|
||||||
|
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
|
||||||
|
|
||||||
|
foreach (var componentName in componentsToCopy)
|
||||||
{
|
{
|
||||||
base.Initialize();
|
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
|
||||||
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
|
||||||
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
|
|
||||||
{
|
|
||||||
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
|
|
||||||
_signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
|
||||||
{
|
|
||||||
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
|
||||||
!EntityManager.EntityExists(entity) ||
|
|
||||||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
|
|
||||||
mindComp.Mind != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
|
|
||||||
_mindSystem.UnVisit(mindId, mind);
|
|
||||||
ClonesWaitingForMind.Remove(mind);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
|
|
||||||
{
|
|
||||||
if (clonedComponent.Parent == EntityUid.Invalid ||
|
|
||||||
!EntityManager.EntityExists(clonedComponent.Parent) ||
|
|
||||||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
|
||||||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
|
||||||
{
|
{
|
||||||
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
|
||||||
return;
|
continue;
|
||||||
}
|
|
||||||
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
|
|
||||||
{
|
|
||||||
pod.ConnectedConsole = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
|
|
||||||
{
|
|
||||||
if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.Anchored)
|
|
||||||
{
|
|
||||||
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
|
|
||||||
return;
|
|
||||||
|
|
||||||
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
|
|
||||||
{
|
|
||||||
if (!Resolve(uid, ref clonePod))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (HasComp<ActiveCloningPodComponent>(uid))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var mind = mindEnt.Comp;
|
|
||||||
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
|
||||||
{
|
|
||||||
if (EntityManager.EntityExists(clone) &&
|
|
||||||
!_mobStateSystem.IsDead(clone) &&
|
|
||||||
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
|
||||||
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
|
||||||
return false; // Mind already has clone
|
|
||||||
|
|
||||||
ClonesWaitingForMind.Remove(mind);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
|
||||||
return false; // Body controlled by mind is not dead
|
|
||||||
|
|
||||||
// Yes, we still need to track down the client because we need to open the Eui
|
|
||||||
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
|
|
||||||
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
|
|
||||||
|
|
||||||
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
|
|
||||||
return false; // whatever body was to be cloned, was not a humanoid
|
|
||||||
|
|
||||||
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var cloningCost = (int) Math.Round(physics.FixturesMass);
|
|
||||||
|
|
||||||
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
|
||||||
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
|
|
||||||
|
|
||||||
// biomass checks
|
|
||||||
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
|
||||||
|
|
||||||
if (biomassAmount < cloningCost)
|
|
||||||
{
|
{
|
||||||
if (clonePod.ConnectedConsole != null)
|
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
|
||||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
|
RemComp(clone.Value, componentRegistration.Type);
|
||||||
return false;
|
CopyComp(original, clone.Value, sourceComp);
|
||||||
}
|
|
||||||
|
|
||||||
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
|
||||||
clonePod.UsedBiomass = cloningCost;
|
|
||||||
// end of biomass checks
|
|
||||||
|
|
||||||
// genetic damage checks
|
|
||||||
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
|
||||||
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
|
||||||
{
|
|
||||||
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
|
|
||||||
chance *= failChanceModifier;
|
|
||||||
|
|
||||||
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
|
||||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
|
||||||
|
|
||||||
if (_robustRandom.Prob(chance))
|
|
||||||
{
|
|
||||||
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
|
||||||
clonePod.FailedClone = true;
|
|
||||||
AddComp<ActiveCloningPodComponent>(uid);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// end of genetic damage checks
|
|
||||||
|
|
||||||
var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
|
|
||||||
_humanoidSystem.CloneAppearance(bodyToClone, mob);
|
|
||||||
|
|
||||||
var ev = new CloningEvent(bodyToClone, mob);
|
|
||||||
RaiseLocalEvent(bodyToClone, ref ev);
|
|
||||||
|
|
||||||
if (!ev.NameHandled)
|
|
||||||
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
|
|
||||||
|
|
||||||
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
|
|
||||||
cloneMindReturn.Mind = mind;
|
|
||||||
cloneMindReturn.Parent = uid;
|
|
||||||
_containerSystem.Insert(mob, clonePod.BodyContainer);
|
|
||||||
ClonesWaitingForMind.Add(mind, mob);
|
|
||||||
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
|
||||||
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
|
||||||
|
|
||||||
AddComp<ActiveCloningPodComponent>(uid);
|
|
||||||
|
|
||||||
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
|
|
||||||
// Add on special job components to the mob.
|
|
||||||
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
|
|
||||||
{
|
|
||||||
foreach (var special in prototype.Special)
|
|
||||||
{
|
|
||||||
if (special is AddComponentSpecial)
|
|
||||||
special.AfterEquip(mob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
|
||||||
{
|
|
||||||
cloningPod.Status = status;
|
|
||||||
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
|
||||||
while (query.MoveNext(out var uid, out var _, out var cloning))
|
|
||||||
{
|
|
||||||
if (!_powerReceiverSystem.IsPowered(uid))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
cloning.CloningProgress += frameTime;
|
|
||||||
if (cloning.CloningProgress < cloning.CloningTime)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (cloning.FailedClone)
|
|
||||||
EndFailedCloning(uid, cloning);
|
|
||||||
else
|
|
||||||
Eject(uid, cloning);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var cloningEv = new CloningEvent(settings, clone.Value);
|
||||||
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
|
||||||
/// </summary>
|
|
||||||
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
|
// Add equipment first so that SetEntityName also renames the ID card.
|
||||||
|
if (settings.CopyEquipment != null)
|
||||||
|
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
|
||||||
|
|
||||||
|
var originalName = Name(original);
|
||||||
|
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
|
||||||
|
originalName = nameModComp.BaseName;
|
||||||
|
|
||||||
|
// This will properly set the BaseName and EntityName for the clone.
|
||||||
|
// Adding the component first before renaming will make sure RefreshNameModifers is called.
|
||||||
|
// Without this the name would get reverted to Urist.
|
||||||
|
// If the clone has no name modifiers, NameModifierComponent will be removed again.
|
||||||
|
EnsureComp<NameModifierComponent>(clone.Value);
|
||||||
|
_metaData.SetEntityName(clone.Value, originalName);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the equipment the original has to the clone.
|
||||||
|
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
|
||||||
|
/// </summary>
|
||||||
|
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||||
|
{
|
||||||
|
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
|
||||||
|
return;
|
||||||
|
// Iterate over all inventory slots
|
||||||
|
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
|
||||||
|
while (slotEnumerator.NextItem(out var item, out var slot))
|
||||||
{
|
{
|
||||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
// Spawn a copy of the item using the original prototype.
|
||||||
return;
|
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
|
||||||
|
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||||
|
|
||||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
|
||||||
return;
|
continue;
|
||||||
|
|
||||||
if (!this.IsPowered(uid, EntityManager))
|
var prototype = MetaData(item).EntityPrototype;
|
||||||
return;
|
if (prototype != null)
|
||||||
|
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
|
||||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
|
||||||
{
|
|
||||||
if (!Resolve(uid, ref clonePod))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
|
||||||
return;
|
|
||||||
|
|
||||||
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
|
||||||
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
|
||||||
clonePod.CloningProgress = 0f;
|
|
||||||
clonePod.UsedBiomass = 0;
|
|
||||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
|
||||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
|
|
||||||
{
|
|
||||||
clonePod.FailedClone = false;
|
|
||||||
clonePod.CloningProgress = 0f;
|
|
||||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
|
||||||
var transform = Transform(uid);
|
|
||||||
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
|
||||||
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
|
||||||
|
|
||||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
|
||||||
{
|
|
||||||
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
|
||||||
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
Solution bloodSolution = new();
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while (i < 1)
|
|
||||||
{
|
|
||||||
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
|
|
||||||
bloodSolution.AddReagent("Blood", 50);
|
|
||||||
if (_robustRandom.Prob(0.2f))
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
|
||||||
|
|
||||||
if (!_emag.CheckFlag(uid, EmagType.Interaction))
|
|
||||||
{
|
|
||||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
clonePod.UsedBiomass = 0;
|
|
||||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset(RoundRestartCleanupEvent ev)
|
|
||||||
{
|
|
||||||
ClonesWaitingForMind.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Content.Shared.Cloning;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is added to a marker entity in order to spawn a clone of a random player.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, EntityCategory("Spawner")]
|
||||||
|
public sealed partial class RandomCloneSpawnerComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cloning settings to be used.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||||
|
}
|
||||||
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Content.Server.Cloning.Components;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This deals with spawning and setting up a clone of a random crew member.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RandomCloneSpawnerSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
QueueDel(ent.Owner);
|
||||||
|
|
||||||
|
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
|
||||||
|
{
|
||||||
|
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allHumans = _mind.GetAliveHumans();
|
||||||
|
|
||||||
|
if (allHumans.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
|
||||||
|
|
||||||
|
if (bodyToClone != null)
|
||||||
|
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Content.Server/Delivery/CargoDeliveryDataComponent.cs
Normal file
51
Content.Server/Delivery/CargoDeliveryDataComponent.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||||
|
|
||||||
|
namespace Content.Server.Delivery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component given to a station to indicate it can have deliveries spawn on it.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, AutoGenerateComponentPause]
|
||||||
|
public sealed partial class CargoDeliveryDataComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which the next delivery will spawn.
|
||||||
|
/// </summary>
|
||||||
|
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||||
|
public TimeSpan NextDelivery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum cooldown after a delivery spawns.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan MinDeliveryCooldown = TimeSpan.FromMinutes(3);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum cooldown after a delivery spawns.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan MaxDeliveryCooldown = TimeSpan.FromMinutes(7);
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ratio at which deliveries will spawn, based on the amount of people in the crew manifest.
|
||||||
|
/// 1 delivery per X players.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float PlayerToDeliveryRatio = 7f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum amount of deliveries that will spawn.
|
||||||
|
/// This is not per spawner unless DistributeRandomly is false.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public int MinimumDeliverySpawn = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should deliveries be randomly split between spawners?
|
||||||
|
/// If true, the amount of deliveries will be spawned randomly across all spawners.
|
||||||
|
/// If false, an amount of mail based on PlayerToDeliveryRatio will be spawned on all spawners.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool DistributeRandomly = true;
|
||||||
|
}
|
||||||
131
Content.Server/Delivery/DeliverySystem.Spawning.cs
Normal file
131
Content.Server/Delivery/DeliverySystem.Spawning.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using Content.Server.Power.EntitySystems;
|
||||||
|
using Content.Server.StationRecords;
|
||||||
|
using Content.Shared.Delivery;
|
||||||
|
using Content.Shared.EntityTable;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Server.Delivery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System for managing deliveries spawned by the mail teleporter.
|
||||||
|
/// This covers for spawning deliveries.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class DeliverySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly EntityTableSystem _entityTable = default!;
|
||||||
|
[Dependency] private readonly PowerReceiverSystem _power = default!;
|
||||||
|
|
||||||
|
private void InitializeSpawning()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<CargoDeliveryDataComponent, MapInitEvent>(OnDataMapInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataMapInit(Entity<CargoDeliveryDataComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
ent.Comp.NextDelivery = _timing.CurTime + ent.Comp.MinDeliveryCooldown; // We want an early wave of mail so cargo doesn't have to wait
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnDelivery(Entity<DeliverySpawnerComponent?> ent, int amount)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent.Owner, ref ent.Comp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var coords = Transform(ent).Coordinates;
|
||||||
|
|
||||||
|
_audio.PlayPvs(ent.Comp.SpawnSound, ent.Owner);
|
||||||
|
|
||||||
|
for (int i = 0; i < amount; i++)
|
||||||
|
{
|
||||||
|
var spawns = _entityTable.GetSpawns(ent.Comp.Table);
|
||||||
|
|
||||||
|
foreach (var id in spawns)
|
||||||
|
{
|
||||||
|
Spawn(id, coords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnStationDeliveries(Entity<CargoDeliveryDataComponent> ent)
|
||||||
|
{
|
||||||
|
if (!TryComp<StationRecordsComponent>(ent, out var records))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var spawners = GetValidSpawners(ent);
|
||||||
|
|
||||||
|
// Skip if theres no spawners available
|
||||||
|
if (spawners.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Skip if there's nobody in crew manifest
|
||||||
|
if (records.Records.Keys.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// We take the amount of mail calculated based on player amount or the minimum, whichever is higher.
|
||||||
|
// We don't want stations with less than the player ratio to not get mail at all
|
||||||
|
var initialDeliveryCount = (int)Math.Ceiling(records.Records.Keys.Count / ent.Comp.PlayerToDeliveryRatio);
|
||||||
|
var deliveryCount = Math.Max(initialDeliveryCount, ent.Comp.MinimumDeliverySpawn);
|
||||||
|
|
||||||
|
if (!ent.Comp.DistributeRandomly)
|
||||||
|
{
|
||||||
|
foreach (var spawner in spawners)
|
||||||
|
{
|
||||||
|
SpawnDelivery(spawner, deliveryCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int[] amounts = new int[spawners.Count];
|
||||||
|
|
||||||
|
// Distribute items randomly
|
||||||
|
for (int i = 0; i < deliveryCount; i++)
|
||||||
|
{
|
||||||
|
var randomListIndex = _random.Next(spawners.Count);
|
||||||
|
amounts[randomListIndex]++;
|
||||||
|
}
|
||||||
|
for (int j = 0; j < spawners.Count; j++)
|
||||||
|
{
|
||||||
|
SpawnDelivery(spawners[j], amounts[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EntityUid> GetValidSpawners(Entity<CargoDeliveryDataComponent> ent)
|
||||||
|
{
|
||||||
|
var validSpawners = new List<EntityUid>();
|
||||||
|
|
||||||
|
var spawners = EntityQueryEnumerator<DeliverySpawnerComponent>();
|
||||||
|
while (spawners.MoveNext(out var spawnerUid, out _))
|
||||||
|
{
|
||||||
|
var spawnerStation = _station.GetOwningStation(spawnerUid);
|
||||||
|
|
||||||
|
if (spawnerStation != ent.Owner)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_power.IsPowered(spawnerUid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
validSpawners.Add(spawnerUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validSpawners;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSpawner(float frameTime)
|
||||||
|
{
|
||||||
|
var dataQuery = EntityQueryEnumerator<CargoDeliveryDataComponent>();
|
||||||
|
var curTime = _timing.CurTime;
|
||||||
|
|
||||||
|
while (dataQuery.MoveNext(out var uid, out var deliveryData))
|
||||||
|
{
|
||||||
|
if (deliveryData.NextDelivery > curTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
deliveryData.NextDelivery += _random.Next(deliveryData.MinDeliveryCooldown, deliveryData.MaxDeliveryCooldown); // Random cooldown between min and max
|
||||||
|
SpawnStationDeliveries((uid, deliveryData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
Content.Server/Delivery/DeliverySystem.cs
Normal file
85
Content.Server/Delivery/DeliverySystem.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using Content.Server.Cargo.Components;
|
||||||
|
using Content.Server.Cargo.Systems;
|
||||||
|
using Content.Server.Station.Systems;
|
||||||
|
using Content.Server.StationRecords.Systems;
|
||||||
|
using Content.Shared.Delivery;
|
||||||
|
using Content.Shared.FingerprintReader;
|
||||||
|
using Content.Shared.Labels.EntitySystems;
|
||||||
|
using Content.Shared.StationRecords;
|
||||||
|
using Robust.Shared.Audio.Systems;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
|
||||||
|
namespace Content.Server.Delivery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System for managing deliveries spawned by the mail teleporter.
|
||||||
|
/// This covers for mail spawning, as well as granting cargo money.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class DeliverySystem : SharedDeliverySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly CargoSystem _cargo = default!;
|
||||||
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly StationRecordsSystem _records = default!;
|
||||||
|
[Dependency] private readonly StationSystem _station = default!;
|
||||||
|
[Dependency] private readonly FingerprintReaderSystem _fingerprintReader = default!;
|
||||||
|
[Dependency] private readonly SharedLabelSystem _label = default!;
|
||||||
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<DeliveryComponent, MapInitEvent>(OnMapInit);
|
||||||
|
|
||||||
|
InitializeSpawning();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(Entity<DeliveryComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
_container.EnsureContainer<Container>(ent, ent.Comp.Container);
|
||||||
|
|
||||||
|
var stationId = _station.GetStationInMap(Transform(ent).MapID);
|
||||||
|
|
||||||
|
if (stationId == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_records.TryGetRandomRecord<GeneralStationRecord>(stationId.Value, out var entry);
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.RecipientName = entry.Name;
|
||||||
|
ent.Comp.RecipientJobTitle = entry.JobTitle;
|
||||||
|
ent.Comp.RecipientStation = stationId.Value;
|
||||||
|
|
||||||
|
_appearance.SetData(ent, DeliveryVisuals.JobIcon, entry.JobIcon);
|
||||||
|
|
||||||
|
_label.Label(ent, ent.Comp.RecipientName);
|
||||||
|
|
||||||
|
if (TryComp<FingerprintReaderComponent>(ent, out var reader) && entry.Fingerprint != null)
|
||||||
|
{
|
||||||
|
_fingerprintReader.AddAllowedFingerprint((ent.Owner, reader), entry.Fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dirty(ent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void GrantSpesoReward(Entity<DeliveryComponent?> ent)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent, ref ent.Comp))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!TryComp<StationBankAccountComponent>(ent.Comp.RecipientStation, out var account))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_cargo.UpdateBankAccount((ent.Comp.RecipientStation.Value, account), ent.Comp.SpesoReward);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
base.Update(frameTime);
|
||||||
|
|
||||||
|
UpdateSpawner(frameTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
using Content.Server.Explosion.EntitySystems;
|
using Content.Server.Explosion.EntitySystems;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Explosion.Components;
|
namespace Content.Server.Explosion.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns a protoype when triggered.
|
||||||
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(TriggerSystem))]
|
[RegisterComponent, Access(typeof(TriggerSystem))]
|
||||||
public sealed partial class SpawnOnTriggerComponent : Component
|
public sealed partial class SpawnOnTriggerComponent : Component
|
||||||
{
|
{
|
||||||
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
/// <summary>
|
||||||
public string Proto = string.Empty;
|
/// The prototype to spawn.
|
||||||
|
/// </summary>
|
||||||
|
[DataField(required: true)]
|
||||||
|
public EntProtoId Proto = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use MapCoordinates for spawning?
|
||||||
|
/// Set to true if you don't want the new entity parented to the spawner.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool mapCoords;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
namespace Content.Server.Explosion.Components
|
namespace Content.Server.Explosion.Components;
|
||||||
{
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed partial class TriggerOnCollideComponent : Component
|
|
||||||
{
|
|
||||||
[DataField("fixtureID", required: true)]
|
|
||||||
public string FixtureID = String.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Doesn't trigger if the other colliding fixture is nonhard.
|
/// Triggers when colliding with another entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("ignoreOtherNonHard")]
|
[RegisterComponent]
|
||||||
public bool IgnoreOtherNonHard = true;
|
public sealed partial class TriggerOnCollideComponent : Component
|
||||||
}
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The fixture with which to collide.
|
||||||
|
/// </summary>
|
||||||
|
[DataField(required: true)]
|
||||||
|
public string FixtureID = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Doesn't trigger if the other colliding fixture is nonhard.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool IgnoreOtherNonHard = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Content.Server.Explosion.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers on use in hand.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class TriggerOnUseComponent : Component { }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Content.Shared.Whitelist;
|
||||||
|
|
||||||
|
namespace Content.Server.Explosion.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the user of a Trigger satisfies a whitelist and blacklist condition.
|
||||||
|
/// Cancels the trigger otherwise.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class TriggerWhitelistComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whitelist for what entites can cause this trigger.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityWhitelist? Whitelist;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blacklist for what entites can cause this trigger.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityWhitelist? Blacklist;
|
||||||
|
}
|
||||||
@@ -53,6 +53,9 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
_adminLogger.Add(LogType.Trigger, LogImpact.High,
|
_adminLogger.Add(LogType.Trigger, LogImpact.High,
|
||||||
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
|
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
|
||||||
Trigger(ent, args.Source);
|
Trigger(ent, args.Source);
|
||||||
|
|
||||||
|
var voice = new VoiceTriggeredEvent(args.Source, message);
|
||||||
|
RaiseLocalEvent(ent, ref voice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,3 +140,12 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a voice trigger is activated, containing the message that triggered it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Source"> The EntityUid of the entity sending the message</param>
|
||||||
|
/// <param name="Message"> The contents of the message</param>
|
||||||
|
[ByRefEvent]
|
||||||
|
public readonly record struct VoiceTriggeredEvent(EntityUid Source, string? Message);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using Content.Shared.Explosion.Components;
|
|||||||
using Content.Shared.Explosion.Components.OnTrigger;
|
using Content.Shared.Explosion.Components.OnTrigger;
|
||||||
using Content.Shared.Implants.Components;
|
using Content.Shared.Implants.Components;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Interaction.Events;
|
||||||
using Content.Shared.Inventory;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Mobs;
|
using Content.Shared.Mobs;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
@@ -23,6 +24,7 @@ using Content.Shared.Slippery;
|
|||||||
using Content.Shared.StepTrigger.Systems;
|
using Content.Shared.StepTrigger.Systems;
|
||||||
using Content.Shared.Trigger;
|
using Content.Shared.Trigger;
|
||||||
using Content.Shared.Weapons.Ranged.Events;
|
using Content.Shared.Weapons.Ranged.Events;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Audio.Systems;
|
using Robust.Shared.Audio.Systems;
|
||||||
@@ -31,10 +33,7 @@ using Robust.Shared.Physics.Events;
|
|||||||
using Robust.Shared.Physics.Systems;
|
using Robust.Shared.Physics.Systems;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Content.Shared.Coordinates;
|
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using Robust.Shared.Timing;
|
|
||||||
|
|
||||||
namespace Content.Server.Explosion.EntitySystems
|
namespace Content.Server.Explosion.EntitySystems
|
||||||
{
|
{
|
||||||
@@ -53,6 +52,12 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised before a trigger is activated.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raised when timer trigger becomes active.
|
/// Raised when timer trigger becomes active.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,6 +83,7 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||||
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
|
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
|
||||||
|
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -93,6 +99,7 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
|
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
|
||||||
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
|
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
|
||||||
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
|
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
|
||||||
|
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
|
||||||
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
|
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
|
||||||
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
|
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
|
||||||
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
||||||
@@ -109,6 +116,13 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
|
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
|
||||||
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
|
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
|
||||||
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
|
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<TriggerWhitelistComponent, BeforeTriggerEvent>(HandleWhitelist);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWhitelist(Entity<TriggerWhitelistComponent> ent, ref BeforeTriggerEvent args)
|
||||||
|
{
|
||||||
|
args.Cancelled = !_whitelist.CheckBoth(args.User, ent.Comp.Blacklist, ent.Comp.Whitelist);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
|
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
|
||||||
@@ -155,16 +169,23 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
RemCompDeferred<AnchorOnTriggerComponent>(uid);
|
RemCompDeferred<AnchorOnTriggerComponent>(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSpawnTrigger(EntityUid uid, SpawnOnTriggerComponent component, TriggerEvent args)
|
private void OnSpawnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
|
||||||
{
|
{
|
||||||
var xform = Transform(uid);
|
var xform = Transform(ent);
|
||||||
|
|
||||||
var coords = xform.Coordinates;
|
if (ent.Comp.mapCoords)
|
||||||
|
{
|
||||||
|
var mapCoords = _transformSystem.GetMapCoordinates(ent, xform);
|
||||||
|
Spawn(ent.Comp.Proto, mapCoords);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var coords = xform.Coordinates;
|
||||||
|
if (!coords.IsValid(EntityManager))
|
||||||
|
return;
|
||||||
|
Spawn(ent.Comp.Proto, coords);
|
||||||
|
|
||||||
if (!coords.IsValid(EntityManager))
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
Spawn(component.Proto, coords);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
||||||
@@ -248,6 +269,15 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnUse(Entity<TriggerOnUseComponent> ent, ref UseInHandEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Trigger(ent.Owner, args.User);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
|
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
|
||||||
{
|
{
|
||||||
args.Handled = Trigger(uid);
|
args.Handled = Trigger(uid);
|
||||||
@@ -275,6 +305,11 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
|
|
||||||
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
||||||
{
|
{
|
||||||
|
var beforeTriggerEvent = new BeforeTriggerEvent(trigger, user);
|
||||||
|
RaiseLocalEvent(trigger, ref beforeTriggerEvent);
|
||||||
|
if (beforeTriggerEvent.Cancelled)
|
||||||
|
return false;
|
||||||
|
|
||||||
var triggerEvent = new TriggerEvent(trigger, user);
|
var triggerEvent = new TriggerEvent(trigger, user);
|
||||||
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
|
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
|
||||||
return triggerEvent.Handled;
|
return triggerEvent.Handled;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Body.Components;
|
using Content.Server.Body.Components;
|
||||||
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.DoAfter;
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Server.Forensics.Components;
|
using Content.Server.Forensics.Components;
|
||||||
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
||||||
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
|
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
|
||||||
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
|
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
|
||||||
|
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
|
||||||
|
|
||||||
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
||||||
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
||||||
@@ -65,18 +67,19 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
||||||
{
|
{
|
||||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
if (ent.Comp.Fingerprint == null)
|
||||||
Dirty(ent);
|
RandomizeFingerprint((ent.Owner, ent.Comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
|
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
|
||||||
{
|
{
|
||||||
if (component.DNA == String.Empty)
|
if (ent.Comp.DNA == null)
|
||||||
|
RandomizeDNA((ent.Owner, ent.Comp));
|
||||||
|
else
|
||||||
{
|
{
|
||||||
component.DNA = GenerateDNA();
|
// If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
|
||||||
|
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||||
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
|
RaiseLocalEvent(ent.Owner, ref ev);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +87,7 @@ namespace Content.Server.Forensics
|
|||||||
{
|
{
|
||||||
string dna = Loc.GetString("forensics-dna-unknown");
|
string dna = Loc.GetString("forensics-dna-unknown");
|
||||||
|
|
||||||
if (TryComp(uid, out DnaComponent? dnaComp))
|
if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
|
||||||
dna = dnaComp.DNA;
|
dna = dnaComp.DNA;
|
||||||
|
|
||||||
foreach (EntityUid part in args.GibbedParts)
|
foreach (EntityUid part in args.GibbedParts)
|
||||||
@@ -103,7 +106,7 @@ namespace Content.Server.Forensics
|
|||||||
{
|
{
|
||||||
foreach (EntityUid hitEntity in args.HitEntities)
|
foreach (EntityUid hitEntity in args.HitEntities)
|
||||||
{
|
{
|
||||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
|
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
|
||||||
component.DNAs.Add(hitEntityComp.DNA);
|
component.DNAs.Add(hitEntityComp.DNA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +304,9 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
||||||
{
|
{
|
||||||
|
if (component.DNA == null)
|
||||||
|
return;
|
||||||
|
|
||||||
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
||||||
recipientComp.DNAs.Add(component.DNA);
|
recipientComp.DNAs.Add(component.DNA);
|
||||||
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
||||||
@@ -308,6 +314,35 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
|
||||||
|
/// Does nothing if it does not have the DnaComponent.
|
||||||
|
/// </summary>
|
||||||
|
public void RandomizeDNA(Entity<DnaComponent?> ent)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.DNA = GenerateDNA();
|
||||||
|
Dirty(ent);
|
||||||
|
|
||||||
|
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||||
|
RaiseLocalEvent(ent.Owner, ref ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Give the entity a new, random fingerprint string.
|
||||||
|
/// Does nothing if it does not have the FingerprintComponent.
|
||||||
|
/// </summary>
|
||||||
|
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||||
|
Dirty(ent);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Transfer DNA from one entity onto the forensics of another
|
/// Transfer DNA from one entity onto the forensics of another
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -316,7 +351,7 @@ namespace Content.Server.Forensics
|
|||||||
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
||||||
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
||||||
{
|
{
|
||||||
if (TryComp<DnaComponent>(donor, out var donorComp))
|
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
|
||||||
{
|
{
|
||||||
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
||||||
recipientComp.DNAs.Add(donorComp.DNA);
|
recipientComp.DNAs.Add(donorComp.DNA);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using Content.Shared.Cloning;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.GameTicking.Rules.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gamerule component for spawning a paradox clone antagonist.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class ParadoxCloneRuleComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cloning settings to be used.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visual effect spawned when gibbing at round end.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntProtoId GibProto = "MobParadoxTimed";
|
||||||
|
}
|
||||||
@@ -24,13 +24,13 @@ public sealed partial class TraitorRuleComponent : Component
|
|||||||
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
|
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
|
public ProtoId<LocalizedDatasetPrototype> CodewordAdjectives = "Adjectives";
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public ProtoId<DatasetPrototype> CodewordVerbs = "verbs";
|
public ProtoId<LocalizedDatasetPrototype> CodewordVerbs = "Verbs";
|
||||||
|
|
||||||
[DataField]
|
[DataField]
|
||||||
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
|
public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Give this traitor an Uplink on spawn.
|
/// Give this traitor an Uplink on spawn.
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
|
|||||||
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent> QueryDelayedRules()
|
||||||
|
{
|
||||||
|
return EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queries all gamerules, regardless of if they're active or not.
|
/// Queries all gamerules, regardless of if they're active or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
83
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal file
83
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using Content.Server.Antag;
|
||||||
|
using Content.Server.Cloning;
|
||||||
|
using Content.Server.GameTicking.Rules.Components;
|
||||||
|
using Content.Server.Objectives.Components;
|
||||||
|
using Content.Shared.GameTicking.Components;
|
||||||
|
using Content.Shared.Gibbing.Components;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.GameTicking.Rules;
|
||||||
|
|
||||||
|
public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComponent>
|
||||||
|
{
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<ParadoxCloneRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||||
|
{
|
||||||
|
base.Started(uid, component, gameRule, args);
|
||||||
|
|
||||||
|
// check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up
|
||||||
|
var allHumans = _mind.GetAliveHumans();
|
||||||
|
|
||||||
|
if (allHumans.Count == 0)
|
||||||
|
{
|
||||||
|
Log.Info("Could not find any alive players to create a paradox clone from! Ending gamerule.");
|
||||||
|
ForceEndSelf(uid, gameRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to do the spawning here so we can transfer the mind to the correct entity and can assign the objectives correctly
|
||||||
|
private void OnAntagSelectEntity(Entity<ParadoxCloneRuleComponent> ent, ref AntagSelectEntityEvent args)
|
||||||
|
{
|
||||||
|
if (args.Session?.AttachedEntity is not { } spawner)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
|
||||||
|
{
|
||||||
|
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for ParadoxCloneRule");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get possible targets
|
||||||
|
var allHumans = _mind.GetAliveHumans();
|
||||||
|
|
||||||
|
// we already checked when starting the gamerule, but someone might have died since then.
|
||||||
|
if (allHumans.Count == 0)
|
||||||
|
{
|
||||||
|
Log.Warning("Could not find any alive players to create a paradox clone from!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick a random player
|
||||||
|
var playerToClone = _random.Pick(allHumans);
|
||||||
|
var bodyToClone = playerToClone.Comp.OwnedEntity;
|
||||||
|
|
||||||
|
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), settings, out var clone))
|
||||||
|
{
|
||||||
|
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
|
||||||
|
targetComp.Target = playerToClone.Owner; // set the kill target
|
||||||
|
|
||||||
|
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
|
||||||
|
gibComp.SpawnProto = ent.Comp.GibProto;
|
||||||
|
gibComp.PreventGibbingObjectives = new() { "ParadoxCloneKillObjective" }; // don't gib them if they killed the original.
|
||||||
|
|
||||||
|
args.Entity = clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,7 +189,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
commandList.Add(id);
|
commandList.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsGroupDetainedOrDead(commandList, true, true);
|
return IsGroupDetainedOrDead(commandList, true, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
|
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
|
||||||
@@ -214,7 +214,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
|
|
||||||
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
|
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
|
||||||
// Cuffing Head Revs is not enough - they must be killed.
|
// Cuffing Head Revs is not enough - they must be killed.
|
||||||
if (IsGroupDetainedOrDead(headRevList, false, false))
|
if (IsGroupDetainedOrDead(headRevList, false, false, false))
|
||||||
{
|
{
|
||||||
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
|
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
|
||||||
while (rev.MoveNext(out var uid, out _, out var mc))
|
while (rev.MoveNext(out var uid, out _, out var mc))
|
||||||
@@ -251,34 +251,45 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
|||||||
/// <param name="list">The list of the entities</param>
|
/// <param name="list">The list of the entities</param>
|
||||||
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)</param>
|
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)</param>
|
||||||
/// <param name="countCuffed">Bool for if you don't want to count cuffed entities.</param>
|
/// <param name="countCuffed">Bool for if you don't want to count cuffed entities.</param>
|
||||||
|
/// <param name="countRevolutionaries">Bool for if you want to count revolutionaries.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed)
|
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed, bool countRevolutionaries)
|
||||||
{
|
{
|
||||||
var gone = 0;
|
var gone = 0;
|
||||||
|
|
||||||
foreach (var entity in list)
|
foreach (var entity in list)
|
||||||
{
|
{
|
||||||
if (TryComp<CuffableComponent>(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
|
if (TryComp<CuffableComponent>(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
|
||||||
{
|
{
|
||||||
gone++;
|
gone++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (TryComp<MobStateComponent>(entity, out var state))
|
||||||
{
|
{
|
||||||
if (TryComp<MobStateComponent>(entity, out var state))
|
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
|
||||||
{
|
|
||||||
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
|
|
||||||
{
|
|
||||||
gone++;
|
|
||||||
}
|
|
||||||
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
|
|
||||||
{
|
|
||||||
gone++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//If they don't have the MobStateComponent they might as well be dead.
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
gone++;
|
gone++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
|
||||||
|
{
|
||||||
|
gone++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//If they don't have the MobStateComponent they might as well be dead.
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gone++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((HasComp<RevolutionaryComponent>(entity) || HasComp<HeadRevolutionaryComponent>(entity)) && countRevolutionaries)
|
||||||
|
{
|
||||||
|
gone++;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Content.Shared.GameTicking.Components;
|
|||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.NPC.Systems;
|
using Content.Shared.NPC.Systems;
|
||||||
using Content.Shared.PDA;
|
using Content.Shared.PDA;
|
||||||
|
using Content.Shared.Random.Helpers;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Roles.Jobs;
|
using Content.Shared.Roles.Jobs;
|
||||||
using Content.Shared.Roles.RoleCodeword;
|
using Content.Shared.Roles.RoleCodeword;
|
||||||
@@ -74,7 +75,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
string[] codewords = new string[finalCodewordCount];
|
string[] codewords = new string[finalCodewordCount];
|
||||||
for (var i = 0; i < finalCodewordCount; i++)
|
for (var i = 0; i < finalCodewordCount; i++)
|
||||||
{
|
{
|
||||||
codewords[i] = _random.PickAndTake(codewordPool);
|
codewords[i] = Loc.GetString(_random.PickAndTake(codewordPool));
|
||||||
}
|
}
|
||||||
return codewords;
|
return codewords;
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
|||||||
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
|
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers));
|
||||||
|
|
||||||
// Uplink code will go here if applicable, but we still need the variable if there aren't any
|
// Uplink code will go here if applicable, but we still need the variable if there aren't any
|
||||||
Note[]? code = null;
|
Note[]? code = null;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Content.Server.Ghost
|
|||||||
mind = _entities.GetComponent<MindComponent>(mindId);
|
mind = _entities.GetComponent<MindComponent>(mindId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind))
|
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind: mind))
|
||||||
{
|
{
|
||||||
shell.WriteLine(Loc.GetString("ghost-command-denied"));
|
shell.WriteLine(Loc.GetString("ghost-command-denied"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,7 +504,7 @@ namespace Content.Server.Ghost
|
|||||||
return ghost;
|
return ghost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
|
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, bool forced = false, MindComponent? mind = null)
|
||||||
{
|
{
|
||||||
if (!Resolve(mindId, ref mind))
|
if (!Resolve(mindId, ref mind))
|
||||||
return false;
|
return false;
|
||||||
@@ -512,7 +512,12 @@ namespace Content.Server.Ghost
|
|||||||
var playerEntity = mind.CurrentEntity;
|
var playerEntity = mind.CurrentEntity;
|
||||||
|
|
||||||
if (playerEntity != null && viaCommand)
|
if (playerEntity != null && viaCommand)
|
||||||
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
|
{
|
||||||
|
if (forced)
|
||||||
|
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} was forced to ghost via command");
|
||||||
|
else
|
||||||
|
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
|
||||||
|
}
|
||||||
|
|
||||||
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
|
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
|
||||||
RaiseLocalEvent(handleEv);
|
RaiseLocalEvent(handleEv);
|
||||||
@@ -521,7 +526,7 @@ namespace Content.Server.Ghost
|
|||||||
if (handleEv.Handled)
|
if (handleEv.Handled)
|
||||||
return handleEv.Result;
|
return handleEv.Result;
|
||||||
|
|
||||||
if (mind.PreventGhosting)
|
if (mind.PreventGhosting && !forced)
|
||||||
{
|
{
|
||||||
if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
|
if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
|
||||||
{
|
{
|
||||||
|
|||||||
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal file
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Content.Shared.GameTicking;
|
||||||
|
using Content.Shared.Gibbing.Components;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Objectives.Systems;
|
||||||
|
using Content.Server.Body.Systems;
|
||||||
|
|
||||||
|
namespace Content.Server.Gibbing.Systems;
|
||||||
|
public sealed class GibOnRoundEndSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly BodySystem _body = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
|
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
// this is raised after RoundEndTextAppendEvent, so they can successfully greentext before we gib them
|
||||||
|
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoundEnd(RoundEndMessageEvent args)
|
||||||
|
{
|
||||||
|
var gibQuery = EntityQueryEnumerator<GibOnRoundEndComponent>();
|
||||||
|
|
||||||
|
// gib everyone with the component
|
||||||
|
while (gibQuery.MoveNext(out var uid, out var gibComp))
|
||||||
|
{
|
||||||
|
var gib = false;
|
||||||
|
// if they fulfill all objectives given in the component they are not gibbed
|
||||||
|
if (_mind.TryGetMind(uid, out var mindId, out var mindComp))
|
||||||
|
{
|
||||||
|
foreach (var objectiveId in gibComp.PreventGibbingObjectives)
|
||||||
|
{
|
||||||
|
if (!_mind.TryFindObjective((mindId, mindComp), objectiveId, out var objective)
|
||||||
|
|| !_objectives.IsCompleted(objective.Value, (mindId, mindComp)))
|
||||||
|
{
|
||||||
|
gib = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
gib = true;
|
||||||
|
|
||||||
|
if (!gib)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (gibComp.SpawnProto != null)
|
||||||
|
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
|
||||||
|
|
||||||
|
_body.GibBody(uid, splatModifier: 5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,18 +216,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
|||||||
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
||||||
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
||||||
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
||||||
if (TryComp<DnaComponent>(ent, out var dna))
|
|
||||||
{
|
|
||||||
dna.DNA = _forensicsSystem.GenerateDNA();
|
|
||||||
|
|
||||||
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
|
// If the entity has the respecive components, then scramble the dna and fingerprint strings
|
||||||
RaiseLocalEvent(ent, ref ev);
|
_forensicsSystem.RandomizeDNA(ent);
|
||||||
}
|
_forensicsSystem.RandomizeFingerprint(ent);
|
||||||
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
|
|
||||||
{
|
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
||||||
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
|
|
||||||
}
|
|
||||||
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
|
||||||
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
||||||
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Content.Shared;
|
using Content.Shared;
|
||||||
using Content.Shared.Light.Components;
|
using Content.Shared.Light.Components;
|
||||||
|
using Content.Shared.Light.EntitySystems;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Light.EntitySystems;
|
namespace Content.Server.Light.EntitySystems;
|
||||||
@@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
|
|||||||
|
|
||||||
if (ent.Comp.InitialOffset)
|
if (ent.Comp.InitialOffset)
|
||||||
{
|
{
|
||||||
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
|
SetOffset(ent, _random.Next(ent.Comp.Duration));
|
||||||
Dirty(ent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Content.Shared.Light.EntitySystems;
|
||||||
|
|
||||||
|
namespace Content.Server.Light.EntitySystems;
|
||||||
|
|
||||||
|
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using Content.Shared.Implants.Components;
|
|||||||
using Content.Shared.Mindshield.Components;
|
using Content.Shared.Mindshield.Components;
|
||||||
using Content.Shared.Revolutionary.Components;
|
using Content.Shared.Revolutionary.Components;
|
||||||
using Content.Shared.Tag;
|
using Content.Shared.Tag;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
|
||||||
namespace Content.Server.Mindshield;
|
namespace Content.Server.Mindshield;
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ public sealed class MindShieldSystem : EntitySystem
|
|||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
|
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
|
||||||
|
SubscribeLocalEvent<MindShieldImplantComponent, EntGotRemovedFromContainerMessage>(OnImplantDraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -61,4 +63,10 @@ public sealed class MindShieldSystem : EntitySystem
|
|||||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
|
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnImplantDraw(Entity<MindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
|
||||||
|
{
|
||||||
|
RemComp<MindShieldComponent>(args.Container.Owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public bool TargetInLOS = false;
|
public bool TargetInLOS = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, only opaque objects will block line of sight.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public bool UseOpaqueForLOSChecks = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delay after target is in LOS before we start shooting.
|
/// Delay after target is in LOS before we start shooting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent
|
|||||||
/// The base task to use for planning
|
/// The base task to use for planning
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite),
|
[ViewVariables(VVAccess.ReadWrite),
|
||||||
DataField("rootTask", required: true)]
|
DataField("rootTask", required: true)]
|
||||||
public HTNCompoundTask RootTask = default!;
|
public HTNCompoundTask RootTask = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
|
|||||||
/// Is this NPC currently planning?
|
/// Is this NPC currently planning?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables] public bool Planning => PlanningJob != null;
|
[ViewVariables] public bool Planning => PlanningJob != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether plans should be made / updated for this entity
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool Enabled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
|
|||||||
component.PlanningJob = null;
|
component.PlanningJob = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable / disable the hierarchical task network of an entity
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
|
||||||
|
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
|
||||||
|
/// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
[PublicAPI]
|
||||||
|
public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
|
||||||
|
{
|
||||||
|
if (ent.Comp.Enabled == state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Enabled = state;
|
||||||
|
ent.Comp.PlanAccumulator = planCooldown;
|
||||||
|
|
||||||
|
ent.Comp.PlanningToken?.Cancel();
|
||||||
|
ent.Comp.PlanningToken = null;
|
||||||
|
|
||||||
|
if (ent.Comp.Plan != null)
|
||||||
|
{
|
||||||
|
var currentOperator = ent.Comp.Plan.CurrentOperator;
|
||||||
|
|
||||||
|
ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
|
||||||
|
ShutdownPlan(ent.Comp);
|
||||||
|
|
||||||
|
ent.Comp.Plan = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
|
||||||
|
RequestPlan(ent.Comp);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forces the NPC to replan.
|
/// Forces the NPC to replan.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem
|
|||||||
_planQueue.Process();
|
_planQueue.Process();
|
||||||
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
|
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
|
||||||
|
|
||||||
while(query.MoveNext(out var uid, out _, out var comp))
|
while (query.MoveNext(out var uid, out _, out var comp))
|
||||||
{
|
{
|
||||||
// If we're over our max count or it's not MapInit then ignore the NPC.
|
// If we're over our max count or it's not MapInit then ignore the NPC.
|
||||||
if (count >= maxUpdates)
|
if (count >= maxUpdates)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
if (!comp.Enabled)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (comp.PlanningJob != null)
|
if (comp.PlanningJob != null)
|
||||||
{
|
{
|
||||||
if (comp.PlanningJob.Exception != null)
|
if (comp.PlanningJob.Exception != null)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Interaction;
|
using Content.Server.Interaction;
|
||||||
|
using Content.Shared.Physics;
|
||||||
|
|
||||||
namespace Content.Server.NPC.HTN.Preconditions;
|
namespace Content.Server.NPC.HTN.Preconditions;
|
||||||
|
|
||||||
@@ -13,6 +14,9 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
|
|||||||
[DataField("rangeKey")]
|
[DataField("rangeKey")]
|
||||||
public string RangeKey = "RangeKey";
|
public string RangeKey = "RangeKey";
|
||||||
|
|
||||||
|
[DataField("opaqueKey")]
|
||||||
|
public bool UseOpaqueForLOSChecksKey = true;
|
||||||
|
|
||||||
public override void Initialize(IEntitySystemManager sysManager)
|
public override void Initialize(IEntitySystemManager sysManager)
|
||||||
{
|
{
|
||||||
base.Initialize(sysManager);
|
base.Initialize(sysManager);
|
||||||
@@ -27,7 +31,8 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
|
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
|
||||||
|
var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
|
||||||
|
|
||||||
return _interaction.InRangeUnobstructed(owner, target, range);
|
return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
|
|||||||
[DataField("requireLOS")]
|
[DataField("requireLOS")]
|
||||||
public bool RequireLOS = false;
|
public bool RequireLOS = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, only opaque objects will block line of sight.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("opaqueKey")]
|
||||||
|
public bool UseOpaqueForLOSChecks = false;
|
||||||
|
|
||||||
// Like movement we add a component and pass it off to the dedicated system.
|
// Like movement we add a component and pass it off to the dedicated system.
|
||||||
|
|
||||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||||
@@ -56,8 +62,10 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
|
|||||||
public override void Startup(NPCBlackboard blackboard)
|
public override void Startup(NPCBlackboard blackboard)
|
||||||
{
|
{
|
||||||
base.Startup(blackboard);
|
base.Startup(blackboard);
|
||||||
|
|
||||||
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
|
||||||
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
|
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
|
||||||
|
ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
|
||||||
|
|
||||||
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
|
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Content.Server.NPC.Queries.Considerations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
|
||||||
|
/// target entity is exempt from being targeted, otherwise it returns 1f.
|
||||||
|
/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
|
||||||
|
/// for further details on turret target validation.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class TurretTargetingCon : UtilityConsideration
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Content.Server.NPC.Components;
|
using Content.Server.NPC.Components;
|
||||||
using Content.Shared.CombatMode;
|
using Content.Shared.CombatMode;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
|
using Content.Shared.Physics;
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
using Content.Shared.Weapons.Ranged.Events;
|
using Content.Shared.Weapons.Ranged.Events;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -132,8 +133,10 @@ public sealed partial class NPCCombatSystem
|
|||||||
if (comp.LOSAccumulator < 0f)
|
if (comp.LOSAccumulator < 0f)
|
||||||
{
|
{
|
||||||
comp.LOSAccumulator += UnoccludedCooldown;
|
comp.LOSAccumulator += UnoccludedCooldown;
|
||||||
|
|
||||||
// For consistency with NPC steering.
|
// For consistency with NPC steering.
|
||||||
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
|
var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
|
||||||
|
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!comp.TargetInLOS)
|
if (!comp.TargetInLOS)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using Content.Shared.NPC.Systems;
|
|||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
using Content.Shared.Nutrition.EntitySystems;
|
using Content.Shared.Nutrition.EntitySystems;
|
||||||
using Content.Shared.Tools.Systems;
|
using Content.Shared.Tools.Systems;
|
||||||
|
using Content.Shared.Turrets;
|
||||||
using Content.Shared.Weapons.Melee;
|
using Content.Shared.Weapons.Melee;
|
||||||
using Content.Shared.Weapons.Ranged.Components;
|
using Content.Shared.Weapons.Ranged.Components;
|
||||||
using Content.Shared.Weapons.Ranged.Events;
|
using Content.Shared.Weapons.Ranged.Events;
|
||||||
@@ -53,6 +54,7 @@ public sealed class NPCUtilitySystem : EntitySystem
|
|||||||
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
||||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
||||||
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
|
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
|
||||||
|
[Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
|
||||||
|
|
||||||
private EntityQuery<PuddleComponent> _puddleQuery;
|
private EntityQuery<PuddleComponent> _puddleQuery;
|
||||||
private EntityQuery<TransformComponent> _xformQuery;
|
private EntityQuery<TransformComponent> _xformQuery;
|
||||||
@@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem
|
|||||||
return 1f;
|
return 1f;
|
||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
case TurretTargetingCon:
|
||||||
|
{
|
||||||
|
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
|
||||||
|
_turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
|
||||||
|
return 1f;
|
||||||
|
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
using Content.Server.Objectives.Systems;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Components;
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
|
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
|
||||||
/// If there are no heads it will fallback to any person.
|
/// If there are no heads it will fallback to any person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
|
[RegisterComponent]
|
||||||
public sealed partial class PickRandomHeadComponent : Component
|
public sealed partial class PickRandomHeadComponent : Component;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
using Content.Server.Objectives.Systems;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Components;
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
|
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
|
[RegisterComponent]
|
||||||
public sealed partial class PickRandomPersonComponent : Component
|
public sealed partial class PickRandomPersonComponent : Component;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets this objective's target to the one given in <see cref="TargetOverrideComponent"/>, if the entity has it.
|
||||||
|
/// This component needs to be added to objective entity itself.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class PickSpecificPersonComponent : Component;
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
using Content.Server.Objectives.Systems;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Components;
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
|
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
|
[RegisterComponent]
|
||||||
public sealed partial class RandomTraitorAliveComponent : Component
|
public sealed partial class RandomTraitorAliveComponent : Component;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
using Content.Server.Objectives.Systems;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Components;
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
|
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
|
[RegisterComponent]
|
||||||
public sealed partial class RandomTraitorProgressComponent : Component
|
public sealed partial class RandomTraitorProgressComponent : Component;
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
using Content.Server.Objectives.Systems;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Components.Targets;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allows an object to become the target of a StealCollection objection
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed partial class StealTargetComponent : Component
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The theft group to which this item belongs.
|
|
||||||
/// </summary>
|
|
||||||
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public string StealGroup;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Content.Server.Objectives.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a target objective to a specific target when receiving it.
|
||||||
|
/// The objective entity needs to have <see cref="PickSpecificPersonComponent"/>.
|
||||||
|
/// This component needs to be added to entity receiving the objective.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class TargetOverrideComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The entity that should be targeted.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityUid? Target;
|
||||||
|
}
|
||||||
@@ -12,9 +12,11 @@ using Robust.Shared.Random;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Content.Server.Objectives.Commands;
|
using Content.Server.Objectives.Commands;
|
||||||
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Prototypes;
|
using Content.Shared.Prototypes;
|
||||||
using Content.Shared.Roles.Jobs;
|
using Content.Shared.Roles.Jobs;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Objectives;
|
namespace Content.Server.Objectives;
|
||||||
@@ -27,15 +29,20 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||||
[Dependency] private readonly SharedJobSystem _job = default!;
|
[Dependency] private readonly SharedJobSystem _job = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||||
|
|
||||||
private IEnumerable<string>? _objectives;
|
private IEnumerable<string>? _objectives;
|
||||||
|
|
||||||
|
private bool _showGreentext;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||||
|
|
||||||
|
Subs.CVar(_cfg, CCVars.GameShowGreentext, value => _showGreentext = value, true);
|
||||||
|
|
||||||
_prototypeManager.PrototypesReloaded += CreateCompletions;
|
_prototypeManager.PrototypesReloaded += CreateCompletions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +169,11 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
|
|||||||
totalObjectives++;
|
totalObjectives++;
|
||||||
|
|
||||||
agentSummary.Append("- ");
|
agentSummary.Append("- ");
|
||||||
if (progress > 0.99f)
|
if (!_showGreentext)
|
||||||
|
{
|
||||||
|
agentSummary.AppendLine(objectiveTitle);
|
||||||
|
}
|
||||||
|
else if (progress > 0.99f)
|
||||||
{
|
{
|
||||||
agentSummary.AppendLine(Loc.GetString(
|
agentSummary.AppendLine(Loc.GetString(
|
||||||
"objectives-objective-success",
|
"objectives-objective-success",
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
using Content.Server.GameTicking.Rules;
|
|
||||||
using Content.Server.Objectives.Components;
|
using Content.Server.Objectives.Components;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
using Content.Shared.Objectives.Components;
|
using Content.Shared.Objectives.Components;
|
||||||
using Content.Shared.Objectives.Systems;
|
using Content.Shared.Objectives.Systems;
|
||||||
using Content.Shared.Roles.Jobs;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Content.Server.Objectives.Systems;
|
namespace Content.Server.Objectives.Systems;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles help progress condition logic and picking random help targets.
|
/// Handles help progress condition logic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class HelpProgressConditionSystem : EntitySystem
|
public sealed class HelpProgressConditionSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||||
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||||
|
|
||||||
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnTraitorAssigned);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||||
@@ -36,55 +28,6 @@ public sealed class HelpProgressConditionSystem : EntitySystem
|
|||||||
args.Progress = GetProgress(target.Value);
|
args.Progress = GetProgress(target.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTraitorAssigned(EntityUid uid, RandomTraitorProgressComponent comp, ref ObjectiveAssignedEvent args)
|
|
||||||
{
|
|
||||||
// invalid prototype
|
|
||||||
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
|
|
||||||
{
|
|
||||||
args.Cancelled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet();
|
|
||||||
|
|
||||||
// cant help anyone who is tasked with helping:
|
|
||||||
// 1. thats boring
|
|
||||||
// 2. no cyclic progress dependencies!!!
|
|
||||||
foreach (var traitor in traitors)
|
|
||||||
{
|
|
||||||
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
|
|
||||||
if (!TryComp<MindComponent>(traitor.Id, out var mind))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var objective in mind.Objectives)
|
|
||||||
{
|
|
||||||
if (HasComp<HelpProgressConditionComponent>(objective))
|
|
||||||
traitors.RemoveWhere(x => x.Mind == mind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't have multiple objectives to help/save the same person
|
|
||||||
foreach (var objective in args.Mind.Objectives)
|
|
||||||
{
|
|
||||||
if (HasComp<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
|
|
||||||
{
|
|
||||||
if (TryComp<TargetObjectiveComponent>(objective, out var help))
|
|
||||||
{
|
|
||||||
traitors.RemoveWhere(x => x.Id == help.Target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no more helpable traitors
|
|
||||||
if (traitors.Count == 0)
|
|
||||||
{
|
|
||||||
args.Cancelled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float GetProgress(EntityUid target)
|
private float GetProgress(EntityUid target)
|
||||||
{
|
{
|
||||||
var total = 0f; // how much progress they have
|
var total = 0f; // how much progress they have
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user