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
This commit is contained in:
slarticodefast
2025-11-30 11:25:22 +01:00
committed by GitHub
parent 4ec41cc8f0
commit 937b61a832
29 changed files with 1450 additions and 1247 deletions

View File

@@ -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();
}
}

View File

@@ -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<NameModifierSystem>();
_powerCell = _entity.System<PowerCellSystem>();
_battery = _entity.System<PredictedBatterySystem>();
_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<MetaDataComponent>(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)));
UpdateBrainButton();
UpdateModulePanel();
("charge", (int)MathF.Round(chargeFraction * 100)));
}
private void UpdateBrainButton()
public void UpdateBatteryButton()
{
EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
}
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;

View File

@@ -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<BorgChassisComponent> _chassisQuery;
private EntityQuery<PowerCellSlotComponent> _slotQuery;
public void InitializeBattery()
{
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
}
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
{
UpdateBatteryAlert((ent.Owner, ent.Comp, null));
}
private void OnPlayerDetached(Entity<BorgChassisComponent> 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<BorgChassisComponent, PowerCellSlotComponent?> 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));
}
}

View File

@@ -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;
/// <inheritdoc/>
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<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
}
private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
{
if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
bui.Update();
}
private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> 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<BorgChassisComponent> 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<BorgChassisComponent> 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<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> 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<MobState>(uid, MobStateVisuals.State, out var state, appearance))
if (_appearance.TryGetData<MobState>(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<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
if (!_appearance.TryGetData<bool>(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);
}
}

View File

@@ -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<HandheldLightComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<HandheldLightComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(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();

View File

@@ -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;
/// <inheritdoc/>
public sealed partial class BorgSystem
{
[Dependency] private readonly SharedRoleSystem _roles = default!;
public void InitializeMMI()
{
SubscribeLocalEvent<MMIComponent, ComponentInit>(OnMMIInit);
SubscribeLocalEvent<MMIComponent, EntInsertedIntoContainerMessage>(OnMMIEntityInserted);
SubscribeLocalEvent<MMIComponent, MindAddedMessage>(OnMMIMindAdded);
SubscribeLocalEvent<MMIComponent, MindRemovedMessage>(OnMMIMindRemoved);
SubscribeLocalEvent<MMILinkedComponent, MindAddedMessage>(OnMMILinkedMindAdded);
SubscribeLocalEvent<MMILinkedComponent, EntGotRemovedFromContainerMessage>(OnMMILinkedRemoved);
}
private void OnMMIInit(EntityUid uid, MMIComponent component, ComponentInit args)
{
if (!TryComp<ItemSlotsComponent>(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<MMILinkedComponent>(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<SiliconBrainRoleComponent>(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<SiliconBrainRoleComponent>(mindId))
_roles.MindRemoveRole<SiliconBrainRoleComponent>(mindId);
_mind.TransferTo(mindId, uid, true, mind: mind);
}
_appearance.SetData(linked, MMIVisuals.BrainPresent, false);
}
}

View File

@@ -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;
/// <inheritdoc/>
public sealed partial class BorgSystem
{
public void InitializeModules()
{
SubscribeLocalEvent<BorgModuleComponent, EntGotInsertedIntoContainerMessage>(OnModuleGotInserted);
SubscribeLocalEvent<BorgModuleComponent, EntGotRemovedFromContainerMessage>(OnModuleGotRemoved);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleInstalledEvent>(OnSelectableInstalled);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleUninstalledEvent>(OnSelectableUninstalled);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleActionSelectedEvent>(OnSelectableAction);
SubscribeLocalEvent<ItemBorgModuleComponent, ComponentStartup>(OnProvideItemStartup);
SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleSelectedEvent>(OnItemModuleSelected);
SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleUnselectedEvent>(OnItemModuleUnselected);
}
private void OnModuleGotInserted(EntityUid uid, BorgModuleComponent component, EntGotInsertedIntoContainerMessage args)
{
var chassis = args.Container.Owner;
if (!TryComp<BorgChassisComponent>(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<BorgChassisComponent>(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<Container>(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<BorgModuleIconComponent>(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<BorgChassisComponent>(chassis, out var chassisComp))
return;
var selected = chassisComp.SelectedModule;
args.Handled = true;
UnselectModule(chassis, chassisComp);
if (selected != uid)
{
SelectModule(chassis, uid, chassisComp, component);
}
}
/// <summary>
/// Selects a module, enabling the borg to use its provided abilities.
/// </summary>
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<SelectableBorgModuleComponent>(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);
}
/// <summary>
/// Unselects a module, removing its provided abilities
/// </summary>
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<HandsComponent>(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<UnremoveableComponent>(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<HandsComponent>(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<UnremoveableComponent>(held.Value);
_container.Insert(held.Value, container);
component.StoredItems[handId] = held.Value;
}
else
{
component.StoredItems.Remove(handId);
}
_hands.RemoveHand(chassis, handId);
}
Dirty(uid, component);
}
/// <summary>
/// Checks if a given module can be inserted into a borg
/// </summary>
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<ItemBorgModuleComponent>(module, out var itemModuleComp))
{
foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities)
{
if (!TryComp<ItemBorgModuleComponent>(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;
}
/// <summary>
/// Check if a module can be removed from a borg.
/// </summary>
/// <param name="borg">The borg that the module is being removed from.</param>
/// <param name="module">The module to remove from the borg.</param>
/// <param name="user">The user attempting to remove the module.</param>
/// <returns>True if the module can be removed.</returns>
public bool CanRemoveModule(
Entity<BorgChassisComponent> borg,
Entity<BorgModuleComponent> module,
EntityUid? user = null)
{
if (module.Comp.DefaultModule)
return false;
return true;
}
/// <summary>
/// Installs and activates all modules currently inside the borg's module container
/// </summary>
public void InstallAllModules(EntityUid uid, BorgChassisComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var query = GetEntityQuery<BorgModuleComponent>();
foreach (var moduleEnt in new List<EntityUid>(component.ModuleContainer.ContainedEntities))
{
if (!query.TryGetComponent(moduleEnt, out var moduleComp))
continue;
InstallModule(uid, moduleEnt, component, moduleComp);
}
}
/// <summary>
/// Deactivates all modules currently inside the borg's module container
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
public void DisableAllModules(EntityUid uid, BorgChassisComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var query = GetEntityQuery<BorgModuleComponent>();
foreach (var moduleEnt in new List<EntityUid>(component.ModuleContainer.ContainedEntities))
{
if (!query.TryGetComponent(moduleEnt, out var moduleComp))
continue;
UninstallModule(uid, moduleEnt, component, moduleComp);
}
}
/// <summary>
/// Installs a single module into a borg.
/// </summary>
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);
}
/// <summary>
/// Uninstalls a single module from a borg.
/// </summary>
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);
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.MaxModules"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="maxModules">The new max module count.</param>
public void SetMaxModules(Entity<BorgChassisComponent> ent, int maxModules)
{
ent.Comp.MaxModules = maxModules;
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.ModuleWhitelist"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="whitelist">The new module whitelist.</param>
public void SetModuleWhitelist(Entity<BorgChassisComponent> ent, EntityWhitelist? whitelist)
{
ent.Comp.ModuleWhitelist = whitelist;
}
}

View File

@@ -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;
/// <inheritdoc/>
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<BorgTransponderComponent, DeviceNetworkPacketEvent>(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;
}

View File

@@ -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;
/// <inheritdoc/>
public sealed partial class BorgSystem
{
// CCvar.
private int _maxNameLength;
public void InitializeUI()
{
SubscribeLocalEvent<BorgChassisComponent, BeforeActivatableUIOpenEvent>(OnBeforeBorgUiOpen);
SubscribeLocalEvent<BorgChassisComponent, BorgEjectBrainBuiMessage>(OnEjectBrainBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgEjectBatteryBuiMessage>(OnEjectBatteryBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgSetNameBuiMessage>(OnSetNameBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgRemoveModuleBuiMessage>(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<BorgModuleComponent>(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<BorgChassisComponent> 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<BorgChassisComponent> 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<BorgChassisComponent>();
while (query.MoveNext(out var uid, out var borgChassis))
{
if (curTime < borgChassis.NextBatteryUpdate)
continue;
UpdateBattery((uid, borgChassis));
borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
}
}
}

View File

@@ -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;
/// <inheritdoc/>
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<JobPrototype> BorgJobId = "Borg";
@@ -71,264 +39,10 @@ public sealed partial class BorgSystem : SharedBorgSystem
{
base.Initialize();
SubscribeLocalEvent<BorgChassisComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BorgChassisComponent, AfterInteractUsingEvent>(OnChassisInteractUsing);
SubscribeLocalEvent<BorgChassisComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<BorgChassisComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
SubscribeLocalEvent<BorgChassisComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(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<BorgBrainComponent>(used, out var brain);
TryComp<BorgModuleComponent>(used, out var module);
if (TryComp<WiresPanelComponent>(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);
}
}
/// <summary>
/// Inserts a new module into a borg, the same as if a player inserted it manually.
/// </summary>
/// <para>
/// This does not run checks to see if the borg is actually allowed to be inserted, such as whitelists.
/// </para>
/// <param name="ent">The borg to insert into.</param>
/// <param name="module">The module to insert.</param>
public void InsertModule(Entity<BorgChassisComponent> 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<BorgBrainComponent>(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<BorgBrainComponent>(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<BorgChassisComponent> ent, ref PowerCellChangedEvent args)
{
UpdateBattery(ent);
}
private void OnBatteryChargeChanged(Entity<BorgChassisComponent> 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<BorgChassisComponent> 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<BorgChassisComponent>(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<EntityUid>? ents)
{
ents = null;
if (!TryComp<PowerCellSlotComponent>(uid, out var slotComp) ||
!Container.TryGetContainer(uid, slotComp.CellSlotId, out var container) ||
!container.ContainedEntities.Any())
return false;
ents = Container.EmptyContainer(container);
return true;
}
/// <summary>
/// Activates a borg when a player occupies it
/// </summary>
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);
}
/// <summary>
/// Deactivates a borg when a player leaves it.
/// </summary>
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);
}
/// <summary>
/// Checks that a player has fulfilled the requirements for the borg job.
/// </summary>
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);
}
}

View File

@@ -233,6 +233,9 @@ namespace Content.Shared.Interaction
/// </summary>
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<UnremoveableComponent>(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<UnremoveableComponent>(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<UnremoveableComponent>(uid);
else

View File

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

View File

@@ -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<HandheldLightComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<HandheldLightComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldLightComponent, GetVerbsEvent<ActivationVerb>>(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))

View File

@@ -8,6 +8,23 @@ namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
/// <summary>
/// Checks if a power cell slot has a battery inside.
/// </summary>
[PublicAPI]
public bool HasBattery(Entity<PowerCellSlotComponent?> 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;
}
/// <summary>
/// Gets the power cell battery inside a power cell slot.
/// </summary>
@@ -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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ent">The entity with the power cell slot.</param>
/// <param name="battery">The power cell that was ejected.</param>
/// <param name="user">The player trying to eject the power cell from the slot.</param>
/// <returns>If a power cell was sucessfully ejected.</returns>
[PublicAPI]
public bool TryEjectBatteryFromSlot(
Entity<PowerCellSlotComponent?> 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;
}
/// <summary>
/// Returns whether the entity has a slotted battery and charge for the requested action.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="ent">The entity with the power cell slot.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
@@ -119,7 +165,7 @@ public sealed partial class PowerCellSystem
/// <summary>
/// Tries to use charge from a slotted battery.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="ent">The entity with the power cell slot.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
@@ -157,7 +203,7 @@ public sealed partial class PowerCellSystem
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="ent">The entity with the power cell slot.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetRemainingUses(Entity<PowerCellSlotComponent?> ent, float cost)
@@ -171,7 +217,7 @@ public sealed partial class PowerCellSystem
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="ent">The entity with the power cell slot.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetMaxUses(Entity<PowerCellSlotComponent?> ent, float cost)

View File

@@ -8,50 +8,38 @@ public enum BorgUiKey : byte
Key
}
/// <summary>
/// Send when a player uses the borg BUI to eject a brain.
/// </summary>
[Serializable, NetSerializable]
public sealed class BorgBuiState : BoundUserInterfaceState
public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage;
/// <summary>
/// Send when a player uses the borg BUI to eject a power cell.
/// </summary>
[Serializable, NetSerializable]
public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage;
/// <summary>
/// Send when a player uses the borg BUI to change a borg's name.
/// </summary>
[Serializable, NetSerializable]
public sealed class BorgSetNameBuiMessage(string name) : BoundUserInterfaceMessage
{
public float ChargePercent;
public bool HasBattery;
public BorgBuiState(float chargePercent, bool hasBattery)
{
ChargePercent = chargePercent;
HasBattery = hasBattery;
}
/// <summary>
/// The new name.
/// </summary>
public string Name = name;
}
/// <summary>
/// Send when a player uses the borg BUI to remove a borg module.
/// </summary>
[Serializable, NetSerializable]
public sealed class BorgEjectBrainBuiMessage : BoundUserInterfaceMessage
public sealed class BorgRemoveModuleBuiMessage(NetEntity module) : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class BorgEjectBatteryBuiMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class BorgSetNameBuiMessage : BoundUserInterfaceMessage
{
public string Name;
public BorgSetNameBuiMessage(string name)
{
Name = name;
}
}
[Serializable, NetSerializable]
public sealed class BorgRemoveModuleBuiMessage : BoundUserInterfaceMessage
{
public NetEntity Module;
public BorgRemoveModuleBuiMessage(NetEntity module)
{
Module = module;
}
/// <summary>
/// The module to eject.
/// </summary>
public NetEntity Module = module;
}

View File

@@ -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.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[AutoGenerateComponentState, AutoGenerateComponentPause]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedBorgSystem))]
public sealed partial class BorgChassisComponent : Component
{
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, AutoNetworkedField]
public bool Active;
/// <summary>
/// The sound to play when the borg activates.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public SoundSpecifier ActivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_on.ogg");
/// <summary>
/// The sound to play when the borg deactivates.
/// </summary>
[DataField]
public SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Items/flashlight_off.ogg");
#region Brain
/// <summary>
/// A whitelist for which entities count as valid brains
/// A whitelist for which entities count as valid brains.
/// </summary>
[DataField("brainWhitelist")]
[DataField]
public EntityWhitelist? BrainWhitelist;
/// <summary>
/// The container ID for the brain
/// The container ID for the posibrain or MMI.
/// </summary>
[DataField("brainContainerId")]
[DataField]
public string BrainContainerId = "borg_brain";
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// The container for the posibrain or MMI.
/// </summary>
[ViewVariables]
public ContainerSlot BrainContainer = default!;
public EntityUid? BrainEntity => BrainContainer.ContainedEntity;
/// <summary>
/// The posibrain or MMI inserted into this borg, if any.
/// </summary>
[ViewVariables]
public EntityUid? BrainEntity => BrainContainer?.ContainedEntity;
#endregion
#region Modules
/// <summary>
/// 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.
/// </summary>
[DataField("moduleWhitelist")]
[DataField, AutoNetworkedField]
public EntityWhitelist? ModuleWhitelist;
/// <summary>
/// How many modules can be installed in this borg
/// How many modules can be installed in this borg?
/// </summary>
[DataField("maxModules"), ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public int MaxModules = 3;
/// <summary>
/// The ID for the module container
/// The ID for the module container.
/// </summary>
[DataField("moduleContainerId")]
[DataField]
public string ModuleContainerId = "borg_module";
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// The module container.
/// </summary>
[ViewVariables]
public Container ModuleContainer = default!;
/// <summary>
/// How many modules are currently installed?
/// </summary>
[ViewVariables]
public int ModuleCount => ModuleContainer.ContainedEntities.Count;
#endregion
/// <summary>
/// The currently selected module
/// The currently selected module.
/// </summary>
[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
/// <summary>
/// The battery charge alert.
/// </summary>
[DataField]
public ProtoId<AlertPrototype> BatteryAlert = "BorgBattery";
/// <summary>
/// The alert for a missing battery.
/// </summary>
[DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
/// <summary>
/// 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.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
[AutoNetworkedField, AutoPausedField]
public TimeSpan NextBatteryUpdate = TimeSpan.Zero;
/// <summary>
@@ -99,7 +152,8 @@ public sealed partial class BorgChassisComponent : Component
[Serializable, NetSerializable]
public enum BorgVisuals : byte
{
HasPlayer
HasPlayer,
Powered,
}
[Serializable, NetSerializable]

View File

@@ -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.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedBorgSystem))]
public sealed partial class BorgModuleComponent : Component
{
/// <summary>
/// The entity this module is installed into
/// The entity this module is installed into.
/// </summary>
[DataField("installedEntity")]
[DataField, AutoNetworkedField]
public EntityUid? InstalledEntity;
/// <summary>
/// Is this module currently installed?
/// </summary>
[ViewVariables]
public bool Installed => InstalledEntity != null;
/// <summary>
/// If true, this is a "default" module that cannot be removed from a borg.
/// </summary>
[DataField]
[AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool DefaultModule;
/// <summary>
@@ -37,13 +39,13 @@ public sealed partial class BorgModuleComponent : Component
/// <summary>
/// Raised on a module when it is installed in order to add specific behavior to an entity.
/// </summary>
/// <param name="ChassisEnt"></param>
/// <param name="ChassisEnt">The borg the module is being installed in.</param>
[ByRefEvent]
public readonly record struct BorgModuleInstalledEvent(EntityUid ChassisEnt);
/// <summary>
/// Raised on a module when it's uninstalled in order to
/// </summary>
/// <param name="ChassisEnt"></param>
/// <param name="ChassisEnt">The borg the module is being uninstalled from.</param>
[ByRefEvent]
public readonly record struct BorgModuleUninstalledEvent(EntityUid ChassisEnt);

View File

@@ -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;
/// <summary>
/// This is used for a <see cref="BorgModuleComponent"/> that provides items to the entity it's installed into.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedBorgSystem))]
public sealed partial class ItemBorgModuleComponent : Component
{
/// <summary>
@@ -19,10 +19,17 @@ public sealed partial class ItemBorgModuleComponent : Component
public List<BorgHand> Hands = new();
/// <summary>
/// The items stored within the hands. Null until the first time items are stored.
/// The items stored within the hands.
/// </summary>
[DataField]
public Dictionary<string, EntityUid>? StoredItems;
[DataField, AutoNetworkedField]
public Dictionary<string, EntityUid> StoredItems = new();
/// <summary>
/// Whether the provided items have been spawned.
/// This happens the first time the module is used.
/// </summary>
[DataField, AutoNetworkedField]
public bool Spawned;
/// <summary>
/// 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";
}
/// <summary>
/// A single hand provided by the module.
/// </summary>
[DataDefinition, Serializable, NetSerializable]
public partial record struct BorgHand
{
/// <summary>
/// The item to spawn in the hand, if any.
/// </summary>
[DataField]
public EntProtoId? Item;
/// <summary>
/// The settings for the hand, including a whitelist.
/// </summary>
[DataField]
public Hand Hand = new();

View File

@@ -9,37 +9,38 @@ namespace Content.Shared.Silicons.Borgs.Components;
/// in an item slot before transferring consciousness.
/// Used for borg stuff.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedBorgSystem))]
public sealed partial class MMIComponent : Component
{
/// <summary>
/// The ID of the itemslot that holds the brain.
/// </summary>
[DataField("brainSlotId")]
[DataField]
public string BrainSlotId = "brain_slot";
/// <summary>
/// The <see cref="ItemSlot"/> for this implanter
/// The <see cref="ItemSlot"/> for this MMI. Holds the brain.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ItemSlot BrainSlot = default!;
[DataField(required: true)]
public ItemSlot BrainSlot = new();
/// <summary>
/// The sprite state when the brain inserted has a mind.
/// </summary>
[DataField("hasMindState")]
[DataField]
public string HasMindState = "mmi_alive";
/// <summary>
/// The sprite state when the brain inserted doesn't have a mind.
/// </summary>
[DataField("noMindState")]
[DataField]
public string NoMindState = "mmi_dead";
/// <summary>
/// The sprite state when there is no brain inserted.
/// </summary>
[DataField("noBrainState")]
[DataField]
public string NoBrainState = "mmi_off";
}

View File

@@ -3,7 +3,7 @@
namespace Content.Shared.Silicons.Borgs.Components;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
@@ -13,6 +13,6 @@ public sealed partial class MMILinkedComponent : Component
/// <summary>
/// The MMI this entity is linked to.
/// </summary>
[DataField("linkedMMI"), AutoNetworkedField]
[DataField, AutoNetworkedField]
public EntityUid? LinkedMMI;
}

View File

@@ -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;
/// <summary>
/// This is used for <see cref="BorgModuleComponent"/>s that can be "swapped" to, as opposed to having passive effects.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedBorgSystem))]
public sealed partial class SelectableBorgModuleComponent : Component
{
[DataField("moduleSwapAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? ModuleSwapActionId = "ActionBorgSwapModule";
/// <summary>
/// The sidebar action prototype for swapping to this module.
/// </summary>
[DataField]
public EntProtoId ModuleSwapAction = "ActionBorgSwapModule";
/// <summary>
/// The sidebar action for swapping to this module.
/// The sidebar action entity for swapping to this module.
/// </summary>
[DataField("moduleSwapActionEntity")] public EntityUid? ModuleSwapActionEntity;
[DataField, AutoNetworkedField]
public EntityUid? ModuleSwapActionEntity;
}
public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent
{
}
/// <summary>
/// Raised on a borg module entity with <see cref="SelectableBorgModuleComponent"/> when a player uses the action provided by it.
/// </summary>
public sealed partial class BorgModuleActionSelectedEvent : InstantActionEvent;
/// <summary>
/// Event raised by-ref on a module when it is selected

View File

@@ -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
{
/// <summary>
/// Can this borg currently activate it's <see cref="ItemToggleComponent"/>?
/// 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).
/// </summary>
public bool CanActivate(Entity<BorgChassisComponent> 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;
}
/// <summary>
/// Activates the borg if the conditions are met.
/// Returns true if the borg was activated.
/// </summary>
public bool TryActivate(Entity<BorgChassisComponent> chassis, EntityUid? user = null)
{
if (chassis.Comp.Active)
return false; // Already active.
if (CanActivate(chassis))
{
SetActive(chassis, true, user);
return true;
}
return false;
}
/// <summary>
/// Activates or deactivates a borg.
/// If active the borg
/// - has door access,
/// - can use modules and
/// - has full movement speed.
/// </summary>
public void SetActive(Entity<BorgChassisComponent> 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ent">The borg to insert into.</param>
/// <param name="module">The module to insert.</param>
public void InsertModule(Entity<BorgChassisComponent> ent, EntityUid module)
{
_container.Insert(module, ent.Comp.ModuleContainer);
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.ModuleWhitelist"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="whitelist">The new module whitelist.</param>
public void SetModuleWhitelist(Entity<BorgChassisComponent> ent, EntityWhitelist? whitelist)
{
ent.Comp.ModuleWhitelist = whitelist;
Dirty(ent);
}
/// <summary>
/// Sets <see cref="BorgChassisComponent.MaxModules"/>.
/// </summary>
/// <param name="ent">The borg to modify.</param>
/// <param name="maxModules">The new max module count.</param>
public void SetMaxModules(Entity<BorgChassisComponent> ent, int maxModules)
{
ent.Comp.MaxModules = maxModules;
Dirty(ent);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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?
/// </remarks>
public virtual bool CanPlayerBeBorged(ICommonSession session)
{
return true;
}
/// <summary>
/// Installs a single module into a borg.
/// </summary>
public void InstallModule(Entity<BorgChassisComponent?> borg, Entity<BorgModuleComponent?> 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);
}
/// <summary>
/// Uninstalls a single module from a borg.
/// </summary>
public void UninstallModule(Entity<BorgChassisComponent?> borg, Entity<BorgModuleComponent?> 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);
}
/// <summary>
/// Installs and activates all modules currently inside the borg's module container.
/// </summary>
public void InstallAllModules(Entity<BorgChassisComponent?> borg)
{
if (!Resolve(borg, ref borg.Comp))
return;
foreach (var moduleEnt in new List<EntityUid>(borg.Comp.ModuleContainer.ContainedEntities))
{
if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp))
continue;
InstallModule(borg, (moduleEnt, moduleComp));
}
}
/// <summary>
/// Deactivates all modules currently inside the borg's module container.
/// </summary>
public void DisableAllModules(Entity<BorgChassisComponent?> borg)
{
if (!Resolve(borg, ref borg.Comp))
return;
foreach (var moduleEnt in new List<EntityUid>(borg.Comp.ModuleContainer.ContainedEntities))
{
if (!_moduleQuery.TryGetComponent(moduleEnt, out var moduleComp))
continue;
UninstallModule(borg, (moduleEnt, moduleComp));
}
}
/// <summary>
/// Sets <see cref="BorgModuleComponent.DefaultModule"/>.
/// </summary>
public void SetBorgModuleDefault(Entity<BorgModuleComponent> ent, bool newDefault)
{
ent.Comp.DefaultModule = newDefault;
Dirty(ent);
}
/// <summary>
/// Checks if a given module can be inserted into a borg.
/// </summary>
public bool CanInsertModule(Entity<BorgChassisComponent?> chassis, Entity<BorgModuleComponent?> 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<ItemBorgModuleComponent>(module, out var itemModuleComp))
{
foreach (var containedModuleUid in chassis.Comp.ModuleContainer.ContainedEntities)
{
if (!TryComp<ItemBorgModuleComponent>(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;
}
/// <summary>
/// Check if a module can be removed from a borg.
/// </summary>
/// <param name="module">The module to remove from the borg.</param>
/// <returns>True if the module can be removed.</returns>
public bool CanRemoveModule(Entity<BorgModuleComponent> module)
{
if (module.Comp.DefaultModule)
return false;
return true;
}
/// <summary>
/// Selects a module, enabling the borg to use its provided abilities.
/// </summary>
public void SelectModule(Entity<BorgChassisComponent?> chassis,
Entity<BorgModuleComponent?> 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<SelectableBorgModuleComponent>(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);
}
/// <summary>
/// Unselects a module, removing its provided abilities.
/// </summary>
public void UnselectModule(Entity<BorgChassisComponent?> 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);
}
}

View File

@@ -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<MMIComponent, ComponentInit>(OnMMIInit);
SubscribeLocalEvent<MMIComponent, EntInsertedIntoContainerMessage>(OnMMIEntityInserted);
SubscribeLocalEvent<MMIComponent, MindAddedMessage>(OnMMIMindAdded);
SubscribeLocalEvent<MMIComponent, MindRemovedMessage>(OnMMIMindRemoved);
SubscribeLocalEvent<MMILinkedComponent, MindAddedMessage>(OnMMILinkedMindAdded);
SubscribeLocalEvent<MMILinkedComponent, EntGotRemovedFromContainerMessage>(OnMMILinkedRemoved);
}
private void OnMMIInit(Entity<MMIComponent> ent, ref ComponentInit args)
{
_itemSlots.AddItemSlot(ent.Owner, ent.Comp.BrainSlotId, ent.Comp.BrainSlot);
}
private void OnMMIEntityInserted(Entity<MMIComponent> 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<MMILinkedComponent>(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<SiliconBrainRoleComponent>(mindId))
_roles.MindAddRole(mindId, SiliconBrainRole, silent: true);
}
_appearance.SetData(ent.Owner, MMIVisuals.BrainPresent, true);
}
private void OnMMIMindAdded(Entity<MMIComponent> ent, ref MindAddedMessage args)
{
_appearance.SetData(ent.Owner, MMIVisuals.HasMind, true);
}
private void OnMMIMindRemoved(Entity<MMIComponent> ent, ref MindRemovedMessage args)
{
_appearance.SetData(ent.Owner, MMIVisuals.HasMind, false);
}
private void OnMMILinkedMindAdded(Entity<MMILinkedComponent> 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<MMILinkedComponent> 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<MMILinkedComponent>(ent.Owner);
if (_mind.TryGetMind(linked, out var mindId, out var mindComp))
{
if (_roles.MindHasRole<SiliconBrainRoleComponent>(mindId))
_roles.MindRemoveRole<SiliconBrainRoleComponent>(mindId);
_mind.TransferTo(mindId, ent.Owner, true, mind: mindComp);
}
_appearance.SetData(linked, MMIVisuals.BrainPresent, false);
}
}

View File

@@ -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<BorgModuleComponent> _moduleQuery;
public void InitializeModule()
{
SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
SubscribeLocalEvent<BorgModuleComponent, EntGotInsertedIntoContainerMessage>(OnModuleGotInserted);
SubscribeLocalEvent<BorgModuleComponent, EntGotRemovedFromContainerMessage>(OnModuleGotRemoved);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleInstalledEvent>(OnSelectableInstalled);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleUninstalledEvent>(OnSelectableUninstalled);
SubscribeLocalEvent<SelectableBorgModuleComponent, BorgModuleActionSelectedEvent>(OnSelectableAction);
SubscribeLocalEvent<ItemBorgModuleComponent, ComponentStartup>(OnProvideItemStartup);
SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleSelectedEvent>(OnItemModuleSelected);
SubscribeLocalEvent<ItemBorgModuleComponent, BorgModuleUnselectedEvent>(OnItemModuleUnselected);
_moduleQuery = GetEntityQuery<BorgModuleComponent>();
}
private void OnModuleExamine(Entity<BorgModuleComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.BorgFitTypes == null)
return;
if (ent.Comp.BorgFitTypes.Count == 0)
return;
var typeList = new List<string>();
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<BorgModuleComponent> 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<BorgChassisComponent>(chassis, out var chassisComp) ||
args.Container != chassisComp.ModuleContainer ||
!chassisComp.Active)
return;
InstallModule((chassis, chassisComp), module.AsNullable());
}
private void OnModuleGotRemoved(Entity<BorgModuleComponent> 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<BorgChassisComponent>(chassis, out var chassisComp) ||
args.Container != chassisComp.ModuleContainer)
return;
UninstallModule((chassis, chassisComp), module.AsNullable());
}
private void OnSelectableInstalled(Entity<SelectableBorgModuleComponent> 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<BorgModuleIconComponent>(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<BorgChassisComponent>(chassis, out var chassisComp))
return;
if (chassisComp.SelectedModule == null)
SelectModule((chassis, chassisComp), module.Owner);
}
private void OnSelectableUninstalled(Entity<SelectableBorgModuleComponent> module, ref BorgModuleUninstalledEvent args)
{
var chassis = args.ChassisEnt;
_actions.RemoveProvidedActions(chassis, module.Owner);
if (!TryComp<BorgChassisComponent>(chassis, out var chassisComp))
return;
if (chassisComp.SelectedModule == module.Owner)
UnselectModule((chassis, chassisComp));
}
private void OnSelectableAction(Entity<SelectableBorgModuleComponent> module, ref BorgModuleActionSelectedEvent args)
{
var chassis = args.Performer;
if (!TryComp<BorgChassisComponent>(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<ItemBorgModuleComponent> module, ref ComponentStartup args)
{
_container.EnsureContainer<Container>(module.Owner, module.Comp.HoldingContainer);
}
private void OnItemModuleSelected(Entity<ItemBorgModuleComponent> module, ref BorgModuleSelectedEvent args)
{
ProvideItems(args.Chassis, module.AsNullable());
}
private void OnItemModuleUnselected(Entity<ItemBorgModuleComponent> module, ref BorgModuleUnselectedEvent args)
{
RemoveProvidedItems(args.Chassis, module.AsNullable());
}
private void ProvideItems(Entity<BorgChassisComponent?> chassis, Entity<ItemBorgModuleComponent?> module)
{
if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp))
return;
if (!TryComp<HandsComponent>(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<UnremoveableComponent>(pickUp);
}
}
}
module.Comp.Spawned = true;
Dirty(module);
}
private void RemoveProvidedItems(Entity<BorgChassisComponent?> chassis, Entity<ItemBorgModuleComponent?> module)
{
if (!Resolve(chassis, ref chassis.Comp) || !Resolve(module, ref module.Comp))
return;
if (!TryComp<HandsComponent>(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<UnremoveableComponent>(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);
}
}

View File

@@ -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<BorgChassisComponent, BorgEjectBrainBuiMessage>(OnEjectBrainBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgEjectBatteryBuiMessage>(OnEjectBatteryBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgSetNameBuiMessage>(OnSetNameBuiMessage);
SubscribeLocalEvent<BorgChassisComponent, BorgRemoveModuleBuiMessage>(OnRemoveModuleBuiMessage);
Subs.CVar(_configuration, CCVars.MaxNameLength, value => _maxNameLength = value, true);
}
public virtual void UpdateUI(Entity<BorgChassisComponent?> chassis) { }
private void OnEjectBrainBuiMessage(Entity<BorgChassisComponent> 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<BorgChassisComponent> chassis, ref BorgEjectBatteryBuiMessage args)
{
if (_powerCell.TryEjectBatteryFromSlot(chassis.Owner, out var powerCell, args.Actor))
_hands.TryPickupAnyHand(args.Actor, powerCell.Value);
}
private void OnSetNameBuiMessage(Entity<BorgChassisComponent> 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<BorgChassisComponent> chassis, ref BorgRemoveModuleBuiMessage args)
{
var module = GetEntity(args.Module);
if (!chassis.Comp.ModuleContainer.Contains(module))
return;
if (!CanRemoveModule((module, Comp<BorgModuleComponent>(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);
}
}

View File

@@ -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;
/// </summary>
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!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
InitializeMMI();
InitializeModule();
InitializeRelay();
InitializeUI();
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
SubscribeLocalEvent<BorgChassisComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<BorgChassisComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BorgChassisComponent, ItemSlotInsertAttemptEvent>(OnItemSlotInsertAttempt);
SubscribeLocalEvent<BorgChassisComponent, ItemSlotEjectAttemptEvent>(OnItemSlotEjectAttempt);
SubscribeLocalEvent<BorgChassisComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<BorgChassisComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<BorgChassisComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<BorgChassisComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<BorgChassisComponent, AfterInteractUsingEvent>(OnChassisInteractUsing);
SubscribeLocalEvent<BorgChassisComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
SubscribeLocalEvent<BorgChassisComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
SubscribeLocalEvent<BorgModuleComponent, ExaminedEvent>(OnModuleExamine);
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
InitializeRelay();
}
SubscribeLocalEvent<BorgBrainComponent, MindAddedMessage>(OnBrainMindAdded);
SubscribeLocalEvent<BorgBrainComponent, PointAttemptEvent>(OnBrainPointAttempt);
private void OnModuleExamine(Entity<BorgModuleComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.BorgFitTypes == null)
return;
if (ent.Comp.BorgFitTypes.Count == 0)
return;
var typeList = new List<string>();
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<BorgChassisComponent>(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<BorgChassisComponent> chassis, ref ComponentStartup args)
{
if (!TryComp<ContainerManagerComponent>(chassis, out var containerManager))
return;
chassis.Comp.BrainContainer = _container.EnsureContainer<ContainerSlot>(chassis.Owner, chassis.Comp.BrainContainerId, containerManager);
chassis.Comp.ModuleContainer = _container.EnsureContainer<Container>(chassis.Owner, chassis.Comp.ModuleContainerId, containerManager);
}
private void OnMapInit(Entity<BorgChassisComponent> chassis, ref MapInitEvent args)
{
_movementSpeedModifier.RefreshMovementSpeedModifiers(chassis.Owner);
}
private void OnItemSlotInsertAttempt(Entity<BorgChassisComponent> chassis, ref ItemSlotInsertAttemptEvent args)
{
if (args.Cancelled)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlotComp) ||
!TryComp<WiresPanelComponent>(uid, out var panel))
if (!TryComp<PowerCellSlotComponent>(chassis, out var cellSlotComp) ||
!TryComp<WiresPanelComponent>(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<BorgChassisComponent> chassis, ref ItemSlotEjectAttemptEvent args)
{
if (args.Cancelled)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlotComp) ||
!TryComp<WiresPanelComponent>(uid, out var panel))
if (!TryComp<PowerCellSlotComponent>(chassis, out var cellSlotComp) ||
!TryComp<WiresPanelComponent>(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<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
{
if (!TryComp<ContainerManagerComponent>(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<ContainerSlot>(uid, component.BrainContainerId, containerManager);
component.ModuleContainer = Container.EnsureContainer<Container>(uid, component.ModuleContainerId, containerManager);
if (HasComp<BorgBrainComponent>(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<BorgChassisComponent> 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
protected virtual void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
{
}
protected virtual void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
{
}
private void OnRefreshMovementSpeedModifiers(EntityUid uid, BorgChassisComponent component, RefreshMovementSpeedModifiersEvent args)
{
if (Toggle.IsActivated(uid))
if (args.Container != chassis.Comp.BrainContainer)
return;
if (!TryComp<MovementSpeedModifierComponent>(uid, out var movement))
if (HasComp<BorgBrainComponent>(args.Entity) && _mind.TryGetMind(chassis.Owner, out var mindId, out var mind))
{
_mind.TransferTo(mindId, args.Entity, mind: mind);
}
}
private void OnMindAdded(Entity<BorgChassisComponent> 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<BorgChassisComponent> 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<HandheldLightComponent>(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);
}
private void OnChassisInteractUsing(Entity<BorgChassisComponent> chassis, ref AfterInteractUsingEvent args)
{
if (!args.CanReach || args.Handled || chassis.Owner == args.User)
return;
var used = args.Used;
TryComp<BorgBrainComponent>(used, out var brain);
TryComp<BorgModuleComponent>(used, out var module);
if (TryComp<WiresPanelComponent>(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<ActorComponent>(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;
}
}
// Make the borg slower without power.
private void OnRefreshMovementSpeedModifiers(Entity<BorgChassisComponent> chassis, ref RefreshMovementSpeedModifiersEvent args)
{
if (chassis.Comp.Active)
return;
if (!TryComp<MovementSpeedModifierComponent>(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);
}
/// <summary>
/// Sets <see cref="BorgModuleComponent.DefaultModule"/>.
/// </summary>
public void SetBorgModuleDefault(Entity<BorgModuleComponent> ent, bool newDefault)
private void OnUIOpenAttempt(Entity<BorgChassisComponent> 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<BorgChassisComponent> 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<BorgChassisComponent> 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<PowerCellSlotComponent>(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<BorgChassisComponent> chassis, ref GetCharactedDeadIcEvent args)
{
args.Dead = true;
}
private void OnGetUnrevivableIC(Entity<BorgChassisComponent> chassis, ref GetCharacterUnrevivableIcEvent args)
{
args.Unrevivable = true;
}
private void OnBrainMindAdded(Entity<BorgBrainComponent> brain, ref MindAddedMessage args)
{
if (!_container.TryGetContainingContainer(brain.Owner, out var container))
return;
var borg = container.Owner;
if (!TryComp<BorgChassisComponent>(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<BorgBrainComponent> brain, ref PointAttemptEvent args)
{
args.Cancel();
}
// Raised when the power cell is empty or removed from the borg.
private void OnPowerCellSlotEmpty(Entity<BorgChassisComponent> chassis, ref PowerCellSlotEmptyEvent args)
{
SetActive(chassis, false);
}
// Raised when a power cell is inserted.
private void OnPowerCellChanged(Entity<BorgChassisComponent> chassis, ref PowerCellChangedEvent args)
{
if (CanActivate(chassis))
SetActive(chassis, true);
}
public override void Update(float frameTime)
{
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<BorgChassisComponent>();
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);
}
}
}

View File

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

View File

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

View File

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