Predict CartridgeLoader (#40302)

* Move CartridgeLoader to shared

* get containered bozo

* the dirtening

* mass shared the cartridges

* predict keb cartridges

* no overpopups

* the tidying continues

* review

* aaaaaa

* fix UI flickers

* dependency me

* mimed

* refactor: removed unused usings, dots at the end of old xml-doc,  additional in line-breaks

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
This commit is contained in:
pathetic meowmeow 2026-07-05 10:13:20 -04:00 • committed by GitHub
parent f563a6a2ce
commit 09fb1b923f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 904 additions and 1046 deletions

View file

@ -1,6 +1,5 @@
using Content.Client.UserInterface.Fragments;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader;
@ -67,7 +66,7 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
protected void ActivateCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Activate);
SendMessage(message);
SendPredictedMessage(message);
}
protected void DeactivateActiveCartridge()
@ -76,19 +75,19 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
return;
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(_activeProgram.Value), CartridgeUiMessageAction.Deactivate);
SendMessage(message);
SendPredictedMessage(message);
}
protected void InstallCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Install);
SendMessage(message);
SendPredictedMessage(message);
}
protected void UninstallCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(_entManager.GetNetEntity(cartridgeUid), CartridgeUiMessageAction.Uninstall);
SendMessage(message);
SendPredictedMessage(message);
}
private List<(EntityUid, CartridgeComponent)> GetCartridgeComponents(List<EntityUid> programs)

View file

@ -1,8 +0,0 @@
using Content.Shared.CartridgeLoader;
namespace Content.Client.CartridgeLoader;
public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
//Empty client system for component replication
}

View file

@ -22,7 +22,7 @@ public sealed partial class LogProbeUi : UIFragment
{
var ev = new LogProbePrintMessage();
var message = new CartridgeUiMessage(ev);
ui.SendMessage(message);
ui.SendPredictedMessage(message);
};
}

View file

@ -1,5 +0,0 @@
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;

View file

@ -1,8 +1,6 @@
using System.Linq;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
@ -44,7 +42,7 @@ public sealed partial class NanoTaskUi : UIFragment
if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
return;
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
description: task.Data.Description,
taskIsFor: task.Data.TaskIsFor,
isTaskDone: !task.Data.IsTaskDone,
@ -53,22 +51,22 @@ public sealed partial class NanoTaskUi : UIFragment
};
_popup.TaskSaved += (id, data) =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
_popup.Close();
};
_popup.TaskDeleted += id =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
_popup.Close();
};
_popup.TaskCreated += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
_popup.Close();
};
_popup.TaskPrinted += data =>
{
userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
userInterface.SendPredictedMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
};
}

View file

@ -1,7 +1,6 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
@ -34,6 +33,6 @@ public sealed partial class NotekeeperUi : UIFragment
{
var notekeeperMessage = new NotekeeperUiMessageEvent(action, note);
var message = new CartridgeUiMessage(notekeeperMessage);
userInterface.SendMessage(message);
userInterface.SendPredictedMessage(message);
}
}

View file

@ -48,12 +48,12 @@
</ContainerButton>
</BoxContainer>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="50 50"
Name="ProgramList"
Margin="4"/>
<GridContainer VerticalExpand="True"
HorizontalExpand="True"
MinSize="50 50"
Name="ProgramList"
Margin="4"
Columns="2" />
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical"

View file

@ -200,10 +200,10 @@ namespace Content.Client.PDA
public void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs)
{
ProgramList.RemoveAllChildren();
if (programs.Count == 0)
{
ProgramList.RemoveAllChildren();
ProgramList.AddChild(new Label()
{
Text = Loc.GetString("comp-pda-io-no-programs-available"),
@ -215,50 +215,43 @@ namespace Content.Client.PDA
return;
}
var row = CreateProgramListRow();
var itemCount = 1;
ProgramList.AddChild(row);
foreach (var (uid, component) in programs)
if (ProgramList.ChildCount >= 1 && ProgramList.Children[0] is Label label)
{
//Create a new row every second program item starting from the first
if (itemCount % 2 != 0)
label.Orphan();
}
while (ProgramList.ChildCount > programs.Count)
{
ProgramList.Children[ProgramList.ChildCount - 1].Orphan();
}
for (var i = 0; i < programs.Count; i++)
{
var cartridge = programs[i];
if (i < ProgramList.ChildCount)
{
row = CreateProgramListRow();
ProgramList.AddChild(row);
var currentItem = ProgramList.Children[i];
if (currentItem is PdaProgramItem programItem)
{
programItem.SetCartridge(cartridge);
continue;
}
DebugTools.Assert(i == ProgramList.ChildCount-1);
currentItem.Orphan();
}
var item = new PdaProgramItem();
if (component.Icon is not null)
item.Icon.SetFromSpriteSpecifier(component.Icon);
item.OnPressed += _ => OnProgramItemPressed?.Invoke(uid);
switch (component.InstallationStatus)
{
case InstallationStatus.Cartridge:
item.InstallButton.Visible = true;
item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
item.InstallButton.OnPressed += _ => OnInstallButtonPressed?.Invoke(uid);
break;
case InstallationStatus.Installed:
item.InstallButton.Visible = true;
item.InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
item.InstallButton.OnPressed += _ => OnUninstallButtonPressed?.Invoke(uid);
break;
}
item.ProgramName.Text = Loc.GetString(component.ProgramName);
item.SetHeight = 20;
row.AddChild(item);
itemCount++;
var item = new PdaProgramItem(cartridge);
item.OnProgramItemPressed += uid => OnProgramItemPressed?.Invoke(uid);
item.OnUninstallButtonPressed += uid => OnUninstallButtonPressed?.Invoke(uid);
item.OnInstallButtonPressed += uid => OnInstallButtonPressed?.Invoke(uid);
ProgramList.AddChild(item);
}
//Add a filler item to the last row when it only contains one item
if (itemCount % 2 == 0)
row.AddChild(new Control() { HorizontalExpand = true });
if (programs.Count % 2 == 0)
ProgramList.AddChild(new Control() { HorizontalExpand = true });
}
/// <summary>
@ -319,15 +312,6 @@ namespace Content.Client.PDA
_currentView = view;
}
private static BoxContainer CreateProgramListRow()
{
return new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true
};
}
private void HideAllViews()
{
var views = ViewContainer.Children;

View file

@ -1,17 +1,17 @@
<pda:PdaProgramItem HorizontalExpand="True" MinHeight="60" Margin="4"
xmlns:pda="clr-namespace:Content.Client.PDA"
xmlns="https://spacestation14.io">
<ContainerButton HorizontalExpand="True" MinHeight="60" Margin="4"
xmlns:pda="clr-namespace:Content.Client.PDA"
xmlns="https://spacestation14.io">
<PanelContainer Name="Panel"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True" MinWidth="60">
<AnimatedTextureRect HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Name="Icon" Access="Public"/>
<AnimatedTextureRect HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Name="Icon" />
</BoxContainer>
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<Label Name="ProgramName" Access="Public" VerticalExpand="True"/>
<Label Name="ProgramName" VerticalExpand="True" />
<BoxContainer HorizontalExpand="True" SetHeight="28" Margin="0 0 4 4">
<BoxContainer HorizontalExpand="True"/>
<Button Name="InstallButton" Access="Public" Visible="False" SetWidth="90" HorizontalAlignment="Right" VerticalExpand="True"></Button>
<BoxContainer HorizontalExpand="True" />
<Button Name="InstallButton" Visible="False" SetWidth="90" HorizontalAlignment="Right" VerticalExpand="True"></Button>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</pda:PdaProgramItem>
</ContainerButton>

View file

@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Content.Shared.CartridgeLoader;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@ -12,6 +13,12 @@ public sealed partial class PdaProgramItem : ContainerButton
public const string NormalBgColor = "#313138";
public const string HoverColor = "#3E6C45";
private Entity<CartridgeComponent> _cartridge;
public event Action<EntityUid>? OnProgramItemPressed;
public event Action<EntityUid>? OnUninstallButtonPressed;
public event Action<EntityUid>? OnInstallButtonPressed;
private readonly StyleBoxFlat _styleBox = new()
{
BackgroundColor = Color.FromHex("#25252a"),
@ -23,10 +30,38 @@ public sealed partial class PdaProgramItem : ContainerButton
set => _styleBox.BackgroundColor = value;
}
public PdaProgramItem()
public PdaProgramItem(Entity<CartridgeComponent> program)
{
RobustXamlLoader.Load(this);
SetCartridge(program);
Panel.PanelOverride = _styleBox;
OnPressed += OnSelfPressed;
InstallButton.OnPressed += OnInstallPressed;
}
private void OnSelfPressed(ButtonEventArgs args)
{
OnProgramItemPressed?.Invoke(_cartridge);
}
private void OnInstallPressed(ButtonEventArgs args)
{
switch (_cartridge.Comp.InstallationStatus)
{
case InstallationStatus.Cartridge:
OnInstallButtonPressed?.Invoke(_cartridge.Owner);
break;
case InstallationStatus.Installed:
OnUninstallButtonPressed?.Invoke(_cartridge.Owner);
break;
case InstallationStatus.Readonly:
default:
break;
}
}
protected override void Draw(DrawingHandleScreen handle)
@ -35,6 +70,29 @@ public sealed partial class PdaProgramItem : ContainerButton
if (TryGetStyleProperty<Color>(StylePropertyBgColor, out var bgColor))
BackgroundColor = bgColor;
}
public void SetCartridge(Entity<CartridgeComponent> program)
{
if (program.Comp.Icon is { } icon)
Icon.SetFromSpriteSpecifier(icon);
_cartridge = program;
ProgramName.Text = Loc.GetString(program.Comp.ProgramName);
switch (program.Comp.InstallationStatus)
{
case InstallationStatus.Cartridge:
InstallButton.Visible = true;
InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-install-button");
break;
case InstallationStatus.Installed:
InstallButton.Visible = true;
InstallButton.Text = Loc.GetString("cartridge-bound-user-interface-uninstall-button");
break;
case InstallationStatus.Readonly:
InstallButton.Visible = false;
break;
}
}
}

View file

@ -1,521 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.PDA;
using Content.Shared.CartridgeLoader;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Interaction;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.CartridgeLoader;
public sealed partial class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
[Dependency] private ContainerSystem _containerSystem = default!;
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private PdaSystem _pda = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CartridgeLoaderComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CartridgeLoaderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<CartridgeLoaderComponent, AfterInteractEvent>(OnUsed);
SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeLoaderUiMessage>(OnLoaderUiMessage);
SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeUiMessage>(OnUiMessage);
}
public IReadOnlyList<EntityUid> GetInstalled(EntityUid uid, ContainerManagerComponent? comp = null)
{
if (_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, comp))
return container.ContainedEntities;
return Array.Empty<EntityUid>();
}
public bool TryGetProgram<T>(
EntityUid uid,
[NotNullWhen(true)] out EntityUid? programUid,
[NotNullWhen(true)] out T? program,
bool installedOnly = false,
CartridgeLoaderComponent? loader = null,
ContainerManagerComponent? containerManager = null) where T : IComponent
{
program = default;
programUid = null;
if (!_containerSystem.TryGetContainer(uid, InstalledContainerId, out var container, containerManager))
return false;
foreach (var prog in container.ContainedEntities)
{
if (!TryComp(prog, out program))
continue;
programUid = prog;
return true;
}
if (installedOnly)
return false;
if (!Resolve(uid, ref loader) || !TryComp(loader.CartridgeSlot.Item, out program))
return false;
programUid = loader.CartridgeSlot.Item;
return true;
}
public bool TryGetProgram<T>(
EntityUid uid,
[NotNullWhen(true)] out EntityUid? programUid,
bool installedOnly = false,
CartridgeLoaderComponent? loader = null,
ContainerManagerComponent? containerManager = null) where T : IComponent
{
return TryGetProgram<T>(uid, out programUid, out _, installedOnly, loader, containerManager);
}
public bool HasProgram<T>(
EntityUid uid,
bool installedOnly = false,
CartridgeLoaderComponent? loader = null,
ContainerManagerComponent? containerManager = null) where T : IComponent
{
return TryGetProgram<T>(uid, out _, out _, installedOnly, loader, containerManager);
}
/// <summary>
/// Updates the cartridge loaders ui state.
/// </summary>
/// <remarks>
/// Because the cartridge loader integrates with the ui of the entity using it, the entities ui state needs to inherit from <see cref="CartridgeLoaderUiState"/>
/// and use this method to update its state so the cartridge loaders state can be added to it.
/// </remarks>
/// <seealso cref="PDA.PdaSystem.UpdatePdaUserInterface"/>
public void UpdateUiState(EntityUid loaderUid, ICommonSession? session, CartridgeLoaderComponent? loader)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!_userInterfaceSystem.HasUi(loaderUid, loader.UiKey))
return;
var programs = GetAvailablePrograms(loaderUid, loader);
var state = new CartridgeLoaderUiState(programs, GetNetEntity(loader.ActiveProgram));
_userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state);
}
/// <summary>
/// Updates the programs ui state
/// </summary>
/// <param name="loaderUid">The cartridge loaders entity uid</param>
/// <param name="state">The programs ui state. Programs should use their own ui state class inheriting from <see cref="BoundUserInterfaceState"/></param>
/// <param name="session">The players session</param>
/// <param name="loader">The cartridge loader component</param>
/// <remarks>
/// This method is called "UpdateCartridgeUiState" but cartridges and a programs are the same. A cartridge is just a program as a visible item.
/// </remarks>
/// <seealso cref="Cartridges.NotekeeperCartridgeSystem.UpdateUiState"/>
public void UpdateCartridgeUiState(EntityUid loaderUid, BoundUserInterfaceState state, ICommonSession? session = default!, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (_userInterfaceSystem.HasUi(loaderUid, loader.UiKey))
_userInterfaceSystem.SetUiState(loaderUid, loader.UiKey, state);
}
/// <summary>
/// Returns a list of all installed programs and the inserted cartridge if it isn't already installed
/// </summary>
/// <param name="uid">The cartridge loaders uid</param>
/// <param name="loader">The cartridge loader component</param>
/// <returns>A list of all the available program entity ids</returns>
public List<NetEntity> GetAvailablePrograms(EntityUid uid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(uid, ref loader))
return new List<NetEntity>();
var available = GetNetEntityList(GetInstalled(uid));
if (loader.CartridgeSlot.Item is not { } cartridge)
return available;
// TODO exclude duplicate programs. Or something I dunno I CBF fixing this mess.
available.Add(GetNetEntity(cartridge));
return available;
}
/// <summary>
/// Installs a cartridge by spawning an invisible version of the cartridges prototype into the cartridge loaders program container program container
/// </summary>
/// <param name="loaderUid">The cartridge loader uid</param>
/// <param name="cartridgeUid">The uid of the cartridge to be installed</param>
/// <param name="loader">The cartridge loader component</param>
/// <returns>Whether installing the cartridge was successful</returns>
public bool InstallCartridge(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return false;
if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge))
return false;
foreach (var program in GetInstalled(loaderUid))
{
if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName)
return false;
}
//This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs
//the data on the cartridge to carry over when installing
// For anyone stumbling onto this: Do not do this or I will cut you.
var prototypeId = Prototype(cartridgeUid)?.ID;
return prototypeId != null && InstallProgram(loaderUid, prototypeId, loader: loader);
}
/// <summary>
/// Installs a program by its prototype
/// </summary>
/// <param name="loaderUid">The cartridge loader uid</param>
/// <param name="prototype">The prototype name</param>
/// <param name="deinstallable">Whether the program can be deinstalled or not</param>
/// <param name="loader">The cartridge loader component</param>
/// <returns>Whether installing the cartridge was successful</returns>
public bool InstallProgram(EntityUid loaderUid, string prototype, bool deinstallable = true, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return false;
if (!_containerSystem.TryGetContainer(loaderUid, InstalledContainerId, out var container))
return false;
if (container.Count >= loader.DiskSpace)
return false;
var ev = new ProgramInstallationAttempt(loaderUid, prototype);
RaiseLocalEvent(ref ev);
if (ev.Cancelled)
return false;
var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0));
if (!TryComp(installedProgram, out CartridgeComponent? cartridge))
return false;
_containerSystem.Insert(installedProgram, container);
UpdateCartridgeInstallationStatus(installedProgram, deinstallable ? InstallationStatus.Installed : InstallationStatus.Readonly, cartridge);
cartridge.LoaderUid = loaderUid;
RaiseLocalEvent(installedProgram, new CartridgeAddedEvent(loaderUid));
UpdateUserInterfaceState(loaderUid, loader);
return true;
}
/// <summary>
/// Uninstalls a program using its uid
/// </summary>
/// <param name="loaderUid">The cartridge loader uid</param>
/// <param name="programUid">The uid of the program to be uninstalled</param>
/// <param name="loader">The cartridge loader component</param>
/// <returns>Whether uninstalling the program was successful</returns>
public bool UninstallProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return false;
if (!GetInstalled(loaderUid).Contains(programUid))
return false;
if (TryComp(programUid, out CartridgeComponent? cartridge))
cartridge.LoaderUid = null;
if (loader.ActiveProgram == programUid)
loader.ActiveProgram = null;
loader.BackgroundPrograms.Remove(programUid);
QueueDel(programUid);
UpdateUserInterfaceState(loaderUid, loader);
return true;
}
/// <summary>
/// Activates a program or cartridge and displays its ui fragment. Deactivates any previously active program.
/// </summary>
public void ActivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!HasProgram(loaderUid, programUid, loader))
return;
if (loader.ActiveProgram.HasValue)
DeactivateProgram(loaderUid, programUid, loader);
if (!loader.BackgroundPrograms.Contains(programUid))
RaiseLocalEvent(programUid, new CartridgeActivatedEvent(loaderUid));
loader.ActiveProgram = programUid;
UpdateUserInterfaceState(loaderUid, loader);
}
/// <summary>
/// Deactivates the currently active program or cartridge.
/// </summary>
public void DeactivateProgram(EntityUid loaderUid, EntityUid programUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!HasProgram(loaderUid, programUid, loader) || loader.ActiveProgram != programUid)
return;
if (!loader.BackgroundPrograms.Contains(programUid))
RaiseLocalEvent(programUid, new CartridgeDeactivatedEvent(programUid));
loader.ActiveProgram = default;
UpdateUserInterfaceState(loaderUid, loader);
}
/// <summary>
/// Registers the given program as a running in the background. Programs running in the background will receive certain events like device net packets but not ui messages
/// </summary>
/// <remarks>
/// Programs wanting to use this functionality will have to provide a way to register and unregister themselves as background programs through their ui fragment.
/// </remarks>
public void RegisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!HasProgram(loaderUid, cartridgeUid, loader))
return;
if (loader.ActiveProgram != cartridgeUid)
RaiseLocalEvent(cartridgeUid, new CartridgeActivatedEvent(loaderUid));
loader.BackgroundPrograms.Add(cartridgeUid);
}
/// <summary>
/// Unregisters the given program as running in the background
/// </summary>
public void UnregisterBackgroundProgram(EntityUid loaderUid, EntityUid cartridgeUid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!HasProgram(loaderUid, cartridgeUid, loader))
return;
if (loader.ActiveProgram != cartridgeUid)
RaiseLocalEvent(cartridgeUid, new CartridgeDeactivatedEvent(loaderUid));
loader.BackgroundPrograms.Remove(cartridgeUid);
}
public void SendNotification(EntityUid loaderUid, string header, string message, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!loader.NotificationsEnabled)
return;
var args = new CartridgeLoaderNotificationSentEvent(header, message);
RaiseLocalEvent(loaderUid, ref args);
}
protected override void OnItemInserted(EntityUid uid, CartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
return;
if (TryComp(args.Entity, out CartridgeComponent? cartridge))
cartridge.LoaderUid = uid;
RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid));
base.OnItemInserted(uid, loader, args);
}
protected override void OnItemRemoved(EntityUid uid, CartridgeLoaderComponent loader, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != InstalledContainerId && args.Container.ID != loader.CartridgeSlot.ID)
return;
var deactivate = loader.BackgroundPrograms.Remove(args.Entity);
if (loader.ActiveProgram == args.Entity)
{
loader.ActiveProgram = default;
deactivate = true;
}
if (deactivate)
RaiseLocalEvent(args.Entity, new CartridgeDeactivatedEvent(uid));
if (TryComp(args.Entity, out CartridgeComponent? cartridge))
cartridge.LoaderUid = null;
RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid));
base.OnItemRemoved(uid, loader, args);
_pda.UpdatePdaUi(uid);
}
/// <summary>
/// Installs programs from the list of preinstalled programs
/// </summary>
private void OnMapInit(EntityUid uid, CartridgeLoaderComponent component, MapInitEvent args)
{
// TODO remove this and use container fill.
foreach (var prototype in component.PreinstalledPrograms)
{
InstallProgram(uid, prototype, deinstallable: false);
}
}
private void OnUsed(EntityUid uid, CartridgeLoaderComponent component, AfterInteractEvent args)
{
RelayEvent(component, new CartridgeAfterInteractEvent(uid, args));
}
private void OnPacketReceived(EntityUid uid, CartridgeLoaderComponent component, DeviceNetworkPacketEvent args)
{
RelayEvent(component, new CartridgeDeviceNetPacketEvent(uid, args));
}
private void OnLoaderUiMessage(EntityUid loaderUid, CartridgeLoaderComponent component, CartridgeLoaderUiMessage message)
{
var cartridge = GetEntity(message.CartridgeUid);
switch (message.Action)
{
case CartridgeUiMessageAction.Activate:
ActivateProgram(loaderUid, cartridge, component);
break;
case CartridgeUiMessageAction.Deactivate:
DeactivateProgram(loaderUid, cartridge, component);
break;
case CartridgeUiMessageAction.Install:
InstallCartridge(loaderUid, cartridge, component);
break;
case CartridgeUiMessageAction.Uninstall:
UninstallProgram(loaderUid, cartridge, component);
break;
case CartridgeUiMessageAction.UIReady:
if (component.ActiveProgram.HasValue)
RaiseLocalEvent(component.ActiveProgram.Value, new CartridgeUiReadyEvent(loaderUid));
break;
default:
throw new ArgumentOutOfRangeException($"Unrecognized UI action passed from cartridge loader ui {message.Action}.");
}
}
/// <summary>
/// Relays ui messages meant for cartridges to the currently active cartridge
/// </summary>
private void OnUiMessage(EntityUid uid, CartridgeLoaderComponent component, CartridgeUiMessage args)
{
var cartridgeEvent = args.MessageEvent;
cartridgeEvent.User = args.Actor;
cartridgeEvent.LoaderUid = GetNetEntity(uid);
cartridgeEvent.Actor = args.Actor;
RelayEvent(component, cartridgeEvent, true);
}
/// <summary>
/// Relays events to the currently active program and and programs running in the background.
/// Skips background programs if "skipBackgroundPrograms" is set to true
/// </summary>
/// <param name="loader">The cartritge loader component</param>
/// <param name="args">The event to be relayed</param>
/// <param name="skipBackgroundPrograms">Whether to skip relaying the event to programs running in the background</param>
private void RelayEvent<TEvent>(CartridgeLoaderComponent loader, TEvent args, bool skipBackgroundPrograms = false) where TEvent : notnull
{
if (loader.ActiveProgram.HasValue)
RaiseLocalEvent(loader.ActiveProgram.Value, args);
if (skipBackgroundPrograms)
return;
foreach (var program in loader.BackgroundPrograms)
{
//Prevent programs registered as running in the background receiving events twice if they are active
if (loader.ActiveProgram.HasValue && loader.ActiveProgram.Value.Equals(program))
continue;
RaiseLocalEvent(program, args);
}
}
/// <summary>
/// Shortcut for updating the loaders user interface state without passing in a subtype of <see cref="CartridgeLoaderUiState"/>
/// like the <see cref="PDA.PdaSystem"/> does when updating its ui state
/// </summary>
/// <seealso cref="PDA.PdaSystem.UpdatePdaUserInterface"/>
private void UpdateUserInterfaceState(EntityUid loaderUid, CartridgeLoaderComponent loader)
{
UpdateUiState(loaderUid, null, loader);
}
private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent cartridgeComponent)
{
cartridgeComponent.InstallationStatus = installationStatus;
Dirty(cartridgeUid, cartridgeComponent);
}
private bool HasProgram(EntityUid loader, EntityUid program, CartridgeLoaderComponent component)
{
return component.CartridgeSlot.Item == program || GetInstalled(loader).Contains(program);
}
}
/// <summary>
/// Gets sent to running programs when the cartridge loader receives a device net package
/// </summary>
/// <seealso cref="DeviceNetworkPacketEvent"/>
public sealed class CartridgeDeviceNetPacketEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public readonly DeviceNetworkPacketEvent PacketEvent;
public CartridgeDeviceNetPacketEvent(EntityUid loader, DeviceNetworkPacketEvent packetEvent)
{
Loader = loader;
PacketEvent = packetEvent;
}
}
/// <summary>
/// Gets sent to running programs when the cartridge loader receives an after interact event
/// </summary>
/// <seealso cref="AfterInteractEvent"/>
public sealed class CartridgeAfterInteractEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public readonly AfterInteractEvent InteractEvent;
public CartridgeAfterInteractEvent(EntityUid loader, AfterInteractEvent interactEvent)
{
Loader = loader;
InteractEvent = interactEvent;
}
}
/// <summary>
/// Raised on an attempt of program installation.
/// </summary>
[ByRefEvent]
public record struct ProgramInstallationAttempt(EntityUid LoaderUid, string Prototype, bool Cancelled = false);

View file

@ -1,9 +0,0 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.GPS;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class AstroNavCartridgeComponent : Component
{
}

View file

@ -1,6 +0,0 @@
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class CrewManifestCartridgeComponent : Component
{
}

View file

@ -5,7 +5,6 @@ using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
namespace Content.Server.CartridgeLoader.Cartridges;
@ -85,17 +84,17 @@ public sealed partial class CrewManifestCartridgeSystem : EntitySystem
{
_unsecureViewersAllowed = unsecureViewersAllowed;
var allCartridgeLoaders = AllEntityQuery<CartridgeLoaderComponent, ContainerManagerComponent>();
while (allCartridgeLoaders.MoveNext(out var loaderUid, out var comp, out var cont))
var allCartridgeLoaders = AllEntityQuery<CartridgeLoaderComponent>();
while (allCartridgeLoaders.MoveNext(out var loaderUid, out var comp))
{
if (_unsecureViewersAllowed)
{
_cartridgeLoader.InstallProgram(loaderUid, CartridgePrototypeName, false, comp);
_cartridgeLoader.InstallProgram((loaderUid, comp), CartridgePrototypeName, false);
return;
}
if (_cartridgeLoader.TryGetProgram<CrewManifestCartridgeComponent>(loaderUid, out var program, true, comp, cont))
_cartridgeLoader.UninstallProgram(loaderUid, program.Value, comp);
if (_cartridgeLoader.TryGetProgram<CrewManifestCartridgeComponent>((loaderUid, comp)) is { } program)
_cartridgeLoader.UninstallProgram((loaderUid, comp), program);
}
}
}

View file

@ -17,13 +17,13 @@ public sealed partial class MedTekCartridgeSystem : EntitySystem
private void OnCartridgeAdded(Entity<MedTekCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
var healthAnalyzer = EnsureComp<HealthAnalyzerComponent>(args.Loader);
EnsureComp<HealthAnalyzerComponent>(args.Loader);
}
private void OnCartridgeRemoved(Entity<MedTekCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
// only remove when the program itself is removed
if (!_cartridgeLoaderSystem.HasProgram<MedTekCartridgeComponent>(args.Loader))
if (!_cartridgeLoaderSystem.HasProgram<MedTekCartridgeComponent>(args.Loader.AsNullable()))
{
RemComp<HealthAnalyzerComponent>(args.Loader);
}

View file

@ -1,8 +0,0 @@
using Content.Shared.Security;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class WantedListCartridgeComponent : Component
{
}

View file

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Access.Systems;
using Content.Server.AlertLevel;
using Content.Server.CartridgeLoader;
@ -202,7 +203,7 @@ namespace Content.Server.PDA
if (!TryComp(uid, out CartridgeLoaderComponent? loader))
return;
var programs = _cartridgeLoader.GetAvailablePrograms(uid, loader);
var programs = GetNetEntityList(_cartridgeLoader.GetAllPrograms(uid).ToList());
var id = CompOrNull<IdCardComponent>(pda.ContainedId);
var state = new PdaUpdateState(
programs,

View file

@ -7,10 +7,11 @@ namespace Content.Shared.CartridgeLoader;
/// <summary>
/// This is used for defining values used for displaying in the program ui in yaml
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
[Access(typeof(CartridgeLoaderSystem))]
public sealed partial class CartridgeComponent : Component
{
[DataField]
[DataField, AutoNetworkedField]
public EntityUid? LoaderUid;
[DataField(required: true)]
@ -19,7 +20,7 @@ public sealed partial class CartridgeComponent : Component
[DataField]
public SpriteSpecifier? Icon;
[AutoNetworkedField]
[DataField, AutoNetworkedField]
public InstallationStatus InstallationStatus = InstallationStatus.Cartridge;
}

View file

@ -3,43 +3,36 @@ using Robust.Shared.GameStates;
namespace Content.Shared.CartridgeLoader;
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(CartridgeLoaderSystem))]
public sealed partial class CartridgeLoaderComponent : Component
{
public const string UnremovableContainerId = "preinstalled-program-container";
public const string RemovableContainerId = "removable-program-container";
public const string CartridgeSlotId = "Cartridge-Slot";
[DataField]
public ItemSlot CartridgeSlot = new();
/// <summary>
/// List of programs that come preinstalled with this cartridge loader
/// </summary>
[DataField("preinstalled")] // TODO remove this and use container fill.
public List<string> PreinstalledPrograms = new();
/// <summary>
/// The currently running program that has its ui showing
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public EntityUid? ActiveProgram = default;
/// <summary>
/// The list of programs running in the background, listening to certain events
/// </summary>
[ViewVariables]
public readonly List<EntityUid> BackgroundPrograms = new();
/// <summary>
/// The maximum amount of programs that can be installed on the cartridge loader entity
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public int DiskSpace = 8;
/// <summary>
/// Controls whether the cartridge loader will play notifications if it supports it at all
/// TODO: Add an option for this to the PDA
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool NotificationsEnabled = true;
[DataField(required: true)]

View file

@ -0,0 +1,171 @@
using System.Linq;
using Robust.Shared.Prototypes;
namespace Content.Shared.CartridgeLoader;
public sealed partial class CartridgeLoaderSystem
{
public Entity<T>? TryGetProgram<T>(Entity<CartridgeLoaderComponent?> ent) where T : IComponent
{
if (!Resolve(ent, ref ent.Comp))
return null;
foreach (var prog in GetAllPrograms(ent))
{
if (!TryComp<T>(prog, out var program))
continue;
return (prog, program);
}
return null;
}
public bool HasProgram<T>(Entity<CartridgeLoaderComponent?> ent) where T : IComponent
{
return TryGetProgram<T>(ent).HasValue;
}
public IEnumerable<EntityUid> GetAllPrograms(EntityUid uid)
{
if (_itemSlotsSystem.GetItemOrNull(uid, CartridgeLoaderComponent.CartridgeSlotId) is { } cartridge)
return GetDiskPrograms(uid).Append(cartridge);
return GetDiskPrograms(uid);
}
public IEnumerable<EntityUid> GetDiskPrograms(EntityUid uid)
{
return GetPreinstalledPrograms(uid).Concat(GetRemovablePrograms(uid));
}
public IReadOnlyList<EntityUid> GetRemovablePrograms(EntityUid uid)
{
if (_container.TryGetContainer(uid, CartridgeLoaderComponent.RemovableContainerId, out var container))
return container.ContainedEntities;
return [];
}
public IReadOnlyList<EntityUid> GetPreinstalledPrograms(EntityUid uid)
{
if (_container.TryGetContainer(uid, CartridgeLoaderComponent.UnremovableContainerId, out var container))
return container.ContainedEntities;
return [];
}
public int UsedDiskSpace(EntityUid uid)
{
var ret = 0;
if (_container.TryGetContainer(uid, CartridgeLoaderComponent.RemovableContainerId, out var removable))
ret += removable.Count;
if (_container.TryGetContainer(uid, CartridgeLoaderComponent.UnremovableContainerId, out var unremovable))
ret += unremovable.Count;
return ret;
}
private bool HasProgram(EntityUid uid, EntityUid program)
{
return GetAllPrograms(uid).Contains(program);
}
public void ActivateProgram(Entity<CartridgeLoaderComponent> ent, EntityUid program)
{
if (!HasProgram(ent, program))
return;
if (ent.Comp.ActiveProgram is { } active)
DeactivateProgram(ent, active);
var evt = new CartridgeActivatedEvent(ent);
RaiseLocalEvent(program, ref evt);
ent.Comp.ActiveProgram = program;
Dirty(ent);
UpdateUiState(ent.AsNullable());
}
public void DeactivateProgram(Entity<CartridgeLoaderComponent> ent, EntityUid program)
{
if (!HasProgram(ent, program) || ent.Comp.ActiveProgram != program)
return;
var evt = new CartridgeDeactivatedEvent(ent);
RaiseLocalEvent(program, ref evt);
ent.Comp.ActiveProgram = null;
Dirty(ent);
UpdateUiState(ent.AsNullable());
}
/// <summary>
/// Installs a program by its prototype.
/// </summary>
/// <param name="ent">The cartridge loader.</param>
/// <param name="prototype">The prototype.</param>
/// <param name="deinstallable">Whether the program can be deinstalled or not.</param>
/// <returns>Whether installing the cartridge was successful.</returns>
public bool InstallProgram(Entity<CartridgeLoaderComponent> ent, EntProtoId prototype, bool deinstallable = true)
{
if (UsedDiskSpace(ent) >= ent.Comp.DiskSpace)
return false;
var ev = new ProgramInstallationAttempt(ent, prototype);
RaiseLocalEvent(ref ev);
if (ev.Cancelled)
return false;
return PredictedTrySpawnInContainer(prototype,
ent,
deinstallable ? CartridgeLoaderComponent.RemovableContainerId : CartridgeLoaderComponent.UnremovableContainerId,
out _);
}
/// <summary>
/// Uninstalls a program using its uid.
/// </summary>
/// <param name="ent">The cartridge loader uid.</param>
/// <param name="program">The uid of the program to be uninstalled.</param>
/// <returns>Whether uninstalling the program was successful.</returns>
public bool UninstallProgram(Entity<CartridgeLoaderComponent> ent, EntityUid program)
{
if (!GetDiskPrograms(ent).Contains(program))
return false;
PredictedQueueDel(program);
return true;
}
/// <summary>
/// Installs a cartridge by spawning an invisible version of the cartridges prototype into the cartridge loaders program container.
/// </summary>
/// <param name="ent">The cartridge loader.</param>
/// <param name="cartridge">The uid of the cartridge to be installed.</param>
/// <returns>Whether installing the cartridge was successful.</returns>
public bool InstallCartridge(Entity<CartridgeLoaderComponent> ent, EntityUid cartridge)
{
if (!HasComp<CartridgeComponent>(cartridge))
return false;
if (MetaData(cartridge).EntityPrototype is not { } cartridgeProto)
return false;
foreach (var program in GetDiskPrograms(ent))
{
if (MetaData(program).EntityPrototype is { } programProto && programProto == cartridgeProto)
return false;
}
return InstallProgram(ent, cartridgeProto);
}
private void UpdateCartridgeInstallationStatus(Entity<CartridgeComponent> ent, InstallationStatus installationStatus)
{
ent.Comp.InstallationStatus = installationStatus;
Dirty(ent);
}
}

View file

@ -0,0 +1,64 @@
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Interaction;
namespace Content.Shared.CartridgeLoader;
public sealed partial class CartridgeLoaderSystem
{
private void InitializeRelay()
{
SubscribeLocalEvent<CartridgeLoaderComponent, AfterInteractEvent>(RelayEvent);
SubscribeLocalEvent<CartridgeLoaderComponent, InteractUsingEvent>(RelayEvent);
SubscribeLocalEvent<CartridgeLoaderComponent, DeviceNetworkPacketEvent>(RelayEvent);
}
private void RefRelayEvent<T>(EntityUid uid, CartridgeLoaderComponent component, ref T args) where T : struct
{
RefRelayEvent((uid, component), ref args);
}
private void RelayEvent<T>(EntityUid uid, CartridgeLoaderComponent component, T args) where T : class
{
RelayEvent((uid, component), args);
}
public void RefRelayEvent<T>(Entity<CartridgeLoaderComponent> ent, ref T args, bool foregroundOnly = false) where T : struct
{
var ev = new CartridgeRelayedEvent<T>(ent, args);
if (foregroundOnly)
{
if (ent.Comp.ActiveProgram is { } foreground)
RaiseLocalEvent(foreground, ref ev);
args = ev.Args;
return;
}
foreach (var program in GetAllPrograms(ent))
{
RaiseLocalEvent(program, ref ev);
}
args = ev.Args;
}
public void RelayEvent<T>(Entity<CartridgeLoaderComponent> ent, T args, bool foregroundOnly = false) where T : class
{
var ev = new CartridgeRelayedEvent<T>(ent, args);
if (foregroundOnly)
{
if (ent.Comp.ActiveProgram is { } foreground)
RaiseLocalEvent(foreground, ref ev);
return;
}
foreach (var program in GetAllPrograms(ent))
{
RaiseLocalEvent(program, ref ev);
}
}
}
/// <summary>
/// Event wrapper for relayed events.
/// </summary>
[ByRefEvent]
public record struct CartridgeRelayedEvent<TEvent>(Entity<CartridgeLoaderComponent> Loader, TEvent Args);

View file

@ -0,0 +1,91 @@
using System.Linq;
namespace Content.Shared.CartridgeLoader;
public sealed partial class CartridgeLoaderSystem
{
private void OnLoaderUiMessage(Entity<CartridgeLoaderComponent> ent, ref CartridgeLoaderUiMessage message)
{
var cartridge = GetEntity(message.CartridgeUid);
switch (message.Action)
{
case CartridgeUiMessageAction.Activate:
ActivateProgram(ent, cartridge);
break;
case CartridgeUiMessageAction.Deactivate:
DeactivateProgram(ent, cartridge);
break;
case CartridgeUiMessageAction.Install:
InstallCartridge(ent, cartridge);
break;
case CartridgeUiMessageAction.Uninstall:
UninstallProgram(ent, cartridge);
break;
case CartridgeUiMessageAction.UIReady:
if (ent.Comp.ActiveProgram is { } foreground)
{
var evt = new CartridgeUiReadyEvent(ent);
RaiseLocalEvent(foreground, ref evt);
}
break;
default:
throw new ArgumentOutOfRangeException($"Unrecognized UI action passed from cartridge loader ui {message.Action}.");
}
}
/// <summary>
/// Relays ui messages meant for cartridges to the currently active cartridge.
/// </summary>
private void OnUiMessage(Entity<CartridgeLoaderComponent> ent, ref CartridgeUiMessage args)
{
var cartridgeEvent = args.MessageEvent;
cartridgeEvent.User = args.Actor;
cartridgeEvent.LoaderUid = GetNetEntity(ent);
cartridgeEvent.Actor = args.Actor;
if (ent.Comp.ActiveProgram is { } foreground)
RaiseLocalEvent(foreground, cartridgeEvent);
}
/// <summary>
/// Updates the programs ui state.
/// </summary>
/// <param name="loaderUid">The cartridge loaders entity uid.</param>
/// <param name="state">The programs ui state. Programs should use their own ui state class inheriting from <see cref="BoundUserInterfaceState"/>.</param>
/// <param name="loader">The cartridge loader component.</param>
/// <remarks>
/// This method is called "UpdateCartridgeUiState" but cartridges and a programs are the same. A cartridge is just a program as a visible item.
/// </remarks>
/// <seealso cref="Cartridges.NotekeeperCartridgeSystem.UpdateUiState"/>
public void UpdateCartridgeUiState(EntityUid loaderUid, BoundUserInterfaceState state, CartridgeLoaderComponent? loader = null)
{
if (!Resolve(loaderUid, ref loader))
return;
if (_userInterface.HasUi(loaderUid, loader.UiKey))
_userInterface.SetUiState(loaderUid, loader.UiKey, state);
}
/// <summary>
/// Updates the cartridge loaders ui state.
/// </summary>
/// <remarks>
/// Because the cartridge loader integrates with the ui of the entity using it, the entities ui state needs to inherit from <see cref="CartridgeLoaderUiState"/>
/// and use this method to update its state so the cartridge loaders state can be added to it.
/// </remarks>
/// <seealso cref="PDA.PdaSystem.UpdatePdaUserInterface"/>
public void UpdateUiState(Entity<CartridgeLoaderComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!_userInterface.HasUi(ent.Owner, ent.Comp.UiKey))
return;
var programs = GetNetEntityList(GetAllPrograms(ent).ToList());
var state = new CartridgeLoaderUiState(programs, GetNetEntity(ent.Comp.ActiveProgram));
_userInterface.SetUiState(ent.Owner, ent.Comp.UiKey, state);
}
}

View file

@ -0,0 +1,185 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.CartridgeLoader;
public sealed partial class CartridgeLoaderSystem : EntitySystem
{
[Dependency] private IGameTiming _timing = default!;
[Dependency] private ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private SharedUserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
InitializeRelay();
SubscribeLocalEvent<CartridgeLoaderComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CartridgeLoaderComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<CartridgeLoaderComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<CartridgeLoaderComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeLoaderUiMessage>(OnLoaderUiMessage);
SubscribeLocalEvent<CartridgeLoaderComponent, CartridgeUiMessage>(OnUiMessage);
SubscribeLocalEvent<CartridgeComponent, AfterAutoHandleStateEvent>(OnCartridgeState);
}
private void OnComponentInit(Entity<CartridgeLoaderComponent> ent, ref ComponentInit args)
{
_itemSlotsSystem.AddItemSlot(ent, CartridgeLoaderComponent.CartridgeSlotId, ent.Comp.CartridgeSlot);
_container.EnsureContainer<Container>(ent, CartridgeLoaderComponent.RemovableContainerId);
_container.EnsureContainer<Container>(ent, CartridgeLoaderComponent.UnremovableContainerId);
}
/// <summary>
/// Marks installed program entities for deletion when the component gets removed
/// </summary>
private void OnComponentRemove(Entity<CartridgeLoaderComponent> ent, ref ComponentRemove args)
{
_itemSlotsSystem.RemoveItemSlot(ent, ent.Comp.CartridgeSlot);
if (_container.TryGetContainer(ent, CartridgeLoaderComponent.RemovableContainerId, out var removable))
_container.ShutdownContainer(removable);
if (_container.TryGetContainer(ent, CartridgeLoaderComponent.UnremovableContainerId, out var unremovable))
_container.ShutdownContainer(unremovable);
}
private void OnItemInserted(Entity<CartridgeLoaderComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (_timing.ApplyingState)
return;
if (args.Container.ID != CartridgeLoaderComponent.RemovableContainerId
&& args.Container.ID != CartridgeLoaderComponent.UnremovableContainerId
&& args.Container.ID != CartridgeLoaderComponent.CartridgeSlotId
) return;
if (TryComp<CartridgeComponent>(args.Entity, out var cartridge))
{
cartridge.LoaderUid = ent;
Dirty(args.Entity, cartridge);
if (args.Container.ID == CartridgeLoaderComponent.RemovableContainerId)
UpdateCartridgeInstallationStatus((args.Entity, cartridge), InstallationStatus.Installed);
else if (args.Container.ID == CartridgeLoaderComponent.UnremovableContainerId)
UpdateCartridgeInstallationStatus((args.Entity, cartridge), InstallationStatus.Readonly);
else
UpdateCartridgeInstallationStatus((args.Entity, cartridge), InstallationStatus.Cartridge);
}
var evt = new CartridgeAddedEvent(ent);
RaiseLocalEvent(args.Entity, ref evt);
UpdateUiState(ent.AsNullable());
UpdateAppearanceData(ent);
}
private void OnItemRemoved(Entity<CartridgeLoaderComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (_timing.ApplyingState)
return;
if (args.Container.ID != CartridgeLoaderComponent.RemovableContainerId
&& args.Container.ID != CartridgeLoaderComponent.UnremovableContainerId
&& args.Container.ID != CartridgeLoaderComponent.CartridgeSlotId
) return;
if (ent.Comp.ActiveProgram == args.Entity)
{
ent.Comp.ActiveProgram = null;
Dirty(ent);
var deactivated = new CartridgeDeactivatedEvent(ent);
RaiseLocalEvent(args.Entity, ref deactivated);
}
if (TryComp<CartridgeComponent>(args.Entity, out var cartridge))
{
cartridge.LoaderUid = null;
Dirty(args.Entity, cartridge);
}
var removed = new CartridgeRemovedEvent(ent);
RaiseLocalEvent(args.Entity, ref removed);
UpdateUiState(ent.AsNullable());
UpdateAppearanceData(ent);
}
private void UpdateAppearanceData(Entity<CartridgeLoaderComponent> ent)
{
_appearanceSystem.SetData(ent.Owner, CartridgeLoaderVisuals.CartridgeInserted, ent.Comp.CartridgeSlot.HasItem);
}
public void SendNotification(EntityUid loaderUid, string header, string message, CartridgeLoaderComponent? loader = default)
{
if (!Resolve(loaderUid, ref loader))
return;
if (!loader.NotificationsEnabled)
return;
var args = new CartridgeLoaderNotificationSentEvent(header, message);
RaiseLocalEvent(loaderUid, ref args);
}
private void OnCartridgeState(Entity<CartridgeComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (ent.Comp.LoaderUid is not { } loader)
return;
UpdateUiState(loader);
}
}
/// <summary>
/// Gets sent to program / cartridge entities when they get inserted or installed.
/// </summary>
[ByRefEvent]
public record struct CartridgeAddedEvent(Entity<CartridgeLoaderComponent> Loader);
/// <summary>
/// Gets sent to cartridge entities when they get ejected.
/// </summary>
[ByRefEvent]
public record struct CartridgeRemovedEvent(Entity<CartridgeLoaderComponent> Loader);
/// <summary>
/// Gets sent to program / cartridge entities when they get activated.
/// </summary>
/// <remarks>
/// Don't update the programs ui state in this events listener.
/// </remarks>
[ByRefEvent]
public record struct CartridgeActivatedEvent(Entity<CartridgeLoaderComponent> Loader);
/// <summary>
/// Gets sent to program / cartridge entities when they get deactivated.
/// </summary>
[ByRefEvent]
public record struct CartridgeDeactivatedEvent(Entity<CartridgeLoaderComponent> Loader);
/// <summary>
/// Gets sent to program / cartridge entities when the ui is ready to be updated by the cartridge.
/// </summary>
/// <remarks>
/// This is used for the initial ui state update because updating the ui in the activate event doesn't work.
/// </remarks>
[ByRefEvent]
public record struct CartridgeUiReadyEvent(Entity<CartridgeLoaderComponent> Loader);
/// <summary>
/// Gets sent by the cartridge loader system to the cartridge loader entity so another system.
/// can handle displaying the notification
/// </summary>
/// <param name="Message">The message to be displayed.</param>
[ByRefEvent]
public record struct CartridgeLoaderNotificationSentEvent(string Header, string Message);
/// <summary>
/// Raised on an attempt of program installation.
/// </summary>
[ByRefEvent]
public record struct ProgramInstallationAttempt(EntityUid LoaderUid, string Prototype, bool Cancelled = false);

View file

@ -0,0 +1,6 @@
using Robust.Shared.GameStates;
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, NetworkedComponent]
public sealed partial class AstroNavCartridgeComponent : Component;

View file

@ -1,8 +1,6 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.GPS.Components;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public sealed partial class AstroNavCartridgeSystem : EntitySystem
{
@ -24,7 +22,7 @@ public sealed partial class AstroNavCartridgeSystem : EntitySystem
private void OnCartridgeRemoved(Entity<AstroNavCartridgeComponent> ent, ref CartridgeRemovedEvent args)
{
// only remove when the program itself is removed
if (!_cartridgeLoaderSystem.HasProgram<AstroNavCartridgeComponent>(args.Loader))
if (!_cartridgeLoaderSystem.HasProgram<AstroNavCartridgeComponent>(args.Loader.AsNullable()))
{
RemComp<HandheldGPSComponent>(args.Loader);
}

View file

@ -0,0 +1,6 @@
using Robust.Shared.GameStates;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent, NetworkedComponent]
public sealed partial class CrewManifestCartridgeComponent : Component;

View file

@ -1,31 +1,31 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Paper;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, Access(typeof(LogProbeCartridgeSystem))]
[AutoGenerateComponentPause]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(LogProbeCartridgeSystem))]
public sealed partial class LogProbeCartridgeComponent : Component
{
/// <summary>
/// The name of the scanned entity, sent to clients when they open the UI.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public string EntityName = string.Empty;
/// <summary>
/// The list of pulled access logs
/// </summary>
[DataField, ViewVariables]
[DataField, AutoNetworkedField]
public List<PulledAccessLog> PulledAccessLogs = new();
/// <summary>
/// The sound to make when we scan something with access
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg", AudioParams.Default.WithVariation(0.25f));
/// <summary>
@ -46,6 +46,6 @@ public sealed partial class LogProbeCartridgeComponent : Component
/// <summary>
/// When anyone is allowed to spawn another printout.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextPrintAllowed = TimeSpan.Zero;
}

View file

@ -1,9 +1,8 @@
using Content.Shared.Access.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Database;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Paper;
using Content.Shared.Popups;
@ -11,7 +10,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
using System.Text;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public sealed partial class LogProbeCartridgeSystem : EntitySystem
{
@ -30,27 +29,24 @@ public sealed partial class LogProbeCartridgeSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeRelayedEvent<AfterInteractEvent>>(AfterInteract);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeMessageEvent>(OnMessage);
}
/// <summary>
/// The <see cref="CartridgeAfterInteractEvent" /> gets relayed to this system if the cartridge loader is running
/// the LogProbe program and someone clicks on something with it. <br/>
/// <br/>
/// Updates the program's list of logs with those from the device.
/// </summary>
private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref CartridgeAfterInteractEvent args)
private void AfterInteract(Entity<LogProbeCartridgeComponent> ent, ref CartridgeRelayedEvent<AfterInteractEvent> args)
{
if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || args.InteractEvent.Target is not { } target)
if (args.Args.Handled || !args.Args.CanReach || args.Args.Target is not { } target)
return;
if (!TryComp(target, out AccessReaderComponent? accessReaderComponent))
return;
//Play scanning sound with slightly randomized pitch
_audio.PlayEntity(ent.Comp.SoundScan, args.InteractEvent.User, target);
_popup.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
_audio.PlayPredicted(ent.Comp.SoundScan, target, args.Args.User);
_popup.PopupPredictedCursor(Loc.GetString("log-probe-scan", ("device", target)), args.Args.User);
ent.Comp.EntityName = Name(target);
ent.Comp.PulledAccessLogs.Clear();
@ -68,6 +64,7 @@ public sealed partial class LogProbeCartridgeSystem : EntitySystem
// Reverse the list so the oldest is at the bottom
ent.Comp.PulledAccessLogs.Reverse();
Dirty(ent);
UpdateUiState(ent, args.Loader);
}
@ -95,7 +92,7 @@ public sealed partial class LogProbeCartridgeSystem : EntitySystem
ent.Comp.NextPrintAllowed = _timing.CurTime + ent.Comp.PrintCooldown;
var paper = Spawn(ent.Comp.PaperPrototype, _transform.GetMapCoordinates(user));
var paper = EntityManager.PredictedSpawn(ent.Comp.PaperPrototype, _transform.GetMapCoordinates(user));
_label.Label(paper, ent.Comp.EntityName); // label it for easy identification
_audio.PlayEntity(ent.Comp.PrintSound, user, paper);
@ -117,6 +114,7 @@ public sealed partial class LogProbeCartridgeSystem : EntitySystem
_paper.SetContent((paper, paperComp), builder.ToString());
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(user):user} printed out LogProbe logs ({paper}) of {ent.Comp.EntityName}");
Dirty(ent);
}
private void UpdateUiState(Entity<LogProbeCartridgeComponent> ent, EntityUid loaderUid)

View file

@ -1,6 +1,6 @@
using Robust.Shared.GameStates;
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class MedTekCartridgeComponent : Component
{
}
[RegisterComponent, NetworkedComponent]
public sealed partial class MedTekCartridgeComponent : Component;

View file

@ -1,29 +1,29 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.GameStates;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Component that indicates a PDA cartridge as containing the NanoTask program
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause, AutoGenerateComponentState]
public sealed partial class NanoTaskCartridgeComponent : Component
{
/// <summary>
/// The list of tasks
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public List<NanoTaskItemAndId> Tasks = new();
/// <summary>
/// counter for generating task IDs
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public int Counter = 1;
/// <summary>
/// When the user can print again
/// </summary>
[DataField, AutoPausedField]
[DataField, AutoNetworkedField, AutoPausedField]
public TimeSpan NextPrintAllowedAfter = TimeSpan.Zero;
/// <summary>
@ -32,11 +32,3 @@ public sealed partial class NanoTaskCartridgeComponent : Component
[DataField]
public TimeSpan PrintDelay = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Component attached to the PDA a NanoTask cartridge is inserted into for interaction handling
/// </summary>
[RegisterComponent]
public sealed partial class NanoTaskInteractionComponent : Component
{
}

View file

@ -1,19 +1,14 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.CartridgeLoader;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Paper;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Server-side class implementing the core UI logic of NanoTask
/// </summary>
public sealed partial class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
public sealed partial class NanoTaskCartridgeSystem : EntitySystem
{
[Dependency] private CartridgeLoaderSystem _cartridgeLoader = default!;
[Dependency] private IGameTiming _timing = default!;
@ -27,36 +22,21 @@ public sealed partial class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSys
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
SubscribeLocalEvent<NanoTaskInteractionComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRelayedEvent<InteractUsingEvent>>(OnInteractUsing);
}
private void OnCartridgeRemoved(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRemovedEvent args)
private void OnInteractUsing(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRelayedEvent<InteractUsingEvent> args)
{
if (!_cartridgeLoader.HasProgram<NanoTaskCartridgeComponent>(args.Loader))
{
RemComp<NanoTaskInteractionComponent>(args.Loader);
}
}
if (!TryComp<NanoTaskPrintedComponent>(args.Args.Used, out var printed))
return;
private void OnInteractUsing(Entity<NanoTaskInteractionComponent> ent, ref InteractUsingEvent args)
{
if (!_cartridgeLoader.TryGetProgram<NanoTaskCartridgeComponent>(ent.Owner, out var uid, out var program))
{
return;
}
if (!TryComp<NanoTaskPrintedComponent>(args.Used, out var printed))
{
return;
}
if (printed.Task is NanoTaskItem item)
{
program.Tasks.Add(new(program.Counter++, printed.Task));
args.Handled = true;
Del(args.Used);
UpdateUiState(new Entity<NanoTaskCartridgeComponent>(uid.Value, program), ent.Owner);
ent.Comp.Tasks.Add(new(ent.Comp.Counter++, printed.Task));
args.Args.Handled = true;
PredictedQueueDel(args.Args.Used);
Dirty(ent);
UpdateUiState(ent, args.Loader);
}
}
@ -105,11 +85,14 @@ public sealed partial class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSys
switch (message.Payload)
{
case NanoTaskAddTask task:
{
if (!task.Item.Validate())
return;
ent.Comp.Tasks.Add(new(ent.Comp.Counter++, task.Item));
Dirty(ent);
break;
}
case NanoTaskUpdateTask task:
{
if (!task.Item.Data.Validate())
@ -118,23 +101,30 @@ public sealed partial class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSys
var idx = ent.Comp.Tasks.FindIndex(t => t.Id == task.Item.Id);
if (idx != -1)
ent.Comp.Tasks[idx] = task.Item;
Dirty(ent);
break;
}
case NanoTaskDeleteTask task:
{
ent.Comp.Tasks.RemoveAll(t => t.Id == task.Id);
Dirty(ent);
break;
}
case NanoTaskPrintTask task:
{
if (!task.Item.Validate())
return;
if (_timing.CurTime < ent.Comp.NextPrintAllowedAfter)
return;
ent.Comp.NextPrintAllowedAfter = _timing.CurTime + ent.Comp.PrintDelay;
var printed = Spawn("PaperNanoTaskItem", Transform(message.Actor).Coordinates);
var printed = PredictedSpawnAtPosition("PaperNanoTaskItem", Transform(message.Actor).Coordinates);
_hands.PickupOrDrop(message.Actor, printed);
_audio.PlayPvs(new SoundPathSpecifier("/Audio/Machines/printer.ogg"), ent.Owner);
SetupPrintedTask(printed, task.Item);
Dirty(ent);
break;
}
}
@ -142,7 +132,6 @@ public sealed partial class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSys
UpdateUiState(ent, GetEntity(args.LoaderUid));
}
private void UpdateUiState(Entity<NanoTaskCartridgeComponent> ent, EntityUid loaderUid)
{
var state = new NanoTaskUiState(ent.Comp.Tasks);

View file

@ -1,16 +1,16 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.GameStates;
namespace Content.Shared.CartridgeLoader.Cartridges;
/// <summary>
/// Component attached to a piece of paper to indicate that it was printed from NanoTask and can be inserted back into it
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class NanoTaskPrintedComponent : Component
{
/// <summary>
/// The task that this item holds
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public NanoTaskItem? Task;
}

View file

@ -17,7 +17,7 @@ public enum NanoTaskPriority : byte
/// The data relating to a single NanoTask item, but not its identifier
/// </summary>
[Serializable, NetSerializable, DataRecord]
public sealed partial class NanoTaskItem
public sealed partial class NanoTaskItem : IRobustCloneable<NanoTaskItem>
{
/// <summary>
/// The maximum length of the Description and TaskIsFor fields
@ -51,10 +51,16 @@ public sealed partial class NanoTaskItem
IsTaskDone = isTaskDone;
Priority = priority;
}
public bool Validate()
{
return Description.Length <= MaximumStringLength && TaskIsFor.Length <= MaximumStringLength;
}
public NanoTaskItem Clone()
{
return new(Description, TaskIsFor, IsTaskDone, Priority);
}
};
/// <summary>

View file

@ -1,24 +1,24 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class NetProbeCartridgeComponent : Component
{
/// <summary>
/// The list of probed network devices
/// </summary>
[DataField("probedDevices")]
[DataField, AutoNetworkedField]
public List<ProbedNetworkDevice> ProbedDevices = new();
/// <summary>
/// Limits the amount of devices that can be saved
/// </summary>
[DataField("maxSavedDevices")]
public int MaxSavedDevices { get; set; } = 9;
[DataField, AutoNetworkedField]
public int MaxSavedDevices = 9;
[DataField("soundScan")]
[DataField]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
}

View file

@ -1,61 +1,54 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public sealed partial class NetProbeCartridgeSystem : EntitySystem
{
[Dependency] private CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private SharedPopupSystem _popupSystem = default!;
[Dependency] private SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NetProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<NetProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<NetProbeCartridgeComponent, CartridgeRelayedEvent<AfterInteractEvent>>(AfterInteract);
}
/// <summary>
/// The <see cref="CartridgeAfterInteractEvent" /> gets relayed to this system if the cartridge loader is running
/// the NetProbe program and someone clicks on something with it. <br/>
/// <br/>
/// Saves name, address... etc. of the device that was clicked into a list on the component when the device isn't already present in that list
/// </summary>
private void AfterInteract(EntityUid uid, NetProbeCartridgeComponent component, CartridgeAfterInteractEvent args)
private void AfterInteract(EntityUid uid, NetProbeCartridgeComponent component, CartridgeRelayedEvent<AfterInteractEvent> args)
{
if (args.InteractEvent.Handled || !args.InteractEvent.CanReach || !args.InteractEvent.Target.HasValue)
if (args.Args.Handled || !args.Args.CanReach || !args.Args.Target.HasValue)
return;
var target = args.InteractEvent.Target.Value;
DeviceNetworkComponent? networkComponent = default;
var target = args.Args.Target.Value;
if (!Resolve(target, ref networkComponent, false))
if (!TryComp<DeviceNetworkComponent>(target, out var networkComponent))
return;
//Ceck if device is already present in list
// Check if device is already present in list
foreach (var probedDevice in component.ProbedDevices)
{
if (probedDevice.Address == networkComponent.Address)
return;
}
//Play scanning sound with slightly randomized pitch
//Why is there no NextFloat(float min, float max)???
var audioParams = AudioParams.Default.WithVolume(-2f).WithPitchScale((float)_random.Next(12, 21) / 10);
_audioSystem.PlayEntity(component.SoundScan, args.InteractEvent.User, target, audioParams);
_popupSystem.PopupCursor(Loc.GetString("net-probe-scan", ("device", target)), args.InteractEvent.User);
// Play scanning sound with slightly randomized pitch
// Why is there no NextFloat(float min, float max)???
var audioParams = AudioParams.Default.WithVolume(-2f).WithVariation(0.2f);
_audioSystem.PlayPredicted(component.SoundScan, target, args.Args.User, audioParams);
_popupSystem.PopupPredictedCursor(Loc.GetString("net-probe-scan", ("device", target)), args.Args.User);
//Limit the amount of saved probe results to 9
//This is hardcoded because the UI doesn't support a dynamic number of results
// Limit the amount of saved probe results to 9
// This is hardcoded because the UI doesn't support a dynamic number of results
if (component.ProbedDevices.Count >= component.MaxSavedDevices)
component.ProbedDevices.RemoveAt(0);
@ -67,6 +60,7 @@ public sealed partial class NetProbeCartridgeSystem : EntitySystem
);
component.ProbedDevices.Add(device);
Dirty(uid, component);
UpdateUiState(uid, args.Loader, component);
}
@ -84,6 +78,6 @@ public sealed partial class NetProbeCartridgeSystem : EntitySystem
return;
var state = new NetProbeUiState(component.ProbedDevices);
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
_cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, state);
}
}

View file

@ -1,6 +1,8 @@
namespace Content.Server.CartridgeLoader.Cartridges;
using Robust.Shared.GameStates;
[RegisterComponent]
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, NetworkedComponent]
public sealed partial class NewsReaderCartridgeComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]

View file

@ -1,11 +1,13 @@
namespace Content.Server.CartridgeLoader.Cartridges;
using Robust.Shared.GameStates;
[RegisterComponent]
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class NotekeeperCartridgeComponent : Component
{
/// <summary>
/// The list of notes that got written down
/// </summary>
[DataField("notes")]
[DataField, AutoNetworkedField]
public List<string> Notes = new();
}

View file

@ -1,18 +1,17 @@
using Content.Server.Administration.Logs;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
namespace Content.Server.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public sealed partial class NotekeeperCartridgeSystem : EntitySystem
{
[Dependency] private CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
[Dependency] private IAdminLogManager _adminLogger = default!;
[Dependency] private ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NotekeeperCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NotekeeperCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
}
@ -49,6 +48,7 @@ public sealed partial class NotekeeperCartridgeSystem : EntitySystem
$"{ToPrettyString(args.Actor)} removed a note from PDA: '{message.Note}' was contained on: {ToPrettyString(uid)}");
}
Dirty(uid, component);
UpdateUiState(uid, GetEntity(args.LoaderUid), component);
}
@ -58,6 +58,6 @@ public sealed partial class NotekeeperCartridgeSystem : EntitySystem
return;
var state = new NotekeeperUiState(component.Notes);
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
_cartridgeLoaderSystem.UpdateCartridgeUiState(loaderUid, state);
}
}

View file

@ -1,19 +0,0 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Shared.CartridgeLoader.Cartridges;
public abstract class SharedNanoTaskCartridgeSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeAddedEvent>(OnCartridgeAdded);
}
private void OnCartridgeAdded(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeAddedEvent args)
{
EnsureComp<NanoTaskInteractionComponent>(args.Loader);
}
}

View file

@ -0,0 +1,6 @@
using Robust.Shared.GameStates;
namespace Content.Shared.CartridgeLoader.Cartridges;
[RegisterComponent, NetworkedComponent]
public sealed partial class WantedListCartridgeComponent : Component;

View file

@ -1,133 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Containers;
namespace Content.Shared.CartridgeLoader;
public abstract partial class SharedCartridgeLoaderSystem : EntitySystem
{
public const string InstalledContainerId = "program-container";
[Dependency] private ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CartridgeLoaderComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CartridgeLoaderComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<CartridgeLoaderComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<CartridgeLoaderComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
}
private void OnComponentInit(EntityUid uid, CartridgeLoaderComponent loader, ComponentInit args)
{
_itemSlotsSystem.AddItemSlot(uid, CartridgeLoaderComponent.CartridgeSlotId, loader.CartridgeSlot);
}
/// <summary>
/// Marks installed program entities for deletion when the component gets removed
/// </summary>
private void OnComponentRemove(EntityUid uid, CartridgeLoaderComponent loader, ComponentRemove args)
{
_itemSlotsSystem.RemoveItemSlot(uid, loader.CartridgeSlot);
if (_container.TryGetContainer(uid, InstalledContainerId, out var cont))
_container.ShutdownContainer(cont);
}
protected virtual void OnItemInserted(EntityUid uid, CartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
{
UpdateAppearanceData(uid, loader);
}
protected virtual void OnItemRemoved(EntityUid uid, CartridgeLoaderComponent loader, EntRemovedFromContainerMessage args)
{
UpdateAppearanceData(uid, loader);
}
private void UpdateAppearanceData(EntityUid uid, CartridgeLoaderComponent loader)
{
_appearanceSystem.SetData(uid, CartridgeLoaderVisuals.CartridgeInserted, loader.CartridgeSlot.HasItem);
}
}
/// <summary>
/// Gets sent to program / cartridge entities when they get inserted or installed
/// </summary>
public sealed class CartridgeAddedEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public CartridgeAddedEvent(EntityUid loader)
{
Loader = loader;
}
}
/// <summary>
/// Gets sent to cartridge entities when they get ejected
/// </summary>
public sealed class CartridgeRemovedEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public CartridgeRemovedEvent(EntityUid loader)
{
Loader = loader;
}
}
/// <summary>
/// Gets sent to program / cartridge entities when they get activated
/// </summary>
/// <remarks>
/// Don't update the programs ui state in this events listener
/// </remarks>
public sealed class CartridgeActivatedEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public CartridgeActivatedEvent(EntityUid loader)
{
Loader = loader;
}
}
/// <summary>
/// Gets sent to program / cartridge entities when they get deactivated
/// </summary>
public sealed class CartridgeDeactivatedEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public CartridgeDeactivatedEvent(EntityUid loader)
{
Loader = loader;
}
}
/// <summary>
/// Gets sent to program / cartridge entities when the ui is ready to be updated by the cartridge.
/// </summary>
/// <remarks>
/// This is used for the initial ui state update because updating the ui in the activate event doesn't work
/// </remarks>
public sealed class CartridgeUiReadyEvent : EntityEventArgs
{
public readonly EntityUid Loader;
public CartridgeUiReadyEvent(EntityUid loader)
{
Loader = loader;
}
}
/// <summary>
/// Gets sent by the cartridge loader system to the cartridge loader entity so another system
/// can handle displaying the notification
/// </summary>
/// <param name="Message">The message to be displayed</param>
[ByRefEvent]
public record struct CartridgeLoaderNotificationSentEvent(string Header, string Message);

View file

@ -54,7 +54,8 @@
PDA-pen: !type:ContainerSlot {}
PDA-pai: !type:ContainerSlot {}
Cartridge-Slot: !type:ContainerSlot {}
program-container: !type:Container
preinstalled-program-container: !type:Container
removable-program-container: !type:Container
- type: ItemSlots
- type: Clothing
quickEquip: false
@ -79,11 +80,6 @@
range: 500
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
cartridgeSlot:
priority: -1
name: device-pda-slot-component-slot-name-cartridge
@ -92,6 +88,14 @@
whitelist:
components:
- Cartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- type: ActivatableUI
key: enum.PdaUiKey.Key
singleUser: true
@ -149,27 +153,30 @@
id: BaseSecurityPDA
abstract: true
components:
- type: CartridgeLoader
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- WantedListCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: WantedListCartridge
- type: entity
parent: BasePDA
id: BaseMedicalPDA
abstract: true
components:
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- MedTekCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: MedTekCartridge
- type: entity
parent: BasePDA
@ -414,9 +421,6 @@
whitelist:
components:
- Cartridge
preinstalled:
- CrewManifestCartridge
- NewsReaderCartridge
- type: Appearance
appearanceDataInit:
enum.PdaVisuals.PdaType:
@ -428,6 +432,12 @@
- type: Icon
state: pda-mime
- type: Muted # for random sentience event
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NewsReaderCartridge
- type: entity
name: chaplain PDA
@ -476,13 +486,15 @@
accentVColor: "#a23e3e"
- type: Icon
state: pda-qm
- type: CartridgeLoader
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- AstroNavCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: AstroNavCartridge
- type: entity
parent: BasePDA
@ -520,14 +532,15 @@
accentVColor: "#8900c9"
- type: Icon
state: pda-miner
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- AstroNavCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: AstroNavCartridge
- type: entity
parent: BasePDA
@ -870,14 +883,16 @@
accentHColor: "#447987"
- type: Icon
state: pda-hos
- type: CartridgeLoader
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- WantedListCartridge
- LogProbeCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: WantedListCartridge
- id: LogProbeCartridge
- type: entity
parent: BaseSecurityPDA
@ -939,17 +954,18 @@
borderColor: "#00842e"
- type: Icon
state: pda-centcom
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- MedTekCartridge
- WantedListCartridge
- LogProbeCartridge
- AstroNavCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: MedTekCartridge
- id: WantedListCartridge
- id: LogProbeCartridge
- id: AstroNavCartridge
- type: entity
parent: CentcomPDA
@ -965,17 +981,19 @@
scanDelay: 0
silent: true
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
notificationsEnabled: false
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- LogProbeCartridge
- WantedListCartridge
- MedTekCartridge
- AstroNavCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: LogProbeCartridge
- id: WantedListCartridge
- id: MedTekCartridge
- id: AstroNavCartridge
- type: Tag # Ignore Chameleon tags
tags:
- DoorBumpOpener
@ -1089,11 +1107,12 @@
borderColor: "#891417"
- type: Icon
state: pda-syndi
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- NotekeeperCartridge
- NanoTaskCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- type: entity
parent: [ SyndiPDA, BaseSyndicateContraband ]
@ -1126,12 +1145,13 @@
pda-syndi-corpsman
- type: Icon
state: pda-syndi-corpsman
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- NotekeeperCartridge
- NanoTaskCartridge
- MedTekCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: MedTekCartridge
- type: entity
parent: [ SyndiOperativePDA, BaseSyndicateContraband ]
@ -1169,17 +1189,18 @@
accentVColor: "#447987"
- type: Icon
state: pda-ert
- type: CartridgeLoader
uiKey: enum.PdaUiKey.Key
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- MedTekCartridge
- WantedListCartridge
- LogProbeCartridge
- AstroNavCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: MedTekCartridge
- id: WantedListCartridge
- id: LogProbeCartridge
- id: AstroNavCartridge
- type: entity
parent: ERTLeaderPDA
@ -1341,14 +1362,16 @@
borderColor: "#774705"
- type: Icon
state: pda-detective
- type: CartridgeLoader
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- WantedListCartridge
- LogProbeCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: WantedListCartridge
- id: LogProbeCartridge
- type: entity
parent: BaseMedicalPDA
@ -1369,14 +1392,16 @@
accentVColor: "#d7d7d0"
- type: Icon
state: pda-brigmedic
- type: CartridgeLoader
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NanoTaskCartridge
- NewsReaderCartridge
- WantedListCartridge
- MedTekCartridge
- type: EntityTableContainerFill
containers:
preinstalled-program-container: !type:AllSelector
children:
- id: CrewManifestCartridge
- id: NotekeeperCartridge
- id: NanoTaskCartridge
- id: NewsReaderCartridge
- id: WantedListCartridge
- id: MedTekCartridge
- type: entity
parent: ClownPDA