Files
crystall-punk-14/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs

1986 lines
72 KiB
C#
Raw Normal View History

using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
2023-09-11 21:20:46 +10:00
using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
2025-02-08 17:17:55 +11:00
using Content.Shared.CCVar;
2023-09-11 21:20:46 +10:00
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
2023-09-11 21:20:46 +10:00
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._CP14.Input;
2023-09-11 21:20:46 +10:00
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
2023-09-11 21:20:46 +10:00
using Content.Shared.Item;
using Content.Shared.Item.ItemToggle.Components;
2023-09-11 21:20:46 +10:00
using Content.Shared.Lock;
using Content.Shared.Materials;
2023-09-11 21:20:46 +10:00
using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
2023-09-11 21:20:46 +10:00
using Content.Shared.Timing;
using Content.Shared.Storage.Events;
2023-09-11 21:20:46 +10:00
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
2025-02-08 17:17:55 +11:00
using Robust.Shared.Configuration;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Input.Binding;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
2023-09-11 21:20:46 +10:00
using Robust.Shared.Random;
using Robust.Shared.Serialization;
2025-02-08 17:17:55 +11:00
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Rounding;
using Robust.Shared.Collections;
using Robust.Shared.Map.Enumerators;
2023-09-11 21:20:46 +10:00
namespace Content.Shared.Storage.EntitySystems;
public abstract class SharedStorageSystem : EntitySystem
{
2025-02-08 17:17:55 +11:00
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly IRobustRandom Random = default!;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!;
2025-02-08 17:17:55 +11:00
[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!;
2025-02-08 17:17:55 +11:00
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] protected readonly SharedItemSystem ItemSystem = default!;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
2025-02-08 17:17:55 +11:00
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
[Dependency] private readonly TagSystem _tag = default!;
2023-09-11 21:20:46 +10:00
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<StackComponent> _stackQuery;
private EntityQuery<TransformComponent> _xformQuery;
2025-02-08 17:17:55 +11:00
private EntityQuery<UserInterfaceUserComponent> _userQuery;
/// <summary>
/// Whether we're allowed to go up-down storage via UI.
/// </summary>
public bool NestedStorage = true;
2023-09-11 21:20:46 +10:00
[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!;
2025-02-08 17:17:55 +11:00
/// <summary>
/// Flag for whether we're checking for nested storage interactions.
/// </summary>
private bool _nestedCheck;
2023-12-11 04:26:19 -05:00
public bool CheckingCanInsert;
2025-02-08 17:17:55 +11:00
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";
2025-02-08 17:17:55 +11:00
/// <summary>
/// How many storage windows are allowed to be open at once.
/// </summary>
private int _openStorageLimit = -1;
protected readonly List<string> CantFillReasons = [];
// Caching for various checks
private readonly Dictionary<Vector2i, ulong> _ignored = new();
private List<Box2i> _itemShape = new();
2023-09-11 21:20:46 +10:00
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_itemQuery = GetEntityQuery<ItemComponent>();
_stackQuery = GetEntityQuery<StackComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
2025-02-08 17:17:55 +11:00
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
_prototype.PrototypesReloaded += OnPrototypesReloaded;
2023-09-11 21:20:46 +10:00
2025-02-08 17:17:55 +11:00
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);
2023-09-11 21:20:46 +10:00
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);
2023-09-11 21:20:46 +10:00
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
2023-11-13 23:43:03 +11:00
SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
2023-09-11 21:20:46 +10:00
SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
2023-09-11 21:20:46 +10:00
2025-02-08 17:17:55 +11:00
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))
//CP14
.Bind(CP14ContentKeyFunctions.OpenBelt2, InputCmdHandler.FromDelegate(HandleOpenBelt2, handle: false))
//CP14 end
.Register<SharedStorageSystem>();
2025-02-08 17:17:55 +11:00
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;
}
UpdateOccupied((container.Owner, storage));
if (!ItemFitsInGridLocation((itemEnt.Owner, itemEnt.Comp), (container.Owner, storage), loc))
{
ContainerSystem.Remove(itemEnt.Owner, container, force: true);
}
}
2025-02-08 17:17:55 +11:00
private void OnNestedStorageCvar(bool obj)
{
NestedStorage = obj;
}
private void OnStorageLimitChanged(int obj)
{
_openStorageLimit = obj;
}
private void OnRemove(Entity<StorageComponent> entity, ref ComponentRemove args)
{
2025-02-08 17:17:55 +11:00
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()
{
2024-01-10 20:07:35 +11:00
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)
{
// TODO: This should update all entities in storage as well.
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();
2023-09-11 21:20:46 +10:00
}
private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args)
{
2025-02-08 17:17:55 +11:00
storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
2023-11-13 23:43:03 +11:00
UpdateAppearance((uid, storageComp, null));
// Make sure the initial starting grid is okay.
UpdateOccupied((uid, storageComp));
2023-09-11 21:20:46 +10:00
}
/// <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;
2023-09-11 21:20:46 +10:00
// 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)
{
2025-02-08 17:17:55 +11:00
UI.CloseUis(entity, actor);
}
}
private void OnBoundUIClosed(EntityUid uid, StorageComponent storageComp, BoundUIClosedEvent args)
{
CloseNestedInterfaces(uid, args.Actor, storageComp);
// If UI is closed for everyone
2025-02-08 17:17:55 +11:00
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)
{
Upstream sync (#1035) * Add new implants to deimplant list (#35563) Initial commit * Doxarubixadone Description Fix (#35568) Changed medicine.ftl for Doxa. * Reptilians Can Eat Chicken Nuggets (#35569) Added meat tag to misc.yml for chicken nuggets. * Automatic changelog update * Unheck Admin Smites (#35348) * Fix admin verb names Fixed admin verb names. * Add antag verb names * Adjust antag verb icons * Amber Station - A Couple Changes (#35548) * [ADMIN] Minor Refactor AdminNameOverlay (#35520) * refactor(src): Minor refactor of Draw in "AdminNameOverlay. And new info about playtime player * fix(src): Add configure classic admin owerlay * fix * tweak(src): Use _antagLabelClassic and tweak style * tweak(src): Add config display overlay for startingJob and playTime * tweak(src): Vector2 is replaced by var * tweak(src): return to the end of the list * Automatic changelog update * Wizard PDA (#35572) * wizard PDA * colour change to brown * Automatic changelog update * Increase line spacing of the admin overlay (#35591) line spacing * make slime hair less transparent (#35158) * blabl blump or something * +0.3 * blimpuf * Automatic changelog update * Fix being able to write on/stamp/fax paper scrap (#35596) * init * item * requested changes * Apply suggestions from code review --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Changed Pride to Hubris in ion_storm.yml (#35602) Update ion_storm.yml * Sentry turrets - Part 3: Turret AI (#35058) * Initial commit * Updated Access/command.yml * Fix for Access/AccessLevelPrototype.cs * Added silicon access levels to admin items * Included self-recharging battery changes * Revert "Included self-recharging battery changes" * Addressed reviewers comments * Additional reviewer comments * DetGadget Hat Revitalization (#35438) * DetGadget Hat * uh... half-assed item description * Reduce hat range to one tile, you have to stand on someone to steal their hat items * Fix Integration Errors * Only the wearer can access voice commands * init work - handscomp is unable to be pulled * second bit of progress * basic working implementation * nuke storageslots and add adminlogging * disallow trolling nukies or hiding objective items * remove unnecessary tags additions * finish nuking unused tags * death to yamllinter * int tests be damned * milon is a furry * address review * upd desc * address reviews part 2 * address more reviews * remove unused refs * fix order of dependencies * add ShowVerb to SharedStorageSystem.cs This will allow or disallow showing the "Open Storage" verb if defined on the component. * orks is a nerd * add proper locale, fix adminlogging * orks is a nerd 2 --------- Co-authored-by: Coenx-flex <coengmurray@gmail.com> * Automatic changelog update * Fingerprint Reader System (#35600) * init * public api * stuff * weh * Remove cellular resistance for slimes (#35583) * Remove cellular resistance for slimes * Update guidebook * Automatic changelog update * Give the station map inhand sprites (#35605) map has inhands * Reagent guidebook reactions UI dividers (#35608) * Update GuideReagentReaction.xaml * Update Content.Client/Guidebook/Controls/GuideReagentReaction.xaml Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> * Update Content.Client/Guidebook/Controls/GuideReagentReaction.xaml Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> --------- Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> * fix cluwne pda pen slot (#35611) Co-authored-by: deltanedas <@deltanedas:kde.org> * Revert "Make radioactive material radioactive" (#35330) * Automatic changelog update * Predict vending machine UI (#33412) * Automatic changelog update * #32209 changelog (#35619) Since it was merged into staging no changelog was made, but we should at least have it for next release. (And vulture) * Automatic changelog update * Cloning Refactor and bugfixes (#35555) * cloning refactor * cleanup and fixes * don't pick from 0 * give dwarves the correct species * fix dna and bloodstream reagent data cloning * don't copy helmets * be less redundant * Automatic changelog update * centcomm update (#35627) * Better Insectoid Glasses (#31812) Sprites and file changes Adds the variants for arachnid and moth glasses, adds the code for those in the meta.json files, and adds the speciesID tag in both arachnid and moth prototype files. * Automatic changelog update * Add GetBaseName method to NameModifierSystem (#35633) * Add GetBaseName method to NameModifierSystem * Use Name * Save Space Station 14 from the Toilet Gibber Forever (#35587) * The evil is defeated * Tag body bags * uwu, cwush me cwusher-chan * absolute 18+ sloggery * botos binted? 👽 * Automatic changelog update * Changed Damage Overlay to check Burn Damage (#34535) * updated BruteLevel to be PainLevel with burn damage checks in DamageOverlayUiController.cs * dehardcoded pain level by adding damage groups to paindamagegroups to affect * re-added the name for painDamageGroups * fixed overlay default and added minimum limit to component check first * renamed to PainDamageGroups and removed obsolete tag * Automatic changelog update * Wizard's Magical Pen (#35623) * wizard pen * description change * Automatic changelog update * Added decelerator percentage drain (#35643) * Added variable PercentageDrain to SinguloFoodComponent * Set percentageDrain to 0.03 (3%) for anti particles * Added percentageDrain logic in public OnConsumed * Simplify SinguloFoodComponent and set percentageDrain to negative * EnergyFactor now applies to positive values too * Better commenting on EnergyFactor * Update Content.Server/Singularity/Components/SinguloFoodComponent.cs * Documentation of EnergyFactor * Fixing spelling mistake --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Made butter require less milk (#35650) made butter take less milk * Automatic changelog update * Delete SolutionContainerVisualsComponent.InitialName (#35654) * Fix name of cotton dough rope (#35657) changed in-game name of cotton dough rope to differentiate from normal dough rope * CVar - Toggle display of round-end greentext (#35651) * hide greentext if cvar false * change IFs around a lil * reviews * Open State for cowtools (#35666) Open State * Make implants unshielded (#35667) * Add undergarments & "Censor Nudity" toggle to options (#33185) * Initial commit * Attribution * Review changes * Added comment for upstream * Automatic changelog update * centcomm update (#35674) * More scars! (#35644) * :3 * whitespace, stomach scar * Automatic changelog update * Lathe menu UI displays a count of available recipes (#35570) * commit * jumped the gun * changes * Players with unknown playtimes now are tagged as new players, take 2 (#35648) * your commit? our commit. * skreee * show joined players before lobby players; comments * comments * playerinfo retains playtime data after disconnect * new connection status symbols * Automatic changelog update * Add firelocks and locked external airlocks to ATS (#35516) * Add firelocks and locked airlocks to ATS * Add fire alarms * Elkridge Tesla and TEG Improvements + Other stuff (#35684) * better tesla, better TEG, better sci maints, chef gets chef closet * added storage room for tesla parts, added captain bathroom, changed vault so nuke can be armed * ran fixgridatmos and added some vacuum markers * unflatpacked containment shit * Cargo Mail System (#35429) * shitcode init * biocoding, SpawnTableOnUse, Moving shit to shared * server :( * fixes * ok works * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * Discard changes to Content.Shared/Forensics/Components/FingerprintMaskComponent.cs * Discard changes to Content.Shared/Forensics/Components/FingerprintComponent.cs * Discard changes to Content.Server/Forensics/Systems/ForensicsSystem.cs * Discard changes to Content.Server/StationRecords/Systems/StationRecordsSystem.cs * Discard changes to Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * big stuff * preperation * temperory spawning thing for testing * Update CargoDeliveryDataComponent.cs * kinda proper spawning idk god save me * cleanup (kinda) * preparation 2.0 * stuff i think * entity table work * renames * spawn ratio based on players * comment * letter tables * more spam * package tables * comment * biocodedn't * builds correctly * cleaning * Update deliveries_tables.yml * labels * package sprites * mail teleporter * revert testing value * fix test * fix other test * i love tests * mail teleporter enabled by default * random cooldowns * fixtures * Discard changes to Content.Shared/FingerprintReader/FingerprintReaderComponent.cs * Discard changes to Content.Shared/FingerprintReader/FingerprintReaderSystem.cs * Discard changes to Content.Shared/Interaction/Events/GettingUsedAttemptEvent.cs * Discard changes to Resources/Locale/en-US/fingerprint-reader/fingerprint-reader.ftl * clean * fuck paper scrap * oops * fuck SpawnTableOnUse * mail teleporter board in QM locker + addressed review * oops * clean * sound on delivery spawn * address review * partial review address * partial review addressing * addressing partial review * pratarial revivew address * misprediction hell * stuff * more stuff * unrelated * TODO * link * partial review * DirtyField --------- Co-authored-by: Milon <milonpl.git@proton.me> * Automatic changelog update * Add AssertMultiple to ContrabandTest (#35662) * add AssertMultiple to ContrabandTest * do the same for magazine visuals test * :trollface: --------- Co-authored-by: deltanedas <@deltanedas:kde.org> * add forceghost admin command (#35518) * add forceghost admin command * sweep linq under the rug * braces * ûse LocalizedEntityCommands * Automatic changelog update * Text related keybinds can now be changed in Controls (#35630) * Add ability to rebind text related keybinds * fix placement of locales * Automatic changelog update * Update b2dynamictree (#30630) * Update submodule to 248.0.0 (#35720) * Add sun shadows (planet lighting stage 2) (#35145) * Implements a Dynamic Lighting System on maps. * Edit: the night should be a little bit brighter and blue now. * Major edit: everything must be done on the client side now, with certain datafield replicated. Changes were outlined in the salvage to accommodate the new lighting system. * Edit: The offset is now serverside, this makes the time accurate in all situations. * Removing ununsed import * Minor tweaks * Tweak in time precision * Minor tweak + Unused import removed * Edit: apparently RealTime is better for what I'm looking for * Fix: Now the time is calculated correctly. * Minor tweaks * Adds condition for when the light should be updated * Add planet lighting * she * close-ish * c * bittersweat * Fixes * Revert "Merge branch '22719' into 2024-09-29-planet-lighting" This reverts commit 9f2785bb16aee47d794aa3eed8ae15004f97fc35, reversing changes made to 19649c07a5fb625423e08fc18d91c9cb101daa86. * Europa and day-night * weh * rooves working * Clean * Remove Europa * Fixes * fix * Update * Fix caves * Update for engine * Add sun shadows (planet lighting v2) For now mostly targeting walls and having the shadows change over time. Got the basic proof-of-concept working just needs a hell of a lot of polish. * Documentation * a * Fixes * Move blur to an overlay * Slughands * Fixes * Apply RoofOverlay per-grid not per-map * Fix light render scales * sangas * Juice it a bit * Better angle * Fixes * Add color support * Rounding bandaid * Wehs * Better * Remember I forgot to do this when writing docs --------- Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com> * Automatic changelog update * Omega Mail Teleporter (#35705) Mail! * Packed Mail Teleporter (#35706) Mail! * Box Mail Teleporter (#35707) Mail! * Oasis Mail Teleporter (#35708) Mail! * Meta Mail Teleporter (#35709) Mail! * Marathon Mail Teleporter (#35710) Mail! * Fland Mail Teleporter (#35711) Mail! * Plasma fixes 4 (#35716) Fixes 15 Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com> * Aroace pride pin, scarf, and cloak (#35718) cloak, pin, and scarf added yayyyy * Automatic changelog update * [Part of #32893] Localize silicon dataset names (#33352) * Localize ai names * Apply requested changes * Localize autoborg * Localize borg names * Localize atv names * Correct prototypes ids to follow naming conventions * Remove AI localization (Moved to another PR) * Weh * [Part of #32893] Localize arachnid dataset names (#33353) * Localize arachnid dataset names * Correct prototype ids to follow naming conventions * Combine arachnid_first.yml and arachnid_last.yml * Upstream names * [Part of #32893] Localize summonable creatures dataset names (#33392) * Localize clown names * Localize golem names * Localize hologram names * Correct prototype ids to follow naming conventions * Update Resources/Locale/en-US/datasets/names/golem.ftl --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize antagonists dataset names (#33393) * Localize fake human names * Localize ninja names * Localize operation names * Localize regalrat names * Localize revenant names * Localize syndicate names * Localize wizard names * Correct prototype ids to follow naming conventions * Combine fake_human_first.yml and fake_human_last.yml * Move contents of ninja_title.yml into ninja.yml * Combine Operation_prefix.yml and Operation_suffix.yml * Combine wizard_first.yml and wizard_last.yml * Upstream names * fix wizard --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize humanoid species dataset names (#33395) * Localize diona names * Localize moth names * Localize mushman names * Localize reptilian names * Localize skeleton names * Upstream diona names * names-moth-male/female-first-dataset -> names-moth-first-male/female-dataset * Correct prototype ids to follow naming conventions * NamesSkeletonFirst -> NamesSkeleton * Combine moth_first_female.yml, moth_first_male.yml and moth_last.yml * Forgot about skeleton prototype * Upstream names * Update Resources/Locale/en-US/datasets/names/diona_last.ftl * Update Resources/Locale/en-US/datasets/names/diona_last.ftl * keep first name for skeleton --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * [Part of #32893] Localize vox dataset names (#33396) * Localize vox names * Correct prototype id to follow naming conventions * Upstream names * [Part of #32893] Localize first & last dataset names (#33401) * Localize first names * Localize last names * Correct prototype ids to follow naming conventions * Combine first.yml and last.yml into base.yml * Forgot about = in last * [Part of #32893] Localize first male & female dataset names (#33402) * Localize first_name * Localize first_female * names-male/female-first-dataset -> names-first-male/female-dataset * Correct prototype ids to follow naming conventions * Combine first_male.yml and first_female.yml into base_gendered.yml * [Part of #32893] Localize misc dataset names (#33404) * Localize cargo_shuttle names * Localize death_commando names * Localize fortunes * Localize military names * Localize rollie names * fortunes.ftl -> cookie_fortune.ftl * Correct prototype ids to follow naming conventions * Localize all dataset names (#32893) * Use `LocalizedDatasetPrototype` instead of `DatasetPrototype` in `RoleLoadoutPrototype` * Localize ai names * Replace to `LocalizedDatasetPrototype` in `NamingSystem` * Localize arachnid first and last names * Localize atv names * Localize autoborg names * Forgot to change type to localizedDataset * Localize borer names * Localize borg names * Localize cargo shuttle names * Localize clown names * Localize death_commando names * Localize diona names * Localize fake_human names * Localize first and last names * Localize first male and female names * Localize fortunes descriptions * Forgot about equal sign * Localize golem names * Localize hologram names * Localize military names * Localize moth first male and female names * Localize moth last names * Fix autoborg name error * Localize mushman first and last names * Localize ninja names * Localize operation names * Localize regalrat names * Fix mushman_first * Forgot about `Loc.GetString` * Move comments into comment section & fix names * Localize reptilian male and female names * Localize revenant names * Fix locale word order in operation * Localize rollie (btw it was never used and was added as "for the futuгe" long time ago) * Localize skeleton_first names * Localize syndicate names * Localize vox names * Localize wizard first and last names * `{owner}-name-dataset` -> `names-{owner}-dataset` * Change `DatasetPrototype` to `LocalizedDatasetPrototype` and make sure it works properly GetFTLName is no more the static method, we need it to be able to use `Loc.GetString` * I hate those mothname comments * Combine name datasets prototypes * Move every ftl from` /en-US/names` to ` /en-US/datasets/names` * Remove ftl files * Get every dataset yml back * Remove changes for planets. Move it in another PR * Revert these changes (Moved to another PR) * How * Apply suggested changes * Fix integration tests (#35727) * test * fix names * fix more * Initial delivery balance changes (#35728) * init * small balance * guess not * Update Content.Server/Delivery/CargoDeliveryDataComponent.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Fixed delivery popups (#35724) * :) * cool stuff * Remove a bonus Loc.GetString (#35731) oops * Bagel Engineering Improvements (#35717) * woe, better engineering be upon ye * im going to lose it * radical plan * oopsie * Revert "oopsie" This reverts commit 45ab057f55b46acd795e58257c3cc5967e5cb946. * Revert "radical plan" This reverts commit 57b1ae081725a47aef3ae03111cecbc91b4f47a8. * Revert "im going to lose it" This reverts commit e7b4afaf5d9a10a42e89831ffc9294d3b9bd96d4. * Revert "woe, better engineering be upon ye" This reverts commit 471dc3716b58a39631aa8bee00de79e981391d63. * complete revamp * revision * oops 2 electric boogaloo * another one * every time i push to fix a minor mistake i found in walking around i get closer to my limit * Update Credits (#35733) Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> * Loop mail teleporter (#35729) * latejoin * youve got mail * Core mail update (#35719) * core mail update * empty * derotate core (#35740) Update default.yml * Elkridge Mail Update (#35738) add mail teleporter and mailing unit system * Automatic changelog update * Plasma Mail Teleporter (#35741) Mail! * Convex Mail Teleporter (#35742) Mail! * Remove unneeded Loc.GetString (#35739) * Steal the mail thieving objective (#35746) * mail theft * networked * Automatic changelog update * fix UpdateBankAccount (#35749) * trolled * fun * fuck me * Slightly better letter loot table (#35748) * init * review --------- Co-authored-by: Milon <milonpl.git@proton.me> * Python Suit Storage Visual (#35593) * Python-SUITSTORAGE-Visuals Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * REVised Sprite Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Copyright Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> * Update Resources/Textures/Objects/Weapons/Guns/Revolvers/python.rsi/meta.json Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * fix nukeops commander name (#35753) * bagel update (#35754) * Predict some power PowerReceiver stuff (#33834) * Predict some power PowerReceiver stuff Need it for some atmos device prediction. * Also this * Localize traitor codeverbs datasets (#35737) * Localize verbs dataset * Localize adjectives dataset * Localize corporations dataset * Update TraitorRuleSystem to use LocalizedDatasetPrototype instead of DatasetPrototype * Fix sun shadows in ANGLE (#35757) I think I fat-fingered a ctrl-Z on this at some point but the intermediate blur is necessary. * Automatic changelog update * Tweak sun shadow rotations (#35758) Won't use the entity's rotation for the matrix, I just forgot to do this. Means shadows will always point in the same direction and the points will correctly adjust as the entity rotates. * Automatic changelog update * Fix Ahelp window playerlist resize (#35747) reorganize bwoink window layout * Automatic changelog update * Ensure speech bubble cap is always respected (#32223) Ensure speech bubble cap is respected, even when messages are sent very fast * Cleanup: Fix ``PaperWriteEvent`` in ``PaperSystem`` (#35763) * Cleanup + fix * Revert * Cleanup: Add missing locale ``cmd-planet-map-prototype`` (#35766) Cleanup * Added New Cocktails and new fill level sprites to existing drinks. (#33570) * Added New Cocktails and new fill level sprites to existing drinks * Updated copyright info and fixed recipies for Caipirinha/Mojito. --------- Co-authored-by: RedBookcase <Usualmoves@gmail.com> * Automatic changelog update * Performer's Wig (#35764) * miku wig * fix to correct json convention Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> --------- Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> * Automatic changelog update * Merge showsubfloorforever into showsubfloor (#33682) * convex fix * Removable mindshields and revolutionary tweaks. (#35769) * I fucking hate revs * Update preset-revolutionary.ftl * fixy fix * Automatic changelog update * Mail Resprite (#35776) * init commit * init commit * delete those * added github to copyright info * Fix Chameleon PDAs renaming the user in station records (#35782) * Automatic changelog update * Restore the order of admin overlay elements (#35783) admin overlay order fix * Automatic changelog update * Fixes and refactoring to discord changelog script (#33859) * Fixes and refactoring to discord changelog script Upstreamed from https://github.com/impstation/imp-station-14/pull/1023 * Add some prints back in * Update to borg ion storms (#35751) * Updates ion storms for borgs. * Remove additional ion laws into future PR * Automatic changelog update * TriggerSystem improvements (#35762) * desynchronizer real * yaml stuff from slarti branch * C# stuff * oops * fix triggers * atomize PR --------- Co-authored-by: Flareguy <woaj9999@outlook.com> * Roleban command error handling (#35784) roleban command jobid fail handling * Localize news dataset (#35774) * Localize news dataset * Remove the `"` * Localize rat king commands datasets (#35780) * Added mail room * Update submodule to 248.0.2 (#35787) * Update Space Law to reflect Implant changes (#35701) * Change implanter Space Law * Add clarification regarding unidentified implanter vs. unidentified implant sentensing * Add support for antag-before-job selection (#35789) * Add support for antag-before-job selection * Include logging * forensics cleanup (#35795) * polymorph popup fixes (#35796) polymorph fixes * fix more syndicate names (#35788) * New Feature: Warden job rolls before security officer/cadet/detective (#35313) Commit * Automatic changelog update * Fix anomaly doublelogging (#34297) cull doublelogging * Add wallmount N2 closets, Revived (#34042) * Add standard, wallmount and improvised N2 closets, Revived * remove improvised locker * Parent>ID * Undo sprite replacement * Update meta.json --------- Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Milon <milonpl.git@proton.me> * Cryo and grinder cleanup (#34842) * cryopod and grinder cleanup * lower case name * 4 spaces * prototype clean * looks like there is some kind of test that prevents removing this * grinder too * both should be empty * cleanup * Add Gold and Coal Rock Anomalies (#34809) * This commit adds 2 new Rock Anomaly types, Coal and Gold. It also adds Resource Crabs, colored crystals, and lights for both. * Added crafting recipes for yellow and black light tubes. Somehow I forgot that the first time. * Sorted tags.yml alphabetically this time instead of not doing that. * Updated Texture Copyright information * Attempted to fix Merge Conflict * Added bulb light variants for both yellow and black crystals. * Automatic changelog update * Tools/Devices: In-hand Sprites (#33689) * Adds in-hand sprites to the barber scissors. * adds in-hand sprites to the floodlight. * adds in-hand sprites to the gas analyzer. * adds in-hand sprites to the gps. * Update copyright wording, linting * resprite gps inhand sprites. * adds in-hand sprites to the mass scanner. * adds in-hand sprites to the spray_painter. * resprite in-hand sprites to the mass_scanner. * fix in-hand sprites to the mass_scanner. * Resprite mass_scanner in-hand sprites. * Automatic changelog update * IconSmooth additional smoothing keys (#35790) * additionalKeys * Update lava.yml * Update Content.Client/IconSmoothing/IconSmoothComponent.cs --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Locks nitrous oxide canisters (#35785) lock nitrous oxide canisters * Automatic changelog update * Cleanup Objective files, add PickSpecificPersonComponent (#35802) * cleanup objectives * remove unrelated access restriction * review * Adds popup when firing gun while gun has no ammo (#34816) * Adds popup when firing gun while gun has no ammo * simplify --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> * Automatic changelog update * Add the ability to pet the mail teleporter (#35819) good mailbox * Automatic changelog update * Whitehole/Singularity grenade price adjustments + whitehole grenade fix (#35821) * prices + adjustments * teehee * Automatic changelog update * Update Lobby Music Attribtions (#35817) Biggest change is updating the attributions and links for Sunbeamstress' to reflect the changes in their online profile as the previous link is now a dead link. Updated Comet Haley's link to go directly to Stellardrone's Bandcamp instead of diverting to Free Music Archive Fixed a double the in the comment for Space Asshole * Paradox Clone (#35794) * polymorph fixes * paradox clone * forensics cleanup * bump doors * 4 * attribution * polymorphn't * clean up objectives * Update Resources/ServerInfo/Guidebook/Antagonist/MinorAntagonists.xml * review * add virtual items to blacklist * allow them to roll sleeper agent * Automatic changelog update * Improvements to antag-before-job selection system (#35822) * Fix the latejoin-antag-deficit bug, add datafield, add logging * Fix multiple roles being made for single-role defs, * HOTFIX: Fix paradox clone event (#35858) fix paradox clone event * Update CP14TownSendConditionSystem.cs --------- Signed-off-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Co-authored-by: Smith <182301147+AgentSmithRadio@users.noreply.github.com> Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com> Co-authored-by: Pancake <Pangogie@users.noreply.github.com> Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com> Co-authored-by: Schrödinger <132720404+Schrodinger71@users.noreply.github.com> Co-authored-by: Velken <8467292+Velken@users.noreply.github.com> Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com> Co-authored-by: LaCumbiaDelCoronavirus <90893484+LaCumbiaDelCoronavirus@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com> Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: Coenx-flex <coengmurray@gmail.com> Co-authored-by: hivehum <ketchupfaced@gmail.com> Co-authored-by: Thomas <87614336+Aeshus@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: Myra <vasilis@pikachu.systems> Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com> Co-authored-by: HTML/Crystal <152909599+HTMLSystem@users.noreply.github.com> Co-authored-by: Tayrtahn <tayrtahn@gmail.com> Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com> Co-authored-by: Coolsurf6 <coolsurf24@yahoo.com.au> Co-authored-by: rokudara-sen <160833839+rokudara-sen@users.noreply.github.com> Co-authored-by: DuckManZach <144298822+DuckManZach@users.noreply.github.com> Co-authored-by: MisterImp <101299120+MisterImp@users.noreply.github.com> Co-authored-by: Killerqu00 <47712032+Killerqu00@users.noreply.github.com> Co-authored-by: Ps3Moira <113228053+ps3moira@users.noreply.github.com> Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com> Co-authored-by: āda <ss.adasts@gmail.com> Co-authored-by: War Pigeon <54217755+minus1over12@users.noreply.github.com> Co-authored-by: Deerstop <edainturner@gmail.com> Co-authored-by: Milon <milonpl.git@proton.me> Co-authored-by: Łukasz Mędrek <lukasz@lukaszm.xyz> Co-authored-by: DoutorWhite <thedoctorwhite@gmail.com> Co-authored-by: compilatron <40789662+Compilatron144@users.noreply.github.com> Co-authored-by: jbox1 <40789662+jbox144@users.noreply.github.com> Co-authored-by: Momo <Rsnesrud@gmail.com> Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: TytosB <54259736+TytosB@users.noreply.github.com> Co-authored-by: Prole <172158352+Prole0@users.noreply.github.com> Co-authored-by: Evelyn Gordon <evelyn.gordon20@gmail.com> Co-authored-by: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> Co-authored-by: RedBookcase <crazykid1590@gmail.com> Co-authored-by: RedBookcase <Usualmoves@gmail.com> Co-authored-by: SpaceManiac <tad@platymuus.com> Co-authored-by: Spessmann <156740760+Spessmann@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> Co-authored-by: imcb <irismessage@protonmail.com> Co-authored-by: valquaint <57813693+valquaint@users.noreply.github.com> Co-authored-by: Flareguy <woaj9999@outlook.com> Co-authored-by: ninruB <ninrub@tuta.io> Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com> Co-authored-by: Velcroboy <velcroboy333@hotmail.com> Co-authored-by: Łukasz Lindert <lukasz.lindert@protonmail.com> Co-authored-by: Firewars763 <35506916+Firewars763@users.noreply.github.com> Co-authored-by: onesch <118821520+onesch@users.noreply.github.com> Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com> Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com> Co-authored-by: Crude Oil <124208219+CroilBird@users.noreply.github.com> Co-authored-by: Lusatia <ultimate_doge@outlook.com>
2025-03-17 11:54:43 +03:00
if (component.ShowVerb == false || !CanInteract(args.User, (uid, component), args.CanAccess && args.CanInteract))
return;
// Does this player currently have the storage UI open?
2025-02-08 17:17:55 +11:00
var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User);
ActivationVerb verb = new()
{
Act = () =>
{
if (uiOpen)
{
2025-02-08 17:17:55 +11:00
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, [NotNullWhen(true)] out StorageComponent? storage, out ItemStorageLocation loc)
{
loc = default;
storage = null;
if (!ContainerSystem.TryGetContainingContainer(itemEnt, out container) ||
container.ID != StorageComponent.ContainerId ||
!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;
2025-02-08 17:17:55 +11:00
if (!UI.TryOpenUi(uid, StorageComponent.StorageUiKey.Key, entity))
return;
if (!silent && !_tag.HasTag(entity, storageComp.SilentStorageUserTag))
{
2025-02-08 17:17:55 +11:00
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
if (useDelay != null)
UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID);
}
}
public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
2023-09-11 21:20:46 +10:00
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)))
2023-09-11 21:20:46 +10:00
return;
// if the target is storage, add a verb to transfer storage.
if (TryComp(args.Target, out StorageComponent? targetStorage)
2023-11-13 23:43:03 +11:00
&& (!TryComp(args.Target, out LockComponent? targetLock) || !targetLock.Locked))
2023-09-11 21:20:46 +10:00
{
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)
2023-09-11 21:20:46 +10:00
};
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))
2023-09-11 21:20:46 +10:00
return;
var attemptEv = new StorageInteractUsingAttemptEvent();
RaiseLocalEvent(uid, ref attemptEv);
if (attemptEv.Cancelled)
2023-09-11 21:20:46 +10:00
return;
2024-06-04 18:16:06 +03:00
if (_whitelistSystem.IsWhitelistPass(storageComp.CP14Ignorelist, args.Used))
return;
PlayerInsertHeldEntity((uid, storageComp), args.User);
2023-09-11 21:20:46 +10:00
// 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.
2023-09-11 21:20:46 +10:00
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)))
2023-09-11 21:20:46 +10:00
return;
// Toggle
2025-02-08 17:17:55 +11:00
if (UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User))
{
2025-02-08 17:17:55 +11:00
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
}
else
{
OpenStorageUI(uid, args.User, storageComp, false);
}
args.Handled = true;
2023-09-11 21:20:46 +10:00
}
2025-02-08 17:17:55 +11:00
protected virtual void HideStorageWindow(EntityUid uid, EntityUid actor)
{
}
protected virtual void ShowStorageWindow(EntityUid uid, EntityUid actor)
{
}
2023-09-11 21:20:46 +10:00
/// <summary>
/// Specifically for storage implants.
/// </summary>
private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args)
{
if (args.Handled)
2023-09-11 21:20:46 +10:00
return;
2025-02-08 17:17:55 +11:00
var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.Performer);
if (uiOpen)
2025-02-08 17:17:55 +11:00
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.Performer);
else
OpenStorageUI(uid, args.Performer, storageComp, false);
args.Handled = true;
2023-09-11 21:20:46 +10:00
}
/// <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))
2023-09-11 21:20:46 +10:00
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;
2023-09-11 21:20:46 +10:00
foreach (var entity in _entSet)
2023-09-11 21:20:46 +10:00
{
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)
2023-09-11 21:20:46 +10:00
|| !_interactionSystem.InRangeUnobstructed(args.User, entity))
{
continue;
}
_entList.Add(entity);
delay += itemSize.Weight * AreaInsertDelayPerItem;
if (_entList.Count >= StorageComponent.AreaPickupLimit)
break;
2023-09-11 21:20:46 +10:00
}
//If there's only one then let's be generous
if (_entList.Count >= 1)
2023-09-11 21:20:46 +10:00
{
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, delay, new AreaPickupDoAfterEvent(GetNetEntityList(_entList)), uid, target: uid)
2023-09-11 21:20:46 +10:00
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
2023-09-11 21:20:46 +10:00
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
2023-11-13 23:43:03 +11:00
args.Handled = true;
2023-09-11 21:20:46 +10:00
}
return;
}
// Pick up the clicked entity
if (storageComp.QuickInsert)
{
if (args.Target is not { Valid: true } target)
return;
2025-02-08 17:17:55 +11:00
if (ContainerSystem.IsEntityInContainer(target)
2023-09-11 21:20:46 +10:00
|| target == args.User
|| !_itemQuery.HasComponent(target))
2023-09-11 21:20:46 +10:00
{
return;
}
if (TryComp(uid, out TransformComponent? transformOwner) && TryComp(target, out TransformComponent? transformEnt))
2023-09-11 21:20:46 +10:00
{
var parent = transformOwner.ParentUid;
var position = TransformSystem.ToCoordinates(
2023-09-11 21:20:46 +10:00
parent.IsValid() ? parent : uid,
TransformSystem.GetMapCoordinates(transformEnt)
2023-09-11 21:20:46 +10:00
);
2023-11-13 23:43:03 +11:00
args.Handled = true;
if (PlayerInsertEntityInWorld((uid, storageComp), args.User, target))
2023-09-11 21:20:46 +10:00
{
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(GetNetEntity(uid),
2023-09-11 21:20:46 +10:00
new List<NetEntity> { GetNetEntity(target) },
new List<NetCoordinates> { GetNetCoordinates(position) },
new List<Angle> { transformOwner.LocalRotation }), args.User);
2023-09-11 21:20:46 +10:00
}
}
}
}
private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
2023-11-13 23:43:03 +11:00
args.Handled = true;
2023-09-11 21:20:46 +10:00
var successfullyInserted = new List<EntityUid>();
var successfullyInsertedPositions = new List<EntityCoordinates>();
var successfullyInsertedAngles = new List<Angle>();
if (!_xformQuery.TryGetComponent(uid, out var xform))
2023-09-11 21:20:46 +10:00
{
return;
}
var entCount = Math.Min(StorageComponent.AreaPickupLimit, args.Entities.Count);
for (var i = 0; i < entCount; i++)
{
var entity = GetEntity(args.Entities[i]);
2023-09-11 21:20:46 +10:00
// Check again, situation may have changed for some entities, but we'll still pick up any that are valid
2025-02-08 17:17:55 +11:00
if (ContainerSystem.IsEntityInContainer(entity)
2023-09-11 21:20:46 +10:00
|| entity == args.Args.User
|| !_itemQuery.HasComponent(entity))
{
2023-09-11 21:20:46 +10:00
continue;
}
2023-09-11 21:20:46 +10:00
if (!_xformQuery.TryGetComponent(entity, out var targetXform) ||
2023-09-11 21:20:46 +10:00
targetXform.MapID != xform.MapID)
{
continue;
}
var position = TransformSystem.ToCoordinates(
2023-09-11 21:20:46 +10:00
xform.ParentUid.IsValid() ? xform.ParentUid : uid,
new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID)
2023-09-11 21:20:46 +10:00
);
var angle = targetXform.LocalRotation;
if (PlayerInsertEntityInWorld((uid, component), args.Args.User, entity, playSound: false))
2023-09-11 21:20:46 +10:00
{
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!
2023-09-11 21:20:46 +10:00
if (successfullyInserted.Count > 0)
{
if (!_tag.HasTag(args.User, component.SilentStorageUserTag))
Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams);
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(
2023-09-11 21:20:46 +10:00
GetNetEntity(uid),
GetNetEntityList(successfullyInserted),
GetNetCoordinatesList(successfullyInsertedPositions),
successfullyInsertedAngles), args.User);
2023-09-11 21:20:46 +10:00
}
args.Handled = true;
}
private void OnReclaimed(EntityUid uid, StorageComponent storageComp, GotReclaimedEvent args)
{
2025-02-08 17:17:55 +11:00
ContainerSystem.EmptyContainer(storageComp.Container, destination: args.ReclaimerCoordinates);
}
2023-09-11 21:20:46 +10:00
private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args)
{
var coordinates = TransformSystem.GetMoverCoordinates(uid);
2023-09-11 21:20:46 +10:00
// Being destroyed so need to recalculate.
2025-02-08 17:17:55 +11:00
ContainerSystem.EmptyContainer(storageComp.Container, destination: coordinates);
2023-09-11 21:20:46 +10:00
}
/// <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)
2023-09-11 21:20:46 +10:00
{
if (!ValidateInput(args, msg.StorageUid, msg.InteractedItemUid, out var player, out var storage, out var item))
2023-09-11 21:20:46 +10:00
return;
// If the user's active hand is empty, try pick up the item.
if (player.Comp.ActiveHandEntity == null)
2023-09-11 21:20:46 +10:00
{
_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))
2023-09-11 21:20:46 +10:00
{
Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
2023-09-11 21:20:46 +10:00
}
return;
2023-09-11 21:20:46 +10:00
}
_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}");
2023-09-11 21:20:46 +10:00
// 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);
2023-09-11 21:20:46 +10:00
}
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);
}
2025-02-08 17:17:55 +11:00
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))
2025-02-08 17:17:55 +11:00
return;
var localPlayer = args.SenderSession.AttachedEntity;
var itemEnt = new Entity<ItemComponent?>(itemUid.Value, itemComp);
2025-02-08 17:17:55 +11:00
// Validate the source storage
if (!TryGetStorageLocation(itemEnt, out var container, out _, out _) ||
!ValidateInput(args, GetNetEntity(container.Owner), out _, out _))
{
2025-02-08 17:17:55 +11:00
return;
}
2025-02-08 17:17:55 +11:00
if (!TryComp(localPlayer, out HandsComponent? handsComp) || !_sharedHandsSystem.TryPickup(localPlayer.Value, itemEnt, handsComp: handsComp, animate: false))
2025-02-08 17:17:55 +11:00
return;
// Validate the target storage
2025-02-08 17:17:55 +11:00
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)
2023-09-11 21:20:46 +10:00
{
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);
2023-09-11 21:20:46 +10:00
}
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);
}
2025-02-08 17:17:55 +11:00
private void OnBoundUIOpen(Entity<StorageComponent> ent, ref BoundUIOpenedEvent args)
2023-09-11 21:20:46 +10:00
{
2025-02-08 17:17:55 +11:00
UpdateAppearance((ent.Owner, ent.Comp, null));
}
private void OnBoundUIAttempt(Entity<StorageComponent> ent, ref BoundUserInterfaceMessageAttempt args)
2025-02-08 17:17:55 +11:00
{
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;
}
}
}
2023-09-11 21:20:46 +10:00
}
private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (entity.Comp.Container == null)
2023-11-13 23:43:03 +11:00
return;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
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))
{
2025-02-08 17:17:55 +11:00
ContainerSystem.Remove(args.Entity, args.Container, force: true);
return;
}
entity.Comp.StoredItems[args.Entity] = location.Value;
AddOccupiedEntity(entity, args.Entity, location.Value);
}
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;
if (entity.Comp.StoredItems.Remove(args.Entity, out var loc))
{
RemoveOccupiedEntity(entity, args.Entity, loc);
}
Dirty(entity, entity.Comp);
UpdateAppearance((entity, entity.Comp, null));
UpdateUI((entity, entity.Comp));
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args)
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
if (args.Cancelled || args.Container.ID != StorageComponent.ContainerId)
2023-09-11 21:20:46 +10:00
return;
2023-12-11 04:26:19 -05:00
// 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
2023-11-13 23:43:03 +11:00
args.Cancel();
}
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
public void UpdateAppearance(Entity<StorageComponent?, AppearanceComponent?> entity)
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// 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;
2023-11-19 15:10:27 +13:00
// 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));
2023-11-13 23:43:03 +11:00
2025-02-08 17:17:55 +11:00
var isOpen = UI.IsUiOpen(entity.Owner, StorageComponent.StorageUiKey.Key);
2023-11-13 23:43:03 +11:00
_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);
2023-09-11 21:20:46 +10:00
}
/// <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())
{
2023-09-12 22:34:04 +10:00
Insert(target, entity, out _, user: user, targetComp, playSound: false);
2023-09-11 21:20:46 +10:00
}
if (user != null
&& (!_tag.HasTag(user.Value, sourceComp.SilentStorageUserTag)
|| !_tag.HasTag(user.Value, targetComp.SilentStorageUserTag)))
Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams);
2023-09-11 21:20:46 +10:00
}
/// <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>
2023-09-11 21:20:46 +10:00
/// <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>
2023-09-11 21:20:46 +10:00
/// <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,
2023-12-11 04:26:19 -05:00
bool ignoreLocation = false)
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false))
2023-09-11 21:20:46 +10:00
{
reason = null;
return false;
}
if (Transform(insertEnt).Anchored)
2023-09-11 21:20:46 +10:00
{
reason = "comp-storage-anchored-failure";
return false;
}
if (_whitelistSystem.IsWhitelistFail(storageComp.Whitelist, insertEnt) ||
_whitelistSystem.IsBlacklistPass(storageComp.Blacklist, insertEnt))
2023-09-11 21:20:46 +10:00
{
reason = "comp-storage-invalid-container";
return false;
}
2023-11-13 23:43:03 +11:00
if (!ignoreStacks
&& _stackQuery.TryGetComponent(insertEnt, out var stack)
&& HasSpaceInStacks((uid, storageComp), stack.StackTypeId))
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
reason = null;
return true;
}
var maxSize = GetMaxItemSize((uid, storageComp));
if (ItemSystem.GetSizePrototype(item.Size) > maxSize)
2023-11-13 23:43:03 +11:00
{
reason = "comp-storage-too-big";
return false;
}
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
if (TryComp<StorageComponent>(insertEnt, out var insertStorage)
&& GetMaxItemSize((insertEnt, insertStorage)) >= maxSize)
2023-11-13 23:43:03 +11:00
{
reason = "comp-storage-too-big";
return false;
}
if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(insertEnt))
2023-11-13 23:43:03 +11:00
{
if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _))
{
reason = "comp-storage-insufficient-capacity";
return false;
}
}
2023-09-11 21:20:46 +10:00
2023-12-11 04:26:19 -05:00
CheckingCanInsert = true;
2025-02-08 17:17:55 +11:00
if (!ContainerSystem.CanInsert(insertEnt, storageComp.Container))
{
2023-12-11 04:26:19 -05:00
CheckingCanInsert = false;
reason = null;
return false;
}
2023-12-11 04:26:19 -05:00
CheckingCanInsert = false;
2023-09-11 21:20:46 +10:00
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;
AddOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
if (Insert(uid,
insertEnt,
out stackedEntity,
out _,
user: user,
storageComp: uid.Comp,
playSound: playSound,
stackAutomatically: stackAutomatically))
{
return true;
}
RemoveOccupiedEntity((uid.Owner, uid.Comp), insertEnt, location);
uid.Comp.StoredItems.Remove(insertEnt);
return false;
}
2023-09-11 21:20:46 +10:00
/// <summary>
/// Inserts into the storage container
/// </summary>
2023-11-13 23:43:03 +11:00
/// <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>
2023-11-13 23:43:03 +11:00
/// <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)
2023-09-11 21:20:46 +10:00
{
2023-09-12 22:34:04 +10:00
stackedEntity = null;
reason = null;
2023-09-12 22:34:04 +10:00
2023-11-13 23:43:03 +11:00
if (!Resolve(uid, ref storageComp))
2023-09-11 21:20:46 +10:00
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))
2023-09-11 21:20:46 +10:00
{
2025-02-08 17:17:55 +11:00
if (!ContainerSystem.Insert(insertEnt, storageComp.Container))
2023-11-13 23:43:03 +11:00
return false;
2023-09-11 21:20:46 +10:00
if (canPlaySound)
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
return true;
}
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
var toInsertCount = insertStack.Count;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
foreach (var ent in storageComp.Container.ContainedEntities)
{
if (!_stackQuery.TryGetComponent(ent, out var containedStack))
continue;
2023-09-11 21:20:46 +10:00
2023-11-13 23:43:03 +11:00
if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack))
continue;
2023-11-13 23:43:03 +11:00
stackedEntity = ent;
if (insertStack.Count == 0)
break;
2023-09-11 21:20:46 +10:00
}
2023-11-13 23:43:03 +11:00
// Still stackable remaining
if (insertStack.Count > 0
2025-02-08 17:17:55 +11:00
&& !ContainerSystem.Insert(insertEnt, storageComp.Container)
2023-11-13 23:43:03 +11:00
&& toInsertCount == insertStack.Count)
2023-09-11 21:20:46 +10:00
{
2023-11-13 23:43:03 +11:00
// Failed to insert anything.
2023-09-11 21:20:46 +10:00
return false;
}
if (canPlaySound)
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
2023-09-11 21:20:46 +10:00
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)
2023-09-11 21:20:46 +10:00
{
if (!Resolve(ent.Owner, ref ent.Comp)
|| !Resolve(player.Owner, ref player.Comp)
|| player.Comp.ActiveHandEntity == null)
2023-09-11 21:20:46 +10:00
return false;
var toInsert = player.Comp.ActiveHandEntity;
2023-09-11 21:20:46 +10:00
if (!CanInsert(ent, toInsert.Value, out var reason, ent.Comp))
2023-09-11 21:20:46 +10:00
{
_popupSystem.PopupClient(Loc.GetString(reason ?? "comp-storage-cant-insert"), ent, player);
2023-09-11 21:20:46 +10:00
return false;
}
if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, player.Comp))
2023-09-11 21:20:46 +10:00
{
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), ent, player);
2023-09-11 21:20:46 +10:00
return false;
}
return PlayerInsertEntityInWorld((ent, ent.Comp), player, toInsert.Value);
2023-09-11 21:20:46 +10:00
}
/// <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?})"/>.
2023-09-11 21:20:46 +10:00
/// </summary>
/// <param name="uid"></param>
2023-09-11 21:20:46 +10:00
/// <param name="player">The player to insert an entity with</param>
/// <param name="toInsert"></param>
2023-09-11 21:20:46 +10:00
/// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertEntityInWorld(Entity<StorageComponent?> uid, EntityUid player, EntityUid toInsert, bool playSound = true)
2023-09-11 21:20:46 +10:00
{
if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid.Owner))
2023-09-11 21:20:46 +10:00
return false;
if (!Insert(uid, toInsert, out _, user: player, uid.Comp, playSound: playSound))
2023-09-11 21:20:46 +10:00
{
_popupSystem.PopupClient(Loc.GetString("comp-storage-cant-insert"), uid, player);
return false;
}
return true;
}
2023-09-12 22:34:04 +10:00
/// <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;
if (storageEnt.Comp.StoredItems.Remove(itemEnt, out var existing))
{
RemoveOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, existing);
}
storageEnt.Comp.StoredItems.Add(itemEnt, location);
AddOccupiedEntity((storageEnt.Owner, storageEnt.Comp), itemEnt, location);
2025-02-08 17:17:55 +11:00
UpdateUI(storageEnt);
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);
}
}
// Ignore the item's existing location for fitting purposes.
_ignored.Clear();
if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
{
AddOccupied(itemEnt, existing, _ignored);
}
// This uses a faster path than the typical codepaths
// as we can cache a bunch more data and re-use it to avoid a bunch of component overhead.
// So if we have an item that occupies 0,0 and is a single rectangle we can assume that the tile itself we're checking
// is always in its shapes regardless of angle. This matches virtually every item in the game and
// means we can skip getting the item's rotated shape at all if the tile is occupied.
// This mostly makes heavy checks (e.g. area insert) much, much faster.
var fastPath = false;
var itemShape = ItemSystem.GetItemShape(itemEnt);
var fastAngles = itemShape.Count == 1;
if (itemShape.Count == 1 && itemShape[0].Contains(Vector2i.Zero))
fastPath = true;
var chunkEnumerator = new ChunkIndicesEnumerator(storageBounding, StorageComponent.ChunkSize);
var angles = new ValueList<Angle>();
if (!fastAngles)
{
angles.Clear();
for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
{
angles.Add(angle);
}
}
else
{
var shape = itemShape[0];
// At least 1 check for a square.
angles.Add(startAngle);
// If it's a rectangle make it 2.
if (shape.Width != shape.Height)
{
// Idk if there's a preferred facing but + or - 90 pick one.
angles.Add(startAngle + Angle.FromDegrees(90));
}
}
while (chunkEnumerator.MoveNext(out var storageChunk))
{
var storageChunkOrigin = storageChunk.Value * StorageComponent.ChunkSize;
var left = Math.Max(storageChunkOrigin.X, storageBounding.Left);
var bottom = Math.Max(storageChunkOrigin.Y, storageBounding.Bottom);
var top = Math.Min(storageChunkOrigin.Y + StorageComponent.ChunkSize - 1, storageBounding.Top);
var right = Math.Min(storageChunkOrigin.X + StorageComponent.ChunkSize - 1, storageBounding.Right);
// No data so assume empty.
if (!storageEnt.Comp.OccupiedGrid.TryGetValue(storageChunkOrigin, out var occupied))
continue;
// This has a lot of redundant tile checks but with the fast path it shouldn't matter for average ss14
// use cases.
for (var y = bottom; y <= top; y++)
{
for (var x = left; x <= right; x++)
{
foreach (var angle in angles)
{
var position = new Vector2i(x, y);
// This bit of code is how area inserts go from tanking frames to being negligible.
if (fastPath)
{
var flag = SharedMapSystem.ToBitmask(SharedMapSystem.GetChunkRelative(position, StorageComponent.ChunkSize), StorageComponent.ChunkSize);
// Occupied so skip.
if ((occupied & flag) == flag)
continue;
}
_itemShape.Clear();
ItemSystem.GetAdjustedItemShape(_itemShape, itemEnt, angle, position);
if (ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, _itemShape, _ignored))
{
storageLocation = new ItemStorageLocation(angle, position);
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);
2025-02-08 17:17:55 +11:00
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);
}
private bool ItemFitsInGridLocation(
Dictionary<Vector2i, ulong> occupied,
IReadOnlyList<Box2i> itemShape,
Dictionary<Vector2i, ulong> ignored)
{
// We pre-cache the occupied / ignored tiles upfront and then can just check each tile 1-by-1.
// We do it by chunk so we can avoid dictionary overhead.
foreach (var box in itemShape)
{
var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunkEnumerator.MoveNext(out var chunk))
{
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
// Box may not necessarily be in 1 chunk so clamp it.
var left = Math.Max(chunkOrigin.X, box.Left);
var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
// Assume it's occupied if no data.
if (!occupied.TryGetValue(chunkOrigin, out var occupiedMask))
{
return false;
}
var ignoredMask = ignored.GetValueOrDefault(chunkOrigin);
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
// Ignore it
if ((ignoredMask & flag) == flag)
continue;
if ((occupiedMask & flag) == flag)
{
return false;
}
}
}
}
}
return true;
}
/// <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);
// Ignore the item's existing location for fitting purposes.
_ignored.Clear();
if (storageEnt.Comp.StoredItems.TryGetValue(itemEnt.Owner, out var existing))
{
AddOccupied(itemEnt, existing, _ignored);
}
return ItemFitsInGridLocation(storageEnt.Comp.OccupiedGrid, itemShape, _ignored);
}
/// <summary>
/// Checks if a space on a grid is valid and not occupied by any other pieces.
/// </summary>
public bool IsGridSpaceEmpty(Entity<StorageComponent?> storageEnt, Vector2i location, Dictionary<Vector2i, ulong>? ignored = null)
{
if (!Resolve(storageEnt, ref storageEnt.Comp))
return false;
var chunkOrigin = SharedMapSystem.GetChunkIndices(location, StorageComponent.ChunkSize) * StorageComponent.ChunkSize;
// No entry so assume it's occupied.
if (!storageEnt.Comp.OccupiedGrid.TryGetValue(chunkOrigin, out var occupiedMask))
return false;
var chunkRelative = SharedMapSystem.GetChunkRelative(location, StorageComponent.ChunkSize);
var occupiedIndex = SharedMapSystem.ToBitmask(chunkRelative);
if (ignored?.TryGetValue(chunkOrigin, out var ignoredMask) == true && (ignoredMask & occupiedIndex) == occupiedIndex)
{
return true;
}
if ((occupiedMask & occupiedIndex) != 0x0)
{
return false;
}
return true;
}
/// <summary>
/// Updates the occupied grid mask for the entity.
/// </summary>
protected void UpdateOccupied(Entity<StorageComponent> ent)
{
ent.Comp.OccupiedGrid.Clear();
RemoveOccupied(ent.Comp.Grid, ent.Comp.OccupiedGrid);
Dirty(ent);
foreach (var (stent, storedItem) in ent.Comp.StoredItems)
{
if (!_itemQuery.TryGetComponent(stent, out var itemComp))
continue;
AddOccupiedEntity(ent, (stent, itemComp), storedItem);
}
}
private void AddOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
{
AddOccupied(itemEnt, location, storageEnt.Comp.OccupiedGrid);
Dirty(storageEnt);
}
private void AddOccupied(Entity<ItemComponent?> itemEnt, ItemStorageLocation location, Dictionary<Vector2i, ulong> occupied)
{
var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
AddOccupied(adjustedShape, occupied);
}
private void RemoveOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
{
foreach (var box in adjustedShape)
{
var chunks = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunks.MoveNext(out var chunk))
{
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
var left = Math.Max(box.Left, chunkOrigin.X);
var bottom = Math.Max(box.Bottom, chunkOrigin.Y);
var right = Math.Min(box.Right, chunkOrigin.X + StorageComponent.ChunkSize - 1);
var top = Math.Min(box.Top, chunkOrigin.Y + StorageComponent.ChunkSize - 1);
var existing = occupied.GetValueOrDefault(chunkOrigin, ulong.MaxValue);
// Unmark all of the tiles that we actually have.
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
existing &= ~flag;
}
}
// My kingdom for collections.marshal
occupied[chunkOrigin] = existing;
}
}
}
private void AddOccupied(IReadOnlyList<Box2i> adjustedShape, Dictionary<Vector2i, ulong> occupied)
{
foreach (var box in adjustedShape)
{
// Reduce dictionary access from every tile to just once per chunk.
// Makes this more complicated but dictionaries are slow af.
// This is how we get savings over IsGridSpaceEmpty.
var chunkEnumerator = new ChunkIndicesEnumerator(box, StorageComponent.ChunkSize);
while (chunkEnumerator.MoveNext(out var chunk))
{
var chunkOrigin = chunk.Value * StorageComponent.ChunkSize;
var existing = occupied.GetOrNew(chunkOrigin);
// Box may not necessarily be in 1 chunk so clamp it.
var left = Math.Max(chunkOrigin.X, box.Left);
var bottom = Math.Max(chunkOrigin.Y, box.Bottom);
var right = Math.Min(chunkOrigin.X + StorageComponent.ChunkSize - 1, box.Right);
var top = Math.Min(chunkOrigin.Y + StorageComponent.ChunkSize - 1, box.Top);
for (var x = left; x <= right; x++)
{
for (var y = bottom; y <= top; y++)
{
var index = new Vector2i(x, y);
var chunkRelative = SharedMapSystem.GetChunkRelative(index, StorageComponent.ChunkSize);
var flag = SharedMapSystem.ToBitmask(chunkRelative, StorageComponent.ChunkSize);
existing |= flag;
}
}
occupied[chunkOrigin] = existing;
}
}
}
private void RemoveOccupiedEntity(Entity<StorageComponent> storageEnt, Entity<ItemComponent?> itemEnt, ItemStorageLocation location)
{
var adjustedShape = ItemSystem.GetAdjustedItemShape((itemEnt.Owner, itemEnt.Comp), location);
RemoveOccupied(adjustedShape, storageEnt.Comp.OccupiedGrid);
Dirty(storageEnt);
}
/// <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
2025-02-08 17:17:55 +11:00
foreach (var actor in UI.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
{
if (!CanInteract(actor, (uid, component)))
2025-02-08 17:17:55 +11:00
UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
}
}
2023-11-13 23:43:03 +11:00
private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
{
2025-02-08 17:17:55 +11:00
if (ContainerSystem.TryGetContainingContainer((uid, null, component), out var container) &&
2023-11-13 23:43:03 +11:00
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");
}
//CP14
private void HandleOpenBelt2(ICommonSession? session)
{
HandleToggleSlotUI(session, "belt2");
}
//CP14 end
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;
2025-02-08 17:17:55 +11:00
if (!UI.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
{
OpenStorageUI(storageEnt.Value, playerEnt, silent: false);
}
else
{
2025-02-08 17:17:55 +11:00
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;
}
2023-09-12 22:34:04 +10:00
/// <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.
2025-02-08 17:17:55 +11:00
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;
}
2023-09-11 21:20:46 +10:00
}