This commit is contained in:
Sir Warock 2026-05-10 11:43:49 +00:00 committed by GitHub
commit e2c55dbb04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 857 additions and 245 deletions

View File

@ -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<LightBulbComponent> _lightBulbQuery;
private EntityQuery<MetaDataComponent> _metaDataQuery;
private SimpleRadialMenu? _menu;
protected override void Open()
{
base.Open();
_lightBulbQuery = EntMan.GetEntityQuery<LightBulbComponent>();
_metaDataQuery = EntMan.GetEntityQuery<MetaDataComponent>();
if (!EntMan.TryGetComponent<LightReplacerComponent>(Owner, out var replacer))
return;
var lightTypes = CreateButtons(replacer);
if (lightTypes == null)
return;
_menu = this.CreateWindow<SimpleRadialMenu>();
_menu.SetButtons(lightTypes);
_menu.OpenCentered();
}
private List<RadialMenuOptionBase>? CreateButtons(LightReplacerComponent replacer)
{
var options = new List<RadialMenuOptionBase>();
Dictionary<string, EntityUid> tubes = [];
Dictionary<string, EntityUid> 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<string>(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<string>(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<RadialMenuOptionBase> 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);
}
}

View File

@ -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;
/// <summary>
/// Handles the label on the light replacer
/// </summary>
public sealed class LightReplacerStatusControl : Control
{
private readonly Entity<LightReplacerComponent> _parent;
private readonly RichTextLabel _label;
private string? _prevActiveLightTube;
private string? _prevActiveLightBulb;
private string? _labelTube;
private string? _labelBulb;
public LightReplacerStatusControl(Entity<LightReplacerComponent> 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)));
}
}

View File

@ -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;
/// <summary>
/// Handles the label on the light replacer
/// </summary>
public sealed class LightReplacerStatusControlSystem : EntitySystem
{
public override void Initialize()
{
Subs.ItemStatus<LightReplacerComponent>(replacer => new LightReplacerStatusControl(replacer));
}
}

View File

@ -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<LightReplacerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<LightReplacerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<LightReplacerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<LightReplacerComponent, InteractUsingEvent>(HandleInteract);
SubscribeLocalEvent<LightReplacerComponent, AfterInteractEvent>(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<string, int>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
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<Container>(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<PoweredLightComponent>(targetUid, out var fixture))
eventArgs.Handled = TryReplaceBulb(uid, targetUid, eventArgs.User, component, fixture);
// add new bulb to light replacer container?
else if (TryComp<LightBulbComponent>(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<LightBulbComponent>(usedUid, out var bulb))
eventArgs.Handled = TryInsertBulb(uid, usedUid, eventArgs.User, true, component, bulb);
// add bulbs from storage?
else if (TryComp<StorageComponent>(usedUid, out var storage))
eventArgs.Handled = TryInsertBulbsFromStorage(uid, usedUid, eventArgs.User, component, storage);
}
/// <summary>
/// Try to replace a light bulb in <paramref name="fixtureUid"/>
/// using light replacer. Light fixture should have <see cref="PoweredLightComponent"/>.
/// </summary>
/// <returns>True if successfully replaced light, false otherwise</returns>
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<LightBulbComponent>(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<LightBulbComponent>(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;
}
/// <summary>
/// Try to insert a new bulb inside light replacer
/// </summary>
/// <returns>True if successfully inserted light, false otherwise</returns>
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;
}
/// <summary>
/// Try to insert all light bulbs from storage (for example light tubes box)
/// </summary>
/// <returns>
/// Returns true if storage contained at least one light bulb
/// which was successfully inserted inside light replacer
/// </returns>
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<LightBulbComponent>(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<LightReplacerComponent, ExaminedEvent>(OnExamined);
// SubscribeLocalEvent<LightReplacerComponent, MapInitEvent>(OnMapInit);
// SubscribeLocalEvent<LightReplacerComponent, ComponentInit>(OnInit);
// SubscribeLocalEvent<LightReplacerComponent, InteractUsingEvent>(HandleInteract);
// SubscribeLocalEvent<LightReplacerComponent, AfterInteractEvent>(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<string, int>();
// var metaQuery = GetEntityQuery<MetaDataComponent>();
// 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<Container>(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<PoweredLightComponent>(targetUid, out var fixture))
// eventArgs.Handled = TryReplaceBulb(uid, targetUid, eventArgs.User, component, fixture);
// // add new bulb to light replacer container?
// else if (TryComp<LightBulbComponent>(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<LightBulbComponent>(usedUid, out var bulb))
// eventArgs.Handled = TryInsertBulb(uid, usedUid, eventArgs.User, true, component, bulb);
// // add bulbs from storage?
// else if (TryComp<StorageComponent>(usedUid, out var storage))
// eventArgs.Handled = TryInsertBulbsFromStorage(uid, usedUid, eventArgs.User, component, storage);
// }
//
// /// <summary>
// /// Try to replace a light bulb in <paramref name="fixtureUid"/>
// /// using light replacer. Light fixture should have <see cref="PoweredLightComponent"/>.
// /// </summary>
// /// <returns>True if successfully replaced light, false otherwise</returns>
// 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<LightBulbComponent>(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<LightBulbComponent>(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;
// }
//
// /// <summary>
// /// Try to insert a new bulb inside light replacer
// /// </summary>
// /// <returns>True if successfully inserted light, false otherwise</returns>
// 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;
// }
//
// /// <summary>
// /// Try to insert all light bulbs from storage (for example light tubes box)
// /// </summary>
// /// <returns>
// /// Returns true if storage contained at least one light bulb
// /// which was successfully inserted inside light replacer
// /// </returns>
// 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<LightBulbComponent>(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;
// }
// }

View File

@ -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 <see cref="PoweredLightComponent"/>
/// Can be reloaded by new light tubes or light bulbs
/// </summary>
[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
/// </summary>
[DataField("contents")]
public List<EntitySpawnEntry> Contents = new();
// Start DeltaV - Predicting Light Replacers & Adding Radial Menu
/// <summary>
/// This string defines what kind of tube will be inserted into light fixtures.
/// </summary>
[DataField, AutoNetworkedField]
public string ActiveLightTube = "fluorescent light tube";
/// <summary>
/// This string defines what kind of bulb will be inserted into light fixtures.
/// </summary>
[DataField, AutoNetworkedField]
public string ActiveLightBulb = "incandescent light bulb";
/// <summary>
/// The Icon Sprite for ejecting tubes in the radial menu.
/// </summary>
[DataField]
public SpriteSpecifier EjectTubes = new SpriteSpecifier.Rsi(new ResPath("_DV/Objects/Specific/Janitorial/light_replacer.rsi"), "eject-tubes");
/// <summary>
/// The Icon Sprite for ejecting bulbs in the radial menu.
/// </summary>
[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
}

View File

@ -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<LightBulbComponent> _lightBulbQuery;
private EntityQuery<MetaDataComponent> _metaDataQuery;
public override void Initialize()
{
SubscribeLocalEvent<LightReplacerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<LightReplacerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<LightReplacerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<LightReplacerComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<LightReplacerComponent, InteractUsingEvent>(HandleInteract);
SubscribeLocalEvent<LightReplacerComponent, AfterInteractEvent>(HandleAfterInteract);
SubscribeLocalEvent<LightReplacerComponent, EjectLightTypeMessage>(OnEjectMessage);
SubscribeLocalEvent<LightReplacerComponent, SwitchLightTypeMessage>(OnSwitchMessage);
_lightBulbQuery = GetEntityQuery<LightBulbComponent>();
_metaDataQuery = GetEntityQuery<MetaDataComponent>();
}
private void OnInit(Entity<LightReplacerComponent> replacer, ref ComponentInit args)
{
// This needs to be handled on CompInit because otherwise, it's empty on the client.
replacer.Comp.InsertedBulbs = _container.EnsureContainer<Container>(replacer, "light_replacer_storage");
}
private void OnMapInit(Entity<LightReplacerComponent> 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<LightReplacerComponent> 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<string, int>();
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<LightReplacerComponent> 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<LightReplacerComponent> 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<PoweredLightComponent>(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<LightReplacerComponent> 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<StorageComponent>(usedUid, out var storage))
eventArgs.Handled = TryInsertBulbsFromStorage(replacer.AsNullable(), (usedUid, storage), eventArgs.User);
}
private void OnEjectMessage(Entity<LightReplacerComponent> replacer, ref EjectLightTypeMessage args)
{
HashSet<EntityUid> 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<LightReplacerComponent> replacer, ref SwitchLightTypeMessage args)
{
if (args.LightType == LightBulbType.Tube)
replacer.Comp.ActiveLightTube = args.LightName;
else
replacer.Comp.ActiveLightBulb = args.LightName;
Dirty(replacer);
}
/// <summary>
/// Try to replace a light bulb in <paramref name="fixture"/>
/// using light replacer. Light fixture should have <see cref="PoweredLightComponent"/>.
/// </summary>
/// <param name="replacer">The light replacer used to replace the bulb.</param>
/// <param name="fixture">The fixture whose light is being replaced.</param>
/// <param name="userUid">The user who is replacing the light.</param>
/// <returns>True if successfully replaced light, false otherwise</returns>
public bool TryReplaceBulb(Entity<LightReplacerComponent?> replacer, Entity<PoweredLightComponent?> 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;
}
/// <summary>
/// Try to insert a new bulb inside light replacer
/// </summary>
/// <param name="replacer">The light replacer to insert a light into.</param>
/// <param name="bulb">The light to insert into the replacer.</param>
/// <param name="userUid">The user who is inserting the light.</param>
/// <param name="showPopup">Whether to show a popup.</param>
/// <returns>True if successfully inserted light, false otherwise</returns>
public bool TryInsertBulb(Entity<LightReplacerComponent?> replacer, Entity<LightBulbComponent?> 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;
}
/// <summary>
/// Try to insert all light bulbs from storage (for example light tubes box)
/// </summary>
/// <param name="replacer">The light replacer to insert bulbs into.</param>
/// <param name="storage">The storage whose contents should be inserted.</param>
/// <param name="userUid">The user who inserts the contents.</param>
/// <returns>
/// Returns true if storage contained at least one light bulb
/// which was successfully inserted inside light replacer
/// </returns>
public bool TryInsertBulbsFromStorage(Entity<LightReplacerComponent?> replacer, Entity<StorageComponent?> 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,
}

View File

@ -0,0 +1,25 @@
using Content.Shared.Light.Components;
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Light.Events;
/// <summary>
/// This message is sent from the client when the player wants to switch the active light bulb.
/// </summary>
/// <param name="light">A mix of the light name and the light bulb type.</param>
[Serializable, NetSerializable]
public sealed class SwitchLightTypeMessage((string, LightBulbType) light) : BoundUserInterfaceMessage
{
public string LightName = light.Item1;
public LightBulbType LightType = light.Item2;
}
/// <summary>
/// This message is sent from the client when the player wants to eject all lights of a specific type.
/// </summary>
/// <param name="lightName">The name of the lights to be ejected.</param>
[Serializable, NetSerializable]
public sealed class EjectLightTypeMessage(string lightName) : BoundUserInterfaceMessage
{
public string LightName = lightName;
}

View File

@ -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]
}

View File

@ -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:

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

View File

@ -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"
}
]
}