diff --git a/Content.Client/ADT/Telephone/ADTPhoneBui.cs b/Content.Client/ADT/Telephone/ADTPhoneBui.cs
new file mode 100644
index 00000000000..c39da7af0ac
--- /dev/null
+++ b/Content.Client/ADT/Telephone/ADTPhoneBui.cs
@@ -0,0 +1,100 @@
+using Content.Shared.ADT.Telephone;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.ADT.Telephone;
+
+///
+/// Client-side telephone window: phone list, call, answer, hang up and DND.
+///
+public sealed class ADTPhoneBui : BoundUserInterface
+{
+ private ADTPhoneWindow? _window;
+ private bool _doNotDisturb;
+
+ public ADTPhoneBui(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ // Reuse the window if it was somehow opened twice.
+ if (_window is { Disposed: false, IsOpen: true })
+ return;
+
+ _window = this.CreateWindow();
+ if (EntMan.TryGetComponent(Owner, out MetaDataComponent? metaData))
+ _window.Title = metaData.EntityName;
+
+ _window.SearchBar.OnTextChanged += OnSearchChanged;
+ _window.AnswerButton.OnPressed += _ => SendMessage(new ADTPhoneAnswerMsg());
+ _window.HangUpButton.OnPressed += _ => SendMessage(new ADTPhoneHangUpMsg());
+ _window.DoNotDisturbButton.OnPressed += _ => SendMessage(new ADTPhoneDoNotDisturbMsg(!_doNotDisturb));
+
+ Refresh();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ Refresh();
+ }
+
+ private void OnSearchChanged(LineEdit.LineEditEventArgs args)
+ {
+ ApplySearchFilter(args.Text);
+ }
+
+ private void ApplySearchFilter(string text)
+ {
+ if (_window == null)
+ return;
+
+ foreach (var child in _window.PhonesList.Children)
+ {
+ if (child is Button button)
+ button.Visible = string.IsNullOrEmpty(text) ||
+ button.Text?.Contains(text, StringComparison.OrdinalIgnoreCase) == true;
+ }
+ }
+
+ private void Refresh()
+ {
+ if (_window is not { IsOpen: true } || State is not ADTPhoneBuiState state)
+ return;
+
+ _doNotDisturb = state.DoNotDisturb;
+
+ _window.PhonesList.DisposeAllChildren();
+
+ if (state.Phones.Count == 0)
+ {
+ _window.PhonesList.AddChild(new Label
+ {
+ Text = Loc.GetString("adt-phone-no-phones"),
+ HorizontalAlignment = Control.HAlignment.Center,
+ Margin = new Thickness(0, 8),
+ });
+ }
+
+ foreach (var phone in state.Phones)
+ {
+ var button = new Button
+ {
+ Text = phone.Name,
+ HorizontalExpand = true,
+ StyleClasses = { "OpenBoth" },
+ };
+ var id = phone.Id;
+ button.OnPressed += _ => SendMessage(new ADTPhoneCallMsg(id));
+ _window.PhonesList.AddChild(button);
+ }
+
+ ApplySearchFilter(_window.SearchBar.Text);
+
+ _window.AnswerButton.Visible = state.Ringing;
+ _window.HangUpButton.Visible = state.Engaged;
+ _window.DoNotDisturbButton.Text = Loc.GetString(state.DoNotDisturb ? "adt-phone-do-not-disturb-on" : "adt-phone-do-not-disturb-off");
+ }
+}
diff --git a/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml b/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml
new file mode 100644
index 00000000000..e8d665641ff
--- /dev/null
+++ b/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml.cs b/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml.cs
new file mode 100644
index 00000000000..136f419fb19
--- /dev/null
+++ b/Content.Client/ADT/Telephone/ADTPhoneWindow.xaml.cs
@@ -0,0 +1,15 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.ADT.Telephone;
+
+[GenerateTypedNameReferences]
+public sealed partial class ADTPhoneWindow : DefaultWindow
+{
+ public ADTPhoneWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Server/ADT/Telephone/ADTPhoneSystem.cs b/Content.Server/ADT/Telephone/ADTPhoneSystem.cs
new file mode 100644
index 00000000000..72fd399b2e3
--- /dev/null
+++ b/Content.Server/ADT/Telephone/ADTPhoneSystem.cs
@@ -0,0 +1,247 @@
+using Content.Server.Access.Systems;
+using Content.Server.Administration.Logs;
+using Content.Server.Telephone;
+using Content.Shared.ADT.Telephone;
+using Content.Shared.Database;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Speech;
+using Content.Shared.Telephone;
+using Content.Shared.UserInterface;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Timing;
+
+namespace Content.Server.ADT.Telephone;
+
+///
+/// Human interface for the handheld telephones: call list, answer, hang up and do-not-disturb.
+///
+public sealed class ADTPhoneSystem : EntitySystem
+{
+ [Dependency] private readonly TelephoneSystem _telephone = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+ [Dependency] private readonly IdCardSystem _idCard = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnUseInHand, before: [typeof(ActivatableUISystem)]);
+ SubscribeLocalEvent(OnBeforeOpen);
+ SubscribeLocalEvent(OnStateChanged);
+
+ Subs.BuiEvents(ADTPhoneUiKey.Key, subs =>
+ {
+ subs.Event(OnCallMsg);
+ subs.Event(OnDoNotDisturbMsg);
+ subs.Event(OnAnswerMsg);
+ subs.Event(OnHangUpMsg);
+ });
+ }
+
+ private void OnBeforeOpen(Entity ent, ref BeforeActivatableUIOpenEvent args)
+ {
+ SendUIState(ent.Owner);
+ }
+
+ private void OnUseInHand(Entity ent, ref UseInHandEvent args)
+ {
+ if (TryAnswerOrHangUp(ent, args.User))
+ args.Handled = true;
+ }
+
+ private bool TryAnswerOrHangUp(Entity ent, EntityUid user)
+ {
+ if (!TryComp(ent.Owner, out var phone))
+ return false;
+
+ if (phone.CurrentState == TelephoneState.Ringing)
+ {
+ _telephone.AnswerTelephone((ent.Owner, phone), user);
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} answered {ToPrettyString(ent.Owner)}");
+ return true;
+ }
+
+ if (phone.CurrentState != TelephoneState.Idle)
+ {
+ _telephone.EndTelephoneCalls((ent.Owner, phone));
+ _popup.PopupEntity(Loc.GetString("adt-phone-hung-up"), ent.Owner, user, PopupType.Medium);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnCallMsg(Entity ent, ref ADTPhoneCallMsg args)
+ {
+ if (!_hands.IsHolding(args.Actor, ent.Owner))
+ return;
+
+ if (!TryComp(ent.Owner, out var phone))
+ return;
+
+ var time = _timing.CurTime;
+ if (time < ent.Comp.LastCall + ent.Comp.CallCooldown)
+ return;
+
+ if (_telephone.IsTelephoneEngaged((ent.Owner, phone)))
+ return;
+
+ if (GetEntity(args.Id) is not { Valid: true } target ||
+ target == ent.Owner ||
+ !TryComp(target, out var targetComp) ||
+ !TryComp(target, out var targetPhone))
+ {
+ return;
+ }
+
+ ent.Comp.LastCall = time;
+
+ if (targetComp.DoNotDisturb)
+ {
+ _audio.PlayPvs(ent.Comp.BusySound, ent.Owner);
+ _popup.PopupEntity(Loc.GetString("adt-phone-call-do-not-disturb"), ent.Owner, args.Actor, PopupType.MediumCaution);
+ return;
+ }
+
+ if (_telephone.IsTelephoneEngaged((target, targetPhone)))
+ {
+ _audio.PlayPvs(ent.Comp.BusySound, ent.Owner);
+ _popup.PopupEntity(Loc.GetString("adt-phone-call-busy"), ent.Owner, args.Actor, PopupType.MediumCaution);
+ return;
+ }
+
+ _telephone.CallTelephone((ent.Owner, phone), (target, targetPhone), args.Actor);
+
+ // The call can still fail if the receiver changed state between the checks above.
+ if (phone.CurrentState == TelephoneState.Idle)
+ {
+ _audio.PlayPvs(ent.Comp.BusySound, ent.Owner);
+ _popup.PopupEntity(Loc.GetString("adt-phone-call-busy"), ent.Owner, args.Actor, PopupType.MediumCaution);
+ return;
+ }
+
+ _popup.PopupEntity(Loc.GetString("adt-phone-calling"), ent.Owner, args.Actor);
+
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Actor)} called {ToPrettyString(target)} from {ToPrettyString(ent.Owner)}");
+
+ SendUIState(ent.Owner);
+ }
+
+ private void OnDoNotDisturbMsg(Entity ent, ref ADTPhoneDoNotDisturbMsg args)
+ {
+ if (!_hands.IsHolding(args.Actor, ent.Owner))
+ return;
+
+ ent.Comp.DoNotDisturb = args.DoNotDisturb;
+ SendUIState(ent.Owner);
+ }
+
+ private void OnAnswerMsg(Entity ent, ref ADTPhoneAnswerMsg args)
+ {
+ if (!_hands.IsHolding(args.Actor, ent.Owner))
+ return;
+
+ if (!TryComp(ent.Owner, out var phone))
+ return;
+
+ _telephone.AnswerTelephone((ent.Owner, phone), args.Actor);
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Actor)} answered {ToPrettyString(ent.Owner)}");
+ }
+
+ private void OnHangUpMsg(Entity ent, ref ADTPhoneHangUpMsg args)
+ {
+ if (!_hands.IsHolding(args.Actor, ent.Owner))
+ return;
+
+ if (!TryComp(ent.Owner, out var phone))
+ return;
+
+ _telephone.EndTelephoneCalls((ent.Owner, phone));
+ _popup.PopupEntity(Loc.GetString("adt-phone-hung-up"), ent.Owner, args.Actor, PopupType.Medium);
+ }
+
+ private void OnStateChanged(Entity ent, ref TelephoneStateChangeEvent args)
+ {
+ switch (args.NewState)
+ {
+ case TelephoneState.Calling:
+ _audio.PlayPvs(ent.Comp.RingOutgoingSound, ent.Owner);
+ break;
+
+ case TelephoneState.Ringing:
+ if (GetHolder(ent.Owner) is { } holder)
+ _popup.PopupEntity(Loc.GetString("adt-phone-ringing"), ent.Owner, holder, PopupType.Medium);
+ else
+ _popup.PopupEntity(Loc.GetString("adt-phone-ringing"), ent.Owner, PopupType.Medium);
+ break;
+
+ case TelephoneState.InCall:
+ _audio.PlayPvs(ent.Comp.PickupSound, ent.Owner);
+ break;
+
+ case TelephoneState.EndingCall:
+ _audio.PlayPvs(ent.Comp.HangUpSound, ent.Owner);
+ break;
+ }
+
+ SendUIState(ent.Owner);
+ }
+
+ private EntityUid? GetHolder(EntityUid phone)
+ {
+ if (_container.TryGetContainingContainer((phone, null, null), out var container) &&
+ HasComp(container.Owner))
+ {
+ return container.Owner;
+ }
+
+ return null;
+ }
+
+ private string GetPhoneName(EntityUid phone)
+ {
+ if (GetHolder(phone) is { } holder)
+ {
+ var name = Identity.Name(holder, EntityManager);
+ if (_idCard.TryFindIdCard(holder, out var idCard))
+ return $"{name} ({idCard.Comp.LocalizedJobTitle})";
+
+ return name;
+ }
+
+ return Name(phone);
+ }
+
+ private void SendUIState(EntityUid phone)
+ {
+ if (!TryComp(phone, out var phoneComp))
+ return;
+
+ var phones = new List();
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _))
+ {
+ if (uid == phone)
+ continue;
+
+ phones.Add(new ADTPhoneInfo(GetNetEntity(uid), GetPhoneName(uid)));
+ }
+
+ var state = new ADTPhoneBuiState(
+ phones,
+ Comp(phone).DoNotDisturb,
+ _telephone.IsTelephoneEngaged((phone, phoneComp)),
+ phoneComp.CurrentState == TelephoneState.Ringing);
+
+ _ui.SetUiState(phone, ADTPhoneUiKey.Key, state);
+ }
+}
diff --git a/Content.Shared/ADT/Telephone/ADTPhoneComponent.cs b/Content.Shared/ADT/Telephone/ADTPhoneComponent.cs
new file mode 100644
index 00000000000..beb433bf09d
--- /dev/null
+++ b/Content.Shared/ADT/Telephone/ADTPhoneComponent.cs
@@ -0,0 +1,33 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.ADT.Telephone;
+
+///
+/// Handheld telephone for the quartermaster and salvage specialists.
+/// Works on top of the vanilla telephone system.
+///
+[RegisterComponent]
+public sealed partial class ADTPhoneComponent : Component
+{
+ [DataField]
+ public bool DoNotDisturb;
+
+ [DataField]
+ public TimeSpan CallCooldown = TimeSpan.FromSeconds(1.5);
+
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan LastCall;
+
+ [DataField]
+ public SoundSpecifier? RingOutgoingSound = new SoundPathSpecifier("/Audio/ADT/Phone/ring_outgoing.ogg");
+
+ [DataField]
+ public SoundSpecifier? BusySound = new SoundPathSpecifier("/Audio/ADT/Phone/phone_busy.ogg");
+
+ [DataField]
+ public SoundSpecifier? PickupSound = new SoundPathSpecifier("/Audio/ADT/Phone/remote_pickup.ogg");
+
+ [DataField]
+ public SoundSpecifier? HangUpSound = new SoundPathSpecifier("/Audio/ADT/Phone/remote_hangup.ogg");
+}
diff --git a/Content.Shared/ADT/Telephone/ADTPhoneUi.cs b/Content.Shared/ADT/Telephone/ADTPhoneUi.cs
new file mode 100644
index 00000000000..8e9e779522b
--- /dev/null
+++ b/Content.Shared/ADT/Telephone/ADTPhoneUi.cs
@@ -0,0 +1,78 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.ADT.Telephone;
+
+///
+/// UI key for the handheld telephone window.
+///
+[Serializable, NetSerializable]
+public enum ADTPhoneUiKey : byte
+{
+ Key,
+}
+
+///
+/// Telephone window state: list of other phones, do-not-disturb and call status.
+///
+[Serializable, NetSerializable]
+public sealed class ADTPhoneBuiState : BoundUserInterfaceState
+{
+ public readonly List Phones;
+ public readonly bool DoNotDisturb;
+ public readonly bool Engaged;
+ public readonly bool Ringing;
+
+ public ADTPhoneBuiState(List phones, bool doNotDisturb, bool engaged, bool ringing)
+ {
+ Phones = phones;
+ DoNotDisturb = doNotDisturb;
+ Engaged = engaged;
+ Ringing = ringing;
+ }
+}
+
+///
+/// Phone id and display name shown in the telephone list.
+///
+[Serializable, NetSerializable]
+public readonly record struct ADTPhoneInfo(NetEntity Id, string Name);
+
+///
+/// Call the phone with the given id.
+///
+[Serializable, NetSerializable]
+public sealed class ADTPhoneCallMsg : BoundUserInterfaceMessage
+{
+ public readonly NetEntity Id;
+
+ public ADTPhoneCallMsg(NetEntity id)
+ {
+ Id = id;
+ }
+}
+
+///
+/// Toggle the do-not-disturb mode.
+///
+[Serializable, NetSerializable]
+public sealed class ADTPhoneDoNotDisturbMsg : BoundUserInterfaceMessage
+{
+ public readonly bool DoNotDisturb;
+
+ public ADTPhoneDoNotDisturbMsg(bool doNotDisturb)
+ {
+ DoNotDisturb = doNotDisturb;
+ }
+}
+
+///
+/// Answer the incoming call.
+///
+[Serializable, NetSerializable]
+public sealed class ADTPhoneAnswerMsg : BoundUserInterfaceMessage;
+
+///
+/// Hang up the current call.
+///
+[Serializable, NetSerializable]
+public sealed class ADTPhoneHangUpMsg : BoundUserInterfaceMessage;
diff --git a/Resources/Audio/ADT/Phone/attributions.yml b/Resources/Audio/ADT/Phone/attributions.yml
new file mode 100644
index 00000000000..1bd2ea69ec2
--- /dev/null
+++ b/Resources/Audio/ADT/Phone/attributions.yml
@@ -0,0 +1,24 @@
+- files: ["telephone_ring.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/master/sound/machines/telephone/telephone_ring.ogg"
+
+- files: ["ring_outgoing.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/master/sound/machines/telephone/ring_outgoing.ogg"
+
+- files: ["phone_busy.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/master/sound/machines/telephone/phone_busy.ogg"
+
+- files: ["remote_pickup.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/master/sound/machines/telephone/remote_pickup.ogg"
+
+- files: ["remote_hangup.ogg"]
+ license: "CC-BY-SA-3.0"
+ copyright: "Taken from cmss13"
+ source: "https://github.com/cmss13-devs/cmss13/blob/master/sound/machines/telephone/remote_hangup.ogg"
diff --git a/Resources/Audio/ADT/Phone/phone_busy.ogg b/Resources/Audio/ADT/Phone/phone_busy.ogg
new file mode 100644
index 00000000000..3ddb26e62cb
Binary files /dev/null and b/Resources/Audio/ADT/Phone/phone_busy.ogg differ
diff --git a/Resources/Audio/ADT/Phone/remote_hangup.ogg b/Resources/Audio/ADT/Phone/remote_hangup.ogg
new file mode 100644
index 00000000000..f646548a5eb
Binary files /dev/null and b/Resources/Audio/ADT/Phone/remote_hangup.ogg differ
diff --git a/Resources/Audio/ADT/Phone/remote_pickup.ogg b/Resources/Audio/ADT/Phone/remote_pickup.ogg
new file mode 100644
index 00000000000..8e4dab8274c
Binary files /dev/null and b/Resources/Audio/ADT/Phone/remote_pickup.ogg differ
diff --git a/Resources/Audio/ADT/Phone/ring_outgoing.ogg b/Resources/Audio/ADT/Phone/ring_outgoing.ogg
new file mode 100644
index 00000000000..d815f3b6ff2
Binary files /dev/null and b/Resources/Audio/ADT/Phone/ring_outgoing.ogg differ
diff --git a/Resources/Audio/ADT/Phone/telephone_ring.ogg b/Resources/Audio/ADT/Phone/telephone_ring.ogg
new file mode 100644
index 00000000000..85b71efe5b9
Binary files /dev/null and b/Resources/Audio/ADT/Phone/telephone_ring.ogg differ
diff --git a/Resources/Locale/en-US/ADT/telephone/phone.ftl b/Resources/Locale/en-US/ADT/telephone/phone.ftl
new file mode 100644
index 00000000000..f7f2f97a315
--- /dev/null
+++ b/Resources/Locale/en-US/ADT/telephone/phone.ftl
@@ -0,0 +1,18 @@
+adt-phone-window-title = Telephone
+adt-phone-search = Search
+adt-phone-no-phones = No other phones
+adt-phone-call-busy = Busy
+adt-phone-call-do-not-disturb = The subscriber is not accepting calls
+adt-phone-calling = Dialing...
+adt-phone-ringing = The phone is ringing!
+adt-phone-hung-up = Call ended
+adt-phone-answer = Answer
+adt-phone-hang-up = Hang up
+adt-phone-do-not-disturb-on = Do not disturb: on
+adt-phone-do-not-disturb-off = Do not disturb: off
+ent-ADTBasePhone = telephone
+ent-ADTBasePhone-desc = A portable telephone for calling other telephones. Speak while holding the handset near you.
+ent-ADTPhoneQM = quartermaster's telephone
+ent-ADTPhoneQM-desc = A portable telephone for contacting the salvage specialists. Kept in the quartermaster's locker.
+ent-ADTPhoneSalvage = salvage specialist's telephone
+ent-ADTPhoneSalvage-desc = A portable telephone for contacting the quartermaster. Kept in a salvage specialist's locker.
diff --git a/Resources/Locale/ru-RU/ADT/telephone/phone.ftl b/Resources/Locale/ru-RU/ADT/telephone/phone.ftl
new file mode 100644
index 00000000000..ac742bcda88
--- /dev/null
+++ b/Resources/Locale/ru-RU/ADT/telephone/phone.ftl
@@ -0,0 +1,18 @@
+adt-phone-window-title = Телефон
+adt-phone-search = Поиск
+adt-phone-no-phones = Нет других телефонов
+adt-phone-call-busy = Занято
+adt-phone-call-do-not-disturb = Абонент не принимает звонки
+adt-phone-calling = Идут гудки...
+adt-phone-ringing = Телефон звонит!
+adt-phone-hung-up = Вызов завершён
+adt-phone-answer = Ответить
+adt-phone-hang-up = Сбросить
+adt-phone-do-not-disturb-on = Не беспокоить: вкл
+adt-phone-do-not-disturb-off = Не беспокоить: выкл
+ent-ADTBasePhone = телефон
+ent-ADTBasePhone-desc = Портативный телефон для звонков другим телефонам. Говорите, держа трубку рядом с собой.
+ent-ADTPhoneQM = телефон квартирмейстера
+ent-ADTPhoneQM-desc = Портативный телефон для связи с утилизаторами. Лежит в шкафу квартирмейстера.
+ent-ADTPhoneSalvage = телефон утилизатора
+ent-ADTPhoneSalvage-desc = Портативный телефон для связи с квартирмейстером. Лежит в шкафчике утилизатора.
diff --git a/Resources/Prototypes/ADT/Entities/Objects/Devices/phones.yml b/Resources/Prototypes/ADT/Entities/Objects/Devices/phones.yml
new file mode 100644
index 00000000000..fe3b4fa083f
--- /dev/null
+++ b/Resources/Prototypes/ADT/Entities/Objects/Devices/phones.yml
@@ -0,0 +1,37 @@
+- type: entity
+ abstract: true
+ parent: BaseItem
+ id: ADTBasePhone
+ components:
+ - type: Sprite
+ sprite: ADT/Objects/Devices/phone.rsi
+ state: rpb_phone
+ - type: Item
+ size: Small
+ - type: Speech
+ - type: Telephone
+ ringTone: /Audio/ADT/Phone/telephone_ring.ogg
+ listeningRange: 1.5
+ speakerVolume: Speak
+ # Lavaland is a separate map, so phones use unlimited range to reach it.
+ transmissionRange: Unlimited
+ compatibleRanges:
+ - Unlimited
+ - type: ADTPhone
+ - type: ActivatableUI
+ key: enum.ADTPhoneUiKey.Key
+ inHandsOnly: true
+ requireActiveHand: true
+ singleUser: true
+ - type: UserInterface
+ interfaces:
+ enum.ADTPhoneUiKey.Key:
+ type: ADTPhoneBui
+
+- type: entity
+ parent: ADTBasePhone
+ id: ADTPhoneQM
+
+- type: entity
+ parent: ADTBasePhone
+ id: ADTPhoneSalvage
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml
index fec60d8d2ff..bab26752031 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/cargo.yml
@@ -29,6 +29,7 @@
prob: 0.5
#ADT-Tweak End - частичный откат нерфа утилей
- id: PlushieLizardJobSalvagespecialist
+ - id: ADTPhoneSalvage # ADT-Tweak: ADT-telephone
- type: entity
id: LockerSalvageSpecialistFilledHardsuit
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
index bc5d5973808..341ae6d593f 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
@@ -29,6 +29,7 @@
# ADT-Tweak End - частичный откат нерфа утилей
- id: PlushieLizardJobQuartermaster
prob: 0.02
+ - id: ADTPhoneQM # ADT-Tweak: ADT-telephone
- type: entity
id: LockerQuarterMasterFilled
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left-ear.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left-ear.png
new file mode 100644
index 00000000000..abcb3ee1e23
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left-ear.png differ
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left.png
new file mode 100644
index 00000000000..244954e124e
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-left.png differ
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right-ear.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right-ear.png
new file mode 100644
index 00000000000..32c079be388
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right-ear.png differ
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right.png
new file mode 100644
index 00000000000..a26ad75b458
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/inhand-right.png differ
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/meta.json b/Resources/Textures/ADT/Objects/Devices/phone.rsi/meta.json
new file mode 100644
index 00000000000..7a1e6f595ba
--- /dev/null
+++ b/Resources/Textures/ADT/Objects/Devices/phone.rsi/meta.json
@@ -0,0 +1,33 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from cmss13 at https://github.com/cmss13-devs/cmss13/blob/9092037df766dbf782056ad537a27b5df53a4d72/icons/obj/items/misc.dmi, https://github.com/cmss13-devs/cmss13/blob/b26246cf7710ac6f2c2e4eddf94a49eb47f40800/icons/obj/structures/phone.dmi, https://github.com/cmss13-devs/cmss13/blob/0525b5ada7da1afcd9b260e76d5fea01500d9c8d/icons/mob/humans/onmob/inhands/equipment/tools_lefthand.dmi, https://github.com/cmss13-devs/cmss13/blob/0525b5ada7da1afcd9b260e76d5fea01500d9c8d/icons/mob/humans/onmob/inhands/equipment/tools_righthand.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "rpb_phone"
+ },
+ {
+ "name": "scout_microphone"
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right-ear",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left-ear",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/rpb_phone.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/rpb_phone.png
new file mode 100644
index 00000000000..e1381f2f63e
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/rpb_phone.png differ
diff --git a/Resources/Textures/ADT/Objects/Devices/phone.rsi/scout_microphone.png b/Resources/Textures/ADT/Objects/Devices/phone.rsi/scout_microphone.png
new file mode 100644
index 00000000000..97096d94005
Binary files /dev/null and b/Resources/Textures/ADT/Objects/Devices/phone.rsi/scout_microphone.png differ