diff --git a/Content.Client/_DV/Light/BoundUserInterfaces/LightReplacerMenuBoundUserInterface.cs b/Content.Client/_DV/Light/BoundUserInterfaces/LightReplacerMenuBoundUserInterface.cs new file mode 100644 index 0000000000..3c77fcbb5f --- /dev/null +++ b/Content.Client/_DV/Light/BoundUserInterfaces/LightReplacerMenuBoundUserInterface.cs @@ -0,0 +1,126 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared._DV.Light.Events; +using Content.Shared.Light.Components; +using JetBrains.Annotations; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; + +namespace Content.Client._DV.Light.BoundUserInterfaces; + +[UsedImplicitly] +public sealed class LightReplacerMenuBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + private EntityQuery _lightBulbQuery; + private EntityQuery _metaDataQuery; + + private SimpleRadialMenu? _menu; + + protected override void Open() + { + base.Open(); + + _lightBulbQuery = EntMan.GetEntityQuery(); + _metaDataQuery = EntMan.GetEntityQuery(); + + if (!EntMan.TryGetComponent(Owner, out var replacer)) + return; + + var lightTypes = CreateButtons(replacer); + + if (lightTypes == null) + return; + + _menu = this.CreateWindow(); + _menu.SetButtons(lightTypes); + + _menu.OpenCentered(); + } + + private List? CreateButtons(LightReplacerComponent replacer) + { + var options = new List(); + + Dictionary tubes = []; + Dictionary bulbs = []; + + var hasActiveTubes = false; + var hasActiveBulbs = false; + + foreach (var light in replacer.InsertedBulbs.ContainedEntities) + { + if (!_lightBulbQuery.TryComp(light, out var bulb) + || !_metaDataQuery.TryComp(light, out var metaData)) + continue; + + if (bulb.Type == LightBulbType.Tube) + { + if (metaData.EntityName != replacer.ActiveLightTube) + tubes.TryAdd(metaData.EntityName, light); + else + hasActiveTubes = true; + } + else + { + if (metaData.EntityName != replacer.ActiveLightBulb) + bulbs.TryAdd(metaData.EntityName, light); + else + hasActiveBulbs = true; + } + } + + if (hasActiveTubes) + { + var toggleLightTubes = new RadialMenuActionOption(EjectLights, replacer.ActiveLightTube) + { + IconSpecifier = RadialMenuIconSpecifier.With(replacer.EjectTubes), + ToolTip = Loc.GetString("comp-light-replacer-eject-specified-lights", ("light-name", replacer.ActiveLightTube)), + }; + options.Add(toggleLightTubes); + } + + if (hasActiveBulbs) + { + var toggleLightBulbs = new RadialMenuActionOption(EjectLights, replacer.ActiveLightBulb) + { + IconSpecifier = RadialMenuIconSpecifier.With(replacer.EjectBulbs), + ToolTip = Loc.GetString("comp-light-replacer-eject-specified-lights", ("light-name", replacer.ActiveLightBulb)), + }; + options.Add(toggleLightBulbs); + } + + // This iterates through every unique light to add them as options. + foreach (var light in tubes) + { + PopulateOptions(light.Key, light.Value, LightBulbType.Tube, ref options); + } + + foreach (var light in bulbs) + { + PopulateOptions(light.Key, light.Value, LightBulbType.Bulb, ref options); + } + + return options; + } + + private void PopulateOptions(string name, EntityUid light, LightBulbType lightType, ref List options) + { + var switchLight = new RadialMenuActionOption<(string, LightBulbType)>(SwitchActiveLight, (name, lightType)) + { + IconSpecifier = RadialMenuIconSpecifier.With(light), + ToolTip = Loc.GetString("comp-light-replacer-select-lights", ("light-name", name)), + }; + options.Add(switchLight); + } + + private void SwitchActiveLight((string, LightBulbType) light) + { + var message = new SwitchLightTypeMessage(light); + SendPredictedMessage(message); + } + + private void EjectLights(string lightName) + { + var message = new EjectLightTypeMessage(lightName); + SendPredictedMessage(message); + } +} diff --git a/Content.Client/_DV/Light/Controls/LightReplacerStatusControl.cs b/Content.Client/_DV/Light/Controls/LightReplacerStatusControl.cs new file mode 100644 index 0000000000..2fcfe88133 --- /dev/null +++ b/Content.Client/_DV/Light/Controls/LightReplacerStatusControl.cs @@ -0,0 +1,66 @@ +using Content.Client.Message; +using Content.Client.Stylesheets; +using Content.Shared.Light.Components; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Timing; + +namespace Content.Client._DV.Light.Controls; + +/// +/// Handles the label on the light replacer +/// +public sealed class LightReplacerStatusControl : Control +{ + + private readonly Entity _parent; + private readonly RichTextLabel _label; + + private string? _prevActiveLightTube; + private string? _prevActiveLightBulb; + private string? _labelTube; + private string? _labelBulb; + + public LightReplacerStatusControl(Entity parent) + { + _parent = parent; + _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; + AddChild(_label); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + // only updates the UI if any of the details are different than they previously were + if (_prevActiveLightTube == _parent.Comp.ActiveLightTube + && _prevActiveLightBulb == _parent.Comp.ActiveLightBulb) + return; + + _prevActiveLightTube = _parent.Comp.ActiveLightTube; + _prevActiveLightBulb = _parent.Comp.ActiveLightBulb; + _labelTube = _prevActiveLightTube; + _labelBulb = _prevActiveLightBulb; + + // Remove " light tube" at the end to save precious label space. + if (_labelTube.EndsWith(" light tube")) + { + _labelTube = _labelTube.Remove(_labelTube.Length - 11); + // Remove " crystal" in case of colored lights + if (_labelTube.EndsWith(" crystal")) + _labelTube = _labelTube.Remove(_labelTube.Length - 8); + } + // Same with bulbs. + if (_labelBulb.EndsWith(" light bulb")) + { + _labelBulb = _labelBulb.Remove(_labelBulb.Length - 11); + if (_labelBulb.EndsWith(" crystal")) + _labelBulb = _labelBulb.Remove(_labelBulb.Length - 8); + } + + // Update current active lights + _label.SetMarkup(Loc.GetString("comp-light-replacer-label", + ("tube", _labelTube), + ("bulb", _labelBulb))); + } +} diff --git a/Content.Client/_DV/Light/EntitySystems/LightReplacerStatusControlSystem.cs b/Content.Client/_DV/Light/EntitySystems/LightReplacerStatusControlSystem.cs new file mode 100644 index 0000000000..e362665605 --- /dev/null +++ b/Content.Client/_DV/Light/EntitySystems/LightReplacerStatusControlSystem.cs @@ -0,0 +1,16 @@ +using Content.Client._DV.Light.Controls; +using Content.Client.Items; +using Content.Shared.Light.Components; + +namespace Content.Client._DV.Light.EntitySystems; + +/// +/// Handles the label on the light replacer +/// +public sealed class LightReplacerStatusControlSystem : EntitySystem +{ + public override void Initialize() + { + Subs.ItemStatus(replacer => new LightReplacerStatusControl(replacer)); + } +} diff --git a/Content.Server/Light/EntitySystems/LightReplacerSystem.cs b/Content.Server/Light/EntitySystems/LightReplacerSystem.cs index 71cc0173da..21e4f1045d 100644 --- a/Content.Server/Light/EntitySystems/LightReplacerSystem.cs +++ b/Content.Server/Light/EntitySystems/LightReplacerSystem.cs @@ -1,242 +1,243 @@ -using System.Linq; -using Content.Server.Light.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; -using Content.Shared.Light.EntitySystems; -using Content.Shared.Light.Components; -using Content.Shared.Popups; -using Content.Shared.Storage; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; - -namespace Content.Server.Light.EntitySystems; - -[UsedImplicitly] -public sealed class LightReplacerSystem : SharedLightReplacerSystem -{ - [Dependency] private readonly PoweredLightSystem _poweredLight = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(HandleInteract); - SubscribeLocalEvent(HandleAfterInteract); - } - - private void OnExamined(EntityUid uid, LightReplacerComponent component, ExaminedEvent args) - { - using (args.PushGroup(nameof(LightReplacerComponent))) - { - if (!component.InsertedBulbs.ContainedEntities.Any()) - { - args.PushMarkup(Loc.GetString("comp-light-replacer-no-lights")); - return; - } - - args.PushMarkup(Loc.GetString("comp-light-replacer-has-lights")); - var groups = new Dictionary(); - var metaQuery = GetEntityQuery(); - foreach (var bulb in component.InsertedBulbs.ContainedEntities) - { - var metaData = metaQuery.GetComponent(bulb); - groups[metaData.EntityName] = groups.GetValueOrDefault(metaData.EntityName) + 1; - } - - foreach (var (name, amount) in groups) - { - args.PushMarkup(Loc.GetString("comp-light-replacer-light-listing", ("amount", amount), ("name", name))); - } - } - } - - private void OnMapInit(EntityUid uid, LightReplacerComponent component, MapInitEvent args) - { - var xform = Transform(uid); - foreach (var spawn in EntitySpawnCollection.GetSpawns(component.Contents)) - { - var ent = Spawn(spawn, xform.Coordinates); - TryInsertBulb(uid, ent, replacer: component); - } - } - - private void OnInit(EntityUid uid, LightReplacerComponent replacer, ComponentInit args) - { - replacer.InsertedBulbs = _container.EnsureContainer(uid, "light_replacer_storage"); - } - - private void HandleAfterInteract(EntityUid uid, LightReplacerComponent component, AfterInteractEvent eventArgs) - { - if (eventArgs.Handled) - return; - - // standard interaction checks - if (!eventArgs.CanReach) - return; - - // behaviour will depends on target type - if (eventArgs.Target != null) - { - var targetUid = (EntityUid) eventArgs.Target; - - // replace broken light in fixture? - if (TryComp(targetUid, out var fixture)) - eventArgs.Handled = TryReplaceBulb(uid, targetUid, eventArgs.User, component, fixture); - // add new bulb to light replacer container? - else if (TryComp(targetUid, out var bulb)) - eventArgs.Handled = TryInsertBulb(uid, targetUid, eventArgs.User, true, component, bulb); - } - } - - private void HandleInteract(EntityUid uid, LightReplacerComponent component, InteractUsingEvent eventArgs) - { - if (eventArgs.Handled) - return; - - var usedUid = eventArgs.Used; - - // want to insert a new light bulb? - if (TryComp(usedUid, out var bulb)) - eventArgs.Handled = TryInsertBulb(uid, usedUid, eventArgs.User, true, component, bulb); - // add bulbs from storage? - else if (TryComp(usedUid, out var storage)) - eventArgs.Handled = TryInsertBulbsFromStorage(uid, usedUid, eventArgs.User, component, storage); - } - - /// - /// Try to replace a light bulb in - /// using light replacer. Light fixture should have . - /// - /// True if successfully replaced light, false otherwise - public bool TryReplaceBulb(EntityUid replacerUid, EntityUid fixtureUid, EntityUid? userUid = null, - LightReplacerComponent? replacer = null, PoweredLightComponent? fixture = null) - { - if (!Resolve(replacerUid, ref replacer)) - return false; - if (!Resolve(fixtureUid, ref fixture)) - return false; - - // check if light bulb is broken or missing - var fixtureBulbUid = _poweredLight.GetBulb(fixtureUid, fixture); - if (fixtureBulbUid != null) - { - if (!TryComp(fixtureBulbUid.Value, out var fixtureBulb)) - return false; - if (fixtureBulb.State == LightBulbState.Normal) - return false; - } - - // try get first inserted bulb of the same type as targeted light fixtutre - var bulb = replacer.InsertedBulbs.ContainedEntities.FirstOrDefault( - e => CompOrNull(e)?.Type == fixture.BulbType); - - // found bulb in inserted storage - if (bulb.Valid) // FirstOrDefault can return default/invalid uid. - { - // try to remove it - var hasRemoved = _container.Remove(bulb, replacer.InsertedBulbs); - if (!hasRemoved) - return false; - } - else - { - if (userUid != null) - { - var msg = Loc.GetString("comp-light-replacer-missing-light", - ("light-replacer", replacerUid)); - _popupSystem.PopupEntity(msg, replacerUid, userUid.Value); - } - return false; - } - - // insert it into fixture - var wasReplaced = _poweredLight.ReplaceBulb(fixtureUid, bulb, fixture); - if (wasReplaced) - { - _audio.PlayPvs(replacer.Sound, replacerUid); - } - - return wasReplaced; - } - - /// - /// Try to insert a new bulb inside light replacer - /// - /// True if successfully inserted light, false otherwise - public bool TryInsertBulb(EntityUid replacerUid, EntityUid bulbUid, EntityUid? userUid = null, bool showTooltip = false, - LightReplacerComponent? replacer = null, LightBulbComponent? bulb = null) - { - if (!Resolve(replacerUid, ref replacer)) - return false; - if (!Resolve(bulbUid, ref bulb)) - return false; - - // only normal (non-broken) bulbs can be inserted inside light replacer - if (bulb.State != LightBulbState.Normal) - { - if (showTooltip && userUid != null) - { - var msg = Loc.GetString("comp-light-replacer-insert-broken-light"); - _popupSystem.PopupEntity(msg, replacerUid, userUid.Value); - } - - return false; - } - - // try insert light and show message - var hasInsert = _container.Insert(bulbUid, replacer.InsertedBulbs); - if (hasInsert && showTooltip && userUid != null) - { - var msg = Loc.GetString("comp-light-replacer-insert-light", - ("light-replacer", replacerUid), ("bulb", bulbUid)); - _popupSystem.PopupEntity(msg, replacerUid, userUid.Value, PopupType.Medium); - } - - return hasInsert; - } - - /// - /// Try to insert all light bulbs from storage (for example light tubes box) - /// - /// - /// Returns true if storage contained at least one light bulb - /// which was successfully inserted inside light replacer - /// - public bool TryInsertBulbsFromStorage(EntityUid replacerUid, EntityUid storageUid, EntityUid? userUid = null, - LightReplacerComponent? replacer = null, StorageComponent? storage = null) - { - if (!Resolve(replacerUid, ref replacer)) - return false; - if (!Resolve(storageUid, ref storage)) - return false; - - var insertedBulbs = 0; - var storagedEnts = storage.Container.ContainedEntities.ToArray(); - - foreach (var ent in storagedEnts) - { - if (TryComp(ent, out var bulb) && - TryInsertBulb(replacerUid, ent, userUid, false, replacer, bulb)) - { - insertedBulbs++; - } - } - - // show some message if success - if (insertedBulbs > 0 && userUid != null) - { - var msg = Loc.GetString("comp-light-replacer-refill-from-storage", ("light-replacer", replacerUid)); - _popupSystem.PopupEntity(msg, replacerUid, userUid.Value, PopupType.Medium); - } - - return insertedBulbs > 0; - } -} +// THIS HAS BEEN REMOVED BECAUSE THIS SYSTEM WAS OVERHAULED IN Delta-V! +// using System.Linq; +// using Content.Server.Light.Components; +// using Content.Shared.Examine; +// using Content.Shared.Interaction; +// using Content.Shared.Light.EntitySystems; +// using Content.Shared.Light.Components; +// using Content.Shared.Popups; +// using Content.Shared.Storage; +// using JetBrains.Annotations; +// using Robust.Shared.Audio; +// using Robust.Shared.Audio.Systems; +// using Robust.Shared.Containers; +// +// namespace Content.Server.Light.EntitySystems; +// +// [UsedImplicitly] +// public sealed class LightReplacerSystem : SharedLightReplacerSystem +// { +// [Dependency] private readonly PoweredLightSystem _poweredLight = default!; +// [Dependency] private readonly SharedAudioSystem _audio = default!; +// [Dependency] private readonly SharedContainerSystem _container = default!; +// [Dependency] private readonly SharedPopupSystem _popupSystem = default!; +// +// public override void Initialize() +// { +// base.Initialize(); +// +// SubscribeLocalEvent(OnExamined); +// SubscribeLocalEvent(OnMapInit); +// SubscribeLocalEvent(OnInit); +// SubscribeLocalEvent(HandleInteract); +// SubscribeLocalEvent(HandleAfterInteract); +// } +// +// private void OnExamined(EntityUid uid, LightReplacerComponent component, ExaminedEvent args) +// { +// using (args.PushGroup(nameof(LightReplacerComponent))) +// { +// if (!component.InsertedBulbs.ContainedEntities.Any()) +// { +// args.PushMarkup(Loc.GetString("comp-light-replacer-no-lights")); +// return; +// } +// +// args.PushMarkup(Loc.GetString("comp-light-replacer-has-lights")); +// var groups = new Dictionary(); +// var metaQuery = GetEntityQuery(); +// foreach (var bulb in component.InsertedBulbs.ContainedEntities) +// { +// var metaData = metaQuery.GetComponent(bulb); +// groups[metaData.EntityName] = groups.GetValueOrDefault(metaData.EntityName) + 1; +// } +// +// foreach (var (name, amount) in groups) +// { +// args.PushMarkup(Loc.GetString("comp-light-replacer-light-listing", ("amount", amount), ("name", name))); +// } +// } +// } +// +// private void OnMapInit(EntityUid uid, LightReplacerComponent component, MapInitEvent args) +// { +// var xform = Transform(uid); +// foreach (var spawn in EntitySpawnCollection.GetSpawns(component.Contents)) +// { +// var ent = Spawn(spawn, xform.Coordinates); +// TryInsertBulb(uid, ent, replacer: component); +// } +// } +// +// private void OnInit(EntityUid uid, LightReplacerComponent replacer, ComponentInit args) +// { +// replacer.InsertedBulbs = _container.EnsureContainer(uid, "light_replacer_storage"); +// } +// +// private void HandleAfterInteract(EntityUid uid, LightReplacerComponent component, AfterInteractEvent eventArgs) +// { +// if (eventArgs.Handled) +// return; +// +// // standard interaction checks +// if (!eventArgs.CanReach) +// return; +// +// // behaviour will depends on target type +// if (eventArgs.Target != null) +// { +// var targetUid = (EntityUid) eventArgs.Target; +// +// // replace broken light in fixture? +// if (TryComp(targetUid, out var fixture)) +// eventArgs.Handled = TryReplaceBulb(uid, targetUid, eventArgs.User, component, fixture); +// // add new bulb to light replacer container? +// else if (TryComp(targetUid, out var bulb)) +// eventArgs.Handled = TryInsertBulb(uid, targetUid, eventArgs.User, true, component, bulb); +// } +// } +// +// private void HandleInteract(EntityUid uid, LightReplacerComponent component, InteractUsingEvent eventArgs) +// { +// if (eventArgs.Handled) +// return; +// +// var usedUid = eventArgs.Used; +// +// // want to insert a new light bulb? +// if (TryComp(usedUid, out var bulb)) +// eventArgs.Handled = TryInsertBulb(uid, usedUid, eventArgs.User, true, component, bulb); +// // add bulbs from storage? +// else if (TryComp(usedUid, out var storage)) +// eventArgs.Handled = TryInsertBulbsFromStorage(uid, usedUid, eventArgs.User, component, storage); +// } +// +// /// +// /// Try to replace a light bulb in +// /// using light replacer. Light fixture should have . +// /// +// /// True if successfully replaced light, false otherwise +// public bool TryReplaceBulb(EntityUid replacerUid, EntityUid fixtureUid, EntityUid? userUid = null, +// LightReplacerComponent? replacer = null, PoweredLightComponent? fixture = null) +// { +// if (!Resolve(replacerUid, ref replacer)) +// return false; +// if (!Resolve(fixtureUid, ref fixture)) +// return false; +// +// // check if light bulb is broken or missing +// var fixtureBulbUid = _poweredLight.GetBulb(fixtureUid, fixture); +// if (fixtureBulbUid != null) +// { +// if (!TryComp(fixtureBulbUid.Value, out var fixtureBulb)) +// return false; +// if (fixtureBulb.State == LightBulbState.Normal) +// return false; +// } +// +// // try get first inserted bulb of the same type as targeted light fixtutre +// var bulb = replacer.InsertedBulbs.ContainedEntities.FirstOrDefault( +// e => CompOrNull(e)?.Type == fixture.BulbType); +// +// // found bulb in inserted storage +// if (bulb.Valid) // FirstOrDefault can return default/invalid uid. +// { +// // try to remove it +// var hasRemoved = _container.Remove(bulb, replacer.InsertedBulbs); +// if (!hasRemoved) +// return false; +// } +// else +// { +// if (userUid != null) +// { +// var msg = Loc.GetString("comp-light-replacer-missing-light", +// ("light-replacer", replacerUid)); +// _popupSystem.PopupEntity(msg, replacerUid, userUid.Value); +// } +// return false; +// } +// +// // insert it into fixture +// var wasReplaced = _poweredLight.ReplaceBulb(fixtureUid, bulb, fixture); +// if (wasReplaced) +// { +// _audio.PlayPvs(replacer.Sound, replacerUid); +// } +// +// return wasReplaced; +// } +// +// /// +// /// Try to insert a new bulb inside light replacer +// /// +// /// True if successfully inserted light, false otherwise +// public bool TryInsertBulb(EntityUid replacerUid, EntityUid bulbUid, EntityUid? userUid = null, bool showTooltip = false, +// LightReplacerComponent? replacer = null, LightBulbComponent? bulb = null) +// { +// if (!Resolve(replacerUid, ref replacer)) +// return false; +// if (!Resolve(bulbUid, ref bulb)) +// return false; +// +// // only normal (non-broken) bulbs can be inserted inside light replacer +// if (bulb.State != LightBulbState.Normal) +// { +// if (showTooltip && userUid != null) +// { +// var msg = Loc.GetString("comp-light-replacer-insert-broken-light"); +// _popupSystem.PopupEntity(msg, replacerUid, userUid.Value); +// } +// +// return false; +// } +// +// // try insert light and show message +// var hasInsert = _container.Insert(bulbUid, replacer.InsertedBulbs); +// if (hasInsert && showTooltip && userUid != null) +// { +// var msg = Loc.GetString("comp-light-replacer-insert-light", +// ("light-replacer", replacerUid), ("bulb", bulbUid)); +// _popupSystem.PopupEntity(msg, replacerUid, userUid.Value, PopupType.Medium); +// } +// +// return hasInsert; +// } +// +// /// +// /// Try to insert all light bulbs from storage (for example light tubes box) +// /// +// /// +// /// Returns true if storage contained at least one light bulb +// /// which was successfully inserted inside light replacer +// /// +// public bool TryInsertBulbsFromStorage(EntityUid replacerUid, EntityUid storageUid, EntityUid? userUid = null, +// LightReplacerComponent? replacer = null, StorageComponent? storage = null) +// { +// if (!Resolve(replacerUid, ref replacer)) +// return false; +// if (!Resolve(storageUid, ref storage)) +// return false; +// +// var insertedBulbs = 0; +// var storagedEnts = storage.Container.ContainedEntities.ToArray(); +// +// foreach (var ent in storagedEnts) +// { +// if (TryComp(ent, out var bulb) && +// TryInsertBulb(replacerUid, ent, userUid, false, replacer, bulb)) +// { +// insertedBulbs++; +// } +// } +// +// // show some message if success +// if (insertedBulbs > 0 && userUid != null) +// { +// var msg = Loc.GetString("comp-light-replacer-refill-from-storage", ("light-replacer", replacerUid)); +// _popupSystem.PopupEntity(msg, replacerUid, userUid.Value, PopupType.Medium); +// } +// +// return insertedBulbs > 0; +// } +// } diff --git a/Content.Shared/Light/Components/LightReplacerComponent.cs b/Content.Shared/Light/Components/LightReplacerComponent.cs index 1276ff9edc..2d85f294c2 100644 --- a/Content.Shared/Light/Components/LightReplacerComponent.cs +++ b/Content.Shared/Light/Components/LightReplacerComponent.cs @@ -4,6 +4,7 @@ using Content.Shared.Storage; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Robust.Shared.Utility; // DeltaV - Predicted Light Replacer & Added Radial Menu namespace Content.Shared.Light.Components; @@ -11,7 +12,8 @@ namespace Content.Shared.Light.Components; /// Device that allows user to quikly change bulbs in /// Can be reloaded by new light tubes or light bulbs /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedLightReplacerSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // DeltaV - Predicted Light Replacer & Added Radial Menu +[Access(typeof(SharedLightReplacerSystem))] public sealed partial class LightReplacerComponent : Component { [DataField("sound")] @@ -34,4 +36,30 @@ public sealed partial class LightReplacerComponent : Component /// [DataField("contents")] public List Contents = new(); + + // Start DeltaV - Predicting Light Replacers & Adding Radial Menu + /// + /// This string defines what kind of tube will be inserted into light fixtures. + /// + [DataField, AutoNetworkedField] + public string ActiveLightTube = "fluorescent light tube"; + + /// + /// This string defines what kind of bulb will be inserted into light fixtures. + /// + [DataField, AutoNetworkedField] + public string ActiveLightBulb = "incandescent light bulb"; + + /// + /// The Icon Sprite for ejecting tubes in the radial menu. + /// + [DataField] + public SpriteSpecifier EjectTubes = new SpriteSpecifier.Rsi(new ResPath("_DV/Objects/Specific/Janitorial/light_replacer.rsi"), "eject-tubes"); + + /// + /// The Icon Sprite for ejecting bulbs in the radial menu. + /// + [DataField] + public SpriteSpecifier EjectBulbs = new SpriteSpecifier.Rsi(new ResPath("_DV/Objects/Specific/Janitorial/light_replacer.rsi"), "eject-bulbs"); + // End DeltaV - Predicting Light Replacers & Adding Radial Menu } diff --git a/Content.Shared/Light/EntitySystems/SharedLightReplacerSystem.cs b/Content.Shared/Light/EntitySystems/SharedLightReplacerSystem.cs index 29001d6c73..964876d09b 100644 --- a/Content.Shared/Light/EntitySystems/SharedLightReplacerSystem.cs +++ b/Content.Shared/Light/EntitySystems/SharedLightReplacerSystem.cs @@ -1,5 +1,308 @@ +using System.Linq; +using Content.Shared._DV.Light.Events; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Light.Components; +using Content.Shared.Popups; +using Content.Shared.Storage; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Serialization; + namespace Content.Shared.Light.EntitySystems; -public abstract class SharedLightReplacerSystem : EntitySystem +public sealed class SharedLightReplacerSystem : EntitySystem // There is no Client and Server System anymore, this has been greatly overhauled in Delta-V. { + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPoweredLightSystem _poweredLight = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + + private EntityQuery _lightBulbQuery; + private EntityQuery _metaDataQuery; + + public override void Initialize() + { + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(HandleInteract); + SubscribeLocalEvent(HandleAfterInteract); + SubscribeLocalEvent(OnEjectMessage); + SubscribeLocalEvent(OnSwitchMessage); + + _lightBulbQuery = GetEntityQuery(); + _metaDataQuery = GetEntityQuery(); + } + + private void OnInit(Entity replacer, ref ComponentInit args) + { + // This needs to be handled on CompInit because otherwise, it's empty on the client. + replacer.Comp.InsertedBulbs = _container.EnsureContainer(replacer, "light_replacer_storage"); + } + + private void OnMapInit(Entity replacer, ref MapInitEvent args) + { + var xform = Transform(replacer); + foreach (var spawn in EntitySpawnCollection.GetSpawns(replacer.Comp.Contents)) + { + var light = Spawn(spawn, xform.Coordinates); + TryInsertBulb(replacer.AsNullable(), light); + } + } + + private void OnExamined(Entity replacer, ref ExaminedEvent args) + { + using (args.PushGroup(nameof(LightReplacerComponent))) + { + if (!replacer.Comp.InsertedBulbs.ContainedEntities.Any()) + { + args.PushMarkup(Loc.GetString("comp-light-replacer-no-lights")); + return; + } + + args.PushMarkup(Loc.GetString("comp-light-replacer-has-lights")); + var groups = new Dictionary(); + foreach (var bulb in replacer.Comp.InsertedBulbs.ContainedEntities) + { + var metaData = _metaDataQuery.GetComponent(bulb); + groups[metaData.EntityName] = groups.GetValueOrDefault(metaData.EntityName) + 1; + } + + foreach (var (name, amount) in groups) + { + args.PushMarkup(Loc.GetString("comp-light-replacer-light-listing-dv", ("amount", amount), ("light-name", name))); + } + } + } + + private void OnUse(Entity replacer, ref UseInHandEvent args) + { + if (args.Handled) + return; + + args.ApplyDelay = false; + + if (!replacer.Comp.InsertedBulbs.ContainedEntities.Any()) + { + _popup.PopupClient(Loc.GetString("comp-light-replacer-open-empty", ("light-replacer", replacer)), replacer, args.User); + return; + } + + args.Handled = true; + _ui.OpenUi(replacer.Owner, LightReplacerUiKey.Key, args.User); + } + + private void HandleAfterInteract(Entity replacer, ref AfterInteractEvent eventArgs) + { + if (eventArgs.Handled + || !eventArgs.CanReach // standard interaction checks + || eventArgs.Target == null) // behavior will depend on the target type + return; + + var targetUid = (EntityUid) eventArgs.Target; + + // replace broken light in fixture? + if (TryComp(targetUid, out var fixture)) + eventArgs.Handled = TryReplaceBulb(replacer.AsNullable(), (targetUid, fixture), eventArgs.User); + // add new bulb to light replacer container? + else if (_lightBulbQuery.TryComp(targetUid, out var bulb)) + eventArgs.Handled = TryInsertBulb(replacer.AsNullable(), (targetUid, bulb), eventArgs.User, true); + } + + private void HandleInteract(Entity replacer, ref InteractUsingEvent eventArgs) + { + if (eventArgs.Handled) + return; + + var usedUid = eventArgs.Used; + + // want to insert a new light bulb? + if (_lightBulbQuery.TryComp(usedUid, out var bulb)) + eventArgs.Handled = TryInsertBulb(replacer.AsNullable(), (usedUid, bulb), eventArgs.User, true); + // add bulbs from storage? + else if (TryComp(usedUid, out var storage)) + eventArgs.Handled = TryInsertBulbsFromStorage(replacer.AsNullable(), (usedUid, storage), eventArgs.User); + } + + private void OnEjectMessage(Entity replacer, ref EjectLightTypeMessage args) + { + HashSet lightsToEject = []; + foreach (var light in replacer.Comp.InsertedBulbs.ContainedEntities) + { + if (_metaDataQuery.TryComp(light, out var metaData) && metaData.EntityName == args.LightName) + lightsToEject.Add(light); + } + + foreach (var light in lightsToEject) + { + _container.Remove(light, replacer.Comp.InsertedBulbs); + } + } + + private void OnSwitchMessage(Entity replacer, ref SwitchLightTypeMessage args) + { + if (args.LightType == LightBulbType.Tube) + replacer.Comp.ActiveLightTube = args.LightName; + else + replacer.Comp.ActiveLightBulb = args.LightName; + Dirty(replacer); + } + + /// + /// Try to replace a light bulb in + /// using light replacer. Light fixture should have . + /// + /// The light replacer used to replace the bulb. + /// The fixture whose light is being replaced. + /// The user who is replacing the light. + /// True if successfully replaced light, false otherwise + public bool TryReplaceBulb(Entity replacer, Entity fixture, EntityUid? userUid = null) + { + if (!Resolve(replacer, ref replacer.Comp) + || !Resolve(fixture, ref fixture.Comp)) + return false; + + var activeType = fixture.Comp.BulbType == LightBulbType.Tube + ? replacer.Comp.ActiveLightTube + : replacer.Comp.ActiveLightBulb; + + // check if light bulb is broken or missing + var fixtureBulbUid = _poweredLight.GetBulb(fixture, fixture.Comp); + if (fixtureBulbUid != null) + { + if (!_lightBulbQuery.TryComp(fixtureBulbUid.Value, out var fixtureBulb)) + return false; + + if (fixtureBulb.State == LightBulbState.Normal + && _metaDataQuery.TryComp(fixtureBulbUid, out var metaData) + && metaData.EntityName == activeType) + { + _popup.PopupClient(Loc.GetString("comp-light-replacer-same-light", ("light-name", fixtureBulbUid)), fixture, userUid); + return false; + } + } + + EntityUid? bulb = null; + foreach (var insertedBulb in replacer.Comp.InsertedBulbs.ContainedEntities) + { + if (!_metaDataQuery.TryComp(insertedBulb, out var metaData) || metaData.EntityName != activeType) + continue; + + bulb = insertedBulb; + break; + } + + // found bulb in inserted storage + if (bulb.HasValue) + { + // try to remove it + var hasRemoved = _container.Remove(bulb.Value, replacer.Comp.InsertedBulbs); + if (!hasRemoved) + return false; + } + else + { + if (userUid == null) + return false; + + var msg = Loc.GetString("comp-light-replacer-missing-light-dv", + ("light-name", activeType), + ("light-replacer", replacer)); + _popup.PopupClient(msg, replacer, userUid.Value); + return false; + } + + // insert it into fixture + var wasReplaced = _poweredLight.ReplaceBulb(fixture, bulb.Value, fixture.Comp); + if (wasReplaced) + { + _audio.PlayPredicted(replacer.Comp.Sound, replacer, userUid); + } + + return wasReplaced; + } + + /// + /// Try to insert a new bulb inside light replacer + /// + /// The light replacer to insert a light into. + /// The light to insert into the replacer. + /// The user who is inserting the light. + /// Whether to show a popup. + /// True if successfully inserted light, false otherwise + public bool TryInsertBulb(Entity replacer, Entity bulb, EntityUid? userUid = null, bool showPopup = false) + { + if (!Resolve(replacer, ref replacer.Comp) + || !Resolve(bulb, ref bulb.Comp)) + return false; + + // only normal (non-broken) bulbs can be inserted inside light replacer + if (bulb.Comp.State != LightBulbState.Normal) + { + if (!showPopup || userUid == null) + return false; + + var error = Loc.GetString("comp-light-replacer-insert-broken-light"); + _popup.PopupClient(error, replacer, userUid.Value); + return false; + } + // try insert light and show message + var hasInsert = _container.Insert(bulb.Owner, replacer.Comp.InsertedBulbs); + + if (!hasInsert || !showPopup || userUid == null) + return hasInsert; + + var message = Loc.GetString("comp-light-replacer-insert-light", + ("light-replacer", replacer), ("bulb", bulb)); + _popup.PopupClient(message, replacer, userUid.Value); + + return hasInsert; + } + + /// + /// Try to insert all light bulbs from storage (for example light tubes box) + /// + /// The light replacer to insert bulbs into. + /// The storage whose contents should be inserted. + /// The user who inserts the contents. + /// + /// Returns true if storage contained at least one light bulb + /// which was successfully inserted inside light replacer + /// + public bool TryInsertBulbsFromStorage(Entity replacer, Entity storage, EntityUid? userUid = null) + { + if (!Resolve(replacer, ref replacer.Comp) + || !Resolve(storage, ref storage.Comp)) + return false; + + var insertedBulbs = 0; + var storedEntities = storage.Comp.Container.ContainedEntities.ToArray(); + + foreach (var ent in storedEntities) + { + if (TryInsertBulb(replacer, ent, userUid)) + { + insertedBulbs++; + } + } + + // show some message if success + if (insertedBulbs > 0 && userUid != null) + { + var msg = Loc.GetString("comp-light-replacer-refill-from-storage", ("light-replacer", replacer)); + _popup.PopupClient(msg, replacer, userUid.Value); + } + + return insertedBulbs > 0; + } +} + +[Serializable, NetSerializable] +public enum LightReplacerUiKey : byte +{ + Key, } diff --git a/Content.Shared/_DV/Light/Events/LightReplacerBUIMessages.cs b/Content.Shared/_DV/Light/Events/LightReplacerBUIMessages.cs new file mode 100644 index 0000000000..e887edd3ac --- /dev/null +++ b/Content.Shared/_DV/Light/Events/LightReplacerBUIMessages.cs @@ -0,0 +1,25 @@ +using Content.Shared.Light.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared._DV.Light.Events; + +/// +/// This message is sent from the client when the player wants to switch the active light bulb. +/// +/// A mix of the light name and the light bulb type. +[Serializable, NetSerializable] +public sealed class SwitchLightTypeMessage((string, LightBulbType) light) : BoundUserInterfaceMessage +{ + public string LightName = light.Item1; + public LightBulbType LightType = light.Item2; +} + +/// +/// This message is sent from the client when the player wants to eject all lights of a specific type. +/// +/// The name of the lights to be ejected. +[Serializable, NetSerializable] +public sealed class EjectLightTypeMessage(string lightName) : BoundUserInterfaceMessage +{ + public string LightName = lightName; +} diff --git a/Resources/Locale/en-US/_DV/light/components/light-replacer-component.ftl b/Resources/Locale/en-US/_DV/light/components/light-replacer-component.ftl new file mode 100644 index 0000000000..8a8cae8287 --- /dev/null +++ b/Resources/Locale/en-US/_DV/light/components/light-replacer-component.ftl @@ -0,0 +1,24 @@ + +### Interaction Messages + +# Shown when player tries to replace light, but there is no lights left +comp-light-replacer-missing-light-dv = No {MAKEPLURAL($light-name)} left in {THE($light-replacer)}. + +# Shown when a player attempts to replace a light with the same color & type as the active light. +comp-light-replacer-same-light = This fixture already holds {INDEFINITE($light-name)} {$light-name}! + +# Radial Menu messages +comp-light-replacer-eject-specified-lights = Eject all {MAKEPLURAL($light-name)}. +comp-light-replacer-select-lights = Select {MAKEPLURAL($light-name)}. +comp-light-replacer-open-empty = {CAPITALIZE(THE($light-replacer))} is completely empty! + +# Label +comp-light-replacer-label = Tube: {$tube} + Bulb: {$bulb} + +### Examine + +comp-light-replacer-light-listing-dv = {$amount -> + [one] [color=yellow]{$amount}[/color] [color=gray]{$light-name}[/color] + *[other] [color=yellow]{$amount}[/color] [color=gray]{MAKEPLURAL($light-name)}[/color] +} diff --git a/Resources/Prototypes/Entities/Clothing/Belt/job.yml b/Resources/Prototypes/Entities/Clothing/Belt/job.yml index c605568653..75af2d97a9 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/job.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/job.yml @@ -418,7 +418,7 @@ - Whistle - BalloonPopper - type: ItemMapper # DeltaV - Custom sprite and item mapper - sprite: _DV/Clothing/Belt/belt_overlay.rsi + sprite: _DV/Clothing/Belt/belt_overlay.rsi mapLayers: baton: whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Tools/light_replacer.yml b/Resources/Prototypes/Entities/Objects/Tools/light_replacer.yml index 34dcd66b71..893b9f2b9d 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/light_replacer.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/light_replacer.yml @@ -20,6 +20,12 @@ - type: ContainerContainer containers: light_replacer_storage: !type:Container + # Begin DeltaV Additions - Added Radial Menu to Light Replacers + - type: UserInterface + interfaces: + enum.LightReplacerUiKey.Key: + type: LightReplacerMenuBoundUserInterface + # Begin DeltaV Additions - Added Radial Menu to Light Replacers - type: entity parent: LightReplacer diff --git a/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-bulbs.png b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-bulbs.png new file mode 100644 index 0000000000..e67817348e Binary files /dev/null and b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-bulbs.png differ diff --git a/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-tubes.png b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-tubes.png new file mode 100644 index 0000000000..5665b1d1a7 Binary files /dev/null and b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/eject-tubes.png differ diff --git a/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/meta.json b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/meta.json new file mode 100644 index 0000000000..05d4107377 --- /dev/null +++ b/Resources/Textures/_DV/Objects/Specific/Janitorial/light_replacer.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Eject sprites made by SirWarock on Github using sprites from cev-eris at https://github.com/discordia-space/CEV-Eris/commit/740ff31a81313086cf16761f3677cf1e2ab46c93", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "eject-tubes" + }, + { + "name": "eject-bulbs" + } + ] +}