diff --git a/Content.Client/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs b/Content.Client/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs
new file mode 100644
index 00000000000..9a6f515c57e
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared._CorvaxNext.Implants.Radio;
+
+namespace Content.Client._CorvaxNext.Implants.Radio;
+
+///
+public sealed class RadioImplantSystem : SharedRadioImplantSystem
+{
+}
diff --git a/Content.Server/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs b/Content.Server/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs
new file mode 100644
index 00000000000..feb1ef2eb09
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Implants/Radio/RadioImplantSystem.cs
@@ -0,0 +1,126 @@
+using Content.Server.Chat.Systems;
+using Content.Server.Radio;
+using Content.Server.Radio.Components;
+using Content.Server.Radio.EntitySystems;
+using Content.Shared._CorvaxNext.Implants.Radio;
+using Content.Shared.Radio.Components;
+using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+
+namespace Content.Server._CorvaxNext.Implants.Radio;
+
+///
+public sealed class RadioImplantSystem : SharedRadioImplantSystem
+{
+ [Dependency] private readonly INetManager _netManager = default!;
+ [Dependency] private readonly RadioSystem _radioSystem = default!;
+
+ private EntityQuery _actorQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnInsertEncryptionKey);
+ SubscribeLocalEvent(OnRemoveEncryptionKey);
+ SubscribeLocalEvent(OnRadioReceive);
+ SubscribeLocalEvent(OnSpeak);
+ _actorQuery = GetEntityQuery();
+ }
+
+ ///
+ /// Ensures implants with fixed channels work.
+ ///
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ UpdateRadioReception(ent);
+ }
+
+ ///
+ /// Handles the implantee's speech being forwarded onto the radio channel of the implant.
+ ///
+ private void OnSpeak(Entity ent, ref EntitySpokeEvent args)
+ {
+ // not a radio message, or already handled by another radio
+ if (args.Channel is null)
+ return;
+
+ // does the implant have access to the channel the implantee is trying to speak on?
+ if (ent.Comp.Implant is {} implant
+ && TryComp(implant, out var radioImplantComponent)
+ && radioImplantComponent.Channels.Contains(args.Channel.ID))
+ {
+ _radioSystem.SendRadioMessage(ent, args.Message, args.Channel.ID, implant);
+ // prevent other radios they might be wearing from sending the message again
+ args.Channel = null;
+ }
+ }
+
+ ///
+ /// Handles receiving radio messages and forwarding them to the implantee.
+ ///
+ private void OnRadioReceive(EntityUid uid, RadioImplantComponent component, ref RadioReceiveEvent args)
+ {
+ if (_actorQuery.TryComp(component.Implantee, out var actorComponent))
+ _netManager.ServerSendMessage(args.ChatMsg, actorComponent.PlayerSession.Channel);
+ }
+
+ ///
+ /// Handles the addition of an encryption key to the implant's storage.
+ ///
+ private void OnInsertEncryptionKey(Entity ent, ref EntInsertedIntoContainerMessage args)
+ {
+ // check if the insertion is actually something getting inserted into the radio implant storage, since
+ // this evt also fires when the radio implant is being inserted into a person.
+ if (ent.Owner != args.Container.Owner
+ || !TryComp(args.Entity, out var encryptionKeyComponent))
+ return;
+
+ // copy over the radio channels that can be accessed
+ ent.Comp.Channels.Clear();
+ foreach (var channel in encryptionKeyComponent.Channels)
+ {
+ ent.Comp.Channels.Add(channel);
+ }
+ Dirty(ent);
+ UpdateRadioReception(ent);
+ }
+
+ ///
+ /// Handles the removal of an encryption key from the implant's storage.
+ ///
+ private void OnRemoveEncryptionKey(Entity ent, ref EntRemovedFromContainerMessage args)
+ {
+ // check if the insertion is actually something getting inserted into the radio implant storage, since
+ // this evt also fires when the radio implant is being inserted into a person.
+ if (ent.Owner != args.Container.Owner
+ || !HasComp(args.Entity))
+ return;
+
+ // clear the radio channels since there's no encryption key inserted anymore.
+ ent.Comp.Channels.Clear();
+ Dirty(ent);
+ UpdateRadioReception(ent);
+ }
+
+ ///
+ /// Ensures that this thing can actually hear radio messages from channels the key provides.
+ ///
+ private void UpdateRadioReception(Entity ent)
+ {
+ if (ent.Comp.Channels.Count != 0)
+ {
+ // we need to add this comp to actually receive radio events.
+ var channels = EnsureComp(ent).Channels;
+ foreach (var channel in ent.Comp.Channels)
+ {
+ channels.Add(channel);
+ }
+ }
+ else
+ {
+ RemComp(ent);
+ }
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Storage/EntitySystems/MouthStorageSystem.cs b/Content.Server/_CorvaxNext/Storage/EntitySystems/MouthStorageSystem.cs
new file mode 100644
index 00000000000..166673683c3
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Storage/EntitySystems/MouthStorageSystem.cs
@@ -0,0 +1,41 @@
+using Content.Server.Nutrition;
+using Content.Server.Speech;
+using Content.Server.Speech.EntitySystems;
+using Content.Shared._CorvaxNext.Storage.Components;
+using Content.Shared._CorvaxNext.Storage.EntitySystems;
+using Content.Shared.Storage;
+
+namespace Content.Server._CorvaxNext.Storage.EntitySystems;
+
+public sealed class MouthStorageSystem : SharedMouthStorageSystem
+{
+ [Dependency] private readonly ReplacementAccentSystem _replacement = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAccent);
+ SubscribeLocalEvent(OnIngestAttempt);
+ }
+
+ // Force you to mumble if you have items in your mouth
+ private void OnAccent(EntityUid uid, MouthStorageComponent component, AccentGetEvent args)
+ {
+ if (IsMouthBlocked(component))
+ args.Message = _replacement.ApplyReplacements(args.Message, "mumble");
+ }
+
+ // Attempting to eat or drink anything with items in your mouth won't work
+ private void OnIngestAttempt(EntityUid uid, MouthStorageComponent component, IngestionAttemptEvent args)
+ {
+ if (!IsMouthBlocked(component))
+ return;
+
+ if (!TryComp(component.MouthId, out var storage))
+ return;
+
+ var firstItem = storage.Container.ContainedEntities[0];
+ args.Blocker = firstItem;
+ args.Cancel();
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs
index 93c4b69e4dd..a0b4be6589b 100644
--- a/Content.Shared/Storage/EntitySystems/DumpableSystem.cs
+++ b/Content.Shared/Storage/EntitySystems/DumpableSystem.cs
@@ -6,6 +6,7 @@
using Content.Shared.Placeable;
using Content.Shared.Storage.Components;
using Content.Shared.Verbs;
+using JetBrains.Annotations;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
@@ -139,27 +140,43 @@ private void StartDoAfter(EntityUid storageUid, EntityUid targetUid, EntityUid u
private void OnDoAfter(EntityUid uid, DumpableComponent component, DumpableDoAfterEvent args)
{
- if (args.Handled || args.Cancelled || !TryComp(uid, out var storage) || storage.Container.ContainedEntities.Count == 0)
+ // Corvax-Next-MouthStorage-Start
+ if (args.Handled || args.Cancelled)
+ return;
+
+ DumpContents(uid, args.Args.Target, args.Args.User, component);
+ }
+
+ // Refactor to allow dumping that doesn't require a verb
+ [PublicAPI]
+ public void DumpContents(EntityUid uid, EntityUid? target, EntityUid user, DumpableComponent? component = null)
+ {
+ if (!TryComp(uid, out var storage)
+ || !Resolve(uid, ref component))
+ return;
+
+ if (storage.Container.ContainedEntities.Count == 0)
+ // Corvax-Next-MouthStorage-End
return;
var dumpQueue = new Queue(storage.Container.ContainedEntities);
var dumped = false;
- if (_disposalUnitSystem.HasDisposals(args.Args.Target))
+ if (_disposalUnitSystem.HasDisposals(target)) // Corvax-Next-MouthStorage
{
dumped = true;
foreach (var entity in dumpQueue)
{
- _disposalUnitSystem.DoInsertDisposalUnit(args.Args.Target.Value, entity, args.Args.User);
+ _disposalUnitSystem.DoInsertDisposalUnit(target.Value, entity, user); // Corvax-Next-MouthStorage
}
}
- else if (HasComp(args.Args.Target))
+ else if (HasComp(target)) // Corvax-Next-MouthStorage
{
dumped = true;
- var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(args.Args.Target.Value);
+ var (targetPos, targetRot) = _transformSystem.GetWorldPositionRotation(target.Value); // Corvax-Next-MouthStorage
foreach (var entity in dumpQueue)
{
@@ -179,7 +196,7 @@ private void OnDoAfter(EntityUid uid, DumpableComponent component, DumpableDoAft
if (dumped)
{
- _audio.PlayPredicted(component.DumpSound, uid, args.User);
+ _audio.PlayPredicted(component.DumpSound, uid, user);// Corvax-Next-MouthStorage
}
}
}
diff --git a/Content.Shared/_CorvaxNext/Implants/Radio/HasRadioImplantComponent.cs b/Content.Shared/_CorvaxNext/Implants/Radio/HasRadioImplantComponent.cs
new file mode 100644
index 00000000000..76cba880ec7
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/Implants/Radio/HasRadioImplantComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._CorvaxNext.Implants.Radio;
+
+///
+/// This indicates this entity has a radio implant implanted into themselves.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedRadioImplantSystem))]
+public sealed partial class HasRadioImplantComponent : Component
+{
+ ///
+ /// The radio implant. We need this to be able to determine encryption keys.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? Implant;
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/Implants/Radio/RadioImplantComponent.cs b/Content.Shared/_CorvaxNext/Implants/Radio/RadioImplantComponent.cs
new file mode 100644
index 00000000000..9e05ddbddfd
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/Implants/Radio/RadioImplantComponent.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Radio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._CorvaxNext.Implants.Radio;
+
+///
+/// This is for radio implants. Might be Syndie, might not be Syndie, but either way, it's an implant.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedRadioImplantSystem))]
+public sealed partial class RadioImplantComponent : Component
+{
+ ///
+ /// The entity this implant got added to.
+ ///
+ [DataField, AutoNetworkedField]
+ public EntityUid? Implantee;
+
+ ///
+ /// The channels this implant can talk on.
+ ///
+ [DataField, AutoNetworkedField]
+ public HashSet> Channels = new();
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/Implants/Radio/SharedRadioImplantSystem.cs b/Content.Shared/_CorvaxNext/Implants/Radio/SharedRadioImplantSystem.cs
new file mode 100644
index 00000000000..3b4ed357984
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/Implants/Radio/SharedRadioImplantSystem.cs
@@ -0,0 +1,56 @@
+using Content.Shared.Actions;
+using Content.Shared.Implants;
+using Content.Shared.Storage;
+using Content.Shared.Storage.EntitySystems;
+using Robust.Shared.Containers;
+
+namespace Content.Shared._CorvaxNext.Implants.Radio;
+
+///
+/// This handles radio implants, which you can implant to get access to a radio channel.
+///
+public abstract class SharedRadioImplantSystem : EntitySystem
+{
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnImplanted);
+ SubscribeLocalEvent(OnPossiblyUnimplanted);
+ }
+
+ ///
+ /// Handles implantation of the implant.
+ ///
+ private void OnImplanted(EntityUid uid, RadioImplantComponent component, ImplantImplantedEvent args)
+ {
+ if (args.Implanted is not { Valid: true })
+ return;
+
+ component.Implantee = args.Implanted.Value;
+ Dirty(uid, component);
+
+ // make sure the person entity gets slapped with a component so it can react to it talking.
+ var hasRadioImplantComponent = EnsureComp(args.Implanted.Value);
+ hasRadioImplantComponent.Implant = uid;
+ Dirty(component.Implantee.Value, hasRadioImplantComponent);
+ }
+
+
+ ///
+ /// Handles removal of the implant from its containing mob.
+ ///
+ /// Done via because there is no specific event for an implant being removed.
+ private void OnPossiblyUnimplanted(EntityUid uid, RadioImplantComponent component, EntGotRemovedFromContainerMessage args)
+ {
+ if (Terminating(uid))
+ return;
+
+ // this gets fired if it gets removed from ANY container but really, we just want to know if it was removed from its owner...
+ // so check if the ent we got implanted into matches the container's owner (here, the container's owner is the entity)
+ if (component.Implantee is not null && component.Implantee == args.Container.Owner)
+ {
+ RemComp(component.Implantee.Value);
+ component.Implantee = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/Storage/Components/MouthStorageComponent.cs b/Content.Shared/_CorvaxNext/Storage/Components/MouthStorageComponent.cs
new file mode 100644
index 00000000000..75bbf603c17
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/Storage/Components/MouthStorageComponent.cs
@@ -0,0 +1,32 @@
+using Content.Shared._CorvaxNext.Storage.EntitySystems;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+namespace Content.Shared._CorvaxNext.Storage.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedMouthStorageSystem))]
+public sealed partial class MouthStorageComponent : Component
+{
+ public const string MouthContainerId = "mouth";
+
+ [DataField, AutoNetworkedField]
+ public EntProtoId? OpenStorageAction;
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? Action;
+
+ [DataField]
+ public EntProtoId MouthProto = "ActionOpenMouthStorage";
+
+ [ViewVariables]
+ public Container Mouth = default!;
+
+ [DataField]
+ public EntityUid? MouthId;
+
+ // Mimimum inflicted damage on hit to spit out items
+ [DataField]
+ public FixedPoint2 SpitDamageThreshold = FixedPoint2.New(2);
+}
\ No newline at end of file
diff --git a/Content.Shared/_CorvaxNext/Storage/EntitySystems/SharedMouthStorageSystem.cs b/Content.Shared/_CorvaxNext/Storage/EntitySystems/SharedMouthStorageSystem.cs
new file mode 100644
index 00000000000..126ada2b272
--- /dev/null
+++ b/Content.Shared/_CorvaxNext/Storage/EntitySystems/SharedMouthStorageSystem.cs
@@ -0,0 +1,84 @@
+using Content.Shared.Actions;
+using Content.Shared.CombatMode;
+using Content.Shared.Damage;
+using Content.Shared._CorvaxNext.Storage.Components;
+using Content.Shared.Examine;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Standing;
+using Content.Shared.Storage;
+using Content.Shared.Storage.EntitySystems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+
+namespace Content.Shared._CorvaxNext.Storage.EntitySystems;
+
+public abstract class SharedMouthStorageSystem : EntitySystem
+{
+ [Dependency] private readonly DumpableSystem _dumpableSystem = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMouthStorageInit);
+ SubscribeLocalEvent(DropAllContents);
+ SubscribeLocalEvent(DropAllContents);
+ SubscribeLocalEvent(OnDamageModified);
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ protected bool IsMouthBlocked(MouthStorageComponent component)
+ {
+ if (!TryComp(component.MouthId, out var storage))
+ return false;
+
+ return storage.Container.ContainedEntities.Count > 0;
+ }
+
+ private void OnMouthStorageInit(EntityUid uid, MouthStorageComponent component, MapInitEvent args)
+ {
+ if (string.IsNullOrWhiteSpace(component.MouthProto))
+ return;
+
+ component.Mouth = _containerSystem.EnsureContainer(uid, MouthStorageComponent.MouthContainerId);
+ component.Mouth.ShowContents = false;
+ component.Mouth.OccludesLight = false;
+
+ var mouth = Spawn(component.MouthProto, new EntityCoordinates(uid, 0, 0));
+ _containerSystem.Insert(mouth, component.Mouth);
+ component.MouthId = mouth;
+
+ if (!string.IsNullOrWhiteSpace(component.OpenStorageAction) && component.Action == null)
+ _actionsSystem.AddAction(uid, ref component.Action, component.OpenStorageAction, mouth);
+ }
+
+ private void DropAllContents(EntityUid uid, MouthStorageComponent component, EntityEventArgs args)
+ {
+ if (component.MouthId == null)
+ return;
+
+ _dumpableSystem.DumpContents(component.MouthId.Value, uid, uid);
+ }
+
+ private void OnDamageModified(EntityUid uid, MouthStorageComponent component, DamageChangedEvent args)
+ {
+ if (args.DamageDelta == null
+ || !args.DamageIncreased
+ || args.DamageDelta.GetTotal() < component.SpitDamageThreshold)
+ return;
+
+ DropAllContents(uid, component, args);
+ }
+
+ // Other people can see if this person has items in their mouth.
+ private void OnExamined(EntityUid uid, MouthStorageComponent component, ExaminedEvent args)
+ {
+ if (IsMouthBlocked(component))
+ {
+ var subject = Identity.Entity(uid, EntityManager);
+ args.PushMarkup(Loc.GetString("mouth-storage-examine-condition-occupied", ("entity", subject)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Resources/Locale/en-US/_corvaxnext/storage/mouth-storage-component.ftl b/Resources/Locale/en-US/_corvaxnext/storage/mouth-storage-component.ftl
new file mode 100644
index 00000000000..135f30bdfa1
--- /dev/null
+++ b/Resources/Locale/en-US/_corvaxnext/storage/mouth-storage-component.ftl
@@ -0,0 +1 @@
+mouth-storage-examine-condition-occupied=[color=yellow]{CAPITALIZE(SUBJECT($entity))} has something in {POSS-ADJ($entity)} mouth.[/color]
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_corvaxnext/storage/mouth-storage-component.ftl b/Resources/Locale/ru-RU/_corvaxnext/storage/mouth-storage-component.ftl
new file mode 100644
index 00000000000..dd4286ed1de
--- /dev/null
+++ b/Resources/Locale/ru-RU/_corvaxnext/storage/mouth-storage-component.ftl
@@ -0,0 +1 @@
+mouth-storage-examine-condition-occupied=[color=yellow]{CAPITALIZE(SUBJECT($entity))} явно что-то держит во рту.[/color]
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/actions/types.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/actions/types.ftl
new file mode 100644
index 00000000000..cc7629e971d
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/actions/types.ftl
@@ -0,0 +1,4 @@
+ent-ActionOpenRadioImplant = Открыть радио-имплант.
+ .desc = Открывает контейнер ключа шифрования имплантированный в ваш череп.
+ent-ActionOpenMouthStorage = Открыть рот.
+ .desc = Позволяет спрятать что-нибудь за щекой.
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/implanters.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/implanters.ftl
new file mode 100644
index 00000000000..66216865bbc
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/implanters.ftl
@@ -0,0 +1,6 @@
+ent-GenericRadioImplanter = имплантер коммуниктора
+ .suffix = НЕ МАППИТЬ
+ .desc = Этот имплантер содержит радиоустройство со скрытым отсеком для ключа шифрования. Он позволяет своему владельцу общаться на каналах, доступных этому ключу.
+ent-SyndicateRadioImplanter = имплантер коммуниктора синдиката
+ .suffix = синдикат
+ .desc = Этот имплантер содержит радиомодуль, который позволяет своему владельцу общаться на канале Синдиката.
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/mouth_storage.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/mouth_storage.ftl
new file mode 100644
index 00000000000..4797569057a
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/mouth_storage.ftl
@@ -0,0 +1,2 @@
+ent-CheekStorage = щёки
+ .desc = Позволяют вам прятать во рту маленькие секреты.
diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/subdermal_implants.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/subdermal_implants.ftl
new file mode 100644
index 00000000000..0c07df9ce55
--- /dev/null
+++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/objects/misc/subdermal_implants.ftl
@@ -0,0 +1,4 @@
+ent-RadioImplant = имплант коммуниктора
+ .desc = Этот имплант содержит радиоустройство со скрытым отсеком для ключа шифрования. Он позволяет своему владельцу общаться на каналах, доступных этому ключу.
+ent-SyndicateRadioImplant = имплант коммуниктора синдиката
+ .desc = Этот имплант содержит радиомодуль, который позволяет своему владельцу общаться на канале Синдиката.
diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml
index 7bcdf52088f..9b4c378369f 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/base.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml
@@ -286,6 +286,10 @@
Asphyxiation: -1.0
- type: FireVisuals
alternateState: Standing
+ - type: MouthStorage # Corvax-Next-MouthStorage
+ mouthProto: CheekStorage
+ openStorageAction: ActionOpenMouthStorage
+ spitDamageThreshold: 3
- type: entity
save: false
diff --git a/Resources/Prototypes/_CorvaxNext/Actions/types.yml b/Resources/Prototypes/_CorvaxNext/Actions/types.yml
index f8c817185c8..65dd3e2a7b3 100644
--- a/Resources/Prototypes/_CorvaxNext/Actions/types.yml
+++ b/Resources/Prototypes/_CorvaxNext/Actions/types.yml
@@ -22,4 +22,28 @@
icon:
sprite: _CorvaxNext/Clothing/Eyes/Goggles/thermal.rsi
state: icon
- event: !type:ToggleThermalVisionEvent
\ No newline at end of file
+ event: !type:ToggleThermalVisionEvent
+
+- type: entity
+ id: ActionOpenRadioImplant
+ name: Open Radio Implant
+ description: Opens the bluespace key compartment of the radio implant embedded in your skull.
+ components:
+ - type: InstantAction
+ itemIconStyle: BigAction
+ priority: -20
+ icon:
+ sprite: Clothing/Ears/Headsets/base.rsi
+ state: icon
+ event: !type:OpenStorageImplantEvent
+
+- type: entity
+ id: ActionOpenMouthStorage
+ name: Open cheek storage
+ description: Allows you to store items in your cheeks.
+ components:
+ - type: InstantAction
+ itemIconStyle: BigAction
+ priority: -10
+ icon: _CorvaxNext/Interface/Actions/mouthStorageOpen.png
+ event: !type:OpenStorageImplantEvent
\ No newline at end of file
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/implanters.yml
new file mode 100644
index 00000000000..4f708bb7c6c
--- /dev/null
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/implanters.yml
@@ -0,0 +1,15 @@
+- type: entity
+ parent: BaseImplantOnlyImplanter
+ id: GenericRadioImplanter
+ suffix: generic radio
+ components:
+ - type: Implanter
+ implant: RadioImplant
+
+- type: entity
+ parent: BaseImplantOnlyImplanterSyndi
+ id: SyndicateRadioImplanter
+ suffix: syndicate radio
+ components:
+ - type: Implanter
+ implant: SyndicateRadioImplant
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/mouth_storage.yml b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/mouth_storage.yml
new file mode 100644
index 00000000000..853c434ee11
--- /dev/null
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/mouth_storage.yml
@@ -0,0 +1,29 @@
+- type: entity
+ id: CheekStorage
+ name: cheek storage
+ description: The cheeks capable of storing small objects.
+ components:
+ - type: Storage
+ clickInsert: false
+ grid:
+ - 0,1,1,1
+ maxItemSize: Small
+ blacklist:
+ components:
+ - Sharp
+ - MindContainer
+ - Injector
+ - Spray
+ - ResearchDisk
+ - BalloonPopper
+ - LightBulb
+ - NukeDisk
+ - type: ContainerContainer
+ containers:
+ storagebase: !type:Container
+ ents: [ ]
+ - type: UserInterface
+ interfaces:
+ enum.StorageUiKey.Key:
+ type: StorageBoundUserInterface
+ - type: Dumpable
diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/subdermal_implants.yml
new file mode 100644
index 00000000000..cd067474c74
--- /dev/null
+++ b/Resources/Prototypes/_CorvaxNext/Entities/Objects/Misc/subdermal_implants.yml
@@ -0,0 +1,33 @@
+- type: entity
+ categories: [ HideSpawnMenu, Spawner ]
+ parent: StorageImplant
+ id: RadioImplant
+ name: generic radio implant
+ description: This implant contains a radio augmentation with a hidden compartment for an encryption key. It allows its user to communicate on the key's channels.
+ components:
+ - type: SubdermalImplant
+ implantAction: ActionOpenRadioImplant
+ whitelist:
+ components:
+ - Hands # the user needs to have hands to actually insert or remove a key, much like the storage implant
+ blacklist:
+ components:
+ - BorgChassis # borgs have "hands", but can't pick stuff up so the implant would be useless for them
+ - type: Storage
+ grid:
+ - 0,0,0,1
+ whitelist:
+ components:
+ - EncryptionKey # encryption keys only!
+ - type: RadioImplant
+
+- type: entity
+ categories: [ HideSpawnMenu, Spawner ]
+ parent: BaseSubdermalImplant
+ id: SyndicateRadioImplant
+ name: syndicate radio implant
+ description: This implant contains a radio augmentation that allows its user to communicate on the Syndicate channel.
+ components:
+ - type: RadioImplant
+ channels:
+ - Syndicate
diff --git a/Resources/Textures/_CorvaxNext/Interface/Actions/mouthStorageOpen.png b/Resources/Textures/_CorvaxNext/Interface/Actions/mouthStorageOpen.png
new file mode 100644
index 00000000000..7586c2f0cbc
Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Interface/Actions/mouthStorageOpen.png differ