mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-30 16:17:27 +00:00
+88









deltanedas
EmoGarbage404
Tayrtahn
Alice4267
PJBot
Pgriha
Milon
TakoDragon
Hyper B
Kyle Tyo
K-Dynamic
YoungThug
lzk
Prole
RedBookcase
RedBookcase
Ed
Booblesnoot42
UBlueberry
Banedon
Lanedon
Princess Cheeseballs
uhbg
uhbg
Nemanja
Jackson Langley
slarticodefast
Entvari
dffdff2423
āda
IProduceWidgets
Spessmann
Leon Friedrich
Thomas
ScarKy0
Phooooooooooooooooooooooooooooooosphate
TytosB
Łukasz Lindert
Hrosts
T
ArtisticRoomba
Verm
J
ShadowCommander
Ghagliiarghii
Michael
mubururu_
pathetic meowmeow
ScarKy0
godisdeadLOL
YourUsername
beck-thompson
Golden Can
SlamBamActionman
Ilya246
Zachary Yona
Zachary Yona
Hanz
catlord
poklj
Vladislav Suchkov
kosticia
Smith
Winkarst
T-Stalker
Saphire Lattice
ArchRBX
archrbx
Southbridge
No Elka
PotRoastPiggy
Samuka-C
Coolsurf6
beck-thompson
deltanedas
SG6732
themias
Preston Smith
Nox
Victor Shen
Alzore
Kresny
chromiumboy
Radezolid
Kiri1674
kaiserbirch
Franz - Josef Björck
Wolfkey-SomeoneElseTookMyUsername
TGRCDev
OnyxTheBrave
Pieter-Jan Briers
SyaoranFox
metalgearsloth
qrwas
UpAndLeaves
Centronias
Tiniest Shark
KingFroozy
22aa10443e
* Fix glass duplication with beakers (#34441) Update chemistry.yml Fixing glass dupe. * Automatic changelog update * Heterochromia for Moth (#36061) * 1 * Fix local * formatting --------- Co-authored-by: Milon <milonpl.git@proton.me> * Salvage Airlock (#36375) * Salvage * atmos a * more muted colours * Mapping Related to AI * Fixed small naming inconsistency * Revert "Fixed small naming inconsistency" This reverts commit 985e1fd46a09e521c11dc345f470228e7907fbb9. * Revert "Mapping Related to AI" This reverts commit 5d5ae4feb819396c5c2a7413399ab5ce34f8683b. * Revert "atmos a" This reverts commit 529831e2584df27fa102ee1c970948fa3b6bab56. * indent --------- Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * Departmental shelves whitelist expansion (Attempt №3) (#34292) (disabled) * Expand-Shelves-Whitelist Made departmental shelves hold a wider variety of items. * Accidentally included "Packet" twice * Automatic changelog update * Replace GetEntitiesInTile with EntityLookupSystem equivalent (#36756) * commit GET!! * minor cleanup * Automatic changelog update * Unused dependency cleanup. (#36760) * take 2! fight!!! * final touches * reeeeeee * Fire protection for ERT engineering hardsuit (#34949) * Automatic changelog update * Nonlethal throwables crate (#35808) * Automatic changelog update * Use non-generic TryComp in `StationSpawningSystem` (#36769) Use non-generic TryComp in StationSpawningSystem * Add marker components for admin shenanigans (#36776) * Automatic changelog update * Restore `LimitedChargesComponent.MaxCharges` default value to 3 (#36778) * Wizard Figure Voiceline Fix #36771 (#36779) DA WIZARDgit add -A * change MV cable outline (#36751) * change MV cable outline * zamn * The Beverage Jug Can Now Look Open (#36070) * GenJug-Changes Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Guh Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Support For the Rest Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * New Sprites Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Newest Sprites Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Pirate Gear Tweaks. (#35986) * Tweaked pirate gear to be in-line with similar items. * Added attribution for flashlight sprites, made hardsuits Major Contraband * Added empty versions of the pirate revolver and flintlock. * Test fail fix hopefully? * Test fail fix hopefully V2.0 * Test fail fix hopefully V3.0 * Test fail fix hopefully V4.0 --------- Co-authored-by: RedBookcase <Usualmoves@gmail.com> * Automatic changelog update * Markings overhaul (#35938) * markings displacement setup * ok i got it! * fix map updating * remove trackingLayers * markings clean up and modernizize * marking disabling displacements * markings restriction * dehihienize * dehihiniezize 2 * aa * nice * Automatic changelog update * Resprite main altars (#35685) * resprite main altars * updated meta.json * Automatic changelog update * Adds beanies to the WinterDrobe! (#32946) * sprites * messing with the ymls * new sprites * that's a lotta yaml * make the sprites gooder * color adjustment * to the winterdrobe with you * layering fix * Automatic changelog update * Santa hat with a foldable beard (#36510) * Added foldable component to santa hat * Modified sprites, modified YAML. YAML is cursed * Changed santa hat description to mention the beard fold * Removed HideLayerClothing since it doesnt work * Changed the prefix from "beard" to "nobeard" to match with the fold/unfold verb * Credits for sprite changes --------- Co-authored-by: Lanedon <matth.galvez@gmail> * Automatic changelog update * Puddle Code Slipping Cleanup (#35845) * Init Commit * windows yelling at me to update commit * working commit, need prediciton and more dehardcoding * Project 0 warnings * Working Commit (Near Final) * ryder got confused commit * I love Merge Conflicts :) * Working commit, no prediction * Forgot the yaml changes * Comments and typos * Apparently while the reduced launch mult of lube was initialized it was never used so I revered back to default * Fixed an incorrect divisor * bit of cleanup * Prediciton fixed, and puddles now affect all entities * FORGOT TO RENAME A VERY IMPORTANT VARIABLE OOPS * Really big I forgor moment * Even bigger I forgor moment * four more merge conflicts to fix four more oopsies * fixed actual divide by zero moment and also im very dumb * Even bigger I forgor moment * four more merge conflicts to fix four more oopsies * fixed actual divide by zero moment and also im very dumb * Fix all test fails * code cleanup * Webedit whitespace * Code cleaup * whitespace webedit * whitespace webedit * whitespace webedit * whitespace removal * Comments and cleanup * Re-Added 20 warnings as per Ork's request * Cleanups * Spacing fix * bugfixes and cleanup * Small bugfix * Actually dirty the slipComp for real * Added Friction field to Reagent Prototype per design discussion * Sliding system is kill * The Bartender can now make Eggnog (#34065) * Preliminary eggnog changes (egg in shaker crashes?) * Fixed eggnog reagent name and description * added - to flavor * deleted a space * Added eggnog glass sprite * changed fill levels to match rsi * Added two more fill levels to eggnog * Removed fizziness, changed physical description, and fixed rsi --------- Co-authored-by: uhbg <29674581+uhbg@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Require traitors to maroon their objective no matter what (#35825) * jesus * okay. * OOPS * ok * ok Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * ok * Update Resources/Prototypes/Objectives/paradoxClone.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Resources/Prototypes/Objectives/base_objectives.yml Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * m * ok * ok --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Wizard Headset (#35732) * wizardcomms * wizheadset * Addressed requested changes and axed wizard channel due to redundancy. * oops overlooked this, dont tell anyone * another mistake fixed * removed wiz encryption key due to redundancy * accidentally left some punctuation in a now unrelated meta.json * Automatic changelog update * Remove MK ULTRA ai name (#35726) * Remove bad AI names * Fine, I won't try to sneak this in here. * Reorder electronics recipe file, add filter categories (#35713) * electronics split * split base recipe and category * all but machines * machine shuffle * reorder * delete unused recipes * rename cargo to supply * git weirdness * git weirdness 2 * Automatic changelog update * Cleanup warnings in `SharedPortalSystem` (#36747) * Cleanup warnings in SharedPortalSystem * Formatting while we're here * Replace uses of !Any() with Count == 0 * AnyEntitiesIntersecting * Binomial Number Selector for EntityTables (#36783) * binomial dist * lol initialize the int tho idiot * xml docs my forgotten * ditto * Add more ruins (#36785) * Fix NRE in GasTileOverlaySystem (#36793) * Automatic changelog update * Add RGA/RSI to Credits (#36704) * Add RGA and RSI to Credits * Move to thread + add directory field * Further Allocation Tweaks (#36800) * init * fix lockboxes * Automatic changelog update * Add the medical HUDs to medical's loadouts (except chem) (#32847) Added the medical HUD and eyepatch to the medical (CMO, doctor, paramed and intern) loadout to let them choose between. Also gives the option for the regular glasses. * Automatic changelog update * new salv ruins (#36798) * hotplate, kitcken grille bugfixes (#34776) * commit * cleanup * Automatic changelog update * Solution drag and drop now checks if container is closed and drainable (#34683) drainable fix * Automatic changelog update * fix some datasets (#36806) * Add rehydratable mop bucket cube, refactor the rehydratable yml (#34586) * Refactor rehydratable items * Finish main refactor * Add mop bucket cube * Move cube sprite and item size to RehydratableAnimalCube * Fix sprite attribution * Make it follow the conventions * Refactor wrapped cube, leave MonkeyCube tag only on the cube and the Syndie Sponge * Fix DehydratedSpaceCarp inheritance overwriting needed components * no need for this comment * remove whitespace Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * indentation Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Restore lost description * reorder stuff * line --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Increase thief to player ratio (#36531) * reduce max number of traitors, increase thief to player ratio * revert traitor max back to 8 * Automatic changelog update * Added a semi-functional genderfluid pin. (#35854) * Added a semi-function GenderfluidPin * Update pins.yml fix bug * Fix meta.json under the newly merged "fix as you go" policy * Update pin description --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Station AI Name Identifier (#36801) init * Automatic changelog update * Makes EventRole Ids properly set ID names. (#28958) * Add id binding system * Change so it activates when added too * Add to eventhumanoid * Add PDA binding, try to fix some things * Still not fixed, PDA is working right * Move to server, add on mind instead * Implement suggestions * Fix things * Finish review stuff * Messed up cluwne * Remove after only changing id too --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Various sharedsystem spring cleaning (#36200) * Various shared system warnings cleanup * More shared system warnings cleanup * Rearranging changes * Using correct transform for unbuckle * Small changes * Revert buckle change * Update Content.Shared/Ghost/SharedGhostSystem.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix some locale strings and make some code for them looks better (#32003) * Fix a lot locale strings add functions using fix identity for meat spike and creaming * comma * fixes for fixes * yeah * fuck * Automatic changelog update * Remove non-functional ninja exemption from `SpawnAndDeleteEntityCountTest` (#36813) Remove non-functional ninja exemption from SpawnAndDeleteEntityCountTest * Firelock temperature and pressure warning lights (#28339) * Add temperature and pressure lights to firelocks * Replace sprites with new pressure and temperature light versions * Remove obsolete deny and locked sprites * Add SpriteSync for warning light animation synchronization * Teal lights * Partial animation implementation * fixup! Teal lights * Temperature lights * Adjusted firelock light heights and added final warning light sprites * Fix colors * Testing colors * Update light sprites * updated sprites --------- Co-authored-by: EmoGarbage404 <retron404@gmail.com> * Automatic changelog update * Fix bots parenting, emotes (#29949) * Bots overhaul * fix * change speech sound to pai * purge a lot * i forgor :trollface: * adjust * that aswell * Automatic changelog update * Fix identity grammar (#36817) Fix identity * Fix Animal Versions of Captain Uniform (#36804) * scienceevasuit * update money uniforms * update meta.json files and hamster hats * revert scienceevasuit * revert accidental prisoner eva change * Automatic changelog update * Fix vox hair (#36816) * Fix vox hair * remove CL * Tweak Security protection values (#30212) * Tweak Hardsuits * HoS/Ward Coats change * Whoopsie wrong number god why am i doing this its 1:33 AM and i made a silly mistake... * Adress Review Revert Warden hardsuit slowdown to 30% again Decreased Security hardsuit heat protection down to 20% * Automatic changelog update * Added more salvage ruins (#36822) * Metal foam grenade rework, small tweaks to grenade timers (#34579) * Automatic changelog update * dragons can now pry doors (#36811) * Automatic changelog update * Make funding allocation computer more configurable (#36790) * Make funding allocation computer more configurable * admin logging * unused * ccvar enabled --------- Co-authored-by: ScarKy0 <scarky0@onet.eu> * Automatic changelog update * Fingerprint taking improvements (#31864) * now using event to check access to fingerprint * Opps actually commiting the changes --------- Co-authored-by: YourUsername <you@example.com> Co-authored-by: beck-thompson <beck314159@hotmail.com> * Banana cream pie loc improvements (#36829) * CAPITALIZE(THE($owner)) * Rename "thrower" to "thrown"; use THE($thrown) * "the banana cream pie" -> "a banana cream pie" * Simplify filter * Automatic changelog update * Command uniform (#32103) * Added generic command jumpskirt, jumpsuit and cap, and made them available in the HoP's clothing printer. * fixed generic command jumpskirt and jumpsuit. * Updated generic command jumpskirt/-suit textures, added generic command beret. * Fixed game-breaking bug in previous commit relating to the command beret. * altered generic command soft cap/baseball cap sprites. * Update Resources/Prototypes/Entities/Clothing/Head/hats.yml Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> * Update Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> * Update Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> * review * whoopsie --------- Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * downprice many cargo orders (#32375) * atmos repricing * emitter crate * materials * lasers * revert gun prices * Automatic changelog update * Ashen hud item status slot fix (#31411) * new patchMargin variable added, horizontals fixed * All sides now work, system is fully integarated --------- Co-authored-by: Zachary Yona <magicalusf@gmail.com> * Ashtray Resprite and Fix (#31643) * The meat and potatoes Yup * Stupid recycler bruh * guh guh * Update MinorAntagonists.xml (#36836) * Simulate command in minutes for real (#36808) * 60 * Fix warning CS0414 instances (#36832) Fix warning cs0414 instances * Nerf Firelock electronic prices (#33469) * Nerf Firelock electronic prices - make firelock electronics price half of the input cost to make one (27) rounded up * Why does firelock make glass? - remove the Physical composition of glass from FirelockElectronics as the input does not contain glass and the output cannot contain half a steel and plastic --------- Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * Give Admins "Tails" (#36842) One line yaml change * Fix construction orientation being ignored for some objects (#36488) * fixes * removed empty "SnapToGrid"s and parent "completed"s * Automatic changelog update * elemental.yml & asteroid.yml cleanup (#36448) * Moth-pockets (#34517) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Four new food crates for the ATS (#33286) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Cleanup: Move folders out of bureaucracy.rsi and paper.yml (#35108) Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Improve LocalizedDataset test to check for low Count (#36848) * Improve LocalizedDataset test to check for low Count * Fix missing entries for FigurinesFootsoldier * Add 1 dragon name (#36846) * this is a commit you cannot skip * why do you have to add a count why * Remove steel sheet hull fixing (#33443) * Remove steel sheet hull fixing It's unintuitive that it's wasteful, there's not really any systems to NOT make it wasteful, and it's a UX trap. No this is not "noob trap", no. It's an UX issue because if hundreds of people do it over and over again without noticing anything wrong, well, something fails to properly note that there's an issue with what they are doing. Aka user experience issue. Aka not a problem with the user but the design. Also could be a shitty doafter action that makes you wonder if something's wrong * Why was this testing using steel of all things * Automatic changelog update * cleanup + add names to some entities (#36856) * Remove redundant fields from `MovementSpeedModifierComponent` (#36859) Delete _baseWalkSpeedVV and _baseSprintSpeedVV * Cleanup warnings in NinjaSuitSystem (#36861) Fix warnings in NinjaSuitSystem * Modernize `GhostComponent` & Ghost API (#36858) * Move CanReturnToBody to system * Move CanGhostInteract to system * Cleanup redundant datafields and viewvariables * Document datafields * Document component * Add SetTimeOfDeath Entity<T> overload, obsolete old version * Document public methods * Cleanup obsoleted method calls * Cleanup warning in `EmptyAllContainersBehaviour` (#36860) Fix warning in EmptyAllContainersBehaviour * Cleanup warnings in `SharedCryostorageSystem` (#36863) Fix warnings in SharedCryostorageSystem * Cleanup warnings in `CableSystem.Placer` (#36868) * Fix warnings in CableSystem.Placer * Formatting * Cleanup warnings in `PolymorphSystem.Map` (#36869) Fix warnings in PolymorphSystem.Map * Cleanup warnings in `WorldgenConfigSystem` (#36864) Fix warnings in WorldgenConfigSystem * Move guardian sounds to component (#36870) * Cleanup warnings in `SpawnSalvageMissionJob` (#36866) * DoMapInitialize and SetMapPaused * GridTileToLocal * Formatting * Remove unused variables * Update Content.Server/Salvage/SpawnSalvageMissionJob.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix warnings in `ServerDbManager` (#36871) Fix warnings in ServerDbManager * pAI Software Catalog (#36857) * initial commit * add the shop and make it work * add existing pai actions to the software shop * added power monitor app * killed the power and crew monitor software, too powercreepy * fix test failures * fix more test failures * fix merge conflicts * code changes to conform with review * fix unintentional submodule update * submodule update * remove unused currency prototype ref --------- Co-authored-by: archrbx <punk.gear5260@fastmail.com> * New Salvage Ruin - Hydro Outpost (#36826) * Automatic changelog update * Make toy sword less obvious (#34199) * YAML fuckery * Remove toy_sword.rsi Not a webedit btw * Automatic changelog update * Adds shorts/pants to ClothesMate vending machine (#35361) * Clothesmate shorts * matches price of vend to the new contents * Moths can eat pills (#35609) * Automatic changelog update * Xenoborgs part 1 (#36830) Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Updated Syndicate and Ammo Contraband (#35102) Co-authored-by: RedBookcase <crazykid1590@gmail.com> * Automatic changelog update * Put items inside cakes! (#31015) * First commit * I'm silly * Please be it * Some more fixes * Cleanup * fine! * removed = false * review --------- Co-authored-by: Milon <milonpl.git@proton.me> * Removed syndicate surgery duffel, added advanced circular saw to Medical Doctor uplink. (#35915) * Removed syndicate surgery duffel, added advanced circular saw to Medical Doctor uplink. * Update Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml Co-authored-by: Tayrtahn <tayrtahn@gmail.com> --------- Co-authored-by: RedBookcase <Usualmoves@gmail.com> Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * unremove surgery duffel * unfunny * Automatic changelog update * fix NoCargoOrderArbitrage fail (#36878) whoopsie * Cotton Grilled Cheese Sandwich (#36135) * Working entity. * New sprite by MadeOfHeartAndStone * Removed deadspace, changed attribution in meta.json * review --------- Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * Meat Patty (#34896) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Remove beaker from the MaterialArbitrageTest whitelist (#36876) * Turnstiles (#36313) * construction rotation fix * Turnstiles * renaming * review-slarticodefast-1 * mild attempts to fix (sorry sloth) * move some more shit * Remove engine dependency * grid agnostic * remove debug string * fix json * Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * remove pass delay for mispredict reasons. * most minor of changes * Give directional indicator on examine --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Genpop Closets & IDs (#36392) * Genpop IDs and Lockers * placeholder generation, no ui yet. * UI * Fix time offset * fix meta.jsons * big speller * Scarkyo review * Add turnstile prototypes * make IDs recyclable --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> * Automatic changelog update * Fix small misc. turnstile issues (#36893) * Fix small misc. turnstile issues * more missing things * quieter * New Spawners, Modified Spawners, new Service/Theatre Airlock, and new Colorful Light Crate (#36750) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Automatic changelog update * Disable shadows for observer pointlight (#36897) no shadows * Fix QSI bringing people into paused maps (#36834) * Automatic changelog update * Fix storage ui hovering over the hotbar (#36770) * Turnstiles lower volume + access reader (#36900) fixes * Fix disposal unit flush animation (#30677) * Fix disposal unit flush animation * Revert "Fix disposal unit flush animation" This reverts commit a596ecfd5f5368f87ef52cfb27884a8e000185c6. * control by setting layer visibility rather than overwriting * restore changes * Correct IdentityBlocker to include specific outer clothing (#33085) * Make IdentityBlocker include outerclothing * Modify Correct Bit * Fix mask/eye cobo * Automatic changelog update * Change some posters to be rules-compliant (#32734) * Initial commit * Minor edit * it's very hard to fit "disorder" * Revert No ERP poster, fix spawner * Attribution * Update text * Missed poster spawner --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Cyborg Rebalance (#34186) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Reptilians Can Eat Orange Creamsicles (#36890) * Automatic changelog update * Add Bloodstream to Goliaths (#33305) * Automatic changelog update * Nerf mining hardsuit's effectiveness against bullets and bombs. (#31450) * Automatic changelog update * Controls page guidebook rework 2025 Q1 (#36363) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Lower interdyne herbals TC cost (#36841) * Automatic changelog update * cleanup instruments yml (#32262) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Automatic changelog update * [FIX] Rubber now uneatable (#34543) * Releasing an under-pressure lockout is now a verb (#36910) Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * fix changelog (#36915) * Fix for ghosts being unable to follow the AI when it uses a holopad (#36355) * Initial commit * Added to-do * Automatic changelog update * Fix random maints room deleting whatever was already there (#36605) * Fix donuts tags breaking the bounty (#36903) * Automatic changelog update * Make 10u of "Atomic Bomb" drink instead of 11u (#36921) changed atomic bomb recipe * Automatic changelog update * PKA Modkits + Rebalance (#31247) * Automatic changelog update * Remove a redundant trycomp from TileAnomalySystem (#36924) * remove a redundant check * revert this space. * Land mine armament (#33883) * Land Mine is now armable, it will not explode unless armed. * Land Mine is now armable, it will not explode unless armed. * Explicitly have Armed as false * SharedLandMineSystem.cs adds the "Arm"-verb in "Content.Shared" with the Arming logic being implemented in "Content.Server" * Land Mines now blink only when armed. * Added prediction components, moved logic to SharedLandMineSystem.cs and inherit it in client content. * Accessing the datafield directly instead of using methods * Mines are now armed by default with a unarmed prototype * Land mine now shows if it is armed when examined and in range. * Landmine is unarmed by default with an armed variant for mapping purposes. * Removed properties that were already defined by inheritance. * Access the bool directly from the component * Add booleans to change if the Arm-verb is showed and if examining the mine shows the status. * Added status message for unarmed mine, removed using PushGroup since only one string is displayed. * Added properties to the explosive floor sign to ensure that it is armed, not showing neither status nor arm-verb. * The prototypes work now as before with added unarmed versions. Sprite is now only one toggable layer. * Make the craftable land mine unarmed. * Refactored the arming mechanic into own component and system. * Reverted the explosive wet floor sign to previous prototype and added the Armable component and ItemToggle to the landmines. * Moved the examination strings from land-mines.ftl to armable.ftl. * Removed unused property. * Formatting and fixing imports * Added prefixes to the ftl naming. Moved LocId from system to component * Added documentation. Moved check for armable to HandleStepTriggerAttempt. Moved the LocId to component. * Removed the TryArming method. Added documentation. * Removed unnecessary TryComp * Simplified the logic for the trigger attempt * HasComp instead of TryComp on logic * EmoGarbage Review --------- Co-authored-by: Franz - Josef Björck <kaiserbirch@proton.me> Co-authored-by: EmoGarbage404 <retron404@gmail.com> * Add Cotton Burgers (#36405) * Adds cotton burgers * Rephrased copyright * Added an extra comment * Adds CottonBurger to tags.yml, Not sure if i am supposed to do this or not. * Reordered Cotton bun YAML, and fixed overwritten tags * Merge issue fix? * Add experiment plushie and remove rubber chicken from cotton burgers * Minor comment change --------- Co-authored-by: beck-thompson <beck314159@hotmail.com> * hide light-switches from build menu (#34664) * Automatic changelog update * Fixed stinger grenade lag spikes (#36641) * Fixed stinger lag spikes * Simplify nullable checks * More cleanup of projectile grenades * Remove null default from ShootProjectile * Sheet-meister 2000 Cloth recipe (#32676) * Sheet-meister 2000 can now make rolls of cloth * Small Cleanup * resolve conflicts --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Automatic changelog update * Clear MIDI masters properly to avoid replay freezes (#36809) While trying to play a replay I noticed that the replay would freeze when seeking in some cases. After some debugging, I discovered that two MIDI renderers had each other as master, which caused an infinite loop processing MIDI events. I'm not entirely sure of the sequence of events that leads to this during replay playback, but I did notice that MIDI render masters are never set to null. This is in the best case just a memory leak, in the worst case probably the source of the bug, so... I fixed that. * Changed soundGunshot for Pulse Pistol and Pulse Carbine from laser_cannon to laser3 (#36952) * Automatic changelog update * The Atmos A Airlock (#36376) * airlocks * Update meta.json Added my modification in the meta * Changed the storage sizes of different swords. (#36564) Co-authored-by: RedBookcase <Usualmoves@gmail.com> * Automatic changelog update * Fix autorecharge (#36955) * Fix inflatable barriers finishing instantly if canceled! (#36937) Fix * Automatic changelog update * Only sec glasses can show contraband: second attempt (#36412) * Add base code for cheking contraband in hud * Fix missing using in InventorySystem.Relay * Fix errors and update HUD yml * Add show contraband with component on entity * fix component description * Update Content.Shared/Contraband/ShowContrabandDetailsComponent.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update "if" for check if ent hasComp in contrabandSystem * Remove << InventorySystem.Relay.cs * Update Content.Shared/Contraband/ShowContrabandDetailsComponent.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Shared/CCVar/CCVars.Game.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fix partial class ShowContrabandSystem Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Shared/Contraband/ShowContrabandDetailsComponent.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Update Content.Shared/Inventory/InventorySystem.Relay.cs Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Some update ShowContrabandSystem * Try with record struct * back again ti default class with EntityEventArgs * Remove EntityEventArgs * Finally use record struct --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Resolve a warning in SharedAnomalySystem and quell some linter whining. (#36939) * resolve a warning and deal with some linter whining. * Update Content.Shared/Anomaly/SharedAnomalySystem.cs --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * Automatic changelog update * Truncate lathe announcement lists (#36945) * Automatic changelog update * Descriptions for .30 Rifle (#36958) Initial commit * CMO Hardsuit: Zombification Resistance tweak (#36957) initial commit * Automatic changelog update * Adds Parcel Wrap (#34471) * Parcel Wrap * fix TG sprite licenses update attribution on modified `unwrapped` sprite to better conform to CC's guidance * ContainerContainer test failure fix * Just easy changes for now. * Imagine building your code before pushing it for review * The rest of the PR comments * PR comments * more comments + cargo orderability * whitespace: deduplicated. * use limitedcharges replace mostly-duped client/server with if(onserver) * cabinet perspective sprites * web edit detected fite me * @ps3moira 's new sprites for me :) * add a touch of attribution * EmoGarbage Review * Merge with master * Merge with master --------- Co-authored-by: EmoGarbage404 <retron404@gmail.com> * Automatic changelog update * Ammo Mag + Speedloader Inhand Sprites (#34235) * Added inhand visuals to (most) ammo magazines and speedloaders * whoops mixed up a single inhand on the rifle mag * Didn't realize rubber ammo got removed. * added attributions * adjusted inhand visuals to account for magazine types * missed one :eye:👁️ --------- Co-authored-by: EmoGarbage404 <retron404@gmail.com> * Automatic changelog update * New Science Biosuit Locker Sprite (#36929) * first commit, slightly broken * fixed meta json * reverse accidental cmo inclusion * Automatic changelog update * Remove legacy cargo shuttle code/prototypes (#36967) * Automatic changelog update * Paramedic suits adjustments (#36707) * Changes * meta * Battery (SMES/substation) interface (#36386) * Add ENERGYWATTHOURS() loc function Takes in joules (energy), displays as watt-hours. * Add simple OnOffButton control * Re-add Inset style class This was sloppily removed at some point?? Whatever, I need it. * Add helper functions for setting title/guidebook IDs on FancyWindow Reagent dispenser uses these, more in the next commits. * Add BuiPredictionState helper This enables me to implement coarse prediction manually in the battery UI. Basically it's a local buffer of predicted inputs that can easily be replayed against future BUI states from the server. * Add input coalescing infrastructure I ran into the following problem: Robust's Slider control absolutely *spams* input events, to such a degree that it actually causes issues for the networking layer if directly passed through. For something like a slider, we just need to send the most recent value. There is no good way for us to handle this in the control itself, as it *really* needs to happen in PreEngine. For simplicity reasons (for BUIs) I came to the conclusion it's best if it's there, as it's *before* any new states from the server can be applied. We can't just do this in Update() or something on the control as the timing just doesn't line up. I made a content system, BuiPreTickUpdateSystem, that runs in the ModRunLevel.PreEngine phase to achieve this. It runs a method on a new IBuiPreTickUpdate interface on all open BUIs. They can then implement their own coalescing logic. In the simplest case, this coalescing logic can just be "save the last value, and if we have any new value since the last update, send an input event." This is what the new InputCoalescer<T> type is for. Adding new coalescing logic should be possible in the future, of course. It's all just small helpers. * Battery interface This adds a proper interface to batteries (SMES/substation). Players can turn IO on and off, and they can change charge and discharge rate. There's also a ton of numbers and stuff. It looks great. This actually enables charge and discharge rates to be changed for these devices. The settings for both have been set between 5kW and 150kW. * Oops, forgot to remove these style class defs. * Automatic changelog update * Use RMC mob collision values (#36851) Use RMC movement values General feedback was it's too slow to push through people but this still prevents stacking so. * Automatic changelog update * early merge fix AAAAAA validcheck * Cherry Pick Round-start Solar Variation (#36825) * Roundstart Variation for Solar Panels (#86) * Added roundstart variation for solar panels * Removed HV cable spawner since i don't want to bother with mapping them * Solar Panel Variation Pass Fix (#96) Added a second variation pass for regular solar assemblies, since otherwise they would spawn both it and the broken variety * Reorganized everything to no longer be in the _Moffstation subdirectory * Removed a forgotten tag for Moffstation * Removed the moffstation namespace settings * Update Content.Server/GameTicking/Rules/VariationPass/Components/SolarPanelReplaceVariationPassComponent.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * Update Content.Server/GameTicking/Rules/VariationPass/Components/ReplacementMarkers/SolarPanelReplacementMarkerComponent.cs Co-authored-by: Tayrtahn <tayrtahn@gmail.com> --------- Co-authored-by: Tayrtahn <tayrtahn@gmail.com> * disable solar panel variation * put surgery borg module icons in _Shitmed * the big fix * abstract the right recipe * goida * no milk * update coscult code * update coscult objectives * migrate SignCansScience away * goida * update engine maybe fix test * GET OUT OF MY HEAD * i give up --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: Alice4267 <psycopompass14@gmail.com> Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Pgriha <133212269+Pgriha@users.noreply.github.com> Co-authored-by: Milon <milonpl.git@proton.me> Co-authored-by: TakoDragon <69509841+BackeTako@users.noreply.github.com> Co-authored-by: Hyper B <137433177+HyperB1@users.noreply.github.com> Co-authored-by: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com> Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com> Co-authored-by: Tayrtahn <tayrtahn@gmail.com> Co-authored-by: YoungThug <ramialanbagy@gmail.com> Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com> Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: RedBookcase <crazykid1590@gmail.com> Co-authored-by: RedBookcase <Usualmoves@gmail.com> Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com> Co-authored-by: Booblesnoot42 <108703193+Booblesnoot42@users.noreply.github.com> Co-authored-by: UBlueberry <161545003+UBlueberry@users.noreply.github.com> Co-authored-by: Banedon <15725971+Lanedon@users.noreply.github.com> Co-authored-by: Lanedon <matth.galvez@gmail> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: uhbg <uhbg@users.noreply.github.com> Co-authored-by: uhbg <29674581+uhbg@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Co-authored-by: Jackson Langley <tripwiregamer@gmail.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: Entvari <195642967+Entvari@users.noreply.github.com> Co-authored-by: dffdff2423 <dffdff2423@gmail.com> Co-authored-by: āda <ss.adasts@gmail.com> Co-authored-by: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com> Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com> Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: Phooooooooooooooooooooooooooooooosphate <184853112+Phooooooooooooooooooooooooooooooosphate@users.noreply.github.com> Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com> Co-authored-by: Łukasz Lindert <lukasz.lindert@protonmail.com> Co-authored-by: Hrosts <35345601+Hrosts@users.noreply.github.com> Co-authored-by: T <theo75cg@gmail.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com> Co-authored-by: J <billsmith116@gmail.com> Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com> Co-authored-by: EmoGarbage404 <retron404@gmail.com> Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com> Co-authored-by: Michael <107807667+Doc-Michael@users.noreply.github.com> Co-authored-by: mubururu_ <139181059+muburu@users.noreply.github.com> Co-authored-by: pathetic meowmeow <uhhadd@gmail.com> Co-authored-by: ScarKy0 <scarky0@onet.eu> Co-authored-by: godisdeadLOL <169250097+godisdeadLOL@users.noreply.github.com> Co-authored-by: YourUsername <you@example.com> Co-authored-by: beck-thompson <beck314159@hotmail.com> Co-authored-by: Golden Can <greentopcan@gmail.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com> Co-authored-by: Zachary Yona <58833995+Magicalus@users.noreply.github.com> Co-authored-by: Zachary Yona <magicalusf@gmail.com> Co-authored-by: Hanz <41141796+Hanzdegloker@users.noreply.github.com> Co-authored-by: catlord <catlord0009@gmail.com> Co-authored-by: poklj <compgeek223@gmail.com> Co-authored-by: Vladislav Suchkov <20380250+murolem@users.noreply.github.com> Co-authored-by: kosticia <kosticia46@gmail.com> Co-authored-by: Smith <182301147+AgentSmithRadio@users.noreply.github.com> Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> Co-authored-by: T-Stalker <43253663+DogZeroX@users.noreply.github.com> Co-authored-by: Saphire Lattice <lattice@saphi.re> Co-authored-by: ArchRBX <5040911+ArchRBX@users.noreply.github.com> Co-authored-by: archrbx <punk.gear5260@fastmail.com> Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> Co-authored-by: No Elka <125199100+NoElkaTheGod@users.noreply.github.com> Co-authored-by: PotRoastPiggy <121025443+PotRoastPiggy@users.noreply.github.com> Co-authored-by: Samuka-C <47865393+Samuka-C@users.noreply.github.com> Co-authored-by: Coolsurf6 <coolsurf24@yahoo.com.au> Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Co-authored-by: deltanedas <@deltanedas:kde.org> Co-authored-by: SG6732 <151011904+SG6732@users.noreply.github.com> Co-authored-by: themias <89101928+themias@users.noreply.github.com> Co-authored-by: Preston Smith <92108534+thetolbean@users.noreply.github.com> Co-authored-by: Nox <nebulousnox38@gmail.com> Co-authored-by: Victor Shen <71985089+Vexerot@users.noreply.github.com> Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com> Co-authored-by: Kresny <krzeszny@gmail.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: Radezolid <snappednexus@gmail.com> Co-authored-by: Kiri1674 <107365596+Kiri-Yoshikage@users.noreply.github.com> Co-authored-by: kaiserbirch <150971100+kaiserbirch@users.noreply.github.com> Co-authored-by: Franz - Josef Björck <kaiserbirch@proton.me> Co-authored-by: Wolfkey-SomeoneElseTookMyUsername <wolfkey75@gmail.com> Co-authored-by: TGRCDev <tgrc@tgrc.dev> Co-authored-by: OnyxTheBrave <131422822+OnyxTheBrave@users.noreply.github.com> Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com> Co-authored-by: SyaoranFox <99053675+SyaoranFox@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: qrwas <aleksandr.vernigora93@gmail.com> Co-authored-by: UpAndLeaves <92269094+Alpha-Two@users.noreply.github.com> Co-authored-by: Centronias <me@centronias.com> Co-authored-by: Tiniest Shark <head.rebel@yahoo.com> Co-authored-by: KingFroozy <140668342+KingFroozy@users.noreply.github.com>
1728 lines
61 KiB
C#
1728 lines
61 KiB
C#
using System.Collections.Frozen;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Administration.Logs;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Containers.ItemSlots;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Destructible;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.Implants.Components;
|
|
using Content.Shared.Input;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Interaction.Components;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Item;
|
|
using Content.Shared.Item.ItemToggle.Components;
|
|
using Content.Shared.Lock;
|
|
using Content.Shared.Nyanotrasen.Item.PseudoItem;
|
|
using Content.Shared.Materials;
|
|
using Content.Shared.Placeable;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Stacks;
|
|
using Content.Shared.Storage.Components;
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Timing;
|
|
using Content.Shared.Storage.Events;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.Whitelist;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.Input.Binding;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using Content.Shared.Rounding;
|
|
|
|
namespace Content.Shared.Storage.EntitySystems;
|
|
|
|
public abstract class SharedStorageSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
|
[Dependency] protected readonly IGameTiming Timing = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
|
[Dependency] protected readonly IRobustRandom Random = default!;
|
|
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
|
|
|
|
[Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!;
|
|
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
|
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
|
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
|
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
|
|
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
|
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
|
|
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
|
[Dependency] protected readonly SharedItemSystem ItemSystem = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
|
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
|
|
[Dependency] private readonly SharedStackSystem _stack = default!;
|
|
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
|
|
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
|
|
[Dependency] private readonly TagSystem _tag = default!;
|
|
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
|
|
|
|
private EntityQuery<ItemComponent> _itemQuery;
|
|
private EntityQuery<StackComponent> _stackQuery;
|
|
private EntityQuery<TransformComponent> _xformQuery;
|
|
private EntityQuery<UserInterfaceUserComponent> _userQuery;
|
|
|
|
/// <summary>
|
|
/// Whether we're allowed to go up-down storage via UI.
|
|
/// </summary>
|
|
public bool NestedStorage = true;
|
|
|
|
[ValidatePrototypeId<ItemSizePrototype>]
|
|
public const string DefaultStorageMaxItemSize = "Normal";
|
|
|
|
public const float AreaInsertDelayPerItem = 0.075f;
|
|
private static AudioParams _audioParams = AudioParams.Default
|
|
.WithMaxDistance(7f)
|
|
.WithVolume(-2f);
|
|
|
|
private ItemSizePrototype _defaultStorageMaxItemSize = default!;
|
|
|
|
/// <summary>
|
|
/// Flag for whether we're checking for nested storage interactions.
|
|
/// </summary>
|
|
private bool _nestedCheck;
|
|
|
|
public bool CheckingCanInsert;
|
|
|
|
private readonly List<EntityUid> _entList = new();
|
|
private readonly HashSet<EntityUid> _entSet = new();
|
|
|
|
private readonly List<ItemSizePrototype> _sortedSizes = new();
|
|
private FrozenDictionary<string, ItemSizePrototype> _nextSmallest = FrozenDictionary<string, ItemSizePrototype>.Empty;
|
|
|
|
private const string QuickInsertUseDelayID = "quickInsert";
|
|
private const string OpenUiUseDelayID = "storage";
|
|
|
|
/// <summary>
|
|
/// How many storage windows are allowed to be open at once.
|
|
/// </summary>
|
|
private int _openStorageLimit = -1;
|
|
|
|
protected readonly List<string> CantFillReasons = [];
|
|
|
|
/// <inheritdoc />
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
_itemQuery = GetEntityQuery<ItemComponent>();
|
|
_stackQuery = GetEntityQuery<StackComponent>();
|
|
_xformQuery = GetEntityQuery<TransformComponent>();
|
|
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
|
|
_prototype.PrototypesReloaded += OnPrototypesReloaded;
|
|
|
|
Subs.CVar(_cfg, CCVars.StorageLimit, OnStorageLimitChanged, true);
|
|
|
|
Subs.BuiEvents<StorageComponent>(StorageComponent.StorageUiKey.Key, subs =>
|
|
{
|
|
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
|
|
});
|
|
|
|
SubscribeLocalEvent<StorageComponent, ComponentRemove>(OnRemove);
|
|
SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit);
|
|
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
|
|
SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState);
|
|
SubscribeLocalEvent<StorageComponent, ComponentInit>(OnComponentInit, before: new[] { typeof(SharedContainerSystem) });
|
|
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<UtilityVerb>>(AddTransferVerbs);
|
|
SubscribeLocalEvent<StorageComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ItemSlotsSystem) });
|
|
SubscribeLocalEvent<StorageComponent, ActivateInWorldEvent>(OnActivate);
|
|
SubscribeLocalEvent<StorageComponent, OpenStorageImplantEvent>(OnImplantActivate);
|
|
SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
|
|
SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
|
|
SubscribeLocalEvent<StorageComponent, BoundUserInterfaceMessageAttempt>(OnBoundUIAttempt);
|
|
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
|
|
SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
|
|
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
|
|
SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
|
|
SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
|
|
SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
|
|
SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
|
|
|
|
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
|
|
|
|
SubscribeAllEvent<OpenNestedStorageEvent>(OnStorageNested);
|
|
SubscribeAllEvent<StorageTransferItemEvent>(OnStorageTransfer);
|
|
SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
|
|
SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
|
|
SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
|
|
SubscribeAllEvent<StorageSaveItemLocationEvent>(OnSaveItemLocation);
|
|
|
|
SubscribeLocalEvent<ItemSizeChangedEvent>(OnItemSizeChanged);
|
|
|
|
CommandBinds.Builder
|
|
.Bind(ContentKeyFunctions.OpenBackpack, InputCmdHandler.FromDelegate(HandleOpenBackpack, handle: false))
|
|
.Bind(ContentKeyFunctions.OpenBelt, InputCmdHandler.FromDelegate(HandleOpenBelt, handle: false))
|
|
.Register<SharedStorageSystem>();
|
|
|
|
Subs.CVar(_cfg, CCVars.NestedStorage, OnNestedStorageCvar, true);
|
|
|
|
UpdatePrototypeCache();
|
|
}
|
|
|
|
private void OnItemSizeChanged(ref ItemSizeChangedEvent ev)
|
|
{
|
|
var itemEnt = new Entity<ItemComponent?>(ev.Entity, null);
|
|
|
|
if (!TryGetStorageLocation(itemEnt, out var container, out var storage, out var loc))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
|
|
{
|
|
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
|
|
}
|
|
}
|
|
|
|
private void OnNestedStorageCvar(bool obj)
|
|
{
|
|
NestedStorage = obj;
|
|
}
|
|
|
|
private void OnStorageLimitChanged(int obj)
|
|
{
|
|
_openStorageLimit = obj;
|
|
}
|
|
|
|
private void OnRemove(Entity<StorageComponent> entity, ref ComponentRemove args)
|
|
{
|
|
UI.CloseUi(entity.Owner, StorageComponent.StorageUiKey.Key);
|
|
}
|
|
|
|
private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args)
|
|
{
|
|
UseDelay.SetLength(entity.Owner, entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID);
|
|
UseDelay.SetLength(entity.Owner, entity.Comp.OpenUiCooldown, OpenUiUseDelayID);
|
|
}
|
|
|
|
private void OnStorageGetState(EntityUid uid, StorageComponent component, ref ComponentGetState args)
|
|
{
|
|
var storedItems = new Dictionary<NetEntity, ItemStorageLocation>();
|
|
|
|
foreach (var (ent, location) in component.StoredItems)
|
|
{
|
|
storedItems[GetNetEntity(ent)] = location;
|
|
}
|
|
|
|
args.State = new StorageComponentState()
|
|
{
|
|
Grid = new List<Box2i>(component.Grid),
|
|
MaxItemSize = component.MaxItemSize,
|
|
StoredItems = storedItems,
|
|
SavedLocations = component.SavedLocations,
|
|
Whitelist = component.Whitelist,
|
|
Blacklist = component.Blacklist
|
|
};
|
|
}
|
|
|
|
public override void Shutdown()
|
|
{
|
|
_prototype.PrototypesReloaded -= OnPrototypesReloaded;
|
|
}
|
|
|
|
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
|
{
|
|
if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
|
|
|| (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
|
|
{
|
|
UpdatePrototypeCache();
|
|
}
|
|
}
|
|
|
|
private void UpdatePrototypeCache()
|
|
{
|
|
_defaultStorageMaxItemSize = _prototype.Index<ItemSizePrototype>(DefaultStorageMaxItemSize);
|
|
_sortedSizes.Clear();
|
|
_sortedSizes.AddRange(_prototype.EnumeratePrototypes<ItemSizePrototype>());
|
|
_sortedSizes.Sort();
|
|
|
|
var nextSmallest = new KeyValuePair<string, ItemSizePrototype>[_sortedSizes.Count];
|
|
for (var i = 0; i < _sortedSizes.Count; i++)
|
|
{
|
|
var k = _sortedSizes[i].ID;
|
|
var v = _sortedSizes[Math.Max(i - 1, 0)];
|
|
nextSmallest[i] = new(k, v);
|
|
}
|
|
|
|
_nextSmallest = nextSmallest.ToFrozenDictionary();
|
|
}
|
|
|
|
private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args)
|
|
{
|
|
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
|
|
UpdateAppearance((uid, storageComp, null));
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the user has nested-UIs open (e.g., PDA UI open when pda is in a backpack), close them.
|
|
/// </summary>
|
|
private void CloseNestedInterfaces(EntityUid uid, EntityUid actor, StorageComponent? storageComp = null)
|
|
{
|
|
if (!Resolve(uid, ref storageComp))
|
|
return;
|
|
|
|
// for each containing thing
|
|
// if it has a storage comp
|
|
// ensure unsubscribe from session
|
|
// if it has a ui component
|
|
// close ui
|
|
foreach (var entity in storageComp.Container.ContainedEntities)
|
|
{
|
|
UI.CloseUis(entity, actor);
|
|
}
|
|
}
|
|
|
|
private void OnBoundUIClosed(EntityUid uid, StorageComponent storageComp, BoundUIClosedEvent args)
|
|
{
|
|
CloseNestedInterfaces(uid, args.Actor, storageComp);
|
|
|
|
// If UI is closed for everyone
|
|
if (!UI.IsUiOpen(uid, args.UiKey))
|
|
{
|
|
UpdateAppearance((uid, storageComp, null));
|
|
if (!_tag.HasTag(args.Actor, storageComp.SilentStorageUserTag))
|
|
Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
|
|
}
|
|
}
|
|
|
|
private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent<ActivationVerb> args)
|
|
{
|
|
if (component.ShowVerb == false || !CanInteract(args.User, (uid, component), args.CanAccess && args.CanInteract))
|
|
return;
|
|
|
|
// Does this player currently have the storage UI open?
|
|
var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User);
|
|
|
|
ActivationVerb verb = new()
|
|
{
|
|
Act = () =>
|
|
{
|
|
if (uiOpen)
|
|
{
|
|
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
|
|
}
|
|
else
|
|
{
|
|
OpenStorageUI(uid, args.User, component, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (uiOpen)
|
|
{
|
|
verb.Text = Loc.GetString("comp-storage-verb-close-storage");
|
|
verb.Icon = new SpriteSpecifier.Texture(
|
|
new("/Textures/Interface/VerbIcons/close.svg.192dpi.png"));
|
|
}
|
|
else
|
|
{
|
|
verb.Text = Loc.GetString("comp-storage-verb-open-storage");
|
|
verb.Icon = new SpriteSpecifier.Texture(
|
|
new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"));
|
|
}
|
|
args.Verbs.Add(verb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the storage location of an item.
|
|
/// </summary>
|
|
public bool TryGetStorageLocation(Entity<ItemComponent?> itemEnt, [NotNullWhen(true)] out BaseContainer? container, out StorageComponent? storage, out ItemStorageLocation loc)
|
|
{
|
|
loc = default;
|
|
storage = null;
|
|
|
|
if (!ContainerSystem.TryGetContainingContainer(itemEnt, out container) ||
|
|
!TryComp(container.Owner, out storage) ||
|
|
!_itemQuery.Resolve(itemEnt, ref itemEnt.Comp, false))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
loc = storage.StoredItems[itemEnt];
|
|
return true;
|
|
}
|
|
|
|
public void OpenStorageUI(EntityUid uid, EntityUid actor, StorageComponent? storageComp = null, bool silent = true)
|
|
{
|
|
// Handle recursively opening nested storages.
|
|
if (ContainerSystem.TryGetContainingContainer(uid, out var container) &&
|
|
UI.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, actor))
|
|
{
|
|
_nestedCheck = true;
|
|
HideStorageWindow(container.Owner, actor);
|
|
OpenStorageUIInternal(uid, actor, storageComp, silent: true);
|
|
_nestedCheck = false;
|
|
}
|
|
else
|
|
{
|
|
// If you need something more sophisticated for multi-UI you'll need to code some smarter
|
|
// interactions.
|
|
if (_openStorageLimit == 1)
|
|
UI.CloseUserUis<StorageComponent.StorageUiKey>(actor);
|
|
|
|
OpenStorageUIInternal(uid, actor, storageComp, silent: silent);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the storage UI for an entity
|
|
/// </summary>
|
|
/// <param name="entity">The entity to open the UI for</param>
|
|
private void OpenStorageUIInternal(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = true)
|
|
{
|
|
if (!Resolve(uid, ref storageComp, false))
|
|
return;
|
|
|
|
// prevent spamming bag open / honkerton honk sound
|
|
silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay), id: OpenUiUseDelayID);
|
|
if (!CanInteract(entity, (uid, storageComp), silent: silent))
|
|
return;
|
|
|
|
if (!UI.TryOpenUi(uid, StorageComponent.StorageUiKey.Key, entity))
|
|
return;
|
|
|
|
if (!silent && !_tag.HasTag(entity, storageComp.SilentStorageUserTag))
|
|
{
|
|
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
|
|
|
|
if (useDelay != null)
|
|
UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID);
|
|
}
|
|
}
|
|
|
|
public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
|
|
|
|
private void AddTransferVerbs(EntityUid uid, StorageComponent component, GetVerbsEvent<UtilityVerb> args)
|
|
{
|
|
if (!args.CanAccess || !args.CanInteract)
|
|
return;
|
|
|
|
var entities = component.Container.ContainedEntities;
|
|
|
|
if (entities.Count == 0 || !CanInteract(args.User, (uid, component)))
|
|
return;
|
|
|
|
// if the target is storage, add a verb to transfer storage.
|
|
if (TryComp(args.Target, out StorageComponent? targetStorage)
|
|
&& (!TryComp(args.Target, out LockComponent? targetLock) || !targetLock.Locked))
|
|
{
|
|
UtilityVerb verb = new()
|
|
{
|
|
Text = Loc.GetString("storage-component-transfer-verb"),
|
|
IconEntity = GetNetEntity(args.Using),
|
|
Act = () => TransferEntities(uid, args.Target, args.User, component, null, targetStorage, targetLock)
|
|
};
|
|
|
|
args.Verbs.Add(verb);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user
|
|
/// </summary>
|
|
/// <returns>true if inserted, false otherwise</returns>
|
|
private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, InteractUsingEvent args)
|
|
{
|
|
if (args.Handled || !storageComp.ClickInsert || !CanInteract(args.User, (uid, storageComp), silent: false))
|
|
return;
|
|
|
|
var attemptEv = new StorageInteractUsingAttemptEvent();
|
|
RaiseLocalEvent(uid, ref attemptEv);
|
|
if (attemptEv.Cancelled)
|
|
return;
|
|
|
|
PlayerInsertHeldEntity((uid, storageComp), args.User);
|
|
// Always handle it, even if insertion fails.
|
|
// We don't want to trigger any AfterInteract logic here.
|
|
// Example issue would be placing wires if item doesn't fit in backpack.
|
|
args.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a message to open the storage UI
|
|
/// </summary>
|
|
private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args)
|
|
{
|
|
if (args.Handled || !args.Complex || !storageComp.OpenOnActivate || !CanInteract(args.User, (uid, storageComp)))
|
|
return;
|
|
|
|
// Toggle
|
|
if (UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User))
|
|
{
|
|
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
|
|
}
|
|
else
|
|
{
|
|
OpenStorageUI(uid, args.User, storageComp, false);
|
|
}
|
|
|
|
args.Handled = true;
|
|
}
|
|
|
|
protected virtual void HideStorageWindow(EntityUid uid, EntityUid actor)
|
|
{
|
|
}
|
|
|
|
protected virtual void ShowStorageWindow(EntityUid uid, EntityUid actor)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifically for storage implants.
|
|
/// </summary>
|
|
private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args)
|
|
{
|
|
if (args.Handled)
|
|
return;
|
|
|
|
var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.Performer);
|
|
|
|
if (uiOpen)
|
|
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.Performer);
|
|
else
|
|
OpenStorageUI(uid, args.Performer, storageComp, false);
|
|
|
|
args.Handled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius
|
|
/// around a click.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInteractEvent args)
|
|
{
|
|
if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true, id: QuickInsertUseDelayID))
|
|
return;
|
|
|
|
// Pick up all entities in a radius around the clicked location.
|
|
// The last half of the if is because carpets exist and this is terrible
|
|
if (storageComp.AreaInsert && (args.Target == null || !HasComp<ItemComponent>(args.Target.Value)))
|
|
{
|
|
_entList.Clear();
|
|
_entSet.Clear();
|
|
_entityLookupSystem.GetEntitiesInRange(args.ClickLocation, storageComp.AreaInsertRadius, _entSet, LookupFlags.Dynamic | LookupFlags.Sundries);
|
|
var delay = 0f;
|
|
|
|
foreach (var entity in _entSet)
|
|
{
|
|
if (entity == args.User
|
|
|| !_itemQuery.TryGetComponent(entity, out var itemComp) // Need comp to get item size to get weight
|
|
|| !_prototype.TryIndex(itemComp.Size, out var itemSize)
|
|
|| !CanInsert(uid, entity, out _, storageComp, item: itemComp)
|
|
|| !_interactionSystem.InRangeUnobstructed(args.User, entity))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_entList.Add(entity);
|
|
delay += itemSize.Weight * AreaInsertDelayPerItem;
|
|
|
|
if (_entList.Count >= StorageComponent.AreaPickupLimit)
|
|
break;
|
|
}
|
|
|
|
//If there's only one then let's be generous
|
|
if (_entList.Count >= 1)
|
|
{
|
|
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, delay, new AreaPickupDoAfterEvent(GetNetEntityList(_entList)), uid, target: uid)
|
|
{
|
|
BreakOnDamage = true,
|
|
BreakOnMove = true,
|
|
NeedHand = true,
|
|
};
|
|
|
|
_doAfterSystem.TryStartDoAfter(doAfterArgs);
|
|
args.Handled = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Pick up the clicked entity
|
|
if (storageComp.QuickInsert)
|
|
{
|
|
if (args.Target is not { Valid: true } target)
|
|
return;
|
|
|
|
if (ContainerSystem.IsEntityInContainer(target)
|
|
|| target == args.User
|
|
|| !_itemQuery.HasComponent(target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TryComp(uid, out TransformComponent? transformOwner) && TryComp(target, out TransformComponent? transformEnt))
|
|
{
|
|
var parent = transformOwner.ParentUid;
|
|
|
|
var position = TransformSystem.ToCoordinates(
|
|
parent.IsValid() ? parent : uid,
|
|
TransformSystem.GetMapCoordinates(transformEnt)
|
|
);
|
|
|
|
args.Handled = true;
|
|
if (PlayerInsertEntityInWorld((uid, storageComp), args.User, target))
|
|
{
|
|
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(GetNetEntity(uid),
|
|
new List<NetEntity> { GetNetEntity(target) },
|
|
new List<NetCoordinates> { GetNetCoordinates(position) },
|
|
new List<Angle> { transformOwner.LocalRotation }), args.User);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAfterEvent args)
|
|
{
|
|
if (args.Handled || args.Cancelled)
|
|
return;
|
|
|
|
args.Handled = true;
|
|
var successfullyInserted = new List<EntityUid>();
|
|
var successfullyInsertedPositions = new List<EntityCoordinates>();
|
|
var successfullyInsertedAngles = new List<Angle>();
|
|
|
|
if (!_xformQuery.TryGetComponent(uid, out var xform))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var entCount = Math.Min(StorageComponent.AreaPickupLimit, args.Entities.Count);
|
|
|
|
for (var i = 0; i < entCount; i++)
|
|
{
|
|
var entity = GetEntity(args.Entities[i]);
|
|
|
|
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
|
|
if (ContainerSystem.IsEntityInContainer(entity)
|
|
|| entity == args.Args.User
|
|
|| !_itemQuery.HasComponent(entity))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!_xformQuery.TryGetComponent(entity, out var targetXform) ||
|
|
targetXform.MapID != xform.MapID)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var position = TransformSystem.ToCoordinates(
|
|
xform.ParentUid.IsValid() ? xform.ParentUid : uid,
|
|
new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID)
|
|
);
|
|
|
|
var angle = targetXform.LocalRotation;
|
|
|
|
if (PlayerInsertEntityInWorld((uid, component), args.Args.User, entity, playSound: false))
|
|
{
|
|
successfullyInserted.Add(entity);
|
|
successfullyInsertedPositions.Add(position);
|
|
successfullyInsertedAngles.Add(angle);
|
|
}
|
|
}
|
|
|
|
// If we picked up at least one thing, play a sound and do a cool animation!
|
|
if (successfullyInserted.Count > 0)
|
|
{
|
|
if (!_tag.HasTag(args.User, component.SilentStorageUserTag))
|
|
Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams);
|
|
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(
|
|
GetNetEntity(uid),
|
|
GetNetEntityList(successfullyInserted),
|
|
GetNetCoordinatesList(successfullyInsertedPositions),
|
|
successfullyInsertedAngles), args.User);
|
|
}
|
|
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnReclaimed(EntityUid uid, StorageComponent storageComp, GotReclaimedEvent args)
|
|
{
|
|
ContainerSystem.EmptyContainer(storageComp.Container, destination: args.ReclaimerCoordinates);
|
|
}
|
|
|
|
private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args)
|
|
{
|
|
var coordinates = TransformSystem.GetMoverCoordinates(uid);
|
|
|
|
// Being destroyed so need to recalculate.
|
|
ContainerSystem.EmptyContainer(storageComp.Container, destination: coordinates);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function gets called when the user clicked on an item in the storage UI. This will either place the
|
|
/// item in the user's hand if it is currently empty, or interact with the item using the user's currently
|
|
/// held item.
|
|
/// </summary>
|
|
private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!ValidateInput(args, msg.StorageUid, msg.InteractedItemUid, out var player, out var storage, out var item))
|
|
return;
|
|
|
|
// If the user's active hand is empty, try pick up the item.
|
|
if (player.Comp.ActiveHandEntity == null)
|
|
{
|
|
_adminLog.Add(
|
|
LogType.Storage,
|
|
LogImpact.Low,
|
|
$"{ToPrettyString(player):player} is attempting to take {ToPrettyString(item):item} out of {ToPrettyString(storage):storage}");
|
|
|
|
if (_sharedHandsSystem.TryPickupAnyHand(player, item, handsComp: player.Comp)
|
|
&& storage.Comp.StorageRemoveSound != null
|
|
&& !_tag.HasTag(player, storage.Comp.SilentStorageUserTag))
|
|
{
|
|
Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_adminLog.Add(
|
|
LogType.Storage,
|
|
LogImpact.Low,
|
|
$"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(player.Comp.ActiveHandEntity):used}");
|
|
|
|
// Else, interact using the held item
|
|
if (_interactionSystem.InteractUsing(player,
|
|
player.Comp.ActiveHandEntity.Value,
|
|
item,
|
|
Transform(item).Coordinates,
|
|
checkCanInteract: false))
|
|
return;
|
|
|
|
var failedEv = new StorageInsertFailedEvent((storage, storage.Comp), (player, player.Comp));
|
|
RaiseLocalEvent(storage, ref failedEv);
|
|
}
|
|
|
|
private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item))
|
|
return;
|
|
|
|
_adminLog.Add(
|
|
LogType.Storage,
|
|
LogImpact.Low,
|
|
$"{ToPrettyString(player):player} is updating the location of {ToPrettyString(item):item} within {ToPrettyString(storage):storage}");
|
|
|
|
TrySetItemStorageLocation(item!, storage!, msg.Location);
|
|
}
|
|
|
|
private void OnStorageNested(OpenNestedStorageEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!NestedStorage)
|
|
return;
|
|
|
|
if (!TryGetEntity(msg.InteractedItemUid, out var itemEnt))
|
|
return;
|
|
|
|
_nestedCheck = true;
|
|
|
|
var result = ValidateInput(args,
|
|
msg.StorageUid,
|
|
msg.InteractedItemUid,
|
|
out var player,
|
|
out var storage,
|
|
out var item);
|
|
|
|
if (!result)
|
|
{
|
|
_nestedCheck = false;
|
|
return;
|
|
}
|
|
|
|
HideStorageWindow(storage.Owner, player.Owner);
|
|
OpenStorageUI(item.Owner, player.Owner, silent: true);
|
|
_nestedCheck = false;
|
|
}
|
|
|
|
private void OnStorageTransfer(StorageTransferItemEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!TryGetEntity(msg.ItemEnt, out var itemUid) || !TryComp(itemUid, out ItemComponent? itemComp))
|
|
return;
|
|
|
|
var localPlayer = args.SenderSession.AttachedEntity;
|
|
var itemEnt = new Entity<ItemComponent?>(itemUid.Value, itemComp);
|
|
|
|
// Validate the source storage
|
|
if (!TryGetStorageLocation(itemEnt, out var container, out _, out _) ||
|
|
!ValidateInput(args, GetNetEntity(container.Owner), out _, out _))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryComp(localPlayer, out HandsComponent? handsComp) || !_sharedHandsSystem.TryPickup(localPlayer.Value, itemEnt, handsComp: handsComp, animate: false))
|
|
return;
|
|
|
|
// Validate the target storage
|
|
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))
|
|
return;
|
|
|
|
_adminLog.Add(
|
|
LogType.Storage,
|
|
LogImpact.Low,
|
|
$"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}");
|
|
InsertAt(storage!, item!, msg.Location, out _, player, stackAutomatically: false);
|
|
}
|
|
|
|
private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))
|
|
return;
|
|
|
|
_adminLog.Add(
|
|
LogType.Storage,
|
|
LogImpact.Low,
|
|
$"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}");
|
|
InsertAt(storage!, item!, msg.Location, out _, player, stackAutomatically: false);
|
|
}
|
|
|
|
private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item))
|
|
return;
|
|
|
|
SaveItemLocation(storage!, item.Owner);
|
|
}
|
|
|
|
private void OnBoundUIOpen(Entity<StorageComponent> ent, ref BoundUIOpenedEvent args)
|
|
{
|
|
UpdateAppearance((ent.Owner, ent.Comp, null));
|
|
}
|
|
|
|
private void OnBoundUIAttempt(Entity<StorageComponent> ent, ref BoundUserInterfaceMessageAttempt args)
|
|
{
|
|
if (args.UiKey is not StorageComponent.StorageUiKey.Key ||
|
|
_openStorageLimit == -1 ||
|
|
_nestedCheck ||
|
|
args.Message is not OpenBoundInterfaceMessage)
|
|
return;
|
|
|
|
var uid = args.Target;
|
|
var actor = args.Actor;
|
|
var count = 0;
|
|
|
|
if (_userQuery.TryComp(actor, out var userComp))
|
|
{
|
|
foreach (var (ui, keys) in userComp.OpenInterfaces)
|
|
{
|
|
if (ui == uid)
|
|
continue;
|
|
|
|
foreach (var key in keys)
|
|
{
|
|
if (key is not StorageComponent.StorageUiKey)
|
|
continue;
|
|
|
|
count++;
|
|
|
|
if (count >= _openStorageLimit)
|
|
{
|
|
args.Cancel();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
|
|
{
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
if (entity.Comp.Container == null)
|
|
return;
|
|
|
|
if (args.Container.ID != StorageComponent.ContainerId)
|
|
return;
|
|
|
|
if (!entity.Comp.StoredItems.ContainsKey(args.Entity))
|
|
{
|
|
if (!TryGetAvailableGridSpace((entity.Owner, entity.Comp), (args.Entity, null), out var location))
|
|
{
|
|
ContainerSystem.Remove(args.Entity, args.Container, force: true);
|
|
return;
|
|
}
|
|
|
|
entity.Comp.StoredItems[args.Entity] = location.Value;
|
|
Dirty(entity, entity.Comp);
|
|
}
|
|
|
|
UpdateAppearance((entity, entity.Comp, null));
|
|
UpdateUI((entity, entity.Comp));
|
|
}
|
|
|
|
private void OnEntRemoved(Entity<StorageComponent> entity, ref EntRemovedFromContainerMessage args)
|
|
{
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
if (entity.Comp.Container == null)
|
|
return;
|
|
|
|
if (args.Container.ID != StorageComponent.ContainerId)
|
|
return;
|
|
|
|
entity.Comp.StoredItems.Remove(args.Entity);
|
|
Dirty(entity, entity.Comp);
|
|
|
|
UpdateAppearance((entity, entity.Comp, null));
|
|
UpdateUI((entity, entity.Comp));
|
|
}
|
|
|
|
private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args)
|
|
{
|
|
if (args.Cancelled || args.Container.ID != StorageComponent.ContainerId)
|
|
return;
|
|
|
|
// don't run cyclical CanInsert() loops
|
|
if (CheckingCanInsert)
|
|
return;
|
|
|
|
if (!CanInsert(uid, args.EntityUid, out var reason, component, ignoreStacks: true))
|
|
{
|
|
#if DEBUG
|
|
if (reason != null)
|
|
CantFillReasons.Add(reason);
|
|
#endif
|
|
|
|
args.Cancel();
|
|
}
|
|
}
|
|
|
|
public void UpdateAppearance(Entity<StorageComponent?, AppearanceComponent?> entity)
|
|
{
|
|
// TODO STORAGE remove appearance data and just use the data on the component.
|
|
var (uid, storage, appearance) = entity;
|
|
if (!Resolve(uid, ref storage, ref appearance, false))
|
|
return;
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
if (storage.Container == null)
|
|
return; // component hasn't yet been initialized.
|
|
|
|
var capacity = storage.Grid.GetArea();
|
|
var used = GetCumulativeItemAreas((uid, storage));
|
|
|
|
var isOpen = UI.IsUiOpen(entity.Owner, StorageComponent.StorageUiKey.Key);
|
|
|
|
_appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance);
|
|
_appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance);
|
|
_appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance);
|
|
_appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
|
|
|
|
if (TryComp<StorageFillVisualizerComponent>(uid, out var storageFillVisualizerComp))
|
|
{
|
|
var level = ContentHelpers.RoundToLevels(used, capacity, storageFillVisualizerComp.MaxFillLevels);
|
|
_appearance.SetData(uid, StorageFillVisuals.FillLevel, level, appearance);
|
|
}
|
|
|
|
// HideClosedStackVisuals true sets the StackVisuals.Hide to the open state of the storage.
|
|
// This is for containers that only show their contents when open. (e.g. donut boxes)
|
|
if (storage.HideStackVisualsWhenClosed)
|
|
_appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move entities from one storage to another.
|
|
/// </summary>
|
|
public void TransferEntities(EntityUid source, EntityUid target, EntityUid? user = null,
|
|
StorageComponent? sourceComp = null, LockComponent? sourceLock = null,
|
|
StorageComponent? targetComp = null, LockComponent? targetLock = null)
|
|
{
|
|
if (!Resolve(source, ref sourceComp) || !Resolve(target, ref targetComp))
|
|
return;
|
|
|
|
var entities = sourceComp.Container.ContainedEntities;
|
|
if (entities.Count == 0)
|
|
return;
|
|
|
|
if (Resolve(source, ref sourceLock, false) && sourceLock.Locked
|
|
|| Resolve(target, ref targetLock, false) && targetLock.Locked)
|
|
return;
|
|
|
|
foreach (var entity in entities.ToArray())
|
|
{
|
|
if (HasComp<PseudoItemComponent>(entity)) // Nyanotrasen - They dont transfer properly
|
|
continue;
|
|
|
|
Insert(target, entity, out _, user: user, targetComp, playSound: false);
|
|
}
|
|
if (user != null
|
|
&& (!_tag.HasTag(user.Value, sourceComp.SilentStorageUserTag)
|
|
|| !_tag.HasTag(user.Value, targetComp.SilentStorageUserTag)))
|
|
Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies if an entity can be stored and if it fits
|
|
/// </summary>
|
|
/// <param name="uid">The entity to check</param>
|
|
/// <param name="insertEnt"></param>
|
|
/// <param name="reason">If returning false, the reason displayed to the player</param>
|
|
/// <param name="storageComp"></param>
|
|
/// <param name="item"></param>
|
|
/// <param name="ignoreStacks"></param>
|
|
/// <param name="ignoreLocation"></param>
|
|
/// <returns>true if it can be inserted, false otherwise</returns>
|
|
public bool CanInsert(
|
|
EntityUid uid,
|
|
EntityUid insertEnt,
|
|
out string? reason,
|
|
StorageComponent? storageComp = null,
|
|
ItemComponent? item = null,
|
|
bool ignoreStacks = false,
|
|
bool ignoreLocation = false)
|
|
{
|
|
if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false))
|
|
{
|
|
reason = null;
|
|
return false;
|
|
}
|
|
|
|
if (Transform(insertEnt).Anchored)
|
|
{
|
|
reason = "comp-storage-anchored-failure";
|
|
return false;
|
|
}
|
|
|
|
if (_whitelistSystem.IsWhitelistFail(storageComp.Whitelist, insertEnt) ||
|
|
_whitelistSystem.IsBlacklistPass(storageComp.Blacklist, insertEnt))
|
|
{
|
|
reason = "comp-storage-invalid-container";
|
|
return false;
|
|
}
|
|
|
|
if (!ignoreStacks
|
|
&& _stackQuery.TryGetComponent(insertEnt, out var stack)
|
|
&& HasSpaceInStacks((uid, storageComp), stack.StackTypeId))
|
|
{
|
|
reason = null;
|
|
return true;
|
|
}
|
|
|
|
var maxSize = GetMaxItemSize((uid, storageComp));
|
|
if (ItemSystem.GetSizePrototype(item.Size) > maxSize)
|
|
{
|
|
reason = "comp-storage-too-big";
|
|
return false;
|
|
}
|
|
|
|
if (TryComp<StorageComponent>(insertEnt, out var insertStorage)
|
|
&& GetMaxItemSize((insertEnt, insertStorage)) >= maxSize)
|
|
{
|
|
reason = "comp-storage-too-big";
|
|
return false;
|
|
}
|
|
|
|
if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(insertEnt))
|
|
{
|
|
if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _))
|
|
{
|
|
reason = "comp-storage-insufficient-capacity";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
CheckingCanInsert = true;
|
|
if (!ContainerSystem.CanInsert(insertEnt, storageComp.Container))
|
|
{
|
|
CheckingCanInsert = false;
|
|
reason = null;
|
|
return false;
|
|
}
|
|
CheckingCanInsert = false;
|
|
|
|
reason = null;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts into the storage container at a given location
|
|
/// </summary>
|
|
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
|
|
/// inserted.</returns>
|
|
public bool InsertAt(
|
|
Entity<StorageComponent?> uid,
|
|
Entity<ItemComponent?> insertEnt,
|
|
ItemStorageLocation location,
|
|
out EntityUid? stackedEntity,
|
|
EntityUid? user = null,
|
|
bool playSound = true,
|
|
bool stackAutomatically = true)
|
|
{
|
|
stackedEntity = null;
|
|
if (!Resolve(uid, ref uid.Comp))
|
|
return false;
|
|
|
|
if (!ItemFitsInGridLocation(insertEnt, uid, location))
|
|
return false;
|
|
|
|
uid.Comp.StoredItems[insertEnt] = location;
|
|
Dirty(uid, uid.Comp);
|
|
|
|
if (Insert(uid,
|
|
insertEnt,
|
|
out stackedEntity,
|
|
out _,
|
|
user: user,
|
|
storageComp: uid.Comp,
|
|
playSound: playSound,
|
|
stackAutomatically: stackAutomatically))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
uid.Comp.StoredItems.Remove(insertEnt);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts into the storage container
|
|
/// </summary>
|
|
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
|
|
/// inserted.</returns>
|
|
public bool Insert(
|
|
EntityUid uid,
|
|
EntityUid insertEnt,
|
|
out EntityUid? stackedEntity,
|
|
EntityUid? user = null,
|
|
StorageComponent? storageComp = null,
|
|
bool playSound = true,
|
|
bool stackAutomatically = true)
|
|
{
|
|
return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: storageComp, playSound: playSound, stackAutomatically: stackAutomatically);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts into the storage container
|
|
/// </summary>
|
|
/// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
|
|
/// inserted</returns>
|
|
public bool Insert(
|
|
EntityUid uid,
|
|
EntityUid insertEnt,
|
|
out EntityUid? stackedEntity,
|
|
out string? reason,
|
|
EntityUid? user = null,
|
|
StorageComponent? storageComp = null,
|
|
bool playSound = true,
|
|
bool stackAutomatically = true)
|
|
{
|
|
stackedEntity = null;
|
|
reason = null;
|
|
|
|
if (!Resolve(uid, ref storageComp))
|
|
return false;
|
|
|
|
/*
|
|
* 1. If the inserted thing is stackable then try to stack it to existing stacks
|
|
* 2. If anything remains insert whatever is possible.
|
|
* 3. If insertion is not possible then leave the stack as is.
|
|
* At either rate still play the insertion sound
|
|
*
|
|
* For now we just treat items as always being the same size regardless of stack count.
|
|
*/
|
|
|
|
// Check if the sound is expected to play.
|
|
// If there is an user, the sound will not play if they have the SilentStorageUserTag
|
|
// If there is no user, only playSound is checked.
|
|
var canPlaySound = playSound && (user == null || !_tag.HasTag(user.Value, storageComp.SilentStorageUserTag));
|
|
|
|
if (!stackAutomatically || !_stackQuery.TryGetComponent(insertEnt, out var insertStack))
|
|
{
|
|
if (!ContainerSystem.Insert(insertEnt, storageComp.Container))
|
|
return false;
|
|
|
|
if (canPlaySound)
|
|
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
|
|
|
|
return true;
|
|
}
|
|
|
|
var toInsertCount = insertStack.Count;
|
|
|
|
foreach (var ent in storageComp.Container.ContainedEntities)
|
|
{
|
|
if (!_stackQuery.TryGetComponent(ent, out var containedStack))
|
|
continue;
|
|
|
|
if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack))
|
|
continue;
|
|
|
|
stackedEntity = ent;
|
|
if (insertStack.Count == 0)
|
|
break;
|
|
}
|
|
|
|
// Still stackable remaining
|
|
if (insertStack.Count > 0
|
|
&& !ContainerSystem.Insert(insertEnt, storageComp.Container)
|
|
&& toInsertCount == insertStack.Count)
|
|
{
|
|
// Failed to insert anything.
|
|
return false;
|
|
}
|
|
|
|
if (canPlaySound)
|
|
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts an entity into storage from the player's active hand
|
|
/// </summary>
|
|
/// <param name="ent">The storage entity and component to insert into.</param>
|
|
/// <param name="player">The player and hands component to insert the held entity from.</param>
|
|
/// <returns>True if inserted, otherwise false.</returns>
|
|
public bool PlayerInsertHeldEntity(Entity<StorageComponent?> ent, Entity<HandsComponent?> player)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp)
|
|
|| !Resolve(player.Owner, ref player.Comp)
|
|
|| player.Comp.ActiveHandEntity == null)
|
|
return false;
|
|
|
|
var toInsert = player.Comp.ActiveHandEntity;
|
|
|
|
if (!CanInsert(ent, toInsert.Value, out var reason, ent.Comp))
|
|
{
|
|
_popupSystem.PopupClient(Loc.GetString(reason ?? "comp-storage-cant-insert"), ent, player);
|
|
return false;
|
|
}
|
|
|
|
if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, player.Comp))
|
|
{
|
|
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), ent, player);
|
|
return false;
|
|
}
|
|
|
|
return PlayerInsertEntityInWorld((ent, ent.Comp), player, toInsert.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts an Entity (<paramref name="toInsert"/>) in the world into storage, informing <paramref name="player"/> if it fails.
|
|
/// <paramref name="toInsert"/> is *NOT* held, see <see cref="PlayerInsertHeldEntity(Entity{StorageComponent?},Entity{HandsComponent?})"/>.
|
|
/// </summary>
|
|
/// <param name="uid"></param>
|
|
/// <param name="player">The player to insert an entity with</param>
|
|
/// <param name="toInsert"></param>
|
|
/// <returns>true if inserted, false otherwise</returns>
|
|
public bool PlayerInsertEntityInWorld(Entity<StorageComponent?> uid, EntityUid player, EntityUid toInsert, bool playSound = true)
|
|
{
|
|
if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid.Owner))
|
|
return false;
|
|
|
|
if (!Insert(uid, toInsert, out _, user: player, uid.Comp, playSound: playSound))
|
|
{
|
|
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-insert"), uid, player);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to set the location of an item already inside of a storage container.
|
|
/// </summary>
|
|
public bool TrySetItemStorageLocation(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, ItemStorageLocation location)
|
|
{
|
|
if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
|
|
return false;
|
|
|
|
if (!storageEnt.Comp.Container.ContainedEntities.Contains(itemEnt))
|
|
return false;
|
|
|
|
if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
|
|
return false;
|
|
|
|
storageEnt.Comp.StoredItems[itemEnt] = location;
|
|
UpdateUI(storageEnt);
|
|
Dirty(storageEnt, storageEnt.Comp);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to find the first available spot on a storage grid.
|
|
/// starts at the top-left and goes right and down.
|
|
/// </summary>
|
|
public bool TryGetAvailableGridSpace(
|
|
Entity<StorageComponent?> storageEnt,
|
|
Entity<ItemComponent?> itemEnt,
|
|
[NotNullWhen(true)] out ItemStorageLocation? storageLocation)
|
|
{
|
|
storageLocation = null;
|
|
|
|
if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp))
|
|
return false;
|
|
|
|
// if the item has an available saved location, use that
|
|
if (FindSavedLocation(storageEnt, itemEnt, out storageLocation))
|
|
return true;
|
|
|
|
var storageBounding = storageEnt.Comp.Grid.GetBoundingBox();
|
|
|
|
Angle startAngle;
|
|
if (storageEnt.Comp.DefaultStorageOrientation == null)
|
|
{
|
|
startAngle = Angle.FromDegrees(-itemEnt.Comp.StoredRotation);
|
|
}
|
|
else
|
|
{
|
|
if (storageBounding.Width < storageBounding.Height)
|
|
{
|
|
startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal
|
|
? Angle.Zero
|
|
: Angle.FromDegrees(90);
|
|
}
|
|
else
|
|
{
|
|
startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical
|
|
? Angle.Zero
|
|
: Angle.FromDegrees(90);
|
|
}
|
|
}
|
|
|
|
for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++)
|
|
{
|
|
for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
|
|
{
|
|
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
|
|
{
|
|
var location = new ItemStorageLocation(angle, (x, y));
|
|
if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
|
|
{
|
|
storageLocation = location;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to find a saved location for an item from its name.
|
|
/// If none are saved or they are all blocked nothing is returned.
|
|
/// </summary>
|
|
public bool FindSavedLocation(
|
|
Entity<StorageComponent?> ent,
|
|
Entity<ItemComponent?> item,
|
|
[NotNullWhen(true)] out ItemStorageLocation? storageLocation)
|
|
{
|
|
storageLocation = null;
|
|
if (!Resolve(ent, ref ent.Comp))
|
|
return false;
|
|
|
|
var name = Name(item);
|
|
if (!ent.Comp.SavedLocations.TryGetValue(name, out var list))
|
|
return false;
|
|
|
|
foreach (var location in list)
|
|
{
|
|
if (ItemFitsInGridLocation(item, ent, location))
|
|
{
|
|
storageLocation = location;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves an item's location in the grid for later insertion to use.
|
|
/// </summary>
|
|
public void SaveItemLocation(Entity<StorageComponent?> ent, Entity<MetaDataComponent?> item)
|
|
{
|
|
if (!Resolve(ent, ref ent.Comp))
|
|
return;
|
|
|
|
// needs to actually be stored in it somewhere to save it
|
|
if (!ent.Comp.StoredItems.TryGetValue(item, out var location))
|
|
return;
|
|
|
|
var name = Name(item, item.Comp);
|
|
if (ent.Comp.SavedLocations.TryGetValue(name, out var list))
|
|
{
|
|
// iterate to make sure its not already been saved
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
var saved = list[i];
|
|
|
|
if (saved == location)
|
|
{
|
|
list.Remove(location);
|
|
return;
|
|
}
|
|
}
|
|
|
|
list.Add(location);
|
|
}
|
|
else
|
|
{
|
|
list = new List<ItemStorageLocation>()
|
|
{
|
|
location
|
|
};
|
|
ent.Comp.SavedLocations[name] = list;
|
|
}
|
|
|
|
Dirty(ent, ent.Comp);
|
|
UpdateUI((ent.Owner, ent.Comp));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an item fits into a specific spot on a storage grid.
|
|
/// </summary>
|
|
public bool ItemFitsInGridLocation(
|
|
Entity<ItemComponent?> itemEnt,
|
|
Entity<StorageComponent?> storageEnt,
|
|
ItemStorageLocation location)
|
|
{
|
|
return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if an item fits into a specific spot on a storage grid.
|
|
/// </summary>
|
|
public bool ItemFitsInGridLocation(
|
|
Entity<ItemComponent?> itemEnt,
|
|
Entity<StorageComponent?> storageEnt,
|
|
Vector2i position,
|
|
Angle rotation)
|
|
{
|
|
if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
|
|
return false;
|
|
|
|
var gridBounds = storageEnt.Comp.Grid.GetBoundingBox();
|
|
if (!gridBounds.Contains(position))
|
|
return false;
|
|
|
|
var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
|
|
|
|
foreach (var box in itemShape)
|
|
{
|
|
for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
|
|
{
|
|
for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
|
|
{
|
|
var pos = (offsetX, offsetY);
|
|
|
|
if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a space on a grid is valid and not occupied by any other pieces.
|
|
/// </summary>
|
|
public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
|
|
{
|
|
if (!Resolve(storageEnt, ref storageEnt.Comp))
|
|
return false;
|
|
|
|
var validGrid = false;
|
|
foreach (var grid in storageEnt.Comp.Grid)
|
|
{
|
|
if (grid.Contains(location))
|
|
{
|
|
validGrid = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!validGrid)
|
|
return false;
|
|
|
|
foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
|
|
{
|
|
if (ent == itemEnt.Owner)
|
|
continue;
|
|
|
|
if (!_itemQuery.TryGetComponent(ent, out var itemComp))
|
|
continue;
|
|
|
|
var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
|
|
foreach (var box in adjustedShape)
|
|
{
|
|
if (box.Contains(location))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if there is enough space to theoretically fit another item.
|
|
/// </summary>
|
|
public bool HasSpace(Entity<StorageComponent?> uid)
|
|
{
|
|
if (!Resolve(uid, ref uid.Comp))
|
|
return false;
|
|
|
|
return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid);
|
|
}
|
|
|
|
private bool HasSpaceInStacks(Entity<StorageComponent?> uid, string? stackType = null)
|
|
{
|
|
if (!Resolve(uid, ref uid.Comp))
|
|
return false;
|
|
|
|
foreach (var contained in uid.Comp.Container.ContainedEntities)
|
|
{
|
|
if (!_stackQuery.TryGetComponent(contained, out var stack))
|
|
continue;
|
|
|
|
if (stackType != null && !stack.StackTypeId.Equals(stackType))
|
|
continue;
|
|
|
|
if (_stack.GetAvailableSpace(stack) == 0)
|
|
continue;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the sum of all the ItemSizes of the items inside of a storage.
|
|
/// </summary>
|
|
public int GetCumulativeItemAreas(Entity<StorageComponent?> entity)
|
|
{
|
|
if (!Resolve(entity, ref entity.Comp))
|
|
return 0;
|
|
|
|
var sum = 0;
|
|
foreach (var item in entity.Comp.Container.ContainedEntities)
|
|
{
|
|
if (!_itemQuery.TryGetComponent(item, out var itemComp))
|
|
continue;
|
|
sum += ItemSystem.GetItemShape((item, itemComp)).GetArea();
|
|
}
|
|
|
|
return sum;
|
|
}
|
|
|
|
public ItemSizePrototype GetMaxItemSize(Entity<StorageComponent?> uid)
|
|
{
|
|
if (!Resolve(uid, ref uid.Comp))
|
|
return _defaultStorageMaxItemSize;
|
|
|
|
// If we specify a max item size, use that
|
|
if (uid.Comp.MaxItemSize != null)
|
|
{
|
|
if (_prototype.TryIndex(uid.Comp.MaxItemSize.Value, out var proto))
|
|
return proto;
|
|
|
|
Log.Error($"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}");
|
|
}
|
|
|
|
if (!_itemQuery.TryGetComponent(uid, out var item))
|
|
return _defaultStorageMaxItemSize;
|
|
|
|
// if there is no max item size specified, the value used
|
|
// is one below the item size of the storage entity.
|
|
return _nextSmallest[item.Size];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a storage's UI is open by anyone when locked, and closes it.
|
|
/// </summary>
|
|
private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args)
|
|
{
|
|
if (!args.Locked)
|
|
return;
|
|
|
|
// Gets everyone looking at the UI
|
|
foreach (var actor in UI.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
|
|
{
|
|
if (!CanInteract(actor, (uid, component)))
|
|
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
|
|
}
|
|
}
|
|
|
|
private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
|
|
{
|
|
if (ContainerSystem.TryGetContainingContainer((uid, null, component), out var container) &&
|
|
container.ID == StorageComponent.ContainerId)
|
|
{
|
|
UpdateAppearance(container.Owner);
|
|
UpdateUI(container.Owner);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void HandleOpenBackpack(ICommonSession? session)
|
|
{
|
|
HandleToggleSlotUI(session, "back");
|
|
}
|
|
|
|
private void HandleOpenBelt(ICommonSession? session)
|
|
{
|
|
HandleToggleSlotUI(session, "belt");
|
|
}
|
|
|
|
private void HandleToggleSlotUI(ICommonSession? session, string slot)
|
|
{
|
|
if (session is not { } playerSession)
|
|
return;
|
|
|
|
if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
|
|
return;
|
|
|
|
if (!_inventory.TryGetSlotEntity(playerEnt, slot, out var storageEnt))
|
|
return;
|
|
|
|
if (!ActionBlocker.CanInteract(playerEnt, storageEnt))
|
|
return;
|
|
|
|
if (!UI.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
|
|
{
|
|
OpenStorageUI(storageEnt.Value, playerEnt, silent: false);
|
|
}
|
|
else
|
|
{
|
|
UI.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt);
|
|
}
|
|
}
|
|
|
|
protected void ClearCantFillReasons()
|
|
{
|
|
#if DEBUG
|
|
CantFillReasons.Clear();
|
|
#endif
|
|
}
|
|
|
|
private bool CanInteract(EntityUid user, Entity<StorageComponent> storage, bool canInteract = true, bool silent = true)
|
|
{
|
|
if (HasComp<BypassInteractionChecksComponent>(user))
|
|
return true;
|
|
|
|
if (!canInteract)
|
|
return false;
|
|
|
|
var ev = new StorageInteractAttemptEvent(silent);
|
|
RaiseLocalEvent(storage, ref ev);
|
|
|
|
return !ev.Cancelled;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plays a clientside pickup animation for the specified uid.
|
|
/// </summary>
|
|
public abstract void PlayPickupAnimation(EntityUid uid, EntityCoordinates initialCoordinates,
|
|
EntityCoordinates finalCoordinates, Angle initialRotation, EntityUid? user = null);
|
|
|
|
private bool ValidateInput(
|
|
EntitySessionEventArgs args,
|
|
NetEntity netStorage,
|
|
out Entity<HandsComponent> player,
|
|
out Entity<StorageComponent> storage)
|
|
{
|
|
player = default;
|
|
storage = default;
|
|
|
|
if (args.SenderSession.AttachedEntity is not { } playerUid)
|
|
return false;
|
|
|
|
if (!TryComp(playerUid, out HandsComponent? hands) || hands.Count == 0)
|
|
return false;
|
|
|
|
if (!TryGetEntity(netStorage, out var storageUid))
|
|
return false;
|
|
|
|
if (!TryComp(storageUid, out StorageComponent? storageComp))
|
|
return false;
|
|
|
|
// TODO STORAGE use BUI events
|
|
// This would automatically validate that the UI is open & that the user can interact.
|
|
// However, we still need to manually validate that items being used are in the users hands or in the storage.
|
|
if (!UI.IsUiOpen(storageUid.Value, StorageComponent.StorageUiKey.Key, playerUid))
|
|
return false;
|
|
|
|
if (!ActionBlocker.CanInteract(playerUid, storageUid))
|
|
return false;
|
|
|
|
player = new(playerUid, hands);
|
|
storage = new(storageUid.Value, storageComp);
|
|
return true;
|
|
}
|
|
|
|
private bool ValidateInput(EntitySessionEventArgs args,
|
|
NetEntity netStorage,
|
|
NetEntity netItem,
|
|
out Entity<HandsComponent> player,
|
|
out Entity<StorageComponent> storage,
|
|
out Entity<ItemComponent> item,
|
|
bool held = false)
|
|
{
|
|
item = default!;
|
|
if (!ValidateInput(args, netStorage, out player, out storage))
|
|
return false;
|
|
|
|
if (!TryGetEntity(netItem, out var itemUid))
|
|
return false;
|
|
|
|
if (held)
|
|
{
|
|
if (!_sharedHandsSystem.IsHolding(player, itemUid, out _))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!storage.Comp.Container.Contains(itemUid.Value))
|
|
return false;
|
|
|
|
DebugTools.Assert(storage.Comp.StoredItems.ContainsKey(itemUid.Value));
|
|
}
|
|
|
|
if (!TryComp(itemUid, out ItemComponent? itemComp))
|
|
return false;
|
|
|
|
if (!ActionBlocker.CanInteract(player, itemUid))
|
|
return false;
|
|
|
|
item = new(itemUid.Value, itemComp);
|
|
return true;
|
|
}
|
|
|
|
[Serializable, NetSerializable]
|
|
protected sealed class StorageComponentState : ComponentState
|
|
{
|
|
public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
|
|
|
|
public Dictionary<string, List<ItemStorageLocation>> SavedLocations = new();
|
|
|
|
public List<Box2i> Grid = new();
|
|
|
|
public ProtoId<ItemSizePrototype>? MaxItemSize;
|
|
|
|
public EntityWhitelist? Whitelist;
|
|
|
|
public EntityWhitelist? Blacklist;
|
|
}
|
|
}
|