From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Sun, 30 Nov 2025 10:25:22 +0000 (+0100) Subject: Predict borgs (#41600) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=937b61a8328b94522349e29a0e539aac16ba64ed;p=space-station-14.git Predict borgs (#41600) * predict borgs * small fix * fix MMI item slot serialization * fix movement speed for mothership core * review and minor improvement * fix resolve * review --- diff --git a/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs b/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs index ed9bf40a48..381bfacefb 100644 --- a/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs +++ b/Content.Client/Silicons/Borgs/BorgBoundUserInterface.cs @@ -1,6 +1,5 @@ using Content.Shared.Silicons.Borgs; using JetBrains.Annotations; -using Robust.Client.GameObjects; using Robust.Client.UserInterface; namespace Content.Client.Silicons.Borgs; @@ -24,31 +23,29 @@ public sealed class BorgBoundUserInterface : BoundUserInterface _menu.BrainButtonPressed += () => { - SendMessage(new BorgEjectBrainBuiMessage()); + SendPredictedMessage(new BorgEjectBrainBuiMessage()); }; _menu.EjectBatteryButtonPressed += () => { - SendMessage(new BorgEjectBatteryBuiMessage()); + SendPredictedMessage(new BorgEjectBatteryBuiMessage()); }; _menu.NameChanged += name => { - SendMessage(new BorgSetNameBuiMessage(name)); + SendPredictedMessage(new BorgSetNameBuiMessage(name)); }; _menu.RemoveModuleButtonPressed += module => { - SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module))); + SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module))); }; } - protected override void UpdateState(BoundUserInterfaceState state) + public override void Update() { - base.UpdateState(state); - - if (state is not BorgBuiState msg) - return; - _menu?.UpdateState(msg); + _menu?.UpdateBatteryButton(); + _menu?.UpdateBrainButton(); + _menu?.UpdateModulePanel(); } } diff --git a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs index aea590e334..0acc3a2646 100644 --- a/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs +++ b/Content.Client/Silicons/Borgs/BorgMenu.xaml.cs @@ -3,8 +3,8 @@ using Content.Client.UserInterface.Controls; using Content.Shared.CCVar; using Content.Shared.NameIdentifier; using Content.Shared.NameModifier.EntitySystems; -using Content.Shared.Preferences; -using Content.Shared.Silicons.Borgs; +using Content.Shared.Power.EntitySystems; +using Content.Shared.PowerCell; using Content.Shared.Silicons.Borgs.Components; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; @@ -20,6 +20,8 @@ public sealed partial class BorgMenu : FancyWindow [Dependency] private readonly IConfigurationManager _cfgManager = default!; [Dependency] private readonly IEntityManager _entity = default!; private readonly NameModifierSystem _nameModifier; + private readonly PowerCellSystem _powerCell; + private readonly PredictedBatterySystem _battery; public Action? BrainButtonPressed; public Action? EjectBatteryButtonPressed; @@ -41,6 +43,8 @@ public sealed partial class BorgMenu : FancyWindow IoCManager.InjectDependencies(this); _nameModifier = _entity.System(); + _powerCell = _entity.System(); + _battery = _entity.System(); _maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength); @@ -52,8 +56,6 @@ public sealed partial class BorgMenu : FancyWindow NameLineEdit.OnTextChanged += OnNameChanged; NameLineEdit.OnTextEntered += OnNameEntered; NameLineEdit.OnFocusExit += OnNameFocusExit; - - UpdateBrainButton(); } public void SetEntity(EntityUid entity) @@ -73,6 +75,10 @@ public sealed partial class BorgMenu : FancyWindow NameIdentifierLabel.Visible = false; NameLineEdit.Text = _entity.GetComponent(Entity).EntityName; } + + UpdateBatteryButton(); + UpdateBrainButton(); + UpdateModulePanel(); } protected override void FrameUpdate(FrameEventArgs args) @@ -80,21 +86,24 @@ public sealed partial class BorgMenu : FancyWindow base.FrameUpdate(args); AccumulatedTime += args.DeltaSeconds; - BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2); - } + BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2); - public void UpdateState(BorgBuiState state) - { - EjectBatteryButton.Disabled = !state.HasBattery; - ChargeBar.Value = state.ChargePercent; + var chargeFraction = 0f; + + if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery)) + chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge; + + ChargeBar.Value = chargeFraction; ChargeLabel.Text = Loc.GetString("borg-ui-charge-label", - ("charge", (int) MathF.Round(state.ChargePercent * 100))); + ("charge", (int)MathF.Round(chargeFraction * 100))); + } - UpdateBrainButton(); - UpdateModulePanel(); + public void UpdateBatteryButton() + { + EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity); } - private void UpdateBrainButton() + public void UpdateBrainButton() { if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain) { @@ -113,7 +122,7 @@ public sealed partial class BorgMenu : FancyWindow } } - private void UpdateModulePanel() + public void UpdateModulePanel() { if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis)) return; diff --git a/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs b/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs new file mode 100644 index 0000000000..52398921d4 --- /dev/null +++ b/Content.Client/Silicons/Borgs/BorgSystem.Battery.cs @@ -0,0 +1,83 @@ +using Content.Shared.PowerCell.Components; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Shared.Player; + +namespace Content.Client.Silicons.Borgs; + +public sealed partial class BorgSystem +{ + // How often to update the battery alert. + // Also gets updated instantly when switching bodies or a battery is inserted or removed. + private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f); + + // Don't put this on the component because we only need to track the time for a single entity + // and we don't want to TryComp it every single tick. + private TimeSpan _nextAlertUpdate = TimeSpan.Zero; + private EntityQuery _chassisQuery; + private EntityQuery _slotQuery; + + public void InitializeBattery() + { + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + _chassisQuery = GetEntityQuery(); + _slotQuery = GetEntityQuery(); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + UpdateBatteryAlert((ent.Owner, ent.Comp, null)); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + // Remove all borg related alerts. + _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert); + _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert); + } + + private void UpdateBatteryAlert(Entity ent) + { + if (!Resolve(ent, ref ent.Comp2, false)) + return; + + if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery)) + { + _alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert); + return; + } + + // Alert levels from 0 to 10. + var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f); + + // we make sure 0 only shows if they have absolutely no battery. + // also account for floating point imprecision + if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2))) + { + chargeLevel = 1; + } + + _alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel); + } + + // Periodically update the charge indicator. + // We do this with a client-side alert so that we don't have to network the charge level. + public void UpdateBattery(float frameTime) + { + if (_player.LocalEntity is not { } localPlayer) + return; + + var curTime = _timing.CurTime; + + if (curTime < _nextAlertUpdate) + return; + + _nextAlertUpdate = curTime + AlertUpdateDelay; + + if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot)) + return; + + UpdateBatteryAlert((localPlayer, chassis, slot)); + } +} diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs index 81689dbd60..a4ca3725dc 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.cs @@ -1,72 +1,93 @@ -using Content.Shared.Mobs; +using Content.Shared.Alert; +using Content.Shared.Mobs; +using Content.Shared.Power.EntitySystems; +using Content.Shared.PowerCell; using Content.Shared.Silicons.Borgs; using Content.Shared.Silicons.Borgs.Components; using Robust.Client.GameObjects; +using Robust.Client.Player; using Robust.Shared.Containers; +using Robust.Shared.Timing; namespace Content.Client.Silicons.Borgs; /// -public sealed class BorgSystem : SharedBorgSystem +public sealed partial class BorgSystem : SharedBorgSystem { [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly PredictedBatterySystem _battery = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPlayerManager _player = default!; public override void Initialize() { base.Initialize(); + InitializeBattery(); + SubscribeLocalEvent(OnBorgAppearanceChanged); SubscribeLocalEvent(OnMMIAppearanceChanged); } - private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args) + public override void UpdateUI(Entity chassis) + { + if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnBorgAppearanceChanged(Entity chassis, ref AppearanceChangeEvent args) { if (args.Sprite == null) return; - UpdateBorgAppearance(uid, component, args.Component, args.Sprite); + + UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite)); } - protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args) + protected override void OnInserted(Entity chassis, ref EntInsertedIntoContainerMessage args) { - if (!component.Initialized) + if (!chassis.Comp.Initialized) return; - base.OnInserted(uid, component, args); - UpdateBorgAppearance(uid, component); + base.OnInserted(chassis, ref args); + UpdateUI(chassis.AsNullable()); + UpdateBorgAppearance((chassis, chassis.Comp)); + UpdateBatteryAlert((chassis.Owner, chassis.Comp, null)); } - protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args) + protected override void OnRemoved(Entity chassis, ref EntRemovedFromContainerMessage args) { - if (!component.Initialized) + if (!chassis.Comp.Initialized) return; - base.OnRemoved(uid, component, args); - UpdateBorgAppearance(uid, component); + base.OnRemoved(chassis, ref args); + UpdateUI(chassis.AsNullable()); + UpdateBorgAppearance((chassis, chassis.Comp)); + UpdateBatteryAlert((chassis.Owner, chassis.Comp, null)); } - private void UpdateBorgAppearance(EntityUid uid, - BorgChassisComponent? component = null, - AppearanceComponent? appearance = null, - SpriteComponent? sprite = null) + private void UpdateBorgAppearance(Entity ent) { - if (!Resolve(uid, ref component, ref appearance, ref sprite)) + if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3)) return; - if (_appearance.TryGetData(uid, MobStateVisuals.State, out var state, appearance)) + if (_appearance.TryGetData(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2)) { if (state != MobState.Alive) { - _sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false); + _sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false); return; } } - if (!_appearance.TryGetData(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance)) + if (!_appearance.TryGetData(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2)) hasPlayer = false; - _sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer); - _sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState); + _sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer); + _sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState); } private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args) @@ -107,4 +128,10 @@ public sealed class BorgSystem : SharedBorgSystem borg.Comp.HasMindState = hasMindState; borg.Comp.NoMindState = noMindState; } + + public override void Update(float frameTime) + { + base.Update(frameTime); + UpdateBattery(frameTime); + } } diff --git a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs index 4102e2dca0..da024ee075 100644 --- a/Content.Server/Light/EntitySystems/HandheldLightSystem.cs +++ b/Content.Server/Light/EntitySystems/HandheldLightSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Actions; using Content.Server.Popups; using Content.Shared.Actions; -using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Light; using Content.Shared.Light.Components; @@ -44,7 +43,6 @@ namespace Content.Server.Light.EntitySystems SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnActivate); @@ -142,13 +140,6 @@ namespace Content.Server.Light.EntitySystems return ent.Comp.Activated ? TurnOff(ent) : TurnOn(user, ent); } - private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args) - { - args.PushMarkup(component.Activated - ? Loc.GetString("handheld-light-component-on-examine-is-on-message") - : Loc.GetString("handheld-light-component-on-examine-is-off-message")); - } - public override void Shutdown() { base.Shutdown(); diff --git a/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs b/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs deleted file mode 100644 index b41f1397ec..0000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.MMI.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Content.Shared.Containers.ItemSlots; -using Content.Shared.Mind.Components; -using Content.Shared.Roles; -using Content.Shared.Roles.Components; -using Content.Shared.Silicons.Borgs.Components; -using Robust.Shared.Containers; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - - [Dependency] private readonly SharedRoleSystem _roles = default!; - - public void InitializeMMI() - { - SubscribeLocalEvent(OnMMIInit); - SubscribeLocalEvent(OnMMIEntityInserted); - SubscribeLocalEvent(OnMMIMindAdded); - SubscribeLocalEvent(OnMMIMindRemoved); - - SubscribeLocalEvent(OnMMILinkedMindAdded); - SubscribeLocalEvent(OnMMILinkedRemoved); - } - - private void OnMMIInit(EntityUid uid, MMIComponent component, ComponentInit args) - { - if (!TryComp(uid, out var itemSlots)) - return; - - if (ItemSlots.TryGetSlot(uid, component.BrainSlotId, out var slot, itemSlots)) - component.BrainSlot = slot; - else - ItemSlots.AddItemSlot(uid, component.BrainSlotId, component.BrainSlot, itemSlots); - } - - private void OnMMIEntityInserted(EntityUid uid, MMIComponent component, EntInsertedIntoContainerMessage args) - { - if (args.Container.ID != component.BrainSlotId) - return; - - var ent = args.Entity; - var linked = EnsureComp(ent); - linked.LinkedMMI = uid; - Dirty(uid, component); - - if (_mind.TryGetMind(ent, out var mindId, out var mind)) - { - _mind.TransferTo(mindId, uid, true, mind: mind); - - if (!_roles.MindHasRole(mindId)) - _roles.MindAddRole(mindId, "MindRoleSiliconBrain", silent: true); - } - - _appearance.SetData(uid, MMIVisuals.BrainPresent, true); - } - - private void OnMMIMindAdded(EntityUid uid, MMIComponent component, MindAddedMessage args) - { - _appearance.SetData(uid, MMIVisuals.HasMind, true); - } - - private void OnMMIMindRemoved(EntityUid uid, MMIComponent component, MindRemovedMessage args) - { - _appearance.SetData(uid, MMIVisuals.HasMind, false); - } - - private void OnMMILinkedMindAdded(EntityUid uid, MMILinkedComponent component, MindAddedMessage args) - { - if (!_mind.TryGetMind(uid, out var mindId, out var mind) || - component.LinkedMMI == null) - return; - - _mind.TransferTo(mindId, component.LinkedMMI, true, mind: mind); - } - - private void OnMMILinkedRemoved(EntityUid uid, MMILinkedComponent component, EntGotRemovedFromContainerMessage args) - { - if (Terminating(uid)) - return; - - if (component.LinkedMMI is not { } linked) - return; - RemComp(uid, component); - - if (_mind.TryGetMind(linked, out var mindId, out var mind)) - { - if (_roles.MindHasRole(mindId)) - _roles.MindRemoveRole(mindId); - - _mind.TransferTo(mindId, uid, true, mind: mind); - } - - _appearance.SetData(linked, MMIVisuals.BrainPresent, false); - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs deleted file mode 100644 index 67408d1d5a..0000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs +++ /dev/null @@ -1,425 +0,0 @@ -using System.Linq; -using Content.Shared.Hands.Components; -using Content.Shared.Interaction.Components; -using Content.Shared.Silicons.Borgs.Components; -using Content.Shared.Whitelist; -using Robust.Shared.Containers; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - public void InitializeModules() - { - SubscribeLocalEvent(OnModuleGotInserted); - SubscribeLocalEvent(OnModuleGotRemoved); - - SubscribeLocalEvent(OnSelectableInstalled); - SubscribeLocalEvent(OnSelectableUninstalled); - SubscribeLocalEvent(OnSelectableAction); - - SubscribeLocalEvent(OnProvideItemStartup); - SubscribeLocalEvent(OnItemModuleSelected); - SubscribeLocalEvent(OnItemModuleUnselected); - } - - private void OnModuleGotInserted(EntityUid uid, BorgModuleComponent component, EntGotInsertedIntoContainerMessage args) - { - var chassis = args.Container.Owner; - - if (!TryComp(chassis, out var chassisComp) || - args.Container != chassisComp.ModuleContainer || - !Toggle.IsActivated(chassis)) - return; - - if (!_powerCell.HasDrawCharge(uid)) - return; - - InstallModule(chassis, uid, chassisComp, component); - } - - private void OnModuleGotRemoved(EntityUid uid, BorgModuleComponent component, EntGotRemovedFromContainerMessage args) - { - var chassis = args.Container.Owner; - - if (!TryComp(chassis, out var chassisComp) || - args.Container != chassisComp.ModuleContainer) - return; - - UninstallModule(chassis, uid, chassisComp, component); - } - - private void OnProvideItemStartup(EntityUid uid, ItemBorgModuleComponent component, ComponentStartup args) - { - Container.EnsureContainer(uid, component.HoldingContainer); - } - - private void OnSelectableInstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleInstalledEvent args) - { - var chassis = args.ChassisEnt; - - if (_actions.AddAction(chassis, ref component.ModuleSwapActionEntity, out var action, component.ModuleSwapActionId, uid)) - { - var actEnt = (component.ModuleSwapActionEntity.Value, action); - _actions.SetEntityIcon(actEnt, uid); - if (TryComp(uid, out var moduleIconComp)) - _actions.SetIcon(actEnt, moduleIconComp.Icon); - - /// Set a custom name and description on the action. The borg module action prototypes are shared across - /// all modules. Extract localized names, then populate variables with the info from the module itself. - var moduleName = Name(uid); - var actionMetaData = MetaData(component.ModuleSwapActionEntity.Value); - - var instanceName = Loc.GetString("borg-module-action-name", ("moduleName", moduleName)); - _metaData.SetEntityName(component.ModuleSwapActionEntity.Value, instanceName, actionMetaData); - var instanceDesc = Loc.GetString("borg-module-action-description", ("moduleName", moduleName)); - _metaData.SetEntityDescription(component.ModuleSwapActionEntity.Value, instanceDesc, actionMetaData); - } - - if (!TryComp(chassis, out BorgChassisComponent? chassisComp)) - return; - - if (chassisComp.SelectedModule == null) - SelectModule(chassis, uid, chassisComp, component); - } - - private void OnSelectableUninstalled(EntityUid uid, SelectableBorgModuleComponent component, ref BorgModuleUninstalledEvent args) - { - var chassis = args.ChassisEnt; - _actions.RemoveProvidedActions(chassis, uid); - if (!TryComp(chassis, out BorgChassisComponent? chassisComp)) - return; - - if (chassisComp.SelectedModule == uid) - UnselectModule(chassis, chassisComp); - } - - private void OnSelectableAction(EntityUid uid, SelectableBorgModuleComponent component, BorgModuleActionSelectedEvent args) - { - var chassis = args.Performer; - if (!TryComp(chassis, out var chassisComp)) - return; - - var selected = chassisComp.SelectedModule; - - args.Handled = true; - UnselectModule(chassis, chassisComp); - - if (selected != uid) - { - SelectModule(chassis, uid, chassisComp, component); - } - } - - /// - /// Selects a module, enabling the borg to use its provided abilities. - /// - public void SelectModule(EntityUid chassis, - EntityUid moduleUid, - BorgChassisComponent? chassisComp = null, - SelectableBorgModuleComponent? selectable = null, - BorgModuleComponent? moduleComp = null) - { - if (LifeStage(chassis) >= EntityLifeStage.Terminating) - return; - - if (!Resolve(chassis, ref chassisComp)) - return; - - if (!Resolve(moduleUid, ref moduleComp) || !moduleComp.Installed || moduleComp.InstalledEntity != chassis) - { - Log.Error($"{ToPrettyString(chassis)} attempted to select uninstalled module {ToPrettyString(moduleUid)}"); - return; - } - - if (selectable == null && !HasComp(moduleUid)) - { - Log.Error($"{ToPrettyString(chassis)} attempted to select invalid module {ToPrettyString(moduleUid)}"); - return; - } - - if (!chassisComp.ModuleContainer.Contains(moduleUid)) - { - Log.Error($"{ToPrettyString(chassis)} does not contain the installed module {ToPrettyString(moduleUid)}"); - return; - } - - if (chassisComp.SelectedModule != null) - return; - - if (chassisComp.SelectedModule == moduleUid) - return; - - UnselectModule(chassis, chassisComp); - - var ev = new BorgModuleSelectedEvent(chassis); - RaiseLocalEvent(moduleUid, ref ev); - chassisComp.SelectedModule = moduleUid; - Dirty(chassis, chassisComp); - } - - /// - /// Unselects a module, removing its provided abilities - /// - public void UnselectModule(EntityUid chassis, BorgChassisComponent? chassisComp = null) - { - if (LifeStage(chassis) >= EntityLifeStage.Terminating) - return; - - if (!Resolve(chassis, ref chassisComp)) - return; - - if (chassisComp.SelectedModule == null) - return; - - var ev = new BorgModuleUnselectedEvent(chassis); - RaiseLocalEvent(chassisComp.SelectedModule.Value, ref ev); - chassisComp.SelectedModule = null; - Dirty(chassis, chassisComp); - } - - private void OnItemModuleSelected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleSelectedEvent args) - { - ProvideItems(args.Chassis, uid, component: component); - } - - private void OnItemModuleUnselected(EntityUid uid, ItemBorgModuleComponent component, ref BorgModuleUnselectedEvent args) - { - RemoveProvidedItems(args.Chassis, uid, component: component); - } - - private void ProvideItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null) - { - if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component)) - return; - - if (!TryComp(chassis, out var hands)) - return; - - if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container)) - return; - - var xform = Transform(chassis); - - for (var i = 0; i < component.Hands.Count; i++) - { - var hand = component.Hands[i]; - var handId = $"{uid}-hand-{i}"; - - _hands.AddHand((chassis, hands), handId, hand.Hand); - EntityUid? item = null; - - if (component.StoredItems is not null) - { - if (component.StoredItems.TryGetValue(handId, out var storedItem)) - { - item = storedItem; - _container.Remove(storedItem, container, force: true); - } - } - else if (hand.Item is { } itemProto) - { - item = Spawn(itemProto, xform.Coordinates); - } - - if (item is { } pickUp) - { - _hands.DoPickup(chassis, handId, pickUp, hands); - if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null) - { - EnsureComp(pickUp); - } - } - } - - Dirty(uid, component); - } - - private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisComponent? chassisComponent = null, ItemBorgModuleComponent? component = null) - { - if (!Resolve(chassis, ref chassisComponent) || !Resolve(uid, ref component)) - return; - - if (!TryComp(chassis, out var hands)) - return; - - if (!_container.TryGetContainer(uid, component.HoldingContainer, out var container)) - return; - - if (TerminatingOrDeleted(uid)) - return; - - component.StoredItems ??= new(); - - for (var i = 0; i < component.Hands.Count; i++) - { - var handId = $"{uid}-hand-{i}"; - - if (_hands.TryGetHeldItem(chassis, handId, out var held)) - { - RemComp(held.Value); - _container.Insert(held.Value, container); - component.StoredItems[handId] = held.Value; - } - else - { - component.StoredItems.Remove(handId); - } - - _hands.RemoveHand(chassis, handId); - } - - Dirty(uid, component); - } - - /// - /// Checks if a given module can be inserted into a borg - /// - public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponent? component = null, BorgModuleComponent? moduleComponent = null, EntityUid? user = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return false; - - if (component.ModuleContainer.ContainedEntities.Count >= component.MaxModules) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-too-many"), uid, user.Value); - return false; - } - - if (_whitelistSystem.IsWhitelistFail(component.ModuleWhitelist, module)) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-whitelist-deny"), uid, user.Value); - return false; - } - - if (TryComp(module, out var itemModuleComp)) - { - foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities) - { - if (!TryComp(containedModuleUid, out var containedItemModuleComp)) - continue; - - if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count && - containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains)) - { - if (user != null) - Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value); - return false; - } - } - } - - return true; - } - - /// - /// Check if a module can be removed from a borg. - /// - /// The borg that the module is being removed from. - /// The module to remove from the borg. - /// The user attempting to remove the module. - /// True if the module can be removed. - public bool CanRemoveModule( - Entity borg, - Entity module, - EntityUid? user = null) - { - if (module.Comp.DefaultModule) - return false; - - return true; - } - - /// - /// Installs and activates all modules currently inside the borg's module container - /// - public void InstallAllModules(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var query = GetEntityQuery(); - foreach (var moduleEnt in new List(component.ModuleContainer.ContainedEntities)) - { - if (!query.TryGetComponent(moduleEnt, out var moduleComp)) - continue; - - InstallModule(uid, moduleEnt, component, moduleComp); - } - } - - /// - /// Deactivates all modules currently inside the borg's module container - /// - /// - /// - public void DisableAllModules(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var query = GetEntityQuery(); - foreach (var moduleEnt in new List(component.ModuleContainer.ContainedEntities)) - { - if (!query.TryGetComponent(moduleEnt, out var moduleComp)) - continue; - - UninstallModule(uid, moduleEnt, component, moduleComp); - } - } - - /// - /// Installs a single module into a borg. - /// - public void InstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return; - - if (moduleComponent.Installed) - return; - - moduleComponent.InstalledEntity = uid; - var ev = new BorgModuleInstalledEvent(uid); - RaiseLocalEvent(module, ref ev); - } - - /// - /// Uninstalls a single module from a borg. - /// - public void UninstallModule(EntityUid uid, EntityUid module, BorgChassisComponent? component, BorgModuleComponent? moduleComponent = null) - { - if (!Resolve(uid, ref component) || !Resolve(module, ref moduleComponent)) - return; - - if (!moduleComponent.Installed) - return; - - moduleComponent.InstalledEntity = null; - var ev = new BorgModuleUninstalledEvent(uid); - RaiseLocalEvent(module, ref ev); - } - - /// - /// Sets . - /// - /// The borg to modify. - /// The new max module count. - public void SetMaxModules(Entity ent, int maxModules) - { - ent.Comp.MaxModules = maxModules; - } - - /// - /// Sets . - /// - /// The borg to modify. - /// The new module whitelist. - public void SetModuleWhitelist(Entity ent, EntityWhitelist? whitelist) - { - ent.Comp.ModuleWhitelist = whitelist; - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs index cc665c9e62..a6d3cf1dff 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs @@ -1,9 +1,7 @@ -using Content.Shared.Containers.ItemSlots; using Content.Shared.DeviceNetwork; using Content.Shared.Damage.Components; using Content.Shared.FixedPoint; using Content.Shared.Mobs; -using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Popups; using Content.Shared.Robotics; @@ -18,10 +16,6 @@ namespace Content.Server.Silicons.Borgs; /// public sealed partial class BorgSystem { - [Dependency] private readonly EmagSystem _emag = default!; - [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; - private void InitializeTransponder() { SubscribeLocalEvent(OnPacketReceived); @@ -83,7 +77,7 @@ public sealed partial class BorgSystem return; var message = Loc.GetString(ent.Comp1.DisabledPopup, ("name", Name(ent, ent.Comp3))); - Popup.PopupEntity(message, ent); + _popup.PopupEntity(message, ent); _container.Remove(brain, ent.Comp2.BrainContainer); } @@ -111,7 +105,7 @@ public sealed partial class BorgSystem if (CheckEmagged(ent, "disabled")) ent.Comp1.FakeDisabling = true; else - Popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); + _popup.PopupEntity(Loc.GetString(ent.Comp1.DisablingPopup), ent); ent.Comp1.NextDisable = _timing.CurTime + ent.Comp1.DisableDelay; } @@ -134,7 +128,7 @@ public sealed partial class BorgSystem } var message = Loc.GetString(ent.Comp.DestroyingPopup, ("name", Name(ent))); - Popup.PopupEntity(message, ent); + _popup.PopupEntity(message, ent); _trigger.ActivateTimerTrigger(ent.Owner); // prevent a shitter borg running into people @@ -145,7 +139,7 @@ public sealed partial class BorgSystem { if (_emag.CheckFlag(uid, EmagType.Interaction)) { - Popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution); + _popup.PopupEntity(Loc.GetString($"borg-transponder-emagged-{name}-popup"), uid, uid, PopupType.LargeCaution); return true; } diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs b/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs deleted file mode 100644 index 1306e5414c..0000000000 --- a/Content.Server/Silicons/Borgs/BorgSystem.Ui.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Linq; -using Content.Shared.UserInterface; -using Content.Shared.CCVar; -using Content.Shared.Database; -using Content.Shared.NameIdentifier; -using Content.Shared.PowerCell.Components; -using Content.Shared.Preferences; -using Content.Shared.Silicons.Borgs; -using Content.Shared.Silicons.Borgs.Components; -using Robust.Shared.Configuration; - -namespace Content.Server.Silicons.Borgs; - -/// -public sealed partial class BorgSystem -{ - // CCvar. - private int _maxNameLength; - - public void InitializeUI() - { - SubscribeLocalEvent(OnBeforeBorgUiOpen); - SubscribeLocalEvent(OnEjectBrainBuiMessage); - SubscribeLocalEvent(OnEjectBatteryBuiMessage); - SubscribeLocalEvent(OnSetNameBuiMessage); - SubscribeLocalEvent(OnRemoveModuleBuiMessage); - - Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); - } - - private void OnBeforeBorgUiOpen(EntityUid uid, BorgChassisComponent component, BeforeActivatableUIOpenEvent args) - { - UpdateUI(uid, component); - } - - private void OnEjectBrainBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBrainBuiMessage args) - { - if (component.BrainEntity is not { } brain) - return; - - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Actor):player} removed brain {ToPrettyString(brain)} from borg {ToPrettyString(uid)}"); - _container.Remove(brain, component.BrainContainer); - _hands.TryPickupAnyHand(args.Actor, brain); - UpdateUI(uid, component); - } - - private void OnEjectBatteryBuiMessage(EntityUid uid, BorgChassisComponent component, BorgEjectBatteryBuiMessage args) - { - if (TryEjectPowerCell(uid, component, out var ents)) - _hands.TryPickupAnyHand(args.Actor, ents.First()); - } - - private void OnSetNameBuiMessage(EntityUid uid, BorgChassisComponent component, BorgSetNameBuiMessage args) - { - if (args.Name.Length > _maxNameLength || - args.Name.Length == 0 || - string.IsNullOrWhiteSpace(args.Name) || - string.IsNullOrEmpty(args.Name)) - { - return; - } - - var name = args.Name.Trim(); - - var metaData = MetaData(uid); - - // don't change the name if the value doesn't actually change - if (metaData.EntityName.Equals(name, StringComparison.InvariantCulture)) - return; - - _adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(args.Actor):player} set borg \"{ToPrettyString(uid)}\"'s name to: {name}"); - _metaData.SetEntityName(uid, name, metaData); - } - - private void OnRemoveModuleBuiMessage(EntityUid uid, BorgChassisComponent component, BorgRemoveModuleBuiMessage args) - { - var module = GetEntity(args.Module); - - if (!component.ModuleContainer.Contains(module)) - return; - - if (!CanRemoveModule((uid, component), (module, Comp(module)), args.Actor)) - return; - - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.Actor):player} removed module {ToPrettyString(module)} from borg {ToPrettyString(uid)}"); - _container.Remove(module, component.ModuleContainer); - _hands.TryPickupAnyHand(args.Actor, module); - - UpdateUI(uid, component); - } - - public void UpdateBattery(Entity ent) - { - UpdateBatteryAlert(ent); - - // if we aren't drawing and suddenly get enough power to draw again, reeanble. - if (_powerCell.HasDrawCharge(ent.Owner)) - { - Toggle.TryActivate(ent.Owner); - } - - UpdateUI(ent, ent); - } - - // TODO: Move to client so we don't have to network this periodically. - private void UpdateBatteryAlert(Entity ent, PowerCellSlotComponent? slotComponent = null) - { - if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, slotComponent), out var battery)) - { - _alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert); - _alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert); - return; - } - - var chargePercent = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f); - - // we make sure 0 only shows if they have absolutely no battery. - // also account for floating point imprecision - if (chargePercent == 0 && _powerCell.HasDrawCharge((ent.Owner, null, slotComponent))) - { - chargePercent = 1; - } - - _alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert); - _alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent); - } - - // TODO: Component states and update this on the client - public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - var chargePercent = 0f; - var hasBattery = false; - if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) - { - hasBattery = true; - chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge; - } - - var state = new BorgBuiState(chargePercent, hasBattery); - _ui.SetUiState(uid, BorgUiKey.Key, state); - } - - // periodically update the charge indicator - // TODO: Move this to the client. - public void UpdateBattery(float frameTime) - { - var curTime = _timing.CurTime; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var borgChassis)) - { - if (curTime < borgChassis.NextBatteryUpdate) - continue; - - UpdateBattery((uid, borgChassis)); - borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1); - } - } -} diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs index 24fde24f33..6f37d55013 100644 --- a/Content.Server/Silicons/Borgs/BorgSystem.cs +++ b/Content.Server/Silicons/Borgs/BorgSystem.cs @@ -1,39 +1,17 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Content.Server.Actions; -using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.DeviceNetwork.Systems; -using Content.Server.Hands.Systems; -using Content.Shared.Alert; -using Content.Shared.Body.Events; -using Content.Shared.Database; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Item.ItemToggle.Components; -using Content.Shared.Mind; -using Content.Shared.Mind.Components; -using Content.Shared.Mobs; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Emag.Systems; using Content.Shared.Mobs.Systems; -using Content.Shared.Movement.Systems; -using Content.Shared.Pointing; -using Content.Shared.Power; +using Content.Shared.Popups; using Content.Shared.Power.EntitySystems; using Content.Shared.PowerCell; -using Content.Shared.PowerCell.Components; using Content.Shared.Roles; using Content.Shared.Silicons.Borgs; -using Content.Shared.Silicons.Borgs.Components; -using Content.Shared.Throwing; using Content.Shared.Trigger.Systems; -using Content.Shared.Whitelist; -using Content.Shared.Wires; -using Robust.Server.GameObjects; -using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Server.Silicons.Borgs; @@ -41,28 +19,18 @@ namespace Content.Server.Silicons.Borgs; /// public sealed partial class BorgSystem : SharedBorgSystem { - [Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IBanManager _banManager = default!; - [Dependency] private readonly IConfigurationManager _cfgManager = default!; [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly ActionsSystem _actions = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TriggerSystem _trigger = default!; - [Dependency] private readonly HandsSystem _hands = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; - [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly ThrowingSystem _throwing = default!; - [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; - [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly PredictedBatterySystem _battery = default!; + [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; public static readonly ProtoId BorgJobId = "Borg"; @@ -71,264 +39,10 @@ public sealed partial class BorgSystem : SharedBorgSystem { base.Initialize(); - SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent(OnChassisInteractUsing); - SubscribeLocalEvent(OnMindAdded); - SubscribeLocalEvent(OnMindRemoved); - SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnBeingGibbed); - SubscribeLocalEvent(OnPowerCellChanged); - SubscribeLocalEvent(OnBatteryChargeChanged); - SubscribeLocalEvent(OnPowerCellSlotEmpty); - SubscribeLocalEvent(OnGetDeadIC); - SubscribeLocalEvent(OnGetUnrevivableIC); - SubscribeLocalEvent(OnToggled); - - SubscribeLocalEvent(OnBrainMindAdded); - SubscribeLocalEvent(OnBrainPointAttempt); - - InitializeModules(); - InitializeMMI(); - InitializeUI(); InitializeTransponder(); } - private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args) - { - UpdateBatteryAlert((uid, component)); - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - private void OnChassisInteractUsing(EntityUid uid, BorgChassisComponent component, AfterInteractUsingEvent args) - { - if (!args.CanReach || args.Handled || uid == args.User) - return; - - var used = args.Used; - TryComp(used, out var brain); - TryComp(used, out var module); - - if (TryComp(uid, out var panel) && !panel.Open) - { - if (brain != null || module != null) - { - Popup.PopupEntity(Loc.GetString("borg-panel-not-open"), uid, args.User); - } - return; - } - - if (component.BrainEntity == null && brain != null && - _whitelistSystem.IsWhitelistPassOrNull(component.BrainWhitelist, used)) - { - if (_mind.TryGetMind(used, out _, out var mind) && - _player.TryGetSessionById(mind.UserId, out var session)) - { - if (!CanPlayerBeBorged(session)) - { - Popup.PopupEntity(Loc.GetString("borg-player-not-allowed"), used, args.User); - return; - } - } - - _container.Insert(used, component.BrainContainer); - _adminLog.Add(LogType.Action, LogImpact.Medium, - $"{ToPrettyString(args.User):player} installed brain {ToPrettyString(used)} into borg {ToPrettyString(uid)}"); - args.Handled = true; - UpdateUI(uid, component); - } - - if (module != null && CanInsertModule(uid, used, component, module, args.User)) - { - InsertModule((uid, component), used); - _adminLog.Add(LogType.Action, LogImpact.Low, - $"{ToPrettyString(args.User):player} installed module {ToPrettyString(used)} into borg {ToPrettyString(uid)}"); - args.Handled = true; - UpdateUI(uid, component); - } - } - - /// - /// Inserts a new module into a borg, the same as if a player inserted it manually. - /// - /// - /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists. - /// - /// The borg to insert into. - /// The module to insert. - public void InsertModule(Entity ent, EntityUid module) - { - _container.Insert(module, ent.Comp.ModuleContainer); - } - - // todo: consider transferring over the ghost role? managing that might suck. - protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args) - { - base.OnInserted(uid, component, args); - - if (HasComp(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind) && args.Container == component.BrainContainer) - { - _mind.TransferTo(mindId, uid, mind: mind); - } - } - - protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args) - { - base.OnRemoved(uid, component, args); - - if (HasComp(args.Entity) && _mind.TryGetMind(uid, out var mindId, out var mind) && args.Container == component.BrainContainer) - { - _mind.TransferTo(mindId, args.Entity, mind: mind); - } - } - - private void OnMindAdded(EntityUid uid, BorgChassisComponent component, MindAddedMessage args) - { - BorgActivate(uid, component); - } - - private void OnMindRemoved(EntityUid uid, BorgChassisComponent component, MindRemovedMessage args) - { - BorgDeactivate(uid, component); - } - - private void OnMobStateChanged(EntityUid uid, BorgChassisComponent component, MobStateChangedEvent args) - { - if (args.NewMobState == MobState.Alive) - { - if (_mind.TryGetMind(uid, out _, out _)) - _powerCell.SetDrawEnabled(uid, true); - } - else - { - _powerCell.SetDrawEnabled(uid, false); - } - } - - private void OnBeingGibbed(EntityUid uid, BorgChassisComponent component, ref BeingGibbedEvent args) - { - TryEjectPowerCell(uid, component, out var _); - - _container.EmptyContainer(component.BrainContainer); - _container.EmptyContainer(component.ModuleContainer); - } - - private void OnPowerCellChanged(Entity ent, ref PowerCellChangedEvent args) - { - UpdateBattery(ent); - } - - private void OnBatteryChargeChanged(Entity ent, ref PredictedBatteryChargeChangedEvent args) - { - UpdateBattery(ent); - } - - private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args) - { - Toggle.TryDeactivate(uid); - UpdateUI(uid, component); - } - - private void OnGetDeadIC(EntityUid uid, BorgChassisComponent component, ref GetCharactedDeadIcEvent args) - { - args.Dead = true; - } - - private void OnGetUnrevivableIC(EntityUid uid, BorgChassisComponent component, ref GetCharacterUnrevivableIcEvent args) - { - args.Unrevivable = true; - } - - private void OnToggled(Entity ent, ref ItemToggledEvent args) - { - var (uid, comp) = ent; - if (args.Activated) - InstallAllModules(uid, comp); - else - DisableAllModules(uid, comp); - - // only enable the powerdraw if there is a player in the chassis - var drawing = _mind.TryGetMind(uid, out _, out _) && _mobState.IsAlive(ent); - _powerCell.SetDrawEnabled(uid, drawing); - - UpdateUI(uid, comp); - - _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); - } - - private void OnBrainMindAdded(EntityUid uid, BorgBrainComponent component, MindAddedMessage args) - { - if (!Container.TryGetOuterContainer(uid, Transform(uid), out var container)) - return; - - var containerEnt = container.Owner; - - if (!TryComp(containerEnt, out var chassisComponent) || - container.ID != chassisComponent.BrainContainerId) - return; - - if (!_mind.TryGetMind(uid, out var mindId, out var mind) || - !_player.TryGetSessionById(mind.UserId, out var session)) - return; - - if (!CanPlayerBeBorged(session)) - { - Popup.PopupEntity(Loc.GetString("borg-player-not-allowed-eject"), uid); - Container.RemoveEntity(containerEnt, uid); - _throwing.TryThrow(uid, _random.NextVector2() * 5, 5f); - return; - } - - _mind.TransferTo(mindId, containerEnt, mind: mind); - } - - private void OnBrainPointAttempt(EntityUid uid, BorgBrainComponent component, PointAttemptEvent args) - { - args.Cancel(); - } - - public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List? ents) - { - ents = null; - - if (!TryComp(uid, out var slotComp) || - !Container.TryGetContainer(uid, slotComp.CellSlotId, out var container) || - !container.ContainedEntities.Any()) - return false; - - ents = Container.EmptyContainer(container); - - return true; - } - - /// - /// Activates a borg when a player occupies it - /// - public void BorgActivate(EntityUid uid, BorgChassisComponent component) - { - Popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(uid, EntityManager))), uid); - if (_powerCell.HasDrawCharge(uid)) - { - Toggle.TryActivate(uid); - _powerCell.SetDrawEnabled(uid, _mobState.IsAlive(uid)); - } - _appearance.SetData(uid, BorgVisuals.HasPlayer, true); - } - - /// - /// Deactivates a borg when a player leaves it. - /// - public void BorgDeactivate(EntityUid uid, BorgChassisComponent component) - { - Popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(uid, EntityManager))), uid); - Toggle.TryDeactivate(uid); - _powerCell.SetDrawEnabled(uid, false); - _appearance.SetData(uid, BorgVisuals.HasPlayer, false); - } - - /// - /// Checks that a player has fulfilled the requirements for the borg job. - /// - public bool CanPlayerBeBorged(ICommonSession session) + public override bool CanPlayerBeBorged(ICommonSession session) { if (_banManager.GetJobBans(session.UserId)?.Contains(BorgJobId) == true) return false; @@ -341,6 +55,5 @@ public sealed partial class BorgSystem : SharedBorgSystem base.Update(frameTime); UpdateTransponder(frameTime); - UpdateBattery(frameTime); } } diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index b4ee94c3aa..d34e86ad5c 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -233,6 +233,9 @@ namespace Content.Shared.Interaction /// private void OnUnequip(EntityUid uid, UnremoveableComponent item, GotUnequippedEvent args) { + if (_gameTiming.ApplyingState) + return; // The changes are already networked with the same gamestate as the container event. + if (!item.DeleteOnDrop) RemCompDeferred(uid); else @@ -241,6 +244,9 @@ namespace Content.Shared.Interaction private void OnUnequipHand(EntityUid uid, UnremoveableComponent item, GotUnequippedHandEvent args) { + if (_gameTiming.ApplyingState) + return; // The changes are already networked with the same gamestate as the container event. + if (!item.DeleteOnDrop) RemCompDeferred(uid); else @@ -249,6 +255,11 @@ namespace Content.Shared.Interaction private void OnDropped(EntityUid uid, UnremoveableComponent item, DroppedEvent args) { + if (_gameTiming.ApplyingState) + return; // The changes are already networked with the same gamestate as the container event. + // Other than the two cases above this is not a container event, but adding and removing hands is networked similarly + // and removing hands causes items to be dropped. + if (!item.DeleteOnDrop) RemCompDeferred(uid); else diff --git a/Content.Shared/Light/Components/HandheldLightComponent.cs b/Content.Shared/Light/Components/HandheldLightComponent.cs index 6c70a82f93..9f6dddb230 100644 --- a/Content.Shared/Light/Components/HandheldLightComponent.cs +++ b/Content.Shared/Light/Components/HandheldLightComponent.cs @@ -10,6 +10,8 @@ namespace Content.Shared.Light.Components; public sealed partial class HandheldLightComponent : Component { public byte? Level; + + [DataField] public bool Activated; [ViewVariables(VVAccess.ReadWrite)] diff --git a/Content.Shared/Light/SharedHandheldLightSystem.cs b/Content.Shared/Light/SharedHandheldLightSystem.cs index 0f507e1365..cbff7f95db 100644 --- a/Content.Shared/Light/SharedHandheldLightSystem.cs +++ b/Content.Shared/Light/SharedHandheldLightSystem.cs @@ -1,7 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Examine; using Content.Shared.Item; -using Content.Shared.Light; using Content.Shared.Light.Components; using Content.Shared.Toggleable; using Content.Shared.Verbs; @@ -24,7 +24,7 @@ public abstract class SharedHandheldLightSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnHandleState); - + SubscribeLocalEvent(OnExamine); SubscribeLocalEvent>(AddToggleLightVerb); } @@ -45,6 +45,13 @@ public abstract class SharedHandheldLightSystem : EntitySystem SetActivated(uid, state.Activated, component, false); } + private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args) + { + args.PushMarkup(component.Activated + ? Loc.GetString("handheld-light-component-on-examine-is-on-message") + : Loc.GetString("handheld-light-component-on-examine-is-off-message")); + } + public void SetActivated(EntityUid uid, bool activated, HandheldLightComponent? component = null, bool makeNoise = true) { if (!Resolve(uid, ref component)) diff --git a/Content.Shared/PowerCell/PowerCellSystem.API.cs b/Content.Shared/PowerCell/PowerCellSystem.API.cs index 30c5228236..15376bda89 100644 --- a/Content.Shared/PowerCell/PowerCellSystem.API.cs +++ b/Content.Shared/PowerCell/PowerCellSystem.API.cs @@ -8,6 +8,23 @@ namespace Content.Shared.PowerCell; public sealed partial class PowerCellSystem { + /// + /// Checks if a power cell slot has a battery inside. + /// + [PublicAPI] + public bool HasBattery(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot)) + { + return false; + } + + return slot.Item != null; + } + /// /// Gets the power cell battery inside a power cell slot. /// @@ -22,7 +39,7 @@ public sealed partial class PowerCellSystem return false; } - if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out ItemSlot? slot)) + if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot)) { battery = null; return false; @@ -77,10 +94,39 @@ public sealed partial class PowerCellSystem return false; } + /// + /// Tries to eject the power cell battery inside a power cell slot. + /// This checks if the user has a free hand to do the ejection and if the slot is locked. + /// + /// The entity with the power cell slot. + /// The power cell that was ejected. + /// The player trying to eject the power cell from the slot. + /// If a power cell was sucessfully ejected. + [PublicAPI] + public bool TryEjectBatteryFromSlot( + Entity ent, + [NotNullWhen(true)] out EntityUid? battery, + EntityUid? user = null) + { + if (!Resolve(ent, ref ent.Comp, false)) + { + battery = null; + return false; + } + + if (!_itemSlots.TryEject(ent.Owner, ent.Comp.CellSlotId, user, out battery, excludeUserAudio: true)) + { + battery = null; + return false; + } + + return true; + } + /// /// Returns whether the entity has a slotted battery and charge for the requested action. /// - /// The power cell. + /// The entity with the power cell slot. /// The charge that is needed. /// Show a popup to this user with the relevant details if specified. /// Whether to predict the popup or not. @@ -119,7 +165,7 @@ public sealed partial class PowerCellSystem /// /// Tries to use charge from a slotted battery. /// - /// The power cell. + /// The entity with the power cell slot. /// The charge that is needed. /// Show a popup to this user with the relevant details if specified. /// Whether to predict the popup or not. @@ -157,7 +203,7 @@ public sealed partial class PowerCellSystem /// /// Gets number of remaining uses for the given charge cost. /// - /// The power cell. + /// The entity with the power cell slot. /// The cost per use. [PublicAPI] public int GetRemainingUses(Entity ent, float cost) @@ -171,7 +217,7 @@ public sealed partial class PowerCellSystem /// /// Gets number of maximum uses at full charge for the given charge cost. /// - /// The power cell. + /// The entity with the power cell slot. /// The cost per use. [PublicAPI] public int GetMaxUses(Entity ent, float cost) diff --git a/Content.Shared/Silicons/Borgs/BorgUI.cs b/Content.Shared/Silicons/Borgs/BorgUI.cs index fd6abc8992..9d843d4046 100644 --- a/Content.Shared/Silicons/Borgs/BorgUI.cs +++ b/Content.Shared/Silicons/Borgs/BorgUI.cs @@ -8,50 +8,38 @@ public enum BorgUiKey : byte Key } +/// +/// Send when a player uses the borg BUI to eject a brain. +/// [Serializable, NetSerializable] -public sealed class BorgBuiState : BoundUserInterfaceState -{ - public float ChargePercent; - - public bool HasBattery; - - public BorgBuiState(float chargePercent, bool hasBattery) - { - ChargePercent = chargePercent; - HasBattery = hasBattery; - } -} - -[Serializable, NetSerializable] -public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage -{ - -} +public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage; +/// +/// Send when a player uses the borg BUI to eject a power cell. +/// [Serializable, NetSerializable] -public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage -{ - -} +public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage; +/// +/// Send when a player uses the borg BUI to change a borg's name. +/// [Serializable, NetSerializable] -public sealed class BorgSetNameBuiMessage : BoundUserInterfaceMessage +public sealed class BorgSetNameBuiMessage(string name) : BoundUserInterfaceMessage { - public string Name; - - public BorgSetNameBuiMessage(string name) - { - Name = name; - } + /// + /// The new name. + /// + public string Name = name; } +/// +/// Send when a player uses the borg BUI to remove a borg module. +/// [Serializable, NetSerializable] -public sealed class BorgRemoveModuleBuiMessage : BoundUserInterfaceMessage +public sealed class BorgRemoveModuleBuiMessage(NetEntity module) : BoundUserInterfaceMessage { - public NetEntity Module; - - public BorgRemoveModuleBuiMessage(NetEntity module) - { - Module = module; - } + /// + /// The module to eject. + /// + public NetEntity Module = module; } diff --git a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs index 98a5b205e9..a6c63bc602 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Alert; using Content.Shared.Whitelist; +using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -13,80 +14,132 @@ namespace Content.Shared.Silicons.Borgs.Components; /// "brain", legs, modules, and battery. Essentially the master component /// for borg logic. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] -[AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedBorgSystem))] public sealed partial class BorgChassisComponent : Component { + /// + /// Is this borg currently activated? + /// If activated the borg + /// - has door access, + /// - can use modules and + /// - has full movement speed. + /// To be activated the borg + /// - needs to have a player controlling it (a mind), + /// - needs enough charge in its power cell and + /// - needs to be alive (not crit or dead). + /// + /// + /// Don't try to use ItemToggle for this. + /// It used that before and it had a ton of conflicts with other item toggling behavior from the billion components that use it somehow. + /// + [DataField, AutoNetworkedField] + public bool Active; + + /// + /// The sound to play when the borg activates. + /// + /// + /// The same as the flashlight. This playing used to be a bug, but the sound is nostalgic at this point, so I'm keeping it. + /// + [DataField] + public SoundSpecifier ActivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg"); + + /// + /// The sound to play when the borg deactivates. + /// + [DataField] + public SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg"); + #region Brain /// - /// A whitelist for which entities count as valid brains + /// A whitelist for which entities count as valid brains. /// - [DataField("brainWhitelist")] + [DataField] public EntityWhitelist? BrainWhitelist; /// - /// The container ID for the brain + /// The container ID for the posibrain or MMI. /// - [DataField("brainContainerId")] + [DataField] public string BrainContainerId = "borg_brain"; - [ViewVariables(VVAccess.ReadWrite)] + /// + /// The container for the posibrain or MMI. + /// + [ViewVariables] public ContainerSlot BrainContainer = default!; - public EntityUid? BrainEntity => BrainContainer.ContainedEntity; + /// + /// The posibrain or MMI inserted into this borg, if any. + /// + [ViewVariables] + public EntityUid? BrainEntity => BrainContainer?.ContainedEntity; #endregion #region Modules /// - /// A whitelist for what types of modules can be installed into this borg + /// A whitelist for what types of modules can be installed into this borg. /// - [DataField("moduleWhitelist")] + [DataField, AutoNetworkedField] public EntityWhitelist? ModuleWhitelist; /// - /// How many modules can be installed in this borg + /// How many modules can be installed in this borg? /// - [DataField("maxModules"), ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public int MaxModules = 3; /// - /// The ID for the module container + /// The ID for the module container. /// - [DataField("moduleContainerId")] + [DataField] public string ModuleContainerId = "borg_module"; - [ViewVariables(VVAccess.ReadWrite)] + /// + /// The module container. + /// + [ViewVariables] public Container ModuleContainer = default!; + /// + /// How many modules are currently installed? + /// + [ViewVariables] public int ModuleCount => ModuleContainer.ContainedEntities.Count; #endregion /// - /// The currently selected module + /// The currently selected module. /// - [DataField("selectedModule"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? SelectedModule; #region Visuals - [DataField("hasMindState")] + [DataField] public string HasMindState = string.Empty; - [DataField("noMindState")] + [DataField] public string NoMindState = string.Empty; #endregion + /// + /// The battery charge alert. + /// [DataField] public ProtoId BatteryAlert = "BorgBattery"; + /// + /// The alert for a missing battery. + /// [DataField] public ProtoId NoBatteryAlert = "BorgBatteryNone"; /// - /// The next update time for the battery charge level. - /// Used for the alert and borg UI. + /// The next update time the battery is checked for automatic reactivation. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - [AutoPausedField] + [AutoNetworkedField, AutoPausedField] public TimeSpan NextBatteryUpdate = TimeSpan.Zero; /// @@ -99,7 +152,8 @@ public sealed partial class BorgChassisComponent : Component [Serializable, NetSerializable] public enum BorgVisuals : byte { - HasPlayer + HasPlayer, + Powered, } [Serializable, NetSerializable] diff --git a/Content.Shared/Silicons/Borgs/Components/BorgModuleComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgModuleComponent.cs index f50b088932..5c7f6cfddb 100644 --- a/Content.Shared/Silicons/Borgs/Components/BorgModuleComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/BorgModuleComponent.cs @@ -1,5 +1,4 @@ using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; namespace Content.Shared.Silicons.Borgs.Components; @@ -7,23 +6,26 @@ namespace Content.Shared.Silicons.Borgs.Components; /// This is used for modules that can be inserted into borgs /// to give them unique abilities and attributes. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] -[AutoGenerateComponentState] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedBorgSystem))] public sealed partial class BorgModuleComponent : Component { /// - /// The entity this module is installed into + /// The entity this module is installed into. /// - [DataField("installedEntity")] + [DataField, AutoNetworkedField] public EntityUid? InstalledEntity; + /// + /// Is this module currently installed? + /// + [ViewVariables] public bool Installed => InstalledEntity != null; /// /// If true, this is a "default" module that cannot be removed from a borg. /// - [DataField] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool DefaultModule; /// @@ -37,13 +39,13 @@ public sealed partial class BorgModuleComponent : Component /// /// Raised on a module when it is installed in order to add specific behavior to an entity. /// -/// +/// The borg the module is being installed in. [ByRefEvent] public readonly record struct BorgModuleInstalledEvent(EntityUid ChassisEnt); /// /// Raised on a module when it's uninstalled in order to /// -/// +/// The borg the module is being uninstalled from. [ByRefEvent] public readonly record struct BorgModuleUninstalledEvent(EntityUid ChassisEnt); diff --git a/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs b/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs index 3680a4cbde..db91e9d8b5 100644 --- a/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Hands.Components; -using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -9,7 +8,8 @@ namespace Content.Shared.Silicons.Borgs.Components; /// /// This is used for a that provides items to the entity it's installed into. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedBorgSystem))] public sealed partial class ItemBorgModuleComponent : Component { /// @@ -19,10 +19,17 @@ public sealed partial class ItemBorgModuleComponent : Component public List Hands = new(); /// - /// The items stored within the hands. Null until the first time items are stored. + /// The items stored within the hands. /// - [DataField] - public Dictionary? StoredItems; + [DataField, AutoNetworkedField] + public Dictionary StoredItems = new(); + + /// + /// Whether the provided items have been spawned. + /// This happens the first time the module is used. + /// + [DataField, AutoNetworkedField] + public bool Spawned; /// /// An ID for the container where items are stored when not in use. @@ -31,12 +38,21 @@ public sealed partial class ItemBorgModuleComponent : Component public string HoldingContainer = "holding_container"; } +/// +/// A single hand provided by the module. +/// [DataDefinition, Serializable, NetSerializable] public partial record struct BorgHand { + /// + /// The item to spawn in the hand, if any. + /// [DataField] public EntProtoId? Item; + /// + /// The settings for the hand, including a whitelist. + /// [DataField] public Hand Hand = new(); diff --git a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs index 3ded725efc..51031c930e 100644 --- a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs @@ -9,37 +9,38 @@ namespace Content.Shared.Silicons.Borgs.Components; /// in an item slot before transferring consciousness. /// Used for borg stuff. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedBorgSystem))] public sealed partial class MMIComponent : Component { /// /// The ID of the itemslot that holds the brain. /// - [DataField("brainSlotId")] + [DataField] public string BrainSlotId = "brain_slot"; /// - /// The for this implanter + /// The for this MMI. Holds the brain. /// - [ViewVariables(VVAccess.ReadWrite)] - public ItemSlot BrainSlot = default!; + [DataField(required: true)] + public ItemSlot BrainSlot = new(); /// /// The sprite state when the brain inserted has a mind. /// - [DataField("hasMindState")] + [DataField] public string HasMindState = "mmi_alive"; /// /// The sprite state when the brain inserted doesn't have a mind. /// - [DataField("noMindState")] + [DataField] public string NoMindState = "mmi_dead"; /// /// The sprite state when there is no brain inserted. /// - [DataField("noBrainState")] + [DataField] public string NoBrainState = "mmi_off"; } diff --git a/Content.Shared/Silicons/Borgs/Components/MMILinkedComponent.cs b/Content.Shared/Silicons/Borgs/Components/MMILinkedComponent.cs index 639c6a4269..cebbc9b746 100644 --- a/Content.Shared/Silicons/Borgs/Components/MMILinkedComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/MMILinkedComponent.cs @@ -3,7 +3,7 @@ namespace Content.Shared.Silicons.Borgs.Components; /// -/// This is used for an entity that is linked to an MMI. +/// This is used for an entity that is linked to an MMI, usually a brain. /// Mostly for receiving events. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] @@ -13,6 +13,6 @@ public sealed partial class MMILinkedComponent : Component /// /// The MMI this entity is linked to. /// - [DataField("linkedMMI"), AutoNetworkedField] + [DataField, AutoNetworkedField] public EntityUid? LinkedMMI; } diff --git a/Content.Shared/Silicons/Borgs/Components/SelectableBorgModuleComponent.cs b/Content.Shared/Silicons/Borgs/Components/SelectableBorgModuleComponent.cs index ee6dee3689..69e58caa28 100644 --- a/Content.Shared/Silicons/Borgs/Components/SelectableBorgModuleComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/SelectableBorgModuleComponent.cs @@ -1,28 +1,33 @@ using Content.Shared.Actions; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Silicons.Borgs.Components; /// /// This is used for s that can be "swapped" to, as opposed to having passive effects. /// -[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedBorgSystem))] public sealed partial class SelectableBorgModuleComponent : Component { - [DataField("moduleSwapAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? ModuleSwapActionId = "ActionBorgSwapModule"; + /// + /// The sidebar action prototype for swapping to this module. + /// + [DataField] + public EntProtoId ModuleSwapAction = "ActionBorgSwapModule"; /// - /// The sidebar action for swapping to this module. + /// The sidebar action entity for swapping to this module. /// - [DataField("moduleSwapActionEntity")] public EntityUid? ModuleSwapActionEntity; + [DataField, AutoNetworkedField] + public EntityUid? ModuleSwapActionEntity; } -public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent -{ -} +/// +/// Raised on a borg module entity with when a player uses the action provided by it. +/// +public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent; /// /// Event raised by-ref on a module when it is selected diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs new file mode 100644 index 0000000000..821ef35d8d --- /dev/null +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.API.cs @@ -0,0 +1,318 @@ +using System.Linq; +using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.Whitelist; +using Robust.Shared.Player; + +namespace Content.Shared.Silicons.Borgs; + +public abstract partial class SharedBorgSystem +{ + /// + /// Can this borg currently activate it's ? + /// The requirements for this are + /// - Having enough power in its power cell + /// - Having a player mind attached + /// - The borg is alive (not crit or dead). + /// + public bool CanActivate(Entity chassis) + { + if (!_powerCell.HasDrawCharge(chassis.Owner)) + return false; + + // TODO: Replace this with something else, only the client's own mind is networked to them, + // so this will always be false for the minds of other clients. + if (!_mind.TryGetMind(chassis.Owner, out _, out _)) + return false; + + if (!_mobState.IsAlive(chassis.Owner)) + return false; + + return true; + } + + /// + /// Activates the borg if the conditions are met. + /// Returns true if the borg was activated. + /// + public bool TryActivate(Entity chassis, EntityUid? user = null) + { + if (chassis.Comp.Active) + return false; // Already active. + + if (CanActivate(chassis)) + { + SetActive(chassis, true, user); + return true; + } + + return false; + } + + /// + /// Activates or deactivates a borg. + /// If active the borg + /// - has door access, + /// - can use modules and + /// - has full movement speed. + /// + public void SetActive(Entity chassis, bool active, EntityUid? user = null) + { + if (chassis.Comp.Active == active) + return; + + chassis.Comp.Active = active; + Dirty(chassis); + + if (active) + InstallAllModules(chassis.AsNullable()); + else + DisableAllModules(chassis.AsNullable()); + + _access.SetAccessEnabled(chassis.Owner, active); // Needs a player so that scientists can't drag around an empty borg for free AA. + _powerCell.SetDrawEnabled(chassis.Owner, active); + _movementSpeedModifier.RefreshMovementSpeedModifiers(chassis); + + var sound = active ? chassis.Comp.ActivateSound : chassis.Comp.DeactivateSound; + // If a user is given predict the audio for them, if not keep it unpredicted. + if (user != null) + _audio.PlayPredicted(sound, chassis.Owner, user); + else if (_net.IsServer) + _audio.PlayPvs(sound, chassis.Owner); + } + + /// + /// Inserts a new module into a borg, the same as if a player inserted it manually. + /// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists. + /// + /// The borg to insert into. + /// The module to insert. + public void InsertModule(Entity ent, EntityUid module) + { + _container.Insert(module, ent.Comp.ModuleContainer); + } + + /// + /// Sets . + /// + /// The borg to modify. + /// The new module whitelist. + public void SetModuleWhitelist(Entity ent, EntityWhitelist? whitelist) + { + ent.Comp.ModuleWhitelist = whitelist; + Dirty(ent); + } + + /// + /// Sets . + /// + /// The borg to modify. + /// The new max module count. + public void SetMaxModules(Entity ent, int maxModules) + { + ent.Comp.MaxModules = maxModules; + Dirty(ent); + } + + /// + /// Checks that a player has fulfilled the requirements for the borg job, i.e. they are not banned from that role. + /// Always true on the client. + /// + /// + /// TODO: This currently causes mispredicts, but we have no way of knowing on the client if a player is banned. + /// Maybe solve this by giving banned players an unborgable trait instead? + /// + public virtual bool CanPlayerBeBorged(ICommonSession session) + { + return true; + } + + /// + /// Installs a single module into a borg. + /// + public void InstallModule(Entity borg, Entity module) + { + if (!Resolve(borg, ref borg.Comp) || !Resolve(module, ref module.Comp)) + return; + + if (module.Comp.Installed) + return; + + module.Comp.InstalledEntity = borg.Owner; + Dirty(module); + var ev = new BorgModuleInstalledEvent(borg.Owner); + RaiseLocalEvent(module, ref ev); + } + + /// + /// Uninstalls a single module from a borg. + /// + public void UninstallModule(Entity borg, Entity module) + { + if (!Resolve(borg, ref borg.Comp) || !Resolve(module, ref module.Comp)) + return; + + if (!module.Comp.Installed) + return; + + module.Comp.InstalledEntity = null; + Dirty(module); + var ev = new BorgModuleUninstalledEvent(borg.Owner); + RaiseLocalEvent(module, ref ev); + } + + /// + /// Installs and activates all modules currently inside the borg's module container. + /// + public void InstallAllModules(Entity borg) + { + if (!Resolve(borg, ref borg.Comp)) + return; + + foreach (var moduleEnt in new List(borg.Comp.ModuleContainer.ContainedEntities)) + { + if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp)) + continue; + + InstallModule(borg, (moduleEnt, moduleComp)); + } + } + + /// + /// Deactivates all modules currently inside the borg's module container. + /// + public void DisableAllModules(Entity borg) + { + if (!Resolve(borg, ref borg.Comp)) + return; + + foreach (var moduleEnt in new List(borg.Comp.ModuleContainer.ContainedEntities)) + { + if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp)) + continue; + + UninstallModule(borg, (moduleEnt, moduleComp)); + } + } + + /// + /// Sets . + /// + public void SetBorgModuleDefault(Entity ent, bool newDefault) + { + ent.Comp.DefaultModule = newDefault; + Dirty(ent); + } + + /// + /// Checks if a given module can be inserted into a borg. + /// + public bool CanInsertModule(Entity chassis, Entity module, EntityUid? user = null) + { + if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp)) + return false; + + if (chassis.Comp.ModuleContainer.ContainedEntities.Count >= chassis.Comp.MaxModules) + { + _popup.PopupClient(Loc.GetString("borg-module-too-many"), chassis.Owner, user); + return false; + } + + if (_whitelist.IsWhitelistFail(chassis.Comp.ModuleWhitelist, module)) + { + _popup.PopupClient(Loc.GetString("borg-module-whitelist-deny"), chassis.Owner, user); + return false; + } + + if (TryComp(module, out var itemModuleComp)) + { + foreach (var containedModuleUid in chassis.Comp.ModuleContainer.ContainedEntities) + { + if (!TryComp(containedModuleUid, out var containedItemModuleComp)) + continue; + + if (containedItemModuleComp.Hands.Count == itemModuleComp.Hands.Count && + containedItemModuleComp.Hands.All(itemModuleComp.Hands.Contains)) + { + _popup.PopupClient(Loc.GetString("borg-module-duplicate"), chassis.Owner, user); + return false; + } + } + } + + return true; + } + + /// + /// Check if a module can be removed from a borg. + /// + /// The module to remove from the borg. + /// True if the module can be removed. + public bool CanRemoveModule(Entity module) + { + if (module.Comp.DefaultModule) + return false; + + return true; + } + + /// + /// Selects a module, enabling the borg to use its provided abilities. + /// + public void SelectModule(Entity chassis, + Entity module) + { + if (LifeStage(chassis) >= EntityLifeStage.Terminating) + return; + + if (!Resolve(chassis, ref chassis.Comp)) + return; + + if (!Resolve(module, ref module.Comp) || !module.Comp.Installed || module.Comp.InstalledEntity != chassis.Owner) + { + Log.Error($"{ToPrettyString(chassis)} attempted to select uninstalled module {ToPrettyString(module)}"); + return; + } + + if (!HasComp(module)) + { + Log.Error($"{ToPrettyString(chassis)} attempted to select invalid module {ToPrettyString(module)}"); + return; + } + + if (!chassis.Comp.ModuleContainer.Contains(module)) + { + Log.Error($"{ToPrettyString(chassis)} does not contain the installed module {ToPrettyString(module)}"); + return; + } + + if (chassis.Comp.SelectedModule == module.Owner) + return; + + UnselectModule(chassis); + + var ev = new BorgModuleSelectedEvent(chassis); + RaiseLocalEvent(module, ref ev); + chassis.Comp.SelectedModule = module.Owner; + Dirty(chassis); + } + + /// + /// Unselects a module, removing its provided abilities. + /// + public void UnselectModule(Entity chassis) + { + if (LifeStage(chassis) >= EntityLifeStage.Terminating) + return; + + if (!Resolve(chassis, ref chassis.Comp)) + return; + + if (chassis.Comp.SelectedModule == null) + return; + + var ev = new BorgModuleUnselectedEvent(chassis); + RaiseLocalEvent(chassis.Comp.SelectedModule.Value, ref ev); + chassis.Comp.SelectedModule = null; + Dirty(chassis); + } +} diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs new file mode 100644 index 0000000000..efcd5a378c --- /dev/null +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.MMI.cs @@ -0,0 +1,94 @@ +using Content.Shared.Mind.Components; +using Content.Shared.Roles.Components; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Silicons.Borgs; + +public abstract partial class SharedBorgSystem +{ + private static readonly EntProtoId SiliconBrainRole = "MindRoleSiliconBrain"; + + public void InitializeMMI() + { + SubscribeLocalEvent(OnMMIInit); + SubscribeLocalEvent(OnMMIEntityInserted); + SubscribeLocalEvent(OnMMIMindAdded); + SubscribeLocalEvent(OnMMIMindRemoved); + + SubscribeLocalEvent(OnMMILinkedMindAdded); + SubscribeLocalEvent(OnMMILinkedRemoved); + } + + private void OnMMIInit(Entity ent, ref ComponentInit args) + { + _itemSlots.AddItemSlot(ent.Owner, ent.Comp.BrainSlotId, ent.Comp.BrainSlot); + } + + private void OnMMIEntityInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + if (args.Container.ID != ent.Comp.BrainSlotId) + return; + + var brain = args.Entity; + var linked = EnsureComp(brain); + linked.LinkedMMI = ent.Owner; + Dirty(brain, linked); + + if (_mind.TryGetMind(brain, out var mindId, out var mindComp)) + { + _mind.TransferTo(mindId, ent.Owner, true, mind: mindComp); + + if (!_roles.MindHasRole(mindId)) + _roles.MindAddRole(mindId, SiliconBrainRole, silent: true); + } + + _appearance.SetData(ent.Owner, MMIVisuals.BrainPresent, true); + } + + private void OnMMIMindAdded(Entity ent, ref MindAddedMessage args) + { + _appearance.SetData(ent.Owner, MMIVisuals.HasMind, true); + } + + private void OnMMIMindRemoved(Entity ent, ref MindRemovedMessage args) + { + _appearance.SetData(ent.Owner, MMIVisuals.HasMind, false); + } + + private void OnMMILinkedMindAdded(Entity ent, ref MindAddedMessage args) + { + if (ent.Comp.LinkedMMI == null || !_mind.TryGetMind(ent.Owner, out var mindId, out var mindComp)) + return; + + _mind.TransferTo(mindId, ent.Comp.LinkedMMI, true, mind: mindComp); + } + + private void OnMMILinkedRemoved(Entity ent, ref EntGotRemovedFromContainerMessage args) + { + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + if (Terminating(ent.Owner)) + return; + + if (ent.Comp.LinkedMMI is not { } linked) + return; + + RemCompDeferred(ent.Owner); + + if (_mind.TryGetMind(linked, out var mindId, out var mindComp)) + { + if (_roles.MindHasRole(mindId)) + _roles.MindRemoveRole(mindId); + + _mind.TransferTo(mindId, ent.Owner, true, mind: mindComp); + } + + _appearance.SetData(linked, MMIVisuals.BrainPresent, false); + } +} diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs new file mode 100644 index 0000000000..3f00695270 --- /dev/null +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Module.cs @@ -0,0 +1,234 @@ +using Content.Shared.Examine; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction.Components; +using Content.Shared.Localizations; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Shared.Containers; + +namespace Content.Shared.Silicons.Borgs; + +public abstract partial class SharedBorgSystem +{ + private EntityQuery _moduleQuery; + + public void InitializeModule() + { + SubscribeLocalEvent(OnModuleExamine); + SubscribeLocalEvent(OnModuleGotInserted); + SubscribeLocalEvent(OnModuleGotRemoved); + + SubscribeLocalEvent(OnSelectableInstalled); + SubscribeLocalEvent(OnSelectableUninstalled); + SubscribeLocalEvent(OnSelectableAction); + + SubscribeLocalEvent(OnProvideItemStartup); + SubscribeLocalEvent(OnItemModuleSelected); + SubscribeLocalEvent(OnItemModuleUnselected); + + _moduleQuery = GetEntityQuery(); + } + + private void OnModuleExamine(Entity ent, ref ExaminedEvent args) + { + if (ent.Comp.BorgFitTypes == null) + return; + + if (ent.Comp.BorgFitTypes.Count == 0) + return; + + var typeList = new List(); + + foreach (var type in ent.Comp.BorgFitTypes) + { + typeList.Add(Loc.GetString(type)); + } + + var types = ContentLocalizationManager.FormatList(typeList); + args.PushMarkup(Loc.GetString("borg-module-fit", ("types", types))); + } + + private void OnModuleGotInserted(Entity module, ref EntGotInsertedIntoContainerMessage args) + { + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + var chassis = args.Container.Owner; + + if (!TryComp(chassis, out var chassisComp) || + args.Container != chassisComp.ModuleContainer || + !chassisComp.Active) + return; + + InstallModule((chassis, chassisComp), module.AsNullable()); + } + + private void OnModuleGotRemoved(Entity module, ref EntGotRemovedFromContainerMessage args) + { + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + var chassis = args.Container.Owner; + + if (!TryComp(chassis, out var chassisComp) || + args.Container != chassisComp.ModuleContainer) + return; + + UninstallModule((chassis, chassisComp), module.AsNullable()); + } + + private void OnSelectableInstalled(Entity module, ref BorgModuleInstalledEvent args) + { + var chassis = args.ChassisEnt; + + if (_actions.AddAction(chassis, ref module.Comp.ModuleSwapActionEntity, out var action, module.Comp.ModuleSwapAction, module.Owner)) + { + Dirty(module); // for ModuleSwapActionEntity after the action has been spawned + var actEnt = (module.Comp.ModuleSwapActionEntity.Value, action); + _actions.SetEntityIcon(actEnt, module.Owner); + if (TryComp(module, out var moduleIconComp)) + _actions.SetIcon(actEnt, moduleIconComp.Icon); + + /// Set a custom name and description on the action. The borg module action prototypes are shared across + /// all modules. Extract localized names, then populate variables with the info from the module itself. + var moduleName = Name(module); + var actionMetaData = MetaData(module.Comp.ModuleSwapActionEntity.Value); + + var instanceName = Loc.GetString("borg-module-action-name", ("moduleName", moduleName)); + _metaData.SetEntityName(module.Comp.ModuleSwapActionEntity.Value, instanceName, actionMetaData); + var instanceDesc = Loc.GetString("borg-module-action-description", ("moduleName", moduleName)); + _metaData.SetEntityDescription(module.Comp.ModuleSwapActionEntity.Value, instanceDesc, actionMetaData); + } + + if (!TryComp(chassis, out var chassisComp)) + return; + + if (chassisComp.SelectedModule == null) + SelectModule((chassis, chassisComp), module.Owner); + } + + private void OnSelectableUninstalled(Entity module, ref BorgModuleUninstalledEvent args) + { + var chassis = args.ChassisEnt; + _actions.RemoveProvidedActions(chassis, module.Owner); + if (!TryComp(chassis, out var chassisComp)) + return; + + if (chassisComp.SelectedModule == module.Owner) + UnselectModule((chassis, chassisComp)); + } + + private void OnSelectableAction(Entity module, ref BorgModuleActionSelectedEvent args) + { + var chassis = args.Performer; + if (!TryComp(chassis, out var chassisComp)) + return; + + var selected = chassisComp.SelectedModule; + + args.Handled = true; + UnselectModule((chassis, chassisComp)); + + if (selected != module.Owner) + { + SelectModule((chassis, chassisComp), module.Owner); + } + } + + private void OnProvideItemStartup(Entity module, ref ComponentStartup args) + { + _container.EnsureContainer(module.Owner, module.Comp.HoldingContainer); + } + + private void OnItemModuleSelected(Entity module, ref BorgModuleSelectedEvent args) + { + ProvideItems(args.Chassis, module.AsNullable()); + } + + private void OnItemModuleUnselected(Entity module, ref BorgModuleUnselectedEvent args) + { + RemoveProvidedItems(args.Chassis, module.AsNullable()); + } + + private void ProvideItems(Entity chassis, Entity module) + { + if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp)) + return; + + if (!TryComp(chassis, out var hands)) + return; + + if (!_container.TryGetContainer(module, module.Comp.HoldingContainer, out var container)) + return; + + var xform = Transform(chassis); + + for (var i = 0; i < module.Comp.Hands.Count; i++) + { + var hand = module.Comp.Hands[i]; + var handId = $"{GetNetEntity(module.Owner)}-hand-{i}"; + + _hands.AddHand((chassis.Owner, hands), handId, hand.Hand); + EntityUid? item = null; + + if (module.Comp.Spawned) + { + if (module.Comp.StoredItems.TryGetValue(handId, out var storedItem)) + { + item = storedItem; + // DoPickup handles removing the item from the container. + } + } + else if (hand.Item is { } itemProto) + { + item = PredictedSpawnAtPosition(itemProto, xform.Coordinates); + } + + if (item is { } pickUp) + { + _hands.DoPickup(chassis, handId, pickUp, hands); + if (!hand.ForceRemovable && hand.Hand.Whitelist == null && hand.Hand.Blacklist == null) + { + EnsureComp(pickUp); + } + } + } + + module.Comp.Spawned = true; + Dirty(module); + } + + private void RemoveProvidedItems(Entity chassis, Entity module) + { + if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp)) + return; + + if (!TryComp(chassis, out var hands)) + return; + + if (!_container.TryGetContainer(module, module.Comp.HoldingContainer, out var container)) + return; + + if (TerminatingOrDeleted(module)) + return; + + for (var i = 0; i < module.Comp.Hands.Count; i++) + { + var handId = $"{GetNetEntity(module.Owner)}-hand-{i}"; + + if (_hands.TryGetHeldItem((chassis.Owner, hands), handId, out var held)) + { + RemComp(held.Value); + _container.Insert(held.Value, container); + module.Comp.StoredItems[handId] = held.Value; + } + else + { + module.Comp.StoredItems.Remove(handId); + } + + _hands.RemoveHand((chassis.Owner, hands), handId); + } + + Dirty(module); + } +} diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs new file mode 100644 index 0000000000..e5e8fa8cd8 --- /dev/null +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.Ui.cs @@ -0,0 +1,79 @@ +using Content.Shared.CCVar; +using Content.Shared.Database; +using Content.Shared.PowerCell.Components; +using Content.Shared.Silicons.Borgs.Components; + +namespace Content.Shared.Silicons.Borgs; + +public abstract partial class SharedBorgSystem +{ + // CCvar + private int _maxNameLength; + + public void InitializeUI() + { + SubscribeLocalEvent(OnEjectBrainBuiMessage); + SubscribeLocalEvent(OnEjectBatteryBuiMessage); + SubscribeLocalEvent(OnSetNameBuiMessage); + SubscribeLocalEvent(OnRemoveModuleBuiMessage); + + Subs.CVar(_configuration, CCVars.MaxNameLength, value => _maxNameLength = value, true); + } + + public virtual void UpdateUI(Entity chassis) { } + + private void OnEjectBrainBuiMessage(Entity chassis, ref BorgEjectBrainBuiMessage args) + { + if (chassis.Comp.BrainEntity is not { } brain) + return; + + _adminLog.Add(LogType.Action, LogImpact.Medium, + $"{args.Actor} removed brain {brain} from borg {chassis.Owner}"); + _container.Remove(brain, chassis.Comp.BrainContainer); + _hands.TryPickupAnyHand(args.Actor, brain); + } + + private void OnEjectBatteryBuiMessage(Entity chassis, ref BorgEjectBatteryBuiMessage args) + { + if (_powerCell.TryEjectBatteryFromSlot(chassis.Owner, out var powerCell, args.Actor)) + _hands.TryPickupAnyHand(args.Actor, powerCell.Value); + } + + private void OnSetNameBuiMessage(Entity chassis, ref BorgSetNameBuiMessage args) + { + if (args.Name.Length > _maxNameLength || + args.Name.Length == 0 || + string.IsNullOrWhiteSpace(args.Name) || + string.IsNullOrEmpty(args.Name)) + { + return; + } + + var name = args.Name.Trim(); + + var metaData = MetaData(chassis); + + // don't change the name if the value doesn't actually change + if (metaData.EntityName.Equals(name, StringComparison.InvariantCulture)) + return; + + _adminLog.Add(LogType.Action, LogImpact.High, $"{args.Actor} set borg \"{chassis.Owner}\"'s name to: {name}"); + _metaData.SetEntityName(chassis, name, metaData); + } + + private void OnRemoveModuleBuiMessage(Entity chassis, ref BorgRemoveModuleBuiMessage args) + { + var module = GetEntity(args.Module); + + if (!chassis.Comp.ModuleContainer.Contains(module)) + return; + + if (!CanRemoveModule((module, Comp(module)))) + return; + + _adminLog.Add(LogType.Action, LogImpact.Medium, + $"{args.Actor} removed module {module} from borg {chassis.Owner}"); + _container.Remove(module, chassis.Comp.ModuleContainer); + _hands.TryPickupAnyHand(args.Actor, module); + } +} diff --git a/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs b/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs index f9b4ec7cb8..d965a362ff 100644 --- a/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs +++ b/Content.Shared/Silicons/Borgs/SharedBorgSystem.cs @@ -1,16 +1,37 @@ +using Content.Shared.Access.Systems; +using Content.Shared.Actions; +using Content.Shared.Administration.Logs; +using Content.Shared.Body.Events; using Content.Shared.Containers.ItemSlots; -using Content.Shared.Examine; +using Content.Shared.Database; +using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; -using Content.Shared.Item.ItemToggle; -using Content.Shared.Localizations; +using Content.Shared.Interaction; +using Content.Shared.Light; +using Content.Shared.Light.Components; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; +using Content.Shared.Pointing; using Content.Shared.Popups; +using Content.Shared.PowerCell; using Content.Shared.PowerCell.Components; +using Content.Shared.Roles; using Content.Shared.Silicons.Borgs.Components; +using Content.Shared.Throwing; using Content.Shared.UserInterface; using Content.Shared.Wires; +using Content.Shared.Whitelist; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Shared.Silicons.Borgs; @@ -19,46 +40,63 @@ namespace Content.Shared.Silicons.Borgs; /// public abstract partial class SharedBorgSystem : EntitySystem { - [Dependency] protected readonly SharedContainerSystem Container = default!; - [Dependency] protected readonly ItemSlotsSystem ItemSlots = default!; - [Dependency] protected readonly ItemToggleSystem Toggle = default!; - [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly ISharedPlayerManager _player = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IConfigurationManager _configuration = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedHandheldLightSystem _handheldLight = default!; + [Dependency] private readonly SharedAccessSystem _access = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; /// public override void Initialize() { base.Initialize(); + InitializeMMI(); + InitializeModule(); + InitializeRelay(); + InitializeUI(); + + SubscribeLocalEvent(OnTryGetIdentityShortInfo); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnItemSlotInsertAttempt); SubscribeLocalEvent(OnItemSlotEjectAttempt); SubscribeLocalEvent(OnInserted); SubscribeLocalEvent(OnRemoved); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindRemoved); + SubscribeLocalEvent(OnChassisInteractUsing); SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); SubscribeLocalEvent(OnUIOpenAttempt); - SubscribeLocalEvent(OnTryGetIdentityShortInfo); - SubscribeLocalEvent(OnModuleExamine); - - InitializeRelay(); - } - - private void OnModuleExamine(Entity ent, ref ExaminedEvent args) - { - if (ent.Comp.BorgFitTypes == null) - return; - - if (ent.Comp.BorgFitTypes.Count == 0) - return; + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent(OnGetDeadIC); + SubscribeLocalEvent(OnGetUnrevivableIC); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + SubscribeLocalEvent(OnPowerCellChanged); - var typeList = new List(); + SubscribeLocalEvent(OnBrainMindAdded); + SubscribeLocalEvent(OnBrainPointAttempt); - foreach (var type in ent.Comp.BorgFitTypes) - { - typeList.Add(Loc.GetString(type)); - } - - var types = ContentLocalizationManager.FormatList(typeList); - args.PushMarkup(Loc.GetString("borg-module-fit", ("types", types))); } private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent args) @@ -68,6 +106,8 @@ public abstract partial class SharedBorgSystem : EntitySystem return; } + // TODO: Why the hell is this only broadcasted and not raised directed on the entity? + // This is doing a ton of HasComps/TryComps. if (!HasComp(args.ForActor)) { return; @@ -77,82 +117,265 @@ public abstract partial class SharedBorgSystem : EntitySystem args.Handled = true; } - private void OnItemSlotInsertAttempt(EntityUid uid, BorgChassisComponent component, ref ItemSlotInsertAttemptEvent args) + private void OnStartup(Entity chassis, ref ComponentStartup args) + { + if (!TryComp(chassis, out var containerManager)) + return; + + chassis.Comp.BrainContainer = _container.EnsureContainer(chassis.Owner, chassis.Comp.BrainContainerId, containerManager); + chassis.Comp.ModuleContainer = _container.EnsureContainer(chassis.Owner, chassis.Comp.ModuleContainerId, containerManager); + } + + private void OnMapInit(Entity chassis, ref MapInitEvent args) + { + _movementSpeedModifier.RefreshMovementSpeedModifiers(chassis.Owner); + } + + private void OnItemSlotInsertAttempt(Entity chassis, ref ItemSlotInsertAttemptEvent args) { if (args.Cancelled) return; - if (!TryComp(uid, out var cellSlotComp) || - !TryComp(uid, out var panel)) + if (!TryComp(chassis, out var cellSlotComp) || + !TryComp(chassis, out var panelComp)) return; - if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) + if (!_itemSlots.TryGetSlot(chassis.Owner, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) return; - if (!panel.Open || args.User == uid) + if (!panelComp.Open || args.User == chassis.Owner) args.Cancelled = true; } - private void OnItemSlotEjectAttempt(EntityUid uid, BorgChassisComponent component, ref ItemSlotEjectAttemptEvent args) + private void OnItemSlotEjectAttempt(Entity chassis, ref ItemSlotEjectAttemptEvent args) { if (args.Cancelled) return; - if (!TryComp(uid, out var cellSlotComp) || - !TryComp(uid, out var panel)) + if (!TryComp(chassis, out var cellSlotComp) || + !TryComp(chassis, out var panel)) return; - if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) + if (!_itemSlots.TryGetSlot(chassis.Owner, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) return; - if (!panel.Open || args.User == uid) + if (!panel.Open || args.User == chassis.Owner) args.Cancelled = true; } - private void OnStartup(EntityUid uid, BorgChassisComponent component, ComponentStartup args) + // TODO: consider transferring over the ghost role? managing that might suck. + protected virtual void OnInserted(Entity chassis, ref EntInsertedIntoContainerMessage args) { - if (!TryComp(uid, out var containerManager)) + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + if (args.Container != chassis.Comp.BrainContainer) return; - component.BrainContainer = Container.EnsureContainer(uid, component.BrainContainerId, containerManager); - component.ModuleContainer = Container.EnsureContainer(uid, component.ModuleContainerId, containerManager); + if (HasComp(args.Entity) && _mind.TryGetMind(args.Entity, out var mindId, out var mind)) + { + _mind.TransferTo(mindId, chassis.Owner, mind: mind); + } } - private void OnUIOpenAttempt(EntityUid uid, BorgChassisComponent component, ActivatableUIOpenAttemptEvent args) + protected virtual void OnRemoved(Entity chassis, ref EntRemovedFromContainerMessage args) { - // borgs generaly can't view their own ui - if (args.User == uid && !component.CanOpenSelfUi) - args.Cancel(); + if (_timing.ApplyingState) + return; // The changes are already networked with the same game state + + if (args.Container != chassis.Comp.BrainContainer) + return; + + if (HasComp(args.Entity) && _mind.TryGetMind(chassis.Owner, out var mindId, out var mind)) + { + _mind.TransferTo(mindId, args.Entity, mind: mind); + } } - protected virtual void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args) + private void OnMindAdded(Entity chassis, ref MindAddedMessage args) { + // Unpredicted because the event is raised on the server. + _popup.PopupEntity(Loc.GetString("borg-mind-added", ("name", Identity.Name(chassis.Owner, EntityManager))), chassis.Owner); + + if (CanActivate(chassis)) + SetActive(chassis, true); + _appearance.SetData(chassis.Owner, BorgVisuals.HasPlayer, true); + } + private void OnMindRemoved(Entity chassis, ref MindRemovedMessage args) + { + // Unpredicted because the event is raised on the server. + _popup.PopupEntity(Loc.GetString("borg-mind-removed", ("name", Identity.Name(chassis.Owner, EntityManager))), chassis.Owner); + + SetActive(chassis, false); + // Turn off the light so that the no-player visuals can be seen. + if (TryComp(chassis.Owner, out var light)) + _handheldLight.TurnOff((chassis.Owner, light), makeNoise: false); // Already plays a sound when toggling the borg off. + _appearance.SetData(chassis.Owner, BorgVisuals.HasPlayer, false); } - protected virtual void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args) + private void OnChassisInteractUsing(Entity chassis, ref AfterInteractUsingEvent args) { + if (!args.CanReach || args.Handled || chassis.Owner == args.User) + return; + var used = args.Used; + TryComp(used, out var brain); + TryComp(used, out var module); + + if (TryComp(chassis, out var panel) && !panel.Open) + { + if (brain != null || module != null) + { + _popup.PopupClient(Loc.GetString("borg-panel-not-open"), chassis, args.User); + } + return; + } + + if (chassis.Comp.BrainEntity == null && brain != null && + _whitelist.IsWhitelistPassOrNull(chassis.Comp.BrainWhitelist, used)) + { + if (TryComp(used, out var actor) && !CanPlayerBeBorged(actor.PlayerSession)) + { + // Don't use PopupClient because CanPlayerBeBorged is not predicted. + _popup.PopupEntity(Loc.GetString("borg-player-not-allowed"), used, args.User); + return; + } + + _container.Insert(used, chassis.Comp.BrainContainer); + _adminLog.Add(LogType.Action, LogImpact.Medium, + $"{args.User} installed brain {used} into borg {chassis.Owner}"); + args.Handled = true; + return; + } + + if (module != null && CanInsertModule(chassis.AsNullable(), (used, module), args.User)) + { + InsertModule(chassis, used); + _adminLog.Add(LogType.Action, LogImpact.Low, + $"{args.User} installed module {used} into borg {chassis.Owner}"); + args.Handled = true; + } } - private void OnRefreshMovementSpeedModifiers(EntityUid uid, BorgChassisComponent component, RefreshMovementSpeedModifiersEvent args) + // Make the borg slower without power. + private void OnRefreshMovementSpeedModifiers(Entity chassis, ref RefreshMovementSpeedModifiersEvent args) { - if (Toggle.IsActivated(uid)) + if (chassis.Comp.Active) return; - if (!TryComp(uid, out var movement)) + if (!TryComp(chassis, out var movement)) return; + if (movement.BaseSprintSpeed == 0f) + return; // We already cannot move. + + // Slow down to walk speed. var sprintDif = movement.BaseWalkSpeed / movement.BaseSprintSpeed; args.ModifySpeed(1f, sprintDif); } - /// - /// Sets . - /// - public void SetBorgModuleDefault(Entity ent, bool newDefault) + private void OnUIOpenAttempt(Entity chassis, ref ActivatableUIOpenAttemptEvent args) { - ent.Comp.DefaultModule = newDefault; - Dirty(ent); + // Borgs generally can't view their own UI. + if (args.User == chassis.Owner && !chassis.Comp.CanOpenSelfUi) + args.Cancel(); + } + + private void OnMobStateChanged(Entity chassis, ref MobStateChangedEvent args) + { + if (args.NewMobState == MobState.Alive) + { + if (CanActivate(chassis)) + SetActive(chassis, true, user: args.Origin); + } + else + { + SetActive(chassis, false, user: args.Origin); + } + } + + private void OnBeingGibbed(Entity chassis, ref BeingGibbedEvent args) + { + // Don't use the ItemSlotsSystem eject method since we don't want to play a sound and want we to eject the battery even if the slot is locked. + if (TryComp(chassis, out var slotComp) && + _container.TryGetContainer(chassis, slotComp.CellSlotId, out var slotContainer)) + _container.EmptyContainer(slotContainer); + + _container.EmptyContainer(chassis.Comp.BrainContainer); + _container.EmptyContainer(chassis.Comp.ModuleContainer); + } + + private void OnGetDeadIC(Entity chassis, ref GetCharactedDeadIcEvent args) + { + args.Dead = true; + } + + private void OnGetUnrevivableIC(Entity chassis, ref GetCharacterUnrevivableIcEvent args) + { + args.Unrevivable = true; + } + + private void OnBrainMindAdded(Entity brain, ref MindAddedMessage args) + { + if (!_container.TryGetContainingContainer(brain.Owner, out var container)) + return; + + var borg = container.Owner; + + if (!TryComp(borg, out var chassisComponent) || + container.ID != chassisComponent.BrainContainerId) + return; + + if (!_mind.TryGetMind(brain.Owner, out var mindId, out var mind) || + !_player.TryGetSessionById(mind.UserId, out var session)) + return; + + if (!CanPlayerBeBorged(session)) + { + // Don't use PopupClient because MindAddedMessage and CanPlayerBeBorged are not predicted. + _popup.PopupEntity(Loc.GetString("borg-player-not-allowed-eject"), brain); + _container.RemoveEntity(borg, brain); + _throwing.TryThrow(brain, _random.NextVector2() * 5, 5f); + return; + } + + _mind.TransferTo(mindId, borg, mind: mind); + } + + private void OnBrainPointAttempt(Entity brain, ref PointAttemptEvent args) + { + args.Cancel(); + } + + // Raised when the power cell is empty or removed from the borg. + private void OnPowerCellSlotEmpty(Entity chassis, ref PowerCellSlotEmptyEvent args) + { + SetActive(chassis, false); + } + + // Raised when a power cell is inserted. + private void OnPowerCellChanged(Entity chassis, ref PowerCellChangedEvent args) + { + if (CanActivate(chassis)) + SetActive(chassis, true); + } + + public override void Update(float frameTime) + { + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var borgChassis)) + { + if (curTime < borgChassis.NextBatteryUpdate) + continue; + + borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1); + Dirty(uid, borgChassis); + + // If we aren't drawing and suddenly get enough power to draw again, reenable. + if (CanActivate((uid, borgChassis))) + SetActive((uid, borgChassis), true); + } } } diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 719fa9379a..5c70b7e5f1 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -283,6 +283,7 @@ description: alerts-battery-desc minSeverity: 0 maxSeverity: 10 + clientHandled: true # the power cell is read on the client so that we don't have to periodically network the charge - type: alert id: BorgBatteryNone @@ -292,6 +293,7 @@ state: battery-none name: alerts-no-battery-name description: alerts-no-battery-desc + clientHandled: true # the power cell battery is read on the client so that we don't have to periodically network the charge - type: alert id: Internals diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 8d8c3671ee..fb0509bedf 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -125,17 +125,11 @@ - type: PowerCellSlot cellSlotId: cell_slot fitsInCharger: true - - type: ItemToggle - onActivate: false # You should not be able to turn off a borg temporarily. - activated: false # gets activated when a mind is added - onUse: false # no item-borg toggling sorry - - type: ItemTogglePointLight - - type: AccessToggle - # TODO: refactor movement to just be based on toggle like speedboots but for the boots themselves - # TODO: or just have sentient speedboots be fast idk + - type: Access + enabled: false # needs a player so that scientists can't drag around an empty borg for free AA - type: PowerCellDraw drawRate: 0.6 - # no ToggleCellDraw since dont want to lose access when power is gone + enabled: false # the borg is only activated when a player takes over, otherwise the battery is likely empty - type: ItemSlots slots: cell_slot: @@ -203,6 +197,11 @@ wattage: 0.2 blinkingBehaviourId: blinking radiatingBehaviourId: radiating + # These two components are required to make HandheldLight work, even though we don't even have ItemToggle. + # The code is a total mess and needs a complete rewrite. + - type: ItemTogglePointLight + - type: ToggleableVisuals + spriteLayer: light - type: LightBehaviour behaviours: - !type:FadeBehaviour @@ -219,8 +218,6 @@ startValue: 0.1 endValue: 2.0 isLooped: true - - type: ToggleableVisuals - spriteLayer: light - type: PointLight enabled: false mask: /Textures/Effects/LightMasks/cone.png @@ -318,7 +315,6 @@ factions: - NanoTrasen - type: Access - enabled: false groups: - AllAccess tags: @@ -376,7 +372,6 @@ factions: - NanoTrasen #The seemingly best fit. It was a regular NT cyborg once, after all. - type: Access - enabled: false groups: - AllAccess #Randomized access would be fun. AllAccess is the best i can think of right now that does make it too hard for it to enter the station or navigate it.. - type: AccessReader @@ -533,7 +528,6 @@ - Robotics - Xenoborgs - type: Access - enabled: false tags: - Xenoborg - type: AccessReader diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 4d27a0f07a..69d84810c6 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -15,6 +15,11 @@ - type: Input context: human - type: MMI + brainSlot: + name: positronic-brain-slot-component-slot-name-brain + whitelist: + components: + - Brain - type: BorgBrain - type: BlockMovement - type: Examiner @@ -35,12 +40,6 @@ - type: Speech speechSounds: Pai - type: ItemSlots - slots: - brain_slot: - name: positronic-brain-slot-component-slot-name-brain - whitelist: - components: - - Brain - type: ContainerContainer containers: brain_slot: !type:ContainerSlot @@ -59,14 +58,13 @@ id: MMIFilled suffix: Filled components: - - type: ItemSlots - slots: - brain_slot: - name: "Brain" - startingItem: OrganHumanBrain - whitelist: - components: - - Brain + - type: MMI + brainSlot: + name: positronic-brain-slot-component-slot-name-brain + startingItem: OrganHumanBrain + whitelist: + components: + - Brain - type: entity parent: BaseItem