diff --git a/Content.Client/DeadSpace/Kitchen/PlateVisualsSystem.cs b/Content.Client/DeadSpace/Kitchen/PlateVisualsSystem.cs new file mode 100644 index 0000000000000..a81bec6a65750 --- /dev/null +++ b/Content.Client/DeadSpace/Kitchen/PlateVisualsSystem.cs @@ -0,0 +1,166 @@ +// Мёртвый Космос, Licensed under custom terms with restrictions on public hosting and commercial use, full text: https://raw.githubusercontent.com/dead-space-server/space-station-14-fobos/master/LICENSE.TXT + +using System.Linq; +using System.Numerics; +using Content.Client.Items.Systems; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.DeadSpace.Kitchen.Components; +using Content.Shared.Item; +using Robust.Client.GameObjects; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; + +namespace Content.Client.DeadSpace.Kitchen; + +public sealed class PlateVisualsSystem : EntitySystem +{ + private const string WorldLayerPrefix = "plate-content-"; + + [Dependency] private readonly ItemSystem _itemSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + private readonly Dictionary> _worldLayerKeys = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnEntInserted); + SubscribeLocalEvent(OnEntRemoved); + SubscribeLocalEvent(OnVisualsChanged); + } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + UpdateWorldVisuals(ent); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + ClearWorldLayers(ent.Owner); + _worldLayerKeys.Remove(ent.Owner); + } + + private void OnEntInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != ent.Comp.SlotId) + return; + + UpdateWorldVisuals(ent); + } + + private void OnEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.SlotId) + return; + + UpdateWorldVisuals(ent); + } + + private void OnVisualsChanged(Entity ent, ref VisualsChangedEvent args) + { + if (args.ContainerId != ent.Comp.SlotId) + return; + + UpdateWorldVisuals(ent); + } + + private void UpdateWorldVisuals(Entity ent) + { + if (!TryComp(ent.Owner, out SpriteComponent? sprite)) + return; + + ClearWorldLayers(ent.Owner, sprite); + + var keys = new HashSet(); + if (TryGetContentSprite(ent.Owner, ent.Comp, out var contentSprite)) + { + AddClonedLayers(ent.Owner, + sprite, + contentSprite, + ent.Comp.ContentOffset, + ent.Comp.ContentScale, + WorldLayerPrefix, + keys); + } + + if (keys.Count > 0) + _worldLayerKeys[ent.Owner] = keys; + + _itemSystem.VisualsChanged(ent.Owner); + } + + private void ClearWorldLayers(EntityUid uid, SpriteComponent? sprite = null) + { + if (!_worldLayerKeys.Remove(uid, out var keys)) + return; + + if (!Resolve(uid, ref sprite, false)) + return; + + foreach (var key in keys) + { + _sprite.RemoveLayer((uid, sprite), key, false); + } + } + + private bool TryGetContentSprite(EntityUid plate, PlateComponent component, out SpriteComponent contentSprite) + { + var content = _itemSlots.GetItemOrNull(plate, component.SlotId); + + if (content != null && + TryComp(content.Value, out SpriteComponent? sprite) && + sprite != null) + { + contentSprite = sprite; + return true; + } + + contentSprite = default!; + return false; + } + + private void AddClonedLayers(EntityUid targetUid, + SpriteComponent targetSprite, + SpriteComponent sourceSprite, + Vector2 offset, + Vector2 scale, + string keyPrefix, + ISet keySink) + { + var layerIndex = 0; + var sourceEntity = sourceSprite.Owner; + Entity target = (targetUid, targetSprite); + + foreach (var i in Enumerable.Range(0, sourceSprite.AllLayers.Count())) + { + if (!_sprite.TryGetLayer((sourceEntity, sourceSprite), i, out var sourceLayer, false) || + !sourceLayer.Visible || + sourceLayer.Blank || + sourceLayer.CopyToShaderParameters != null) + { + continue; + } + + var clone = new SpriteComponent.Layer(sourceLayer, targetSprite); + + var key = $"{keyPrefix}{layerIndex}"; + var index = _sprite.AddLayer(target, clone); + _sprite.LayerMapSet(target, key, index); + + if (clone.RSI == null && sourceLayer.ActualRsi != null) + { + _sprite.LayerSetRsi(clone, sourceLayer.ActualRsi, sourceLayer.State); + _sprite.LayerSetAnimationTime(clone, sourceLayer.AnimationTime); + } + + _sprite.LayerSetOffset(clone, clone.Offset + offset); + _sprite.LayerSetScale(clone, clone.Scale * scale); + keySink.Add(key); + layerIndex++; + } + } +} diff --git a/Content.Shared/DeadSpace/Kitchen/Components/PlateComponent.cs b/Content.Shared/DeadSpace/Kitchen/Components/PlateComponent.cs new file mode 100644 index 0000000000000..315bb5b361acc --- /dev/null +++ b/Content.Shared/DeadSpace/Kitchen/Components/PlateComponent.cs @@ -0,0 +1,29 @@ +// Мёртвый Космос, Licensed under custom terms with restrictions on public hosting and commercial use, full text: https://raw.githubusercontent.com/dead-space-server/space-station-14-fobos/master/LICENSE.TXT + +using System.Numerics; +using Content.Shared.Item; +using Robust.Shared.Prototypes; + +namespace Content.Shared.DeadSpace.Kitchen.Components; + +[RegisterComponent] +public sealed partial class PlateComponent : Component +{ + [DataField("slotId")] + public string SlotId = "plate_slot"; + + [DataField("contentOffset")] + public Vector2 ContentOffset = Vector2.Zero; + + [DataField("heldContentOffsetLeft")] + public Vector2 HeldContentOffsetLeft = Vector2.Zero; + + [DataField("heldContentOffsetRight")] + public Vector2 HeldContentOffsetRight = Vector2.Zero; + + [DataField("contentScale")] + public Vector2 ContentScale = Vector2.One; + + [DataField("maxItemSize")] + public ProtoId MaxItemSize = "Normal"; +} diff --git a/Content.Shared/DeadSpace/Kitchen/SharedPlateSystem.cs b/Content.Shared/DeadSpace/Kitchen/SharedPlateSystem.cs new file mode 100644 index 0000000000000..0ad51f4264a32 --- /dev/null +++ b/Content.Shared/DeadSpace/Kitchen/SharedPlateSystem.cs @@ -0,0 +1,168 @@ +// Мёртвый Космос, Licensed under custom terms with restrictions on public hosting and commercial use, full text: https://raw.githubusercontent.com/dead-space-server/space-station-14-fobos/master/LICENSE.TXT + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.DeadSpace.Kitchen.Components; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; +using Content.Shared.Nutrition; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Verbs; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared.DeadSpace.Kitchen; + +public sealed class SharedPlateSystem : EntitySystem +{ + private const int EatAltVerbPriority = 10; + + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly IngestionSystem _ingestion = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedItemSystem _item = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private readonly Dictionary, EntityWhitelist> _sizeWhitelistCache = new(); + + public override void Initialize() + { + base.Initialize(); + + if (!_net.IsClient) + SubscribeLocalEvent(OnPlateMapInit); + SubscribeLocalEvent(OnUseInHand, before: [typeof(ItemSlotsSystem)]); + SubscribeLocalEvent>(OnGetAlternativeVerbs); + SubscribeLocalEvent(OnAccessibleOverride); + SubscribeLocalEvent(OnInRangeOverride); + } + + private void OnPlateMapInit(Entity ent, ref MapInitEvent args) + { + if (!TryComp(ent.Owner, out var itemSlots) || + !_itemSlots.TryGetSlot(ent.Owner, ent.Comp.SlotId, out var slot, itemSlots)) + { + return; + } + + slot.Whitelist = GetSizeWhitelist(ent.Comp.MaxItemSize); + Dirty(ent.Owner, itemSlots); + } + + private void OnUseInHand(Entity ent, ref UseInHandEvent args) + { + if (args.Handled) + return; + + if (!TryGetEdibleContent(ent.Owner, ent.Comp, out _)) + return; + + TryUsePlateContent(ent.Owner, ent.Comp, args.User); + args.Handled = true; + } + + private void OnGetAlternativeVerbs(Entity ent, ref GetVerbsEvent args) + { + if (args.Hands == null || + !args.CanAccess || + !args.CanInteract || + _hands.IsHolding((args.User, args.Hands), ent.Owner)) + { + return; + } + + var content = _itemSlots.GetItemOrNull(ent.Owner, ent.Comp.SlotId); + if (content == null || !TryGetIngestionVerb(args.User, content.Value, out var verb) || verb == null) + return; + + var user = args.User; + if (verb.Priority < EatAltVerbPriority) + verb.Priority = EatAltVerbPriority; + verb.Act = () => TryUsePlateContent(ent.Owner, ent.Comp, user); + args.Verbs.Add(verb); + } + + private bool TryUsePlateContent(EntityUid plate, PlateComponent component, EntityUid user) + { + if (!TryGetEdibleContent(plate, component, out var content)) + return false; + + return _ingestion.TryIngest(user, user, content.Value); + } + + private bool TryGetIngestionVerb(EntityUid user, EntityUid content, [NotNullWhen(true)] out AlternativeVerb? verb) + { + verb = null; + var type = _ingestion.GetEdibleType((content, CompOrNull(content))); + return type != null && _ingestion.TryGetIngestionVerb(user, content, type.Value, out verb); + } + + private bool TryGetEdibleContent(EntityUid plate, PlateComponent component, [NotNullWhen(true)] out EntityUid? content) + { + content = _itemSlots.GetItemOrNull(plate, component.SlotId); + return content != null && _ingestion.GetEdibleType((content.Value, CompOrNull(content.Value))) != null; + } + + private void OnAccessibleOverride(Entity ent, ref AccessibleOverrideEvent args) + { + if (!TryGetPlate(args.Target, out var plate) || !_interaction.CanAccess(ent.Owner, plate.Value)) + return; + + args.Handled = true; + args.Accessible = true; + } + + private void OnInRangeOverride(Entity ent, ref InRangeOverrideEvent args) + { + if (!TryGetPlate(args.Target, out var plate) || !_interaction.InRangeUnobstructed(ent.Owner, plate.Value)) + return; + + args.Handled = true; + args.InRange = true; + } + + private bool TryGetPlate(EntityUid target, [NotNullWhen(true)] out EntityUid? plate) + { + plate = null; + + if (!_containers.TryGetContainingContainer(target, out var container) || + !TryComp(container.Owner, out PlateComponent? plateComp) || + container.ID != plateComp.SlotId) + { + return false; + } + + plate = container.Owner; + return true; + } + + private EntityWhitelist GetSizeWhitelist(ProtoId maxItemSize) + { + if (_sizeWhitelistCache.TryGetValue(maxItemSize, out var whitelist)) + return whitelist; + + var maxSize = _item.GetSizePrototype(maxItemSize); + whitelist = new EntityWhitelist + { + Sizes = _prototype.EnumeratePrototypes() + .Where(size => size <= maxSize) + .OrderBy(size => size.Weight) + .Select(size => (ProtoId) size.ID) + .ToList(), + }; + + _sizeWhitelistCache[maxItemSize] = whitelist; + return whitelist; + } +} diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml index 55d22e3980b47..fb557031126b7 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml @@ -15,6 +15,11 @@ - type: Sprite sprite: Objects/Consumable/Food/plates.rsi state: plate + # DS14-start + - type: ContainerContainer + containers: + plate_slot: !type:ContainerSlot + # DS14-end - type: Item shape: - 0,0,1,0 @@ -24,6 +29,20 @@ - state: plate-inhand-left right: - state: plate-inhand-right + # DS14-start + - type: ItemSlots + slots: + plate_slot: + insertOnInteract: true + ejectOnInteract: false + ejectOnUse: true + disableEject: false + swap: true + ejectOnBreak: false + - type: Plate + contentOffset: 0, -0.1 + maxItemSize: Normal + # DS14-end - type: DamageOnLand damage: types: @@ -45,6 +64,7 @@ collection: GlassBreak params: volume: -8 + - !type:EmptyAllContainersBehaviour #DS14 - !type:SpawnEntitiesBehavior spawn: FoodPlateTrash: @@ -96,6 +116,11 @@ - state: plate-inhand-left right: - state: plate-inhand-right + # DS14-start + - type: Plate + contentOffset: 0, 0 + maxItemSize: Normal + # DS14-end # Needs the full thing because inherting is dumb sometimes. - type: Destructible thresholds: @@ -108,6 +133,7 @@ collection: GlassBreak params: volume: -8 + - !type:EmptyAllContainersBehaviour #DS14 - !type:SpawnEntitiesBehavior spawn: FoodPlateSmallTrash: @@ -140,6 +166,11 @@ - type: Sprite sprite: Objects/Consumable/Food/plates.rsi state: plate-plastic + # DS14-start + - type: ContainerContainer + containers: + plate_slot: !type:ContainerSlot + # DS14-end - type: Item shape: - 0,0,1,0 @@ -149,6 +180,20 @@ - state: plate-plastic-inhand-left right: - state: plate-plastic-inhand-right + # DS14-start + - type: ItemSlots + slots: + plate_slot: + insertOnInteract: true + ejectOnInteract: false + ejectOnUse: true + disableEject: false + swap: true + ejectOnBreak: false + - type: Plate + contentOffset: 0, -0.1 + maxItemSize: Normal + # DS14-end - type: Tag tags: - Trash @@ -162,6 +207,11 @@ - type: Sprite sprite: Objects/Consumable/Food/plates.rsi state: plate-small-plastic + # DS14-start + - type: ContainerContainer + containers: + plate_slot: !type:ContainerSlot + # DS14-end - type: Item shape: - 0,0,1,0 @@ -171,6 +221,20 @@ - state: plate-plastic-inhand-left right: - state: plate-plastic-inhand-right + # DS14-start + - type: ItemSlots + slots: + plate_slot: + insertOnInteract: true + ejectOnInteract: false + ejectOnUse: true + disableEject: false + swap: true + ejectOnBreak: false + - type: Plate + contentOffset: 0, 0 + maxItemSize: Normal + # DS14-end - type: Tag tags: - Trash @@ -186,11 +250,29 @@ - type: Sprite sprite: Objects/Consumable/Food/plates.rsi state: tin + # DS14-start + - type: ContainerContainer + containers: + plate_slot: !type:ContainerSlot + # DS14-end - type: Item size: Small shape: - 0,0,1,0 storedOffset: 0,-3 + # DS14-start + - type: ItemSlots + slots: + plate_slot: + insertOnInteract: true + ejectOnInteract: false + ejectOnUse: true + disableEject: false + swap: true + ejectOnBreak: false + - type: Plate + maxItemSize: Normal + # DS14-end - type: Tag tags: - Trash @@ -210,11 +292,29 @@ - type: Sprite sprite: Objects/Consumable/Food/plates.rsi state: muffin-tin + # DS14-start + - type: ContainerContainer + containers: + plate_slot: !type:ContainerSlot + # DS14-end - type: Item size: Tiny shape: - 0,0,0,0 storedOffset: 0,-2 + # DS14-start + - type: ItemSlots + slots: + plate_slot: + insertOnInteract: true + ejectOnInteract: false + ejectOnUse: true + disableEject: false + swap: true + ejectOnBreak: false + - type: Plate + maxItemSize: Small + # DS14-end - type: Tag tags: - Trash