* 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>
1294 lines
43 KiB
C#
1294 lines
43 KiB
C#
#nullable enable
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using Content.Client.Construction;
|
|
using Content.Server.Atmos.EntitySystems;
|
|
using Content.Server.Construction.Components;
|
|
using Content.Server.Gravity;
|
|
using Content.Server.Power.Components;
|
|
using Content.Shared.Atmos;
|
|
using Content.Shared.Construction.Prototypes;
|
|
using Content.Shared.Gravity;
|
|
using Content.Shared.Item;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.CustomControls;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Maths;
|
|
using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent;
|
|
|
|
namespace Content.IntegrationTests.Tests.Interaction;
|
|
|
|
// This partial class defines various methods that are useful for performing & validating interactions
|
|
public abstract partial class InteractionTest
|
|
{
|
|
/// <summary>
|
|
/// Begin constructing an entity.
|
|
/// </summary>
|
|
protected async Task StartConstruction(string prototype, bool shouldSucceed = true)
|
|
{
|
|
var proto = ProtoMan.Index<ConstructionPrototype>(prototype);
|
|
Assert.That(proto.Type, Is.EqualTo(ConstructionType.Structure));
|
|
|
|
await Client.WaitPost(() =>
|
|
{
|
|
Assert.That(CConSys.TrySpawnGhost(proto, CEntMan.GetCoordinates(TargetCoords), Direction.South, out var clientTarget),
|
|
Is.EqualTo(shouldSucceed));
|
|
|
|
if (!shouldSucceed)
|
|
return;
|
|
|
|
var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
|
|
Target = CEntMan.GetNetEntity(clientTarget.Value);
|
|
Assert.That(Target.Value.IsClientSide());
|
|
ConstructionGhostId = clientTarget.Value.GetHashCode();
|
|
});
|
|
|
|
await RunTicks(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Craft an item.
|
|
/// </summary>
|
|
protected async Task CraftItem(string prototype, bool shouldSucceed = true)
|
|
{
|
|
Assert.That(ProtoMan.Index<ConstructionPrototype>(prototype).Type, Is.EqualTo(ConstructionType.Item));
|
|
|
|
// Please someone purge async construction code
|
|
Task<bool> task = default!;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
|
|
});
|
|
|
|
Task? tickTask = null;
|
|
while (!task.IsCompleted)
|
|
{
|
|
tickTask = Pair.RunTicksSync(1);
|
|
await Task.WhenAny(task, tickTask);
|
|
}
|
|
|
|
if (tickTask != null)
|
|
await tickTask;
|
|
|
|
#pragma warning disable RA0004
|
|
Assert.That(task.Result, Is.EqualTo(shouldSucceed));
|
|
#pragma warning restore RA0004
|
|
|
|
await RunTicks(5);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawn an entity entity and set it as the target.
|
|
/// </summary>
|
|
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
|
|
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
|
|
protected async Task<NetEntity> SpawnTarget(string prototype)
|
|
{
|
|
Target = NetEntity.Invalid;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
|
|
});
|
|
|
|
await RunTicks(5);
|
|
AssertPrototype(prototype);
|
|
return Target!.Value;
|
|
}
|
|
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
|
|
|
|
/// <summary>
|
|
/// Spawn an entity in preparation for deconstruction
|
|
/// </summary>
|
|
protected async Task StartDeconstruction(string prototype)
|
|
{
|
|
await SpawnTarget(prototype);
|
|
var serverTarget = SEntMan.GetEntity(Target);
|
|
Assert.That(SEntMan.TryGetComponent(serverTarget, out ConstructionComponent? comp));
|
|
await Server.WaitPost(() => SConstruction.SetPathfindingTarget(serverTarget!.Value, comp!.DeconstructionNode, comp));
|
|
await RunTicks(5);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops and deletes the currently held entity.
|
|
/// </summary>
|
|
protected async Task DeleteHeldEntity()
|
|
{
|
|
if (Hands.ActiveHandEntity is { } held)
|
|
{
|
|
await Server.WaitPost(() =>
|
|
{
|
|
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), null, false, true, Hands));
|
|
SEntMan.DeleteEntity(held);
|
|
SLogger.Debug($"Deleting held entity");
|
|
});
|
|
}
|
|
|
|
await RunTicks(1);
|
|
Assert.That(Hands.ActiveHandEntity, Is.Null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Place an entity prototype into the players hand. Deletes any currently held entity.
|
|
/// </summary>
|
|
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
|
|
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
|
|
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
|
|
protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
|
|
{
|
|
return await PlaceInHands((id, quantity), enableToggleable);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Place an entity prototype into the players hand. Deletes any currently held entity.
|
|
/// </summary>
|
|
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
|
|
/// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
|
|
protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
|
|
{
|
|
if (Hands.ActiveHand == null)
|
|
{
|
|
Assert.Fail("No active hand");
|
|
return default;
|
|
}
|
|
|
|
Assert.That(!string.IsNullOrWhiteSpace(entity.Prototype));
|
|
await DeleteHeldEntity();
|
|
|
|
// spawn and pick up the new item
|
|
var item = await SpawnEntity(entity, SEntMan.GetCoordinates(PlayerCoords));
|
|
ItemToggleComponent? itemToggle = null;
|
|
|
|
await Server.WaitPost(() =>
|
|
{
|
|
var playerEnt = SEntMan.GetEntity(Player);
|
|
|
|
Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
|
|
|
|
// turn on welders
|
|
if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
|
|
{
|
|
Assert.That(ItemToggleSys.TryActivate((item, itemToggle), user: playerEnt));
|
|
}
|
|
});
|
|
|
|
await RunTicks(1);
|
|
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
|
|
if (enableToggleable && itemToggle != null)
|
|
Assert.That(itemToggle.Activated);
|
|
|
|
return SEntMan.GetNetEntity(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pick up an entity. Defaults to just deleting the previously held entity.
|
|
/// </summary>
|
|
protected async Task Pickup(NetEntity? entity = null, bool deleteHeld = true)
|
|
{
|
|
entity ??= Target;
|
|
|
|
if (Hands.ActiveHand == null)
|
|
{
|
|
Assert.Fail("No active hand");
|
|
return;
|
|
}
|
|
|
|
if (deleteHeld)
|
|
await DeleteHeldEntity();
|
|
|
|
var uid = SEntMan.GetEntity(entity);
|
|
|
|
if (!SEntMan.TryGetComponent(uid, out ItemComponent? item))
|
|
{
|
|
Assert.Fail($"Entity {entity} is not an item");
|
|
return;
|
|
}
|
|
|
|
await Server.WaitPost(() =>
|
|
{
|
|
Assert.That(HandSys.TryPickup(SEntMan.GetEntity(Player), uid.Value, Hands.ActiveHand, false, false, Hands, item));
|
|
});
|
|
|
|
await RunTicks(1);
|
|
Assert.That(Hands.ActiveHandEntity, Is.EqualTo(uid));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops the currently held entity.
|
|
/// </summary>
|
|
protected async Task Drop()
|
|
{
|
|
if (Hands.ActiveHandEntity == null)
|
|
{
|
|
Assert.Fail("Not holding any entity to drop");
|
|
return;
|
|
}
|
|
|
|
await Server.WaitPost(() =>
|
|
{
|
|
Assert.That(HandSys.TryDrop(SEntMan.GetEntity(Player), handsComp: Hands));
|
|
});
|
|
|
|
await RunTicks(1);
|
|
Assert.That(Hands.ActiveHandEntity, Is.Null);
|
|
}
|
|
|
|
#region Interact
|
|
|
|
/// <summary>
|
|
/// Use the currently held entity.
|
|
/// </summary>
|
|
protected async Task UseInHand()
|
|
{
|
|
if (Hands.ActiveHandEntity is not { } target)
|
|
{
|
|
Assert.Fail("Not holding any entity");
|
|
return;
|
|
}
|
|
|
|
await Server.WaitPost(() =>
|
|
{
|
|
InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetComponent<TransformComponent>(target).Coordinates, target);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Place an entity prototype into the players hand and interact with the given entity (or target position)
|
|
/// </summary>
|
|
/// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
|
|
/// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
|
|
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
|
|
protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
|
|
{
|
|
await InteractUsing((id, quantity), awaitDoAfters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Place an entity prototype into the players hand and interact with the given entity (or target position).
|
|
/// </summary>
|
|
/// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
|
|
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
|
|
protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
|
|
{
|
|
// For every interaction, we will also examine the entity, just in case this breaks something, somehow.
|
|
// (e.g., servers attempt to assemble construction examine hints).
|
|
if (Target != null)
|
|
{
|
|
await Client.WaitPost(() => ExamineSys.DoExamine(CEntMan.GetEntity(Target.Value)));
|
|
}
|
|
|
|
await PlaceInHands(entity);
|
|
await Interact(awaitDoAfters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interact with an entity using the currently held entity.
|
|
/// </summary>
|
|
/// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
|
|
protected async Task Interact(bool awaitDoAfters = true)
|
|
{
|
|
if (Target == null || !Target.Value.IsClientSide())
|
|
{
|
|
await Interact(Target, TargetCoords, awaitDoAfters);
|
|
return;
|
|
}
|
|
|
|
// The target is a client-side entity, so we will just attempt to start construction under the assumption that
|
|
// it is a construction ghost.
|
|
|
|
await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
|
|
await RunTicks(5);
|
|
|
|
if (awaitDoAfters)
|
|
await AwaitDoAfters();
|
|
|
|
await CheckTargetChange();
|
|
}
|
|
|
|
/// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
|
|
protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
|
|
{
|
|
Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
|
|
var coords = SEntMan.GetCoordinates(coordinates);
|
|
Assert.That(coords.IsValid(SEntMan));
|
|
await Interact(sTarget, coords, awaitDoAfters);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interact with an entity using the currently held entity.
|
|
/// </summary>
|
|
protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
|
|
{
|
|
Assert.That(SEntMan.TryGetEntity(Player, out var player));
|
|
|
|
await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
|
|
await RunTicks(1);
|
|
|
|
if (awaitDoAfters)
|
|
await AwaitDoAfters();
|
|
|
|
await CheckTargetChange();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Activate an entity.
|
|
/// </summary>
|
|
protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
|
|
{
|
|
target ??= Target;
|
|
Assert.That(target, Is.Not.Null);
|
|
Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
|
|
Assert.That(SEntMan.TryGetEntity(Player, out var player));
|
|
|
|
await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
|
|
await RunTicks(1);
|
|
|
|
if (awaitDoAfters)
|
|
await AwaitDoAfters();
|
|
|
|
await CheckTargetChange();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
|
|
/// Useful for quickly finishing multiple construction steps.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Empty strings imply empty hands.
|
|
/// </remarks>
|
|
protected async Task Interact(params EntitySpecifier[] specifiers)
|
|
{
|
|
foreach (var spec in specifiers)
|
|
{
|
|
await InteractUsing(spec);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Throw the currently held entity. Defaults to targeting the current <see cref="TargetCoords"/>
|
|
/// </summary>
|
|
protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDistance = 4)
|
|
{
|
|
var actualTarget = SEntMan.GetCoordinates(target ?? TargetCoords);
|
|
var result = false;
|
|
await Server.WaitPost(() => result = HandSys.ThrowHeldItem(SEntMan.GetEntity(Player), actualTarget, minDistance));
|
|
return result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Wait for any currently active DoAfters to finish.
|
|
/// </summary>
|
|
protected async Task AwaitDoAfters(int maxExpected = 1)
|
|
{
|
|
if (!ActiveDoAfters.Any())
|
|
return;
|
|
|
|
// Generally expect interactions to only start one DoAfter.
|
|
Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
|
|
|
|
// wait out the DoAfters.
|
|
var doAfters = ActiveDoAfters.ToList();
|
|
while (ActiveDoAfters.Any())
|
|
{
|
|
await RunTicks(10);
|
|
}
|
|
|
|
foreach (var doAfter in doAfters)
|
|
{
|
|
Assert.That(!doAfter.Cancelled);
|
|
}
|
|
|
|
await RunTicks(5);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancel any currently active DoAfters. Default arguments are such that it also checks that there is at least one
|
|
/// active DoAfter to cancel.
|
|
/// </summary>
|
|
protected async Task CancelDoAfters(int minExpected = 1, int maxExpected = 1)
|
|
{
|
|
Assert.That(ActiveDoAfters.Count(), Is.GreaterThanOrEqualTo(minExpected));
|
|
Assert.That(ActiveDoAfters.Count(), Is.LessThanOrEqualTo(maxExpected));
|
|
|
|
if (!ActiveDoAfters.Any())
|
|
return;
|
|
|
|
// Cancel all the do-afters
|
|
var doAfters = ActiveDoAfters.ToList();
|
|
await Server.WaitPost(() =>
|
|
{
|
|
foreach (var doAfter in doAfters)
|
|
{
|
|
DoAfterSys.Cancel(SEntMan.GetEntity(Player), doAfter.Index, DoAfters);
|
|
}
|
|
});
|
|
|
|
await RunTicks(1);
|
|
|
|
foreach (var doAfter in doAfters)
|
|
{
|
|
Assert.That(doAfter.Cancelled);
|
|
}
|
|
|
|
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
|
|
/// a structure is being built.
|
|
/// </summary>
|
|
protected async Task CheckTargetChange()
|
|
{
|
|
if (Target == null)
|
|
return;
|
|
|
|
var originalTarget = Target.Value;
|
|
await RunTicks(5);
|
|
|
|
if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
|
|
{
|
|
CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
|
|
Target = newWeh;
|
|
}
|
|
|
|
if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
|
|
{
|
|
SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
|
|
Target = newServerWeh;
|
|
}
|
|
|
|
if (Target != originalTarget)
|
|
await CheckTargetChange();
|
|
}
|
|
|
|
#region Asserts
|
|
|
|
protected void ClientAssertPrototype(string? prototype, NetEntity? target = null)
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
|
|
Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
|
|
}
|
|
|
|
protected void AssertPrototype(string? prototype, NetEntity? target = null)
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
|
|
Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
|
|
}
|
|
|
|
protected void AssertAnchored(bool anchored = true, NetEntity? target = null)
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
var sXform = SEntMan.GetComponent<TransformComponent>(SEntMan.GetEntity(target.Value));
|
|
var cXform = CEntMan.GetComponent<TransformComponent>(CEntMan.GetEntity(target.Value));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(sXform.Anchored, Is.EqualTo(anchored));
|
|
Assert.That(cXform.Anchored, Is.EqualTo(anchored));
|
|
});
|
|
}
|
|
|
|
protected void AssertDeleted(NetEntity? target = null)
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(SEntMan.Deleted(SEntMan.GetEntity(target)));
|
|
Assert.That(CEntMan.Deleted(CEntMan.GetEntity(target)));
|
|
});
|
|
}
|
|
|
|
protected void AssertExists(NetEntity? target = null)
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(SEntMan.EntityExists(SEntMan.GetEntity(target)));
|
|
Assert.That(CEntMan.EntityExists(CEntMan.GetEntity(target)));
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assert whether or not the target has the given component.
|
|
/// </summary>
|
|
protected void AssertComp<T>(bool hasComp = true, NetEntity? target = null) where T : IComponent
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return;
|
|
}
|
|
|
|
Assert.That(SEntMan.HasComponent<T>(SEntMan.GetEntity(target)), Is.EqualTo(hasComp));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check that the tile at the target position matches some prototype.
|
|
/// </summary>
|
|
protected async Task AssertTile(string? proto, NetCoordinates? coords = null)
|
|
{
|
|
var targetTile = proto == null
|
|
? Tile.Empty
|
|
: new Tile(TileMan[proto].TileId);
|
|
|
|
var tile = Tile.Empty;
|
|
var serverCoords = SEntMan.GetCoordinates(coords ?? TargetCoords);
|
|
var pos = Transform.ToMapCoordinates(serverCoords);
|
|
await Server.WaitPost(() =>
|
|
{
|
|
if (MapMan.TryFindGridAt(pos, out var gridUid, out var grid))
|
|
tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile;
|
|
});
|
|
|
|
Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
|
|
}
|
|
|
|
protected void AssertGridCount(int value)
|
|
{
|
|
var count = 0;
|
|
var query = SEntMan.AllEntityQueryEnumerator<MapGridComponent, TransformComponent>();
|
|
while (query.MoveNext(out _, out var xform))
|
|
{
|
|
if (xform.MapUid == MapData.MapUid)
|
|
count++;
|
|
}
|
|
|
|
Assert.That(count, Is.EqualTo(value));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Entity lookups
|
|
|
|
/// <summary>
|
|
/// Returns entities in an area around the target. Ignores the map, grid, player, target, and contained entities.
|
|
/// </summary>
|
|
protected async Task<HashSet<EntityUid>> DoEntityLookup(LookupFlags flags = LookupFlags.Uncontained)
|
|
{
|
|
var lookup = SEntMan.System<EntityLookupSystem>();
|
|
|
|
HashSet<EntityUid> entities = default!;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
// Get all entities left behind by deconstruction
|
|
entities = lookup.GetEntitiesIntersecting(MapId, Box2.CentredAroundZero(new Vector2(10, 10)), flags);
|
|
|
|
var xformQuery = SEntMan.GetEntityQuery<TransformComponent>();
|
|
|
|
HashSet<EntityUid> toRemove = new();
|
|
foreach (var ent in entities)
|
|
{
|
|
var transform = xformQuery.GetComponent(ent);
|
|
var netEnt = SEntMan.GetNetEntity(ent);
|
|
|
|
if (ent == transform.MapUid
|
|
|| ent == transform.GridUid
|
|
|| netEnt == Player
|
|
|| netEnt == Target)
|
|
{
|
|
toRemove.Add(ent);
|
|
}
|
|
}
|
|
|
|
entities.ExceptWith(toRemove);
|
|
});
|
|
|
|
return entities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
|
|
/// Ignores the grid, map, player, target and contained entities.
|
|
/// </summary>
|
|
protected async Task AssertEntityLookup(params EntitySpecifier[] entities)
|
|
{
|
|
var collection = new EntitySpecifierCollection(entities);
|
|
await AssertEntityLookup(collection);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an entity lookup and asserts that only the listed entities exist and that they are all present.
|
|
/// Ignores the grid, map, player, target, contained entities, and entities with null prototypes.
|
|
/// </summary>
|
|
protected async Task AssertEntityLookup(
|
|
EntitySpecifierCollection collection,
|
|
bool failOnMissing = true,
|
|
bool failOnExcess = true,
|
|
LookupFlags flags = LookupFlags.Uncontained)
|
|
{
|
|
var expected = collection.Clone();
|
|
var entities = await DoEntityLookup(flags);
|
|
var found = ToEntityCollection(entities);
|
|
expected.Remove(found);
|
|
await expected.ConvertToStacks(ProtoMan, Factory, Server);
|
|
|
|
if (expected.Entities.Count == 0)
|
|
return;
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
foreach (var (proto, quantity) in expected.Entities)
|
|
{
|
|
if (proto == "Audio")
|
|
continue;
|
|
|
|
if (quantity < 0 && failOnExcess)
|
|
Assert.Fail($"Unexpected entity/stack: {proto}, quantity: {-quantity}");
|
|
|
|
if (quantity > 0 && failOnMissing)
|
|
Assert.Fail($"Missing entity/stack: {proto}, quantity: {quantity}");
|
|
|
|
if (quantity == 0)
|
|
throw new Exception("Error in entity collection math.");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an entity lookup and attempts to find an entity matching the given entity specifier.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is used to check that an item-crafting attempt was successful. Ideally crafting items would just return the
|
|
/// entity or raise an event or something.
|
|
/// </remarks>
|
|
protected async Task<EntityUid> FindEntity(
|
|
EntitySpecifier spec,
|
|
LookupFlags flags = LookupFlags.Uncontained | LookupFlags.Contained,
|
|
bool shouldSucceed = true)
|
|
{
|
|
await spec.ConvertToStack(ProtoMan, Factory, Server);
|
|
|
|
var entities = await DoEntityLookup(flags);
|
|
foreach (var uid in entities)
|
|
{
|
|
var found = ToEntitySpecifier(uid);
|
|
if (found is null)
|
|
continue;
|
|
|
|
if (spec.Prototype != found.Prototype)
|
|
continue;
|
|
|
|
if (found.Quantity >= spec.Quantity)
|
|
return uid;
|
|
|
|
// TODO combine stacks?
|
|
}
|
|
|
|
if (shouldSucceed)
|
|
Assert.Fail($"Could not find stack/entity with prototype {spec.Prototype}");
|
|
|
|
return default;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// List of currently active DoAfters on the player.
|
|
/// </summary>
|
|
protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
|
|
=> DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
|
|
|
|
#region Component
|
|
|
|
/// <summary>
|
|
/// Convenience method to get components on the target. Returns SERVER-SIDE components.
|
|
/// </summary>
|
|
protected T Comp<T>(NetEntity? target = null) where T : IComponent
|
|
{
|
|
target ??= Target;
|
|
if (target == null)
|
|
Assert.Fail("No target specified");
|
|
|
|
return SEntMan.GetComponent<T>(ToServer(target!.Value));
|
|
}
|
|
|
|
/// <inheritdoc cref="Comp{T}"/>
|
|
protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
|
|
{
|
|
return SEntMan.TryGetComponent(ToServer(target), out comp);
|
|
}
|
|
|
|
/// <inheritdoc cref="Comp{T}"/>
|
|
protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
|
|
{
|
|
return SEntMan.TryGetComponent(STarget, out comp);
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Set the tile at the target position to some prototype.
|
|
/// </summary>
|
|
protected async Task SetTile(string? proto, NetCoordinates? coords = null, Entity<MapGridComponent>? grid = null)
|
|
{
|
|
var tile = proto == null
|
|
? Tile.Empty
|
|
: new Tile(TileMan[proto].TileId);
|
|
|
|
var pos = Transform.ToMapCoordinates(SEntMan.GetCoordinates(coords ?? TargetCoords));
|
|
|
|
EntityUid gridUid;
|
|
MapGridComponent? gridComp;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
if (grid is { } gridEnt)
|
|
{
|
|
MapSystem.SetTile(gridEnt, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
|
|
return;
|
|
}
|
|
else if (MapMan.TryFindGridAt(pos, out var gUid, out var gComp))
|
|
{
|
|
MapSystem.SetTile(gUid, gComp, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
|
|
return;
|
|
}
|
|
|
|
if (proto == null)
|
|
return;
|
|
|
|
gridEnt = MapMan.CreateGridEntity(MapData.MapId);
|
|
grid = gridEnt;
|
|
gridUid = gridEnt;
|
|
gridComp = gridEnt.Comp;
|
|
var gridXform = SEntMan.GetComponent<TransformComponent>(gridUid);
|
|
Transform.SetWorldPosition(gridXform, pos.Position);
|
|
MapSystem.SetTile((gridUid, gridComp), SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
|
|
|
|
if (!MapMan.TryFindGridAt(pos, out _, out _))
|
|
Assert.Fail("Failed to create grid?");
|
|
});
|
|
await AssertTile(proto, coords);
|
|
}
|
|
|
|
protected async Task Delete(EntityUid uid)
|
|
{
|
|
await Server.WaitPost(() => SEntMan.DeleteEntity(uid));
|
|
await RunTicks(5);
|
|
}
|
|
|
|
protected Task Delete(NetEntity nuid)
|
|
{
|
|
return Delete(SEntMan.GetEntity(nuid));
|
|
}
|
|
|
|
#region Time/Tick managment
|
|
|
|
protected async Task RunTicks(int ticks)
|
|
{
|
|
await Pair.RunTicksSync(ticks);
|
|
}
|
|
|
|
protected async Task RunSeconds(float seconds)
|
|
{
|
|
await Pair.RunSeconds(seconds);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region BUI
|
|
/// <summary>
|
|
/// Sends a bui message using the given bui key.
|
|
/// </summary>
|
|
protected async Task SendBui(Enum key, BoundUserInterfaceMessage msg, EntityUid? _ = null)
|
|
{
|
|
if (!TryGetBui(key, out var bui))
|
|
return;
|
|
|
|
await Client.WaitPost(() => bui.SendMessage(msg));
|
|
|
|
// allow for client -> server and server -> client messages to be sent.
|
|
await RunTicks(15);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a bui message using the given bui key.
|
|
/// </summary>
|
|
protected async Task CloseBui(Enum key, EntityUid? _ = null)
|
|
{
|
|
if (!TryGetBui(key, out var bui))
|
|
return;
|
|
|
|
await Client.WaitPost(() => bui.Close());
|
|
|
|
// allow for client -> server and server -> client messages to be sent.
|
|
await RunTicks(15);
|
|
}
|
|
|
|
protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? bui, NetEntity? target = null, bool shouldSucceed = true)
|
|
{
|
|
bui = null;
|
|
target ??= Target;
|
|
if (target == null)
|
|
{
|
|
Assert.Fail("No target specified");
|
|
return false;
|
|
}
|
|
|
|
if (!CEntMan.TryGetComponent<UserInterfaceComponent>(CEntMan.GetEntity(target), out var ui))
|
|
{
|
|
if (shouldSucceed)
|
|
Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have a bui component");
|
|
return false;
|
|
}
|
|
|
|
if (!ui.ClientOpenInterfaces.TryGetValue(key, out bui))
|
|
{
|
|
if (shouldSucceed)
|
|
Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have an open bui with key {key.GetType()}.{key}.");
|
|
return false;
|
|
}
|
|
|
|
var bui2 = bui;
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(bui2.UiKey, Is.EqualTo(key), $"Bound user interface {bui2} is indexed by a key other than the one assigned to it somehow. {bui2.UiKey} != {key}");
|
|
Assert.That(shouldSucceed, Is.True);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
protected bool IsUiOpen(Enum key)
|
|
{
|
|
if (!TryComp(Player, out UserInterfaceUserComponent? user))
|
|
return false;
|
|
|
|
foreach (var keys in user.OpenInterfaces.Values)
|
|
{
|
|
if (keys.Contains(key))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UI
|
|
|
|
/// <summary>
|
|
/// Attempts to find, and then presses and releases a control on some client-side window.
|
|
/// Will fail if the control cannot be found.
|
|
/// </summary>
|
|
protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
|
|
where TWindow : BaseWindow
|
|
where TControl : Control
|
|
{
|
|
var window = GetWindow<TWindow>();
|
|
var control = GetControlFromField<TControl>(name, window);
|
|
await ClickControl(control, function);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to find, and then presses and releases a control on some client-side widget.
|
|
/// Will fail if the control cannot be found.
|
|
/// </summary>
|
|
protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
|
|
where TWidget : UIWidget, new()
|
|
where TControl : Control
|
|
{
|
|
var widget = GetWidget<TWidget>();
|
|
var control = GetControlFromField<TControl>(name, widget);
|
|
await ClickControl(control, function);
|
|
}
|
|
|
|
/// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
|
|
protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
|
|
where TWindow : BaseWindow
|
|
{
|
|
await ClickControl<TWindow, Control>(name, function);
|
|
}
|
|
|
|
/// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
|
|
protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
|
|
where TWidget : UIWidget, new()
|
|
{
|
|
await ClickWidgetControl<TWidget, Control>(name, function);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulates a click and release at the center of some UI control.
|
|
/// </summary>
|
|
protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
|
|
{
|
|
function ??= EngineKeyFunctions.UIClick;
|
|
var screenCoords = new ScreenCoordinates(
|
|
control.GlobalPixelPosition + control.PixelSize / 2,
|
|
control.Window?.Id ?? default);
|
|
|
|
var relativePos = screenCoords.Position / control.UIScale - control.GlobalPosition;
|
|
var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
|
|
|
|
var args = new GUIBoundKeyEventArgs(
|
|
function.Value,
|
|
BoundKeyState.Down,
|
|
screenCoords,
|
|
default,
|
|
relativePos,
|
|
relativePixelPos);
|
|
|
|
await Client.DoGuiEvent(control, args);
|
|
await RunTicks(1);
|
|
|
|
args = new GUIBoundKeyEventArgs(
|
|
function.Value,
|
|
BoundKeyState.Up,
|
|
screenCoords,
|
|
default,
|
|
relativePos,
|
|
relativePixelPos);
|
|
|
|
await Client.DoGuiEvent(control, args);
|
|
await RunTicks(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to retrieve a control by looking for a field on some other control.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Will fail if the control cannot be found.
|
|
/// </remarks>
|
|
protected TControl GetControlFromField<TControl>(string name, Control parent)
|
|
where TControl : Control
|
|
{
|
|
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
var parentType = parent.GetType();
|
|
var field = parentType.GetField(name, flags);
|
|
var prop = parentType.GetProperty(name, flags);
|
|
|
|
if (field == null && prop == null)
|
|
{
|
|
Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
|
|
return default!;
|
|
}
|
|
|
|
var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
|
|
|
|
if (fieldOrProp is not Control control)
|
|
{
|
|
Assert.Fail($"{name} was null or was not a control.");
|
|
return default!;
|
|
}
|
|
|
|
Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
|
|
return (TControl) control;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Will fail if the control cannot be found.
|
|
/// </remarks>
|
|
protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
|
|
where TControl : Control
|
|
{
|
|
if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
|
|
return control;
|
|
|
|
Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
|
|
return default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to retrieve a control of a given type by iterating through a control's children.
|
|
/// </summary>
|
|
protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
|
|
where TControl : Control
|
|
{
|
|
return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
|
|
/// </summary>
|
|
protected bool TryGetControlFromChildren<TControl>(
|
|
Func<TControl, bool> predicate,
|
|
Control parent,
|
|
[NotNullWhen(true)] out TControl? control,
|
|
bool recursive = true)
|
|
where TControl : Control
|
|
{
|
|
foreach (var ctrl in parent.Children)
|
|
{
|
|
if (ctrl is TControl cast && predicate(cast))
|
|
{
|
|
control = cast;
|
|
return true;
|
|
}
|
|
|
|
if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
|
|
return true;
|
|
}
|
|
|
|
control = null;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to find a currently open client-side window. Will fail if the window cannot be found.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that this just returns the very first open window of this type that is found.
|
|
/// </remarks>
|
|
protected TWindow GetWindow<TWindow>() where TWindow : BaseWindow
|
|
{
|
|
if (TryFindWindow(out TWindow? window))
|
|
return window;
|
|
|
|
Assert.Fail($"Could not find a window assignable to {nameof(TWindow)}");
|
|
return default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to find a currently open client-side window.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that this just returns the very first open window of this type that is found.
|
|
/// </remarks>
|
|
protected bool TryFindWindow<TWindow>([NotNullWhen(true)] out TWindow? window) where TWindow : BaseWindow
|
|
{
|
|
TryFindWindow(typeof(TWindow), out var control);
|
|
window = control as TWindow;
|
|
return window != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to find a currently open client-side window.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Note that this just returns the very first open window of this type that is found.
|
|
/// </remarks>
|
|
protected bool TryFindWindow(Type type, [NotNullWhen(true)] out BaseWindow? window)
|
|
{
|
|
Assert.That(type.IsAssignableTo(typeof(BaseWindow)));
|
|
window = UiMan.WindowRoot.Children
|
|
.OfType<BaseWindow>()
|
|
.Where(x => x.IsOpen)
|
|
.FirstOrDefault(x => x.GetType().IsAssignableTo(type));
|
|
|
|
return window != null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Attempts to find client-side UI widget.
|
|
/// </summary>
|
|
protected UIWidget GetWidget<TWidget>()
|
|
where TWidget : UIWidget, new()
|
|
{
|
|
if (TryFindWidget(out TWidget? widget))
|
|
return widget;
|
|
|
|
Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
|
|
return default!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to find client-side UI widget.
|
|
/// </summary>
|
|
private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
|
|
where TWidget : UIWidget, new()
|
|
{
|
|
uiWidget = null;
|
|
var screen = UiMan.ActiveScreen;
|
|
if (screen == null)
|
|
return false;
|
|
|
|
return screen.TryGetWidget(out uiWidget);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Power
|
|
|
|
protected void ToggleNeedPower(NetEntity? target = null)
|
|
{
|
|
var comp = Comp<ApcPowerReceiverComponent>(target);
|
|
comp.NeedsPower = !comp.NeedsPower;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Map Setup
|
|
|
|
/// <summary>
|
|
/// Adds gravity to a given entity. Defaults to the grid if no entity is specified.
|
|
/// </summary>
|
|
protected async Task AddGravity(EntityUid? uid = null)
|
|
{
|
|
var target = uid ?? MapData.Grid;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
var gravity = SEntMan.EnsureComponent<GravityComponent>(target);
|
|
SEntMan.System<GravitySystem>().EnableGravity(target, gravity);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a default atmosphere to the test map.
|
|
/// </summary>
|
|
protected async Task AddAtmosphere(EntityUid? uid = null)
|
|
{
|
|
var target = uid ?? MapData.MapUid;
|
|
await Server.WaitPost(() =>
|
|
{
|
|
var atmosSystem = SEntMan.System<AtmosphereSystem>();
|
|
var moles = new float[Atmospherics.AdjustedNumberOfGases];
|
|
moles[(int) Gas.Oxygen] = 21.824779f;
|
|
moles[(int) Gas.Nitrogen] = 82.10312f;
|
|
atmosSystem.SetMapAtmosphere(target, false, new GasMixture(moles, Atmospherics.T20C));
|
|
});
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Inputs
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Make the client press and then release a key. This assumes the key is currently released.
|
|
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
|
|
/// </summary>
|
|
protected async Task PressKey(
|
|
BoundKeyFunction key,
|
|
int ticks = 1,
|
|
NetCoordinates? coordinates = null,
|
|
NetEntity? cursorEntity = null)
|
|
{
|
|
await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
|
|
await RunTicks(ticks);
|
|
await SetKey(key, BoundKeyState.Up, coordinates, cursorEntity);
|
|
await RunTicks(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make the client press or release a key.
|
|
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
|
|
/// </summary>
|
|
protected async Task SetKey(
|
|
BoundKeyFunction key,
|
|
BoundKeyState state,
|
|
NetCoordinates? coordinates = null,
|
|
NetEntity? cursorEntity = null,
|
|
ScreenCoordinates? screenCoordinates = null)
|
|
{
|
|
var coords = coordinates ?? TargetCoords;
|
|
var target = cursorEntity ?? Target ?? default;
|
|
var screen = screenCoordinates ?? default;
|
|
|
|
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
|
|
var message = new ClientFullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId)
|
|
{
|
|
State = state,
|
|
Coordinates = CEntMan.GetCoordinates(coords),
|
|
ScreenCoordinates = screen,
|
|
Uid = CEntMan.GetEntity(target),
|
|
};
|
|
|
|
await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Variant of <see cref="SetKey"/> for setting movement keys.
|
|
/// </summary>
|
|
protected async Task SetMovementKey(DirectionFlag dir, BoundKeyState state)
|
|
{
|
|
if ((dir & DirectionFlag.South) != 0)
|
|
await SetKey(EngineKeyFunctions.MoveDown, state);
|
|
|
|
if ((dir & DirectionFlag.East) != 0)
|
|
await SetKey(EngineKeyFunctions.MoveRight, state);
|
|
|
|
if ((dir & DirectionFlag.North) != 0)
|
|
await SetKey(EngineKeyFunctions.MoveUp, state);
|
|
|
|
if ((dir & DirectionFlag.West) != 0)
|
|
await SetKey(EngineKeyFunctions.MoveLeft, state);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make the client hold the move key in some direction for some amount of time.
|
|
/// </summary>
|
|
protected async Task Move(DirectionFlag dir, float seconds)
|
|
{
|
|
await SetMovementKey(dir, BoundKeyState.Down);
|
|
await RunSeconds(seconds);
|
|
await SetMovementKey(dir, BoundKeyState.Up);
|
|
await RunTicks(1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Networking
|
|
|
|
protected EntityUid ToServer(NetEntity nent) => SEntMan.GetEntity(nent);
|
|
protected EntityUid ToClient(NetEntity nent) => CEntMan.GetEntity(nent);
|
|
protected EntityUid? ToServer(NetEntity? nent) => SEntMan.GetEntity(nent);
|
|
protected EntityUid? ToClient(NetEntity? nent) => CEntMan.GetEntity(nent);
|
|
protected EntityUid ToServer(EntityUid cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
|
|
protected EntityUid ToClient(EntityUid cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
|
|
protected EntityUid? ToServer(EntityUid? cuid) => SEntMan.GetEntity(CEntMan.GetNetEntity(cuid));
|
|
protected EntityUid? ToClient(EntityUid? cuid) => CEntMan.GetEntity(SEntMan.GetNetEntity(cuid));
|
|
|
|
protected EntityCoordinates ToServer(NetCoordinates coords) => SEntMan.GetCoordinates(coords);
|
|
protected EntityCoordinates ToClient(NetCoordinates coords) => CEntMan.GetCoordinates(coords);
|
|
protected EntityCoordinates? ToServer(NetCoordinates? coords) => SEntMan.GetCoordinates(coords);
|
|
protected EntityCoordinates? ToClient(NetCoordinates? coords) => CEntMan.GetCoordinates(coords);
|
|
|
|
#endregion
|
|
|
|
#region Metadata & Transforms
|
|
|
|
protected MetaDataComponent Meta(NetEntity uid) => Meta(ToServer(uid));
|
|
protected MetaDataComponent Meta(EntityUid uid) => SEntMan.GetComponent<MetaDataComponent>(uid);
|
|
|
|
protected TransformComponent Xform(NetEntity uid) => Xform(ToServer(uid));
|
|
protected TransformComponent Xform(EntityUid uid) => SEntMan.GetComponent<TransformComponent>(uid);
|
|
|
|
protected EntityCoordinates Position(NetEntity uid) => Position(ToServer(uid));
|
|
protected EntityCoordinates Position(EntityUid uid) => Xform(uid).Coordinates;
|
|
|
|
#endregion
|
|
}
|