diff --git a/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs
new file mode 100644
index 0000000000..147441412d
--- /dev/null
+++ b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs
@@ -0,0 +1,90 @@
+using Robust.Shared.Audio;
+
+namespace Content.Server.SimpleStation14.LoudSpeakers;
+
+[RegisterComponent]
+public sealed class LoudSpeakerComponent : Component
+{
+ public IPlayingAudioStream? CurrentPlayingSound;
+
+ public TimeSpan NextPlayTime = TimeSpan.Zero;
+
+ ///
+ /// The port to look for a signal on.
+ ///
+ public string PlaySoundPort = "Trigger";
+
+ ///
+ /// Whether or not this loudspeaker has ports.
+ ///
+ [DataField("ports")]
+ public bool Ports = true;
+
+ ///
+ /// The name of the container the payload is in.
+ /// This is specified in the construction graph.
+ ///
+ [DataField("containerSlot")]
+ public string ContainerSlot = "payload";
+
+ ///
+ /// Can this loudspeaker be triggered by interacting with it?
+ ///
+ [DataField("triggerOnInteract")]
+ public bool TriggerOnInteract = false;
+
+ ///
+ /// Should this loudspeaker interrupt already playing sounds if triggered?
+ /// If false, the sounds will overlap.
+ ///
+ ///
+ /// Warning: If this is false, the speaker will not clean up after itself properly.
+ /// Since it only saves one sound at a time Use with caution.
+ ///
+ [DataField("interrupt")]
+ public bool Interrupt = true;
+
+ ///
+ /// Cool down time between playing sounds.
+ ///
+ [DataField("cooldown")]
+ public TimeSpan Cooldown = TimeSpan.FromSeconds(4);
+
+ ///
+ /// The sound to play if no other sound is specified.
+ ///
+ [DataField("defaultSound")]
+ public SoundSpecifier DefaultSound = new SoundPathSpecifier("/Audio/SimpleStation14/Effects/metaldink.ogg");
+
+ ///
+ /// Default variance to be used, if no other variance is specified.
+ /// Is still subject to .
+ ///
+ [DataField("defaultVariance")]
+ public float DefaultVariance = 0.125f;
+
+ ///
+ /// The amount to multiply the volume by.
+ ///
+ [DataField("volumeMod")]
+ public float VolumeMod = 3.5f;
+
+ ///
+ /// The amount to multiply the range by.
+ ///
+ [DataField("rangeMod")]
+ public float RangeMod = 3.5f;
+
+ ///
+ /// The amount to multiply the rolloff by.
+ ///
+ [DataField("rolloffMod")]
+ public float RolloffMod = 0.3f;
+
+ ///
+ /// Amount to multiply the variance by, if the sound has one.
+ /// If the sound has a variance of 0, default variance is used.
+ ///
+ [DataField("varianceMod")]
+ public float VarianceMod = 1.5f;
+}
diff --git a/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs
new file mode 100644
index 0000000000..8d7aceb5d7
--- /dev/null
+++ b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs
@@ -0,0 +1,133 @@
+using Content.Server.MachineLinking.Events;
+using Content.Server.MachineLinking.System;
+using Content.Server.Power.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Containers;
+using Content.Server.Sound.Components;
+using Content.Shared.Sound.Components;
+using Content.Shared.Interaction;
+using Robust.Shared.Timing;
+
+namespace Content.Server.SimpleStation14.LoudSpeakers;
+
+public sealed class DoorSignalControlSystem : EntitySystem
+{
+ [Dependency] private readonly SignalLinkerSystem _signal = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnShutdown);
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnSignalReceived);
+
+ SubscribeLocalEvent(OnInteractHand);
+ }
+
+ private void OnShutdown(EntityUid uid, LoudSpeakerComponent component, ComponentShutdown args)
+ {
+ if (component.CurrentPlayingSound != null)
+ component.CurrentPlayingSound.Stop();
+ }
+
+ private void OnInit(EntityUid uid, LoudSpeakerComponent component, ComponentInit args)
+ {
+ if (component.Ports)
+ _signal.EnsureReceiverPorts(uid, component.PlaySoundPort);
+ }
+
+ ///
+ /// Tries to play a loudspeaker.
+ ///
+ /// The Loudspeaker to play.
+ /// The Loudspeaker component.
+ /// True if the Loudspeaker was played, false otherwise.
+ public bool TryPlayLoudSpeaker(EntityUid uid, LoudSpeakerComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ if (component.NextPlayTime > _timing.CurTime)
+ return false;
+
+ if (TryComp(uid, out var powerComp) && !powerComp.Powered)
+ return false;
+
+ PlayLoudSpeaker(uid, component, GetSpeakerSound(uid, component));
+
+ return true;
+ }
+
+ private void PlayLoudSpeaker(EntityUid uid, LoudSpeakerComponent component, SoundSpecifier sound)
+ {
+ var newParams = sound.Params
+ .WithVolume(sound.Params.Volume * component.VolumeMod)
+ .WithMaxDistance(sound.Params.MaxDistance * component.RangeMod)
+ .WithRolloffFactor(sound.Params.RolloffFactor * component.RolloffMod)
+ .WithVariation((sound.Params.Variation !> 0 ? component.DefaultVariance : sound.Params.Variation) * component.VarianceMod);
+
+ if (component.Interrupt && component.CurrentPlayingSound != null)
+ component.CurrentPlayingSound.Stop();
+
+ component.NextPlayTime = _timing.CurTime + component.Cooldown;
+
+ component.CurrentPlayingSound = _audio.Play(sound, Filter.Pvs(uid, component.RangeMod), uid, true, newParams);
+ }
+
+ private SoundSpecifier GetSpeakerSound(EntityUid uid, LoudSpeakerComponent component)
+ {
+ if (!TryComp(uid, out var containerManager) ||
+ !containerManager.TryGetContainer(component.ContainerSlot, out var container))
+ return component.DefaultSound;
+
+ if (container.ContainedEntities.Count == 0)
+ return component.DefaultSound;
+
+ var entity = container.ContainedEntities[0];
+
+ switch (entity)
+ {
+ case { } when TryComp(entity, out var trigger) && trigger.Sound != null:
+ return trigger.Sound;
+
+ case { } when TryComp(entity, out var activate) && activate.Sound != null:
+ return activate.Sound;
+
+ case { } when TryComp(entity, out var use) && use.Sound != null:
+ return use.Sound;
+
+ case { } when TryComp(entity, out var drop) && drop.Sound != null:
+ return drop.Sound;
+
+ case { } when TryComp(entity, out var land) && land.Sound != null:
+ return land.Sound;
+
+ default:
+ return component.DefaultSound;
+ }
+ }
+
+ private void OnSignalReceived(EntityUid uid, LoudSpeakerComponent component, SignalReceivedEvent args)
+ {
+ if (args.Port == component.PlaySoundPort)
+ {
+ TryPlayLoudSpeaker(uid, component);
+ }
+ }
+
+ private void OnInteractHand(EntityUid uid, LoudSpeakerComponent component, InteractHandEvent args)
+ {
+ if (!component.TriggerOnInteract)
+ return;
+
+ if (!TryPlayLoudSpeaker(uid, component))
+ return;
+
+ args.Handled = true;
+ }
+}
diff --git a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs
index 63e55e0d14..02993ad224 100644
--- a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs
+++ b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs
@@ -3,6 +3,7 @@
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
+using Content.Shared.SimpleStation14.Construction.Steps; // Parkstation-LoudSpeakers
namespace Content.Shared.Construction.Steps
{
@@ -41,6 +42,13 @@ public sealed class ConstructionGraphStepTypeSerializer : ITypeReader CompBlacklist { get; } = new List();
+ [DataField("tagBlacklist")] public List TagBlacklist { get; } = new List();
+
+ public override bool EntityValid(EntityUid uid, IEntityManager entityManager, IComponentFactory compFactory)
+ {
+ if (!entityManager.TryGetComponent(uid, out var item) || item.Size > Size)
+ return false;
+
+ foreach (var component in CompBlacklist)
+ {
+ if (entityManager.HasComponent(uid, compFactory.GetComponent(component).GetType()))
+ return false;
+ }
+
+ var tagSystem = entityManager.EntitySysManager.GetEntitySystem();
+
+ if (tagSystem.HasAnyTag(uid, TagBlacklist))
+ return false;
+
+ return true;
+ }
+
+ public override void DoExamine(ExaminedEvent examinedEvent)
+ {
+ examinedEvent.Message.AddMarkup(string.IsNullOrEmpty(Name)
+ ? Loc.GetString(
+ "construction-insert-entity-below-size",
+ ("size", Size))
+ : Loc.GetString(
+ "construction-insert-exact-entity",
+ ("entityName", Name)));
+ }
+ }
+}
diff --git a/Resources/Audio/SimpleStation14/Effects/attributions.yml b/Resources/Audio/SimpleStation14/Effects/attributions.yml
new file mode 100644
index 0000000000..6c2a4d76d0
--- /dev/null
+++ b/Resources/Audio/SimpleStation14/Effects/attributions.yml
@@ -0,0 +1,9 @@
+- files: ["metaldink.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Recorded and edited by @Pspritechologist#9442"
+ source: "Parkstation COMMIT HERE"
+
+- files: ["buzzer_one.ogg", "buzzer_two.ogg"]
+ license: "Pixabay Content License"
+ copyright: "By Pixabay"
+ source: "https://pixabay.com/sound-effects/buzz-buzz-95806/"
\ No newline at end of file
diff --git a/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg b/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg
new file mode 100644
index 0000000000..0cdeac73b7
Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg differ
diff --git a/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg b/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg
new file mode 100644
index 0000000000..a2a417c9a1
Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg differ
diff --git a/Resources/Audio/SimpleStation14/Effects/metaldink.ogg b/Resources/Audio/SimpleStation14/Effects/metaldink.ogg
new file mode 100644
index 0000000000..7acfd4617f
Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/metaldink.ogg differ
diff --git a/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl b/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl
new file mode 100644
index 0000000000..b2920740a7
--- /dev/null
+++ b/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl
@@ -0,0 +1,3 @@
+# Parkstation
+# Shown when examining an in-construction object
+construction-insert-entity-below-size = Next, insert an entity no larger than {$size}.
diff --git a/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml
new file mode 100644
index 0000000000..a7e986b611
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml
@@ -0,0 +1,16 @@
+- type: entity
+ id: LoudSpeakerElectronics
+ parent: BaseElectronics
+ name: loud speaker electronics
+ description: An electronics board used in loud speakers
+ components:
+ - type: Sprite
+ sprite: Objects/Misc/module.rsi
+ state: id_mod
+ - type: Tag
+ tags:
+ - DroneUsable
+ - LoudSpeakerElectronics
+ - type: ReverseEngineering
+ recipes:
+ - LoudSpeakerElectronics
diff --git a/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml
new file mode 100644
index 0000000000..be7e04e821
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml
@@ -0,0 +1,27 @@
+- type: entity
+ abstract: true
+ id: SoundBoxBase
+ parent: BaseItem
+ name: sound box
+ description: A small box designed to play a specific audio. It has no way to play on its own.
+ components:
+ - type: Sprite
+ sprite: Objects/Misc/module.rsi
+ state: id_mod
+ - type: Tag
+ tags:
+ - SoundBox
+ - type: EmitSoundOnTrigger
+ sound: Buzzer
+
+- type: entity
+ id: SoundBoxBuzzer
+ parent: SoundBoxBase
+ name: buzzer sound box
+ description: A sound box containing a small metal disc that vibrates when electricity is applied to it.
+ components:
+ - type: EmitSoundOnTrigger
+ sound: Buzzer
+ - type: Sprite
+ sprite: Objects/Misc/module.rsi
+ state: id_mod
diff --git a/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml
new file mode 100644
index 0000000000..6e61764042
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml
@@ -0,0 +1,148 @@
+- type: entity
+ # parent: BaseMachine # ConstructibleMachine Work out dual inheritance
+ id: LoudSpeakerBase
+ name: loudspeaker
+ description: An loudspeaker. For when the station just needs to know something.
+ components:
+ - type: WallMount
+ - type: ApcPowerReceiver
+ - type: Electrified
+ enabled: false
+ usesApcPower: true
+ - type: ExtensionCableReceiver
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Appearance
+ - type: Sprite
+ noRot: false
+ sprite: Structures/Wallmounts/intercom.rsi
+ layers:
+ - state: base
+ - state: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ # - state: broadcasting
+ # map: ["enum.RadioDeviceVisualLayers.Broadcasting"]
+ # shader: unshaded
+ # visible: false
+ - type: Transform
+ noRot: false
+ anchored: true
+ # - type: Wires
+ # BoardName: "LoudSpeaker"
+ # LayoutId: LoudSpeaker
+ - type: LoudSpeaker
+ - type: Construction
+ graph: LoudSpeaker
+ node: loudspeaker
+ containers:
+ - board
+ - payload
+ - type: ContainerContainer
+ containers:
+ payload: !type:Container
+ board: !type:Container
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: Metallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 160
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - trigger:
+ !type:DamageTrigger
+ damage: 80
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ path: /Audio/Effects/metalbreak.ogg
+ - !type:EmptyAllContainersBehaviour
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetSteel1:
+ min: 1
+ max: 2
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: GenericVisualizer
+ visuals:
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: true }
+ False: { visible: false }
+ # enum.RadioDeviceVisuals.Broadcasting:
+ # enum.RadioDeviceVisualLayers.Broadcasting:
+ # True: { visible: true }
+ # False: { visible: false }
+ # placement:
+ # mode: SnapgridCenter
+ # snap:
+ # - Wallmount
+
+- type: entity
+ id: LoudSpeakerAssesmbly
+ name: loudspeaker assembly
+ description: A loudspeaker. It doesn't seem very helpful right now.
+ components:
+ - type: WallMount
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Sprite
+ sprite: Structures/Wallmounts/intercom.rsi
+ state: build
+ - type: Construction
+ graph: LoudSpeaker
+ node: assembly
+ containers:
+ - board
+ - payload
+ - type: Transform
+ anchored: true
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wallmount
+
+- type: entity
+ parent: LoudSpeakerBase
+ id: LoudSpeakerBuzzer
+ name: buzzer
+ description: A buzzer to indicate that someone wants your attention.
+ components:
+ - type: ContainerFill
+ containers:
+ payload:
+ - BikeHorn
+ board:
+ - LoudSpeakerElectronics
+
+- type: entity
+ parent: LoudSpeakerBase
+ id: LoudSpeakerBuzzerService
+ suffix: Service
+ name: buzzer
+ description: A buzzer to indicate that someone wants your attention.
+ components:
+ - type: ContainerFill
+ containers:
+ payload:
+ - DeskBell
+ board:
+ - LoudSpeakerElectronics
+
+- type: entity
+ parent: LoudSpeakerBase
+ id: LoudSpeakerSec
+ suffix: Sec
+ name: buzzer
+ description: A buzzer to indicate that someone wants your attention.
+ components:
+ - type: ContainerFill
+ containers:
+ payload:
+ - BikeHorn
+ board:
+ - LoudSpeakerElectronics
diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml
new file mode 100644
index 0000000000..b693533466
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml
@@ -0,0 +1,17 @@
+- type: construction
+ name: loudspeaker
+ id: LoudSpeakerAssesmbly
+ graph: LoudSpeaker
+ startNode: start
+ targetNode: loudspeaker
+ category: construction-category-structures
+ description: A loudspeaker. When you want to make sure someone's attention is always ready to be grabbed.
+ icon:
+ sprite: Structures/Wallmounts/intercom.rsi
+ state: base
+ placementMode: SnapgridCenter
+ objectType: Structure
+ canRotate: true
+ canBuildInImpassable: true
+ conditions:
+ - !type:WallmountCondition {}
diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml
new file mode 100644
index 0000000000..3e2428cfc0
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml
@@ -0,0 +1,113 @@
+- type: constructionGraph
+ id: LoudSpeaker
+ start: start
+ graph:
+ - node: start
+ edges:
+ - to: assembly
+ steps:
+ - material: Steel
+ amount: 2
+ doAfter: 2.0
+
+ - node: assembly
+ entity: LoudSpeakerAssesmbly
+ edges:
+ - to: wired
+ steps:
+ - material: Cable
+ amount: 2
+ doAfter: 1
+ - to: start
+ completed:
+ - !type:GivePrototype
+ prototype: SheetSteel1
+ amount: 2
+ - !type:DeleteEntity {}
+ steps:
+ - tool: Welding
+ doAfter: 2
+
+ - node: wired
+ entity: LoudSpeakerAssesmbly
+ edges:
+ - to: electronics
+ steps:
+ - tag: LoudSpeakerElectronics
+ store: board
+ name: "loudspeaker electronics"
+ icon:
+ sprite: "Objects/Misc/module.rsi"
+ state: "id_mod"
+ doAfter: 1
+ - to: assembly
+ completed:
+ - !type:GivePrototype
+ prototype: CableApcStack1
+ amount: 2
+ steps:
+ - tool: Cutting
+ doAfter: 1
+
+ - node: electronics
+ entity: LoudSpeakerAssesmbly
+ edges:
+ - to: payload
+ steps:
+ - size: 8
+ store: payload
+ name: "payload"
+ compBlacklist: ["Tool"]
+ - tool: Screwing
+ doAfter: 3
+ - to: payload
+ steps:
+ - tool: Screwing
+ doAfter: 2
+ - to: wired
+ completed:
+ - !type:EmptyContainer
+ container: board
+ steps:
+ - tool: Prying
+ doAfter: 1
+
+ - node: payload
+ entity: LoudSpeakerAssesmbly
+ edges:
+ - to: loudspeaker
+ steps:
+ - tool: Screwing
+ doAfter: 1
+ - to: electronics
+ conditions:
+ - !type:ContainerNotEmpty
+ container: board
+ - !type:ContainerNotEmpty
+ container: payload
+ completed:
+ - !type:EmptyContainer
+ container: payload
+ steps:
+ - tool: Prying
+ doAfter: 2
+ - to: wired
+ conditions:
+ - !type:ContainerNotEmpty
+ container: board
+ - !type:ContainerEmpty
+ container: payload
+ completed:
+ - !type:EmptyContainer
+ container: board
+ steps:
+ - tool: Prying
+ doAfter: 1
+
+ - node: loudspeaker
+ entity: LoudSpeakerBase
+ edges:
+ - to: payload
+ steps:
+ - tool: Screwing
+ doAfter: 1
diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml b/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml
new file mode 100644
index 0000000000..db40c13dfe
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml
@@ -0,0 +1,7 @@
+- type: latheRecipe
+ id: LoudSpeakerElectronics
+ result: LoudSpeakerElectronics
+ completetime: 2
+ materials:
+ Steel: 40
+ Plastic: 60
diff --git a/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml b/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml
new file mode 100644
index 0000000000..6242ee3e3d
--- /dev/null
+++ b/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml
@@ -0,0 +1,5 @@
+- type: soundCollection
+ id: Buzzer
+ files:
+ - /Audio/SimpleStation14/Effects/buzzer_1.ogg
+ - /Audio/SimpleStation14/Effects/buzzer_2.ogg
diff --git a/Resources/Prototypes/SimpleStation14/tags.yml b/Resources/Prototypes/SimpleStation14/tags.yml
index ffe8954d1d..5452e9e3f8 100644
--- a/Resources/Prototypes/SimpleStation14/tags.yml
+++ b/Resources/Prototypes/SimpleStation14/tags.yml
@@ -1,6 +1,12 @@
+- type: Tag
+ id: SoundBox
+
- type: Tag
id: GlassesNearsight
+- type: Tag
+ id: LoudSpeakerElectronics
+
- type: Tag
id: Plushie