PDA UI refactor and cartridges (#11335)

* Work on cartridges

* Work on PDA UI

* Work on PDA UIs program list

* Work on PDA UI borders

* Add DeviceNetworkingComponent to the pda base prototype

* Fix submodule version

* Fix cartridge loader ui key

* Fix pda menu xaml

* Implement relaying ui messages

* Finish implementing the notekeeper cartridge

* Fix submodule version

* Fix errors from merging master

* Fix test failing

* Implement setting preinstalled programs

* Add some documentation to CartridgeLoaderSystem

* Add more doc comments

* Add localization to program names

* Implement review suggestions

* Fix background programs receiving events twice when active
This commit is contained in:
Julian Giebel
2022-11-08 21:00:20 +01:00
committed by GitHub
parent 1151ca42e5
commit e11cf969fa
79 changed files with 2323 additions and 94 deletions

View File

@@ -0,0 +1,136 @@
using Content.Shared.CartridgeLoader;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader;
public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager? _entityManager = default!;
private EntityUid? _activeProgram;
private CartridgeUI? _activeCartridgeUI;
private Control? _activeUiFragment;
protected CartridgeLoaderBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not CartridgeLoaderUiState loaderUiState)
{
_activeCartridgeUI?.UpdateState(state);
return;
}
var programs = GetCartridgeComponents(loaderUiState.Programs);
UpdateAvailablePrograms(programs);
_activeProgram = loaderUiState.ActiveUI;
var ui = RetrieveCartridgeUI(loaderUiState.ActiveUI);
var comp = RetrieveCartridgeComponent(loaderUiState.ActiveUI);
var control = ui?.GetUIFragmentRoot();
//Prevent the same UI fragment from getting disposed and attached multiple times
if (_activeUiFragment?.GetType() == control?.GetType())
return;
if (_activeUiFragment is not null)
DetachCartridgeUI(_activeUiFragment);
if (control is not null && _activeProgram.HasValue)
{
AttachCartridgeUI(control, Loc.GetString(comp?.ProgramName ?? "default-program-name"));
SendCartridgeUiReadyEvent(_activeProgram.Value);
}
_activeCartridgeUI = ui;
_activeUiFragment?.Dispose();
_activeUiFragment = control;
}
protected void ActivateCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(cartridgeUid, CartridgeUiMessageAction.Activate);
SendMessage(message);
}
protected void DeactivateActiveCartridge()
{
if (!_activeProgram.HasValue)
return;
var message = new CartridgeLoaderUiMessage(_activeProgram.Value, CartridgeUiMessageAction.Deactivate);
SendMessage(message);
}
protected void InstallCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(cartridgeUid, CartridgeUiMessageAction.Install);
SendMessage(message);
}
protected void UninstallCartridge(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(cartridgeUid, CartridgeUiMessageAction.Uninstall);
SendMessage(message);
}
private List<(EntityUid, CartridgeComponent)> GetCartridgeComponents(List<EntityUid> programs)
{
var components = new List<(EntityUid, CartridgeComponent)>();
foreach (var program in programs)
{
var component = RetrieveCartridgeComponent(program);
if (component is not null)
components.Add((program, component));
}
return components;
}
/// <summary>
/// The implementing ui needs to add the passed ui fragment as a child to itself
/// </summary>
protected abstract void AttachCartridgeUI(Control cartridgeUIFragment, string? title);
/// <summary>
/// The implementing ui needs to remove the passed ui from itself
/// </summary>
protected abstract void DetachCartridgeUI(Control cartridgeUIFragment);
protected abstract void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs);
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_activeUiFragment?.Dispose();
}
protected CartridgeComponent? RetrieveCartridgeComponent(EntityUid? cartridgeUid)
{
return _entityManager?.GetComponentOrNull<CartridgeComponent>(cartridgeUid);
}
private void SendCartridgeUiReadyEvent(EntityUid cartridgeUid)
{
var message = new CartridgeLoaderUiMessage(cartridgeUid, CartridgeUiMessageAction.UIReady);
SendMessage(message);
}
private CartridgeUI? RetrieveCartridgeUI(EntityUid? cartridgeUid)
{
var component = _entityManager?.GetComponentOrNull<CartridgeUiComponent>(cartridgeUid);
component?.Ui?.Setup(this);
return component?.Ui;
}
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.CartridgeLoader;
namespace Content.Client.CartridgeLoader;
public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
//Empty client system for component replication
}
[RegisterComponent]
[ComponentReference(typeof(SharedCartridgeLoaderComponent))]
public sealed class CartridgeLoaderComponent : SharedCartridgeLoaderComponent
{
}

View File

@@ -0,0 +1,25 @@
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader;
/// <summary>
/// Cartridge ui fragments need to inherit this class. The subclass is then used in yaml to tell the cartridge loader ui to use it as the cartridges ui fragment.
/// </summary>
/// <example>
/// This is an example from the yaml definition from the notekeeper ui
/// <code>
/// - type: CartridgeUi
/// ui: !type:NotekeeperUi
/// </code>
/// </example>
[ImplicitDataDefinitionForInheritors]
public abstract class CartridgeUI
{
public abstract Control GetUIFragmentRoot();
public abstract void Setup(BoundUserInterface userInterface);
public abstract void UpdateState(BoundUserInterfaceState state);
}

View File

@@ -0,0 +1,44 @@
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Content.Client.CartridgeLoader;
/// <summary>
/// Boilerplate serializer for defining the ui fragment used for a cartridge in yaml
/// </summary>
/// <example>
/// This is an example from the yaml definition from the notekeeper ui
/// <code>
/// - type: CartridgeUi
/// ui: !type:NotekeeperUi
/// </code>
/// </example>
public sealed class CartridgeUISerializer : ITypeSerializer<CartridgeUI, ValueDataNode>
{
public ValidationNode Validate(ISerializationManager serializationManager, ValueDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
return serializationManager.ValidateNode<CartridgeUI>(node, context);
}
public CartridgeUI Read(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies,
bool skipHook, ISerializationContext? context = null, CartridgeUI? value = default)
{
return serializationManager.Read(node, context, skipHook, value);
}
public CartridgeUI Copy(ISerializationManager serializationManager, CartridgeUI source, CartridgeUI target, bool skipHook,
ISerializationContext? context = null)
{
return serializationManager.Copy(source, context, skipHook);
}
public DataNode Write(ISerializationManager serializationManager, CartridgeUI value, IDependencyCollection dependencies,
bool alwaysWrite = false, ISerializationContext? context = null)
{
return serializationManager.WriteValue(value, alwaysWrite, context);
}
}

View File

@@ -0,0 +1,14 @@

namespace Content.Client.CartridgeLoader;
/// <summary>
/// The component used for defining which ui fragment to use for a cartridge
/// </summary>
/// <seealso cref="CartridgeUI"/>
/// <seealso cref="CartridgeUISerializer"/>
[RegisterComponent]
public sealed class CartridgeUiComponent : Component
{
[DataField("ui", true, customTypeSerializer: typeof(CartridgeUISerializer))]
public CartridgeUI? Ui = default;
}

View File

@@ -0,0 +1,39 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed class NotekeeperUi : CartridgeUI
{
private NotekeeperUiFragment? _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface)
{
_fragment = new NotekeeperUiFragment();
_fragment.OnNoteRemoved += note => SendNotekeeperMessage(NotekeeperUiAction.Remove, note, userInterface);
_fragment.OnNoteAdded += note => SendNotekeeperMessage(NotekeeperUiAction.Add, note, userInterface);
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not NotekeeperUiState notekeepeerState)
return;
_fragment?.UpdateState(notekeepeerState.Notes);
}
private void SendNotekeeperMessage(NotekeeperUiAction action, string note, BoundUserInterface userInterface)
{
var notekeeperMessage = new NotekeeperUiMessageEvent(action, note);
var message = new CartridgeUiMessage(notekeeperMessage);
userInterface.SendMessage(message);
}
}

View File

@@ -0,0 +1,10 @@
<cartridges:NotekeeperUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns="https://spacestation14.io" Margin="1 0 2 0">
<PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical" Name="MessageContainer" HorizontalExpand="True" VerticalExpand="True"/>
</ScrollContainer>
<LineEdit Name="Input" HorizontalExpand="True" SetHeight="32"/>
</BoxContainer>
</cartridges:NotekeeperUiFragment>

View File

@@ -0,0 +1,62 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NotekeeperUiFragment : BoxContainer
{
public event Action<string>? OnNoteAdded;
public event Action<string>? OnNoteRemoved;
public NotekeeperUiFragment()
{
RobustXamlLoader.Load(this);
Orientation = LayoutOrientation.Vertical;
HorizontalExpand = true;
VerticalExpand = true;
Input.OnTextEntered += _ =>
{
AddNote(Input.Text);
OnNoteAdded?.Invoke(Input.Text);
Input.Clear();
};
UpdateState(new List<string>());
}
public void UpdateState(List<string> notes)
{
MessageContainer.RemoveAllChildren();
foreach (var note in notes)
{
AddNote(note);
}
}
private void AddNote(string note)
{
var row = new BoxContainer();
row.HorizontalExpand = true;
row.Orientation = LayoutOrientation.Horizontal;
row.Margin = new Thickness(4);
var label = new Label();
label.Text = note;
label.HorizontalExpand = true;
label.ClipText = true;
var removeButton = new TextureButton();
removeButton.AddStyleClass("windowCloseButton");
removeButton.OnPressed += _ => OnNoteRemoved?.Invoke(label.Text);
row.AddChild(label);
row.AddChild(removeButton);
MessageContainer.AddChild(row);
}
}

View File

@@ -0,0 +1,19 @@
namespace Content.Client.PDA;
/// <summary>
/// Used for specifying the pda windows border colors
/// </summary>
[RegisterComponent]
public sealed class PDABorderColorComponent : Component
{
[DataField("borderColor", required: true)]
public string? BorderColor;
[DataField("accentHColor")]
public string? AccentHColor;
[DataField("accentVColor")]
public string? AccentVColor;
}

View File

@@ -1,17 +1,20 @@
using Content.Client.Message; using Content.Client.CartridgeLoader;
using Content.Shared.CartridgeLoader;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.CrewManifest; using Content.Shared.CrewManifest;
using Content.Shared.PDA; using Content.Shared.PDA;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
namespace Content.Client.PDA namespace Content.Client.PDA
{ {
[UsedImplicitly] [UsedImplicitly]
public sealed class PDABoundUserInterface : BoundUserInterface public sealed class PDABoundUserInterface : CartridgeLoaderBoundUserInterface
{ {
[Dependency] private readonly IEntityManager? _entityManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IConfigurationManager _configManager = default!;
private PDAMenu? _menu; private PDAMenu? _menu;
@@ -67,54 +70,52 @@ namespace Content.Client.PDA
SendMessage(new PDAShowRingtoneMessage()); SendMessage(new PDAShowRingtoneMessage());
}; };
_menu.OnProgramItemPressed += ActivateCartridge;
_menu.OnInstallButtonPressed += InstallCartridge;
_menu.OnUninstallButtonPressed += UninstallCartridge;
_menu.ProgramCloseButton.OnPressed += _ => DeactivateActiveCartridge();
var borderColorComponent = GetBorderColorComponent();
if (borderColorComponent == null)
return;
_menu.BorderColor = borderColorComponent.BorderColor;
_menu.AccentHColor = borderColorComponent.AccentHColor;
_menu.AccentVColor = borderColorComponent.AccentVColor;
} }
protected override void UpdateState(BoundUserInterfaceState state) protected override void UpdateState(BoundUserInterfaceState state)
{ {
base.UpdateState(state); base.UpdateState(state);
if (_menu == null) if (state is not PDAUpdateState updateState)
{
return; return;
}
switch (state) _menu?.UpdateState(updateState);
{
case PDAUpdateState msg:
{
_menu.FlashLightToggleButton.Pressed = msg.FlashlightEnabled;
if (msg.PDAOwnerInfo.ActualOwnerName != null)
{
_menu.PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("ActualOwnerName", msg.PDAOwnerInfo.ActualOwnerName)));
}
if (msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null)
{
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
("Owner",msg.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
("JobTitle",msg.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
}
else
{
_menu.IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui-blank"));
}
_menu.StationNameLabel.SetMarkup(Loc.GetString("comp-pda-ui-station", ("Station",msg.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
_menu.EjectIdButton.Visible = msg.PDAOwnerInfo.IdOwner != null || msg.PDAOwnerInfo.JobTitle != null;
_menu.EjectPenButton.Visible = msg.HasPen;
_menu.ActivateUplinkButton.Visible = msg.HasUplink;
_menu.ActivateMusicButton.Visible = msg.CanPlayMusic;
break;
}
}
} }
protected override void AttachCartridgeUI(Control cartridgeUIFragment, string? title)
{
_menu?.ProgramView.AddChild(cartridgeUIFragment);
_menu?.ToProgramView(title ?? Loc.GetString("comp-pda-io-program-fallback-title"));
}
protected override void DetachCartridgeUI(Control cartridgeUIFragment)
{
if (_menu is null)
return;
_menu.ToHomeScreen();
_menu.HideProgramHeader();
_menu.ProgramView.RemoveChild(cartridgeUIFragment);
}
protected override void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs)
{
_menu?.UpdateAvailablePrograms(programs);
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
@@ -123,5 +124,10 @@ namespace Content.Client.PDA
_menu?.Dispose(); _menu?.Dispose();
} }
private PDABorderColorComponent? GetBorderColorComponent()
{
return _entityManager?.GetComponentOrNull<PDABorderColorComponent>(Owner.Owner);
}
} }
} }

View File

@@ -1,12 +1,32 @@
<DefaultWindow xmlns="https://spacestation14.io" <pda:PDAWindow xmlns="https://spacestation14.io"
Title="{Loc 'comp-pda-ui-menu-title'}" xmlns:pda="clr-namespace:Content.Client.PDA"
MinSize="576 256" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="576 256"> MinSize="576 450"
<TabContainer Name="MasterTabContainer"> SetSize="576 450">
<!-- This: (Margin="1 1 3 0") is necessary so the navigation bar doesn't sticks into the black content border. -->
<BoxContainer Name="NavigationBar" HorizontalExpand="True" MinHeight="32" Margin="1 1 3 0">
<pda:PDANavigationButton Name="HomeButton" SetWidth="32" CurrentTabBorderThickness="0 0 2 0" IsCurrent="True"/>
<pda:PDANavigationButton Name="ProgramListButton" Access="Public" MinWidth="100" LabelText="{Loc 'comp-pda-io-program-list-button'}"/>
<pda:PDANavigationButton Name="SettingsButton" MinWidth="100" LabelText="{Loc 'comp-pda-io-settings-button'}"/>
<pda:PDANavigationButton Name="ProgramTitle" Access="Public" BorderThickness="0 0 0 2" CurrentTabBorderThickness="2 0 0 2"
ActiveBgColor="#202023" Visible="False"/>
<pda:PDANavigationButton HorizontalExpand="True"/>
<pda:PDANavigationButton Name="ProgramCloseButton" Access="Public" IconScale="0.5 0.5" BorderThickness="0 0 2 2"
Visible="False" IsActive="False" SetWidth="32"/>
<pda:PDANavigationButton Name="FlashLightToggleButton" Access="Public" ToggleMode="True" ActiveFgColor="#EAEFBB" SetWidth="32"/>
<pda:PDANavigationButton Name="EjectPenButton" Access="Public" SetWidth="32"/>
<pda:PDANavigationButton Name="EjectIdButton" Access="Public" SetWidth="32"/>
</BoxContainer>
<BoxContainer Name="ViewContainer" HorizontalExpand="True" VerticalExpand="True" Access="Public">
<BoxContainer Orientation="Vertical" <BoxContainer Orientation="Vertical"
VerticalExpand="True" VerticalExpand="True"
HorizontalExpand="True" HorizontalExpand="True"
MinSize="50 50"> MinSize="50 50"
Margin="8">
<RichTextLabel Name="PdaOwnerLabel" Access="Public" /> <RichTextLabel Name="PdaOwnerLabel" Access="Public" />
<RichTextLabel Name="IdInfoLabel" <RichTextLabel Name="IdInfoLabel"
Access="Public" Access="Public"
@@ -16,37 +36,53 @@
<RichTextLabel Name="StationNameLabel" <RichTextLabel Name="StationNameLabel"
Access="Public" Access="Public"
HorizontalExpand="True" /> HorizontalExpand="True" />
<Button Name="EjectIdButton"
Access="Public"
Text="{Loc 'comp-pda-ui-eject-id-button'}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Button Name="EjectPenButton"
Access="Public"
Text="{Loc 'comp-pda-ui-eject-pen-button'}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Button Name="AccessRingtoneButton"
Access="Public"
Text="{Loc 'comp-pda-ui-ringtone-button'}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</BoxContainer> </BoxContainer>
</PanelContainer> </PanelContainer>
<Button Name="FlashLightToggleButton"
Access="Public"
Text="{Loc 'comp-pda-ui-toggle-flashlight-button'}"
ToggleMode="True" />
<Button Name="CrewManifestButton"
Access="Public"
Text="{Loc 'crew-manifest-button-label'}"
Visible="False" />
<Button Name="ActivateUplinkButton"
Access="Public"
Text="{Loc 'pda-bound-user-interface-uplink-tab-title'}" />
<Button Name="ActivateMusicButton"
Access="Public"
Text="{Loc 'pda-bound-user-interface-music-button'}" />
</BoxContainer> </BoxContainer>
</TabContainer> <ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
</DefaultWindow> <BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="50 50"
Name="ProgramList"
Margin="4"/>
</ScrollContainer>
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="50 50"
Name="Settings">
<pda:PDASettingsButton Name="AccessRingtoneButton"
Access="Public"
Text="{Loc 'comp-pda-ui-ringtone-button'}"
Description="{Loc 'comp-pda-ui-ringtone-button-description'}"/>
<pda:PDASettingsButton Name="CrewManifestButton"
Access="Public"
Text="{Loc 'crew-manifest-button-label'}"
Description="{Loc 'crew-manifest-button-description'}"
Visible="False" />
<pda:PDASettingsButton Name="ActivateUplinkButton"
Access="Public"
Text="{Loc 'pda-bound-user-interface-uplink-tab-title'}"
Description="{Loc 'pda-bound-user-interface-uplink-tab-description'}"/>
<pda:PDASettingsButton Name="ActivateMusicButton"
Access="Public"
Text="{Loc 'pda-bound-user-interface-music-button'}"
Description="{Loc 'pda-bound-user-interface-music-button-description'}"/>
</BoxContainer>
</ScrollContainer>
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Name="ProgramView"
Access="Public">
</BoxContainer>
</BoxContainer>
<BoxContainer Name="ContentFooter" HorizontalExpand="True" SetHeight="28" Margin="1 0 2 1">
<controls:StripeBack HasBottomEdge="False" HasMargins="False" HorizontalExpand="True">
<Label Text="Robust#OS ™" VerticalAlignment="Center" Margin="6 0" StyleClasses="PDAContentFooterText"/>
<Label Name="AddressLabel" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="6 0" StyleClasses="PDAContentFooterText"/>
</controls:StripeBack>
</BoxContainer>
</pda:PDAWindow>

View File

@@ -1,18 +1,252 @@
using Robust.Client.AutoGenerated; using Content.Client.Message;
using Robust.Client.UserInterface.CustomControls; using Content.Shared.CartridgeLoader;
using Content.Shared.PDA;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization; using Robust.Shared.Utility;
namespace Content.Client.PDA namespace Content.Client.PDA
{ {
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class PDAMenu : DefaultWindow public sealed partial class PDAMenu : PDAWindow
{ {
public const int HomeView = 0;
public const int ProgramListView = 1;
public const int SettingsView = 2;
public const int ProgramContentView = 3;
private int _currentView = 0;
public event Action<EntityUid>? OnProgramItemPressed;
public event Action<EntityUid>? OnUninstallButtonPressed;
public event Action<EntityUid>? OnInstallButtonPressed;
public PDAMenu() public PDAMenu()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle(0, Loc.GetString("pda-bound-user-interface-main-menu-tab-title")); ViewContainer.OnChildAdded += control => control.Visible = false;
HomeButton.IconTexture = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/home.png"));
FlashLightToggleButton.IconTexture = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/light.png"));
EjectPenButton.IconTexture = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/pencil.png"));
EjectIdButton.IconTexture = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/eject.png"));
ProgramCloseButton.IconTexture = new SpriteSpecifier.Texture(new ResourcePath("/Textures/Interface/Nano/cross.svg.png"));
HomeButton.OnPressed += _ => ToHomeScreen();
ProgramListButton.OnPressed += _ =>
{
HomeButton.IsCurrent = false;
ProgramListButton.IsCurrent = true;
SettingsButton.IsCurrent = false;
ProgramTitle.IsCurrent = false;
ChangeView(ProgramListView);
};
SettingsButton.OnPressed += _ =>
{
HomeButton.IsCurrent = false;
ProgramListButton.IsCurrent = false;
SettingsButton.IsCurrent = true;
ProgramTitle.IsCurrent = false;
ChangeView(SettingsView);
};
ProgramTitle.OnPressed += _ =>
{
HomeButton.IsCurrent = false;
ProgramListButton.IsCurrent = false;
SettingsButton.IsCurrent = false;
ProgramTitle.IsCurrent = true;
ChangeView(ProgramContentView);
};
ProgramCloseButton.OnPressed += _ =>
{
HideProgramHeader();
ToHomeScreen();
};
HideAllViews();
ToHomeScreen();
}
public void UpdateState(PDAUpdateState state)
{
FlashLightToggleButton.IsActive = state.FlashlightEnabled;
if (state.PDAOwnerInfo.ActualOwnerName != null)
{
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
("ActualOwnerName", state.PDAOwnerInfo.ActualOwnerName)));
}
if (state.PDAOwnerInfo.IdOwner != null || state.PDAOwnerInfo.JobTitle != null)
{
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui",
("Owner",state.PDAOwnerInfo.IdOwner ?? Loc.GetString("comp-pda-ui-unknown")),
("JobTitle",state.PDAOwnerInfo.JobTitle ?? Loc.GetString("comp-pda-ui-unassigned"))));
}
else
{
IdInfoLabel.SetMarkup(Loc.GetString("comp-pda-ui-blank"));
}
StationNameLabel.SetMarkup(Loc.GetString("comp-pda-ui-station", ("Station",state.StationName ?? Loc.GetString("comp-pda-ui-unknown"))));
AddressLabel.Text = state.Address?.ToUpper() ?? " - ";
EjectIdButton.IsActive = state.PDAOwnerInfo.IdOwner != null || state.PDAOwnerInfo.JobTitle != null;
EjectPenButton.IsActive = state.HasPen;
ActivateUplinkButton.Visible = state.HasUplink;
ActivateMusicButton.Visible = state.CanPlayMusic;
}
public void UpdateAvailablePrograms(List<(EntityUid, CartridgeComponent)> programs)
{
ProgramList.RemoveAllChildren();
if (programs.Count == 0)
{
ProgramList.AddChild(new Label()
{
Text = Loc.GetString("comp-pda-io-no-programs-available"),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
VerticalExpand = true
});
return;
}
var row = CreateProgramListRow();
var itemCount = 1;
ProgramList.AddChild(row);
foreach (var (uid, component) in programs)
{
//Create a new row every second program item starting from the first
if (itemCount % 2 != 0)
{
row = CreateProgramListRow();
ProgramList.AddChild(row);
}
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++;
}
//Add a filler item to the last row when it only contains one item
if (itemCount % 2 == 0)
row.AddChild(new Control() { HorizontalExpand = true });
}
/// <summary>
/// Changes the current view to the home screen (view 0) and sets the tabs `IsCurrent` flag accordingly
/// </summary>
public void ToHomeScreen()
{
HomeButton.IsCurrent = true;
ProgramListButton.IsCurrent = false;
SettingsButton.IsCurrent = false;
ProgramTitle.IsCurrent = false;
ChangeView(HomeView);
}
/// <summary>
/// Hides the program title and close button.
/// </summary>
public void HideProgramHeader()
{
ProgramTitle.IsCurrent = false;
ProgramTitle.Visible = false;
ProgramCloseButton.Visible = false;
ProgramListButton.Visible = true;
SettingsButton.Visible = true;
}
/// <summary>
/// Changes the current view to the program content view (view 3), sets the program title and sets the tabs `IsCurrent` flag accordingly
/// </summary>
public void ToProgramView(string title)
{
HomeButton.IsCurrent = false;
ProgramListButton.IsCurrent = false;
SettingsButton.IsCurrent = false;
ProgramTitle.IsCurrent = false;
ProgramTitle.IsCurrent = true;
ProgramTitle.Visible = true;
ProgramCloseButton.Visible = true;
ProgramListButton.Visible = false;
SettingsButton.Visible = false;
ProgramTitle.LabelText = title;
ChangeView(ProgramContentView);
}
/// <summary>
/// Changes the current view to the given view number
/// </summary>
public void ChangeView(int view)
{
if (ViewContainer.ChildCount <= view)
return;
ViewContainer.GetChild(_currentView).Visible = false;
ViewContainer.GetChild(view).Visible = true;
_currentView = view;
}
private BoxContainer CreateProgramListRow()
{
return new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true
};
}
private void HideAllViews()
{
var views = ViewContainer.Children;
foreach (var view in views)
{
view.Visible = false;
}
} }
} }
} }

View File

@@ -0,0 +1,5 @@
<pda:PDANavigationButton xmlns="https://spacestation14.io" xmlns:pda="clr-namespace:Content.Client.PDA">
<PanelContainer Name="Background"/>
<AnimatedTextureRect Margin="0 0 0 2" Visible="False" Name="Icon" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Label Visible="True" Name="Label" Margin="8 0 8 2" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</pda:PDANavigationButton>

View File

@@ -0,0 +1,108 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.PDA;
[GenerateTypedNameReferences]
public sealed partial class PDANavigationButton : ContainerButton
{
private bool _isCurrent;
private bool _isActive = true;
private Thickness _borderThickness = new(0, 0, 0, 2);
private Thickness _currentTabBorderThickness = new(2, 0, 2, 0);
private readonly StyleBoxFlat _styleBox = new()
{
BackgroundColor = Color.FromHex("#202023"),
BorderColor = Color.FromHex("#5a5a5a"),
BorderThickness = new Thickness(0, 0, 0, 2)
};
public string InactiveBgColor { get; set; } = "#202023";
public string ActiveBgColor { get; set; } = "#25252a";
public string InactiveFgColor { get; set; } = "#5a5a5a";
public string ActiveFgColor { get; set; } = "#FFFFFF";
public SpriteSpecifier? IconTexture
{
set
{
Icon.Visible = value != null;
Label.Visible = value == null;
if (value is not null)
Icon.SetFromSpriteSpecifier(value);
}
}
public Vector2 IconScale
{
get => Icon.DisplayRect.TextureScale;
set => Icon.DisplayRect.TextureScale = value;
}
public string? LabelText
{
get => Label.Text;
set => Label.Text = value;
}
/// <summary>
/// Sets the border thickness when the tab is not the currently active one
/// </summary>
public Thickness BorderThickness
{
get => _borderThickness;
set
{
_borderThickness = value;
_styleBox.BorderThickness = _isCurrent ? _currentTabBorderThickness : value;
}
}
/// <summary>
/// Sets the border thickness when this tab is the currently active tab
/// </summary>
public Thickness CurrentTabBorderThickness
{
get => _currentTabBorderThickness;
set
{
_currentTabBorderThickness = value;
_styleBox.BorderThickness = _isCurrent ? value : _borderThickness;
}
}
public bool IsCurrent
{
get => _isCurrent;
set
{
_isCurrent = value;
_styleBox.BackgroundColor = Color.FromHex(value ? ActiveBgColor : InactiveBgColor);
_styleBox.BorderThickness = value ? CurrentTabBorderThickness : BorderThickness;
}
}
public bool IsActive
{
get => _isActive;
set
{
_isActive = value;
Icon.Modulate = Color.FromHex(value ? ActiveFgColor : InactiveFgColor);
Label.FontColorOverride = Color.FromHex(value ? ActiveFgColor : InactiveFgColor);
}
}
public PDANavigationButton()
{
RobustXamlLoader.Load(this);
Background.PanelOverride = _styleBox;
}
}

View File

@@ -0,0 +1,17 @@
<pda:PDAProgramItem 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"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True">
<Label Name="ProgramName" Access="Public" 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>
</BoxContainer>
</BoxContainer>
</pda:PDAProgramItem>

View File

@@ -0,0 +1,42 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
namespace Content.Client.PDA;
[GenerateTypedNameReferences]
public sealed partial class PDAProgramItem : ContainerButton
{
public const string StylePropertyBgColor = "backgroundColor";
public const string NormalBgColor = "#313138";
public const string HoverColor = "#3E6C45";
private readonly StyleBoxFlat _styleBox = new()
{
BackgroundColor = Color.FromHex("#25252a"),
};
public Color BackgroundColor
{
get => _styleBox.BackgroundColor;
set => _styleBox.BackgroundColor = value;
}
public PDAProgramItem()
{
RobustXamlLoader.Load(this);
Panel.PanelOverride = _styleBox;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (TryGetStyleProperty<Color>(StylePropertyBgColor, out var bgColor))
BackgroundColor = bgColor;
}
}

View File

@@ -0,0 +1,11 @@
<pda:PDASettingsButton xmlns="https://spacestation14.io"
xmlns:pda="clr-namespace:Content.Client.PDA"
HorizontalExpand="True"
MinHeight="48"
Margin="5 4 6 0">
<PanelContainer Name="Panel"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="8 4 0 4">
<Label Name="OptionName"/>
<Label Name="OptionDescription"/>
</BoxContainer>
</pda:PDASettingsButton>

View File

@@ -0,0 +1,69 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.PDA;
[GenerateTypedNameReferences]
public sealed partial class PDASettingsButton : ContainerButton
{
public const string StylePropertyFgColor = "foregroundColor";
public const string StylePropertyBgColor = "backgroundColor";
public const string NormalBgColor = "#313138";
public const string HoverColor = "#3E6C45";
public const string PressedColor = "#3E6C45";
public const string DisabledFgColor = "#5a5a5a";
public const string EnabledFgColor = "#FFFFFF";
private readonly StyleBoxFlat _styleBox = new()
{
BackgroundColor = Color.FromHex("#25252a")
};
public string? Text
{
get => OptionName.Text;
set => OptionName.Text = value;
}
public string? Description
{
get => OptionDescription.Text;
set => OptionDescription.Text = value;
}
public Color BackgroundColor
{
get => _styleBox.BackgroundColor;
set => _styleBox.BackgroundColor = value;
}
public Color? ForegroundColor
{
get => OptionName.FontColorOverride;
set
{
OptionName.FontColorOverride = value;
OptionDescription.FontColorOverride = value;
}
}
public PDASettingsButton()
{
RobustXamlLoader.Load(this);
Panel.PanelOverride = _styleBox;
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (TryGetStyleProperty<Color>(StylePropertyBgColor, out var bgColor))
BackgroundColor = bgColor;
if (TryGetStyleProperty<Color>(StylePropertyFgColor, out var fgColor))
ForegroundColor = fgColor;
}
}

View File

@@ -0,0 +1,31 @@
<pda:PDAWindow xmlns="https://spacestation14.io"
xmlns:pda="clr-namespace:Content.Client.PDA"
MouseFilter="Stop">
<PanelContainer Name="Background" Access="Public" StyleClasses="PDABackgroundRect" />
<!-- The negative markin fixes a gap between the window edges and the decorative panel -->
<PanelContainer Name="AccentH" Margin="-1 170 -2 170" Access="Public" StyleClasses="PDABackground" />
<PanelContainer Name="AccentV" Margin="220 -1 220 -1" Access="Public" StyleClasses="PDABackground" />
<PanelContainer Name="Border" StyleClasses="PDABorderRect" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<!--Heading-->
<BoxContainer SetHeight="26" Margin="4 2 8 0" Orientation="Horizontal">
<Control HorizontalExpand="True"/>
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
VerticalAlignment="Center" Margin="0 4 4 0"/>
</BoxContainer>
<!--Content-->
<Control Margin="18 0" RectClipContent="True" VerticalExpand="true"
HorizontalExpand="True">
<PanelContainer Name="ContentBorder" StyleClasses="PDABackground"/>
<Control Margin="3 3">
<PanelContainer Name="ContentBackground" StyleClasses="PDAContentBackground"/>
<BoxContainer Access="Public" Name="ContentsContainer" Orientation="Vertical" StyleClasses="PDAContent"/>
</Control>
</Control>
<!--Footer-->
<BoxContainer Orientation="Horizontal" SetHeight="28">
<Label Text="Personal Digital Assistant" StyleClasses="PDAWindowFooterText" Margin="32 0 0 6"/>
</BoxContainer>
</BoxContainer>
</pda:PDAWindow>

View File

@@ -0,0 +1,56 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.PDA;
[Virtual]
[GenerateTypedNameReferences]
public partial class PDAWindow : BaseWindow
{
public string? BorderColor
{
get => Background.ActualModulateSelf.ToHex();
set => Background.ModulateSelfOverride = Color.FromHex(value, Color.White);
}
public string? AccentHColor
{
get => AccentH.ActualModulateSelf.ToHex();
set
{
AccentH.ModulateSelfOverride = Color. FromHex(value, Color.White);
AccentH.Visible = value != null;
}
}
public string? AccentVColor
{
get => AccentV.ActualModulateSelf.ToHex();
set
{
AccentV.ModulateSelfOverride = Color. FromHex(value, Color.White);
AccentV.Visible = value != null;
}
}
public PDAWindow()
{
RobustXamlLoader.Load(this);
CloseButton.OnPressed += _ => Close();
XamlChildren = ContentsContainer.Children;
AccentH.Visible = false;
AccentV.Visible = false;
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
}

View File

@@ -38,6 +38,7 @@ namespace Content.Client.Stylesheets
protected StyleBoxTexture BaseButtonSquare { get; } protected StyleBoxTexture BaseButtonSquare { get; }
protected StyleBoxTexture BaseAngleRect { get; } protected StyleBoxTexture BaseAngleRect { get; }
protected StyleBoxTexture AngleBorderRect { get; }
protected StyleBase(IResourceCache resCache) protected StyleBase(IResourceCache resCache)
{ {
@@ -114,6 +115,12 @@ namespace Content.Client.Stylesheets
}; };
BaseAngleRect.SetPatchMargin(StyleBox.Margin.All, 10); BaseAngleRect.SetPatchMargin(StyleBox.Margin.All, 10);
AngleBorderRect = new StyleBoxTexture
{
Texture = resCache.GetTexture("/Textures/Interface/Nano/geometric_panel_border.svg.96dpi.png"),
};
AngleBorderRect.SetPatchMargin(StyleBox.Margin.All, 10);
var vScrollBarGrabberNormal = new StyleBoxFlat var vScrollBarGrabberNormal = new StyleBoxFlat
{ {
BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = DefaultGrabberSize, BackgroundColor = Color.Gray.WithAlpha(0.35f), ContentMarginLeftOverride = DefaultGrabberSize,

View File

@@ -1,9 +1,8 @@
using System.Linq; using System.Linq;
using Content.Client.Actions.UI;
using Content.Client.ContextMenu.UI; using Content.Client.ContextMenu.UI;
using Content.Client.Examine; using Content.Client.Examine;
using Content.Client.PDA;
using Content.Client.Resources; using Content.Client.Resources;
using Content.Client.Targeting;
using Content.Client.Targeting.UI; using Content.Client.Targeting.UI;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Client.Verbs.UI; using Content.Client.Verbs.UI;
@@ -13,7 +12,6 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Maths;
using static Robust.Client.UserInterface.StylesheetHelpers; using static Robust.Client.UserInterface.StylesheetHelpers;
namespace Content.Client.Stylesheets namespace Content.Client.Stylesheets
@@ -1395,6 +1393,60 @@ namespace Content.Client.Stylesheets
Element<Label>().Class("Disabled") Element<Label>().Class("Disabled")
.Prop("font-color", DisabledFore), .Prop("font-color", DisabledFore),
//PDA - Backgrounds
Element<PanelContainer>().Class("PDAContentBackground")
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#25252a")),
Element<PanelContainer>().Class("PDABackground")
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#000000")),
Element<PanelContainer>().Class("PDABackgroundRect")
.Prop(PanelContainer.StylePropertyPanel, BaseAngleRect)
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#717059")),
Element<PanelContainer>().Class("PDABorderRect")
.Prop(PanelContainer.StylePropertyPanel, AngleBorderRect),
Element<PanelContainer>().Class("BackgroundDark")
.Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat(Color.FromHex("#25252A"))),
//PDA - Buttons
Element<PDASettingsButton>().Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(PDASettingsButton.StylePropertyBgColor, Color.FromHex(PDASettingsButton.NormalBgColor))
.Prop(PDASettingsButton.StylePropertyFgColor, Color.FromHex(PDASettingsButton.EnabledFgColor)),
Element<PDASettingsButton>().Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(PDASettingsButton.StylePropertyBgColor, Color.FromHex(PDASettingsButton.HoverColor))
.Prop(PDASettingsButton.StylePropertyFgColor, Color.FromHex(PDASettingsButton.EnabledFgColor)),
Element<PDASettingsButton>().Pseudo(ContainerButton.StylePseudoClassPressed)
.Prop(PDASettingsButton.StylePropertyBgColor, Color.FromHex(PDASettingsButton.PressedColor))
.Prop(PDASettingsButton.StylePropertyFgColor, Color.FromHex(PDASettingsButton.EnabledFgColor)),
Element<PDASettingsButton>().Pseudo(ContainerButton.StylePseudoClassDisabled)
.Prop(PDASettingsButton.StylePropertyBgColor, Color.FromHex(PDASettingsButton.NormalBgColor))
.Prop(PDASettingsButton.StylePropertyFgColor, Color.FromHex(PDASettingsButton.DisabledFgColor)),
Element<PDAProgramItem>().Pseudo(ContainerButton.StylePseudoClassNormal)
.Prop(PDAProgramItem.StylePropertyBgColor, Color.FromHex(PDAProgramItem.NormalBgColor)),
Element<PDAProgramItem>().Pseudo(ContainerButton.StylePseudoClassHover)
.Prop(PDAProgramItem.StylePropertyBgColor, Color.FromHex(PDAProgramItem.HoverColor)),
Element<PDAProgramItem>().Pseudo(ContainerButton.StylePseudoClassPressed)
.Prop(PDAProgramItem.StylePropertyBgColor, Color.FromHex(PDAProgramItem.HoverColor)),
//PDA - Text
Element<Label>().Class("PDAContentFooterText")
.Prop(Label.StylePropertyFont, notoSans10)
.Prop(Label.StylePropertyFontColor, Color.FromHex("#757575")),
Element<Label>().Class("PDAWindowFooterText")
.Prop(Label.StylePropertyFont, notoSans10)
.Prop(Label.StylePropertyFontColor, Color.FromHex("#333d3b")),
}).ToList()); }).ToList());
} }
} }

View File

@@ -0,0 +1,18 @@
using Content.Shared.CartridgeLoader;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Server.CartridgeLoader;
[RegisterComponent]
[ComponentReference(typeof(SharedCartridgeLoaderComponent))]
public sealed class CartridgeLoaderComponent : SharedCartridgeLoaderComponent
{
/// <summary>
/// The maximum amount of programs that can be installed on the cartridge loader entity
/// </summary>
[DataField("diskSpace")]
public int DiskSpace = 5;
[DataField("uiKey", readOnly: true, required: true, customTypeSerializer: typeof(EnumSerializer))]
public Enum UiKey = default!;
}

View File

@@ -0,0 +1,417 @@
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.Interaction;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.Map;
namespace Content.Server.CartridgeLoader;
public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
{
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
private const string ContainerName = "program-container";
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);
}
/// <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, CartridgeLoaderUiState state, IPlayerSession? session = default!, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
state.ActiveUI = loader.ActiveProgram;
state.Programs = GetAvailablePrograms(loaderUid, loader);
var ui = _userInterfaceSystem.GetUiOrNull(loader.Owner, loader.UiKey);
if (ui != null)
_userInterfaceSystem.SetUiState(ui, state, session);
}
/// <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, IPlayerSession? session = default!, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader))
return;
var ui = _userInterfaceSystem.GetUiOrNull(loader.Owner, loader.UiKey);
if (ui != null)
_userInterfaceSystem.SetUiState(ui, state, session);
}
/// <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<EntityUid> GetAvailablePrograms(EntityUid uid, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(uid, ref loader))
return new List<EntityUid>();
//Don't count a cartridge that has already been installed as available to avoid confusion
if (loader.CartridgeSlot.HasItem && IsInstalled(Prototype(loader.CartridgeSlot.Item!.Value)?.ID, loader))
return loader.InstalledPrograms;
var available = new List<EntityUid>();
available.AddRange(loader.InstalledPrograms);
if (loader.CartridgeSlot.HasItem)
available.Add(loader.CartridgeSlot.Item!.Value);
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) || loader.InstalledPrograms.Count >= loader.DiskSpace)
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
var prototypeId = Prototype(cartridgeUid)?.ID;
return prototypeId != null && InstallProgram(loaderUid, prototypeId, 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="loader">The cartridge loader component</param>
/// <returns>Whether installing the cartridge was successful</returns>
public bool InstallProgram(EntityUid loaderUid, string prototype, CartridgeLoaderComponent? loader = default!)
{
if (!Resolve(loaderUid, ref loader) || loader.InstalledPrograms.Count >= loader.DiskSpace)
return false;
if (!_containerSystem.TryGetContainer(loaderUid, ContainerName, out var container))
return false;
//Prevent installing cartridges that have already been installed
if (IsInstalled(prototype, loader))
return false;
var installedProgram = Spawn(prototype, new EntityCoordinates(loaderUid, 0, 0));
container?.Insert(installedProgram);
UpdateCartridgeInstallationStatus(installedProgram, InstallationStatus.Installed);
loader.InstalledPrograms.Add(installedProgram);
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) || !ContainsCartridge(programUid, loader, true))
return false;
if (loader.ActiveProgram == programUid)
loader.ActiveProgram = null;
loader.BackgroundPrograms.Remove(programUid);
loader.InstalledPrograms.Remove(programUid);
EntityManager.QueueDeleteEntity(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 (!ContainsCartridge(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 (!ContainsCartridge(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 (!ContainsCartridge(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 (!ContainsCartridge(cartridgeUid, loader))
return;
if (loader.ActiveProgram != cartridgeUid)
RaiseLocalEvent(cartridgeUid, new CartridgeDeactivatedEvent(loaderUid));
loader.BackgroundPrograms.Remove(cartridgeUid);
}
protected override void OnItemInserted(EntityUid uid, SharedCartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
{
RaiseLocalEvent(args.Entity, new CartridgeAddedEvent(uid));
base.OnItemInserted(uid, loader, args);
}
protected override void OnItemRemoved(EntityUid uid, SharedCartridgeLoaderComponent loader, EntRemovedFromContainerMessage args)
{
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));
RaiseLocalEvent(args.Entity, new CartridgeRemovedEvent(uid));
base.OnItemRemoved(uid, loader, args);
}
/// <summary>
/// Installs programs from the list of preinstalled programs
/// </summary>
private void OnMapInit(EntityUid uid, CartridgeLoaderComponent component, MapInitEvent args)
{
foreach (var prototype in component.PreinstalledPrograms)
{
InstallProgram(uid, prototype);
}
}
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)
{
switch (message.Action)
{
case CartridgeUiMessageAction.Activate:
ActivateProgram(loaderUid, message.CartridgeUid, component);
break;
case CartridgeUiMessageAction.Deactivate:
DeactivateProgram(loaderUid, message.CartridgeUid, component);
break;
case CartridgeUiMessageAction.Install:
InstallCartridge(loaderUid, message.CartridgeUid, component);
break;
case CartridgeUiMessageAction.Uninstall:
UninstallProgram(loaderUid, message.CartridgeUid, component);
break;
case CartridgeUiMessageAction.UIReady:
if (component.ActiveProgram.HasValue)
RaiseLocalEvent(component.ActiveProgram.Value, new CartridgeUiReadyEvent(loaderUid));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
/// <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.LoaderUid = uid;
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>
/// Checks if a program is already installed by searching for its prototype name in the list of installed programs
/// </summary>
private bool IsInstalled(string? prototype, CartridgeLoaderComponent loader)
{
foreach (var program in loader.InstalledPrograms)
{
if (Prototype(program)?.ID == prototype)
return true;
}
return false;
}
/// <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, new CartridgeLoaderUiState(), null, loader);
}
private void UpdateCartridgeInstallationStatus(EntityUid cartridgeUid, InstallationStatus installationStatus, CartridgeComponent? cartridgeComponent = default!)
{
if (Resolve(cartridgeUid, ref cartridgeComponent))
{
cartridgeComponent.InstallationStatus = installationStatus;
Dirty(cartridgeComponent);
}
}
private bool ContainsCartridge(EntityUid cartridgeUid, CartridgeLoaderComponent loader , bool onlyInstalled = false)
{
return !onlyInstalled && loader.CartridgeSlot.Item?.Equals(cartridgeUid) == true || loader.InstalledPrograms.Contains(cartridgeUid);
}
}
/// <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;
}
}

View File

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

View File

@@ -0,0 +1,57 @@
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class NotekeeperCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NotekeeperCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NotekeeperCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(EntityUid uid, NotekeeperCartridgeComponent component, CartridgeUiReadyEvent args)
{
UpdateUiState(uid, args.Loader, component);
}
/// <summary>
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
/// </summary>
/// <remarks>
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
/// </remarks>
private void OnUiMessage(EntityUid uid, NotekeeperCartridgeComponent component, CartridgeMessageEvent args)
{
if (args is not NotekeeperUiMessageEvent message)
return;
if (message.Action == NotekeeperUiAction.Add)
{
component.Notes.Add(message.Note);
}
else
{
component.Notes.Remove(message.Note);
}
UpdateUiState(uid, args.LoaderUid, component);
}
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NotekeeperCartridgeComponent? component)
{
if (!Resolve(uid, ref component))
return;
var state = new NotekeeperUiState(component.Notes);
_cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
}
}

View File

@@ -15,6 +15,8 @@ namespace Content.Server.Entry
"ClientEntitySpawner", "ClientEntitySpawner",
"HandheldGPS", "HandheldGPS",
"CableVisualizer", "CableVisualizer",
"CartridgeUi",
"PDABorderColor",
}; };
} }
} }

View File

@@ -1,3 +1,5 @@
using Content.Server.CartridgeLoader;
using Content.Server.DeviceNetwork.Components;
using Content.Server.Instruments; using Content.Server.Instruments;
using Content.Server.Light.Components; using Content.Server.Light.Components;
using Content.Server.Light.EntitySystems; using Content.Server.Light.EntitySystems;
@@ -23,6 +25,7 @@ namespace Content.Server.PDA
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!; [Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoaderSystem = default!;
[Dependency] private readonly StoreSystem _storeSystem = default!; [Dependency] private readonly StoreSystem _storeSystem = default!;
public override void Initialize() public override void Initialize()
@@ -101,10 +104,11 @@ namespace Content.Server.PDA
if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui)) if (!_uiSystem.TryGetUi(pda.Owner, PDAUiKey.Key, out var ui))
return; return;
var address = GetDeviceNetAddress(pda.Owner);
var hasInstrument = HasComp<InstrumentComponent>(pda.Owner); var hasInstrument = HasComp<InstrumentComponent>(pda.Owner);
var state = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, false, hasInstrument); var state = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, false, hasInstrument, address);
ui.SetState(state); _cartridgeLoaderSystem?.UpdateUiState(pda.Owner, state);
// TODO UPLINK RINGTONES/SECRETS This is just a janky placeholder way of hiding uplinks from non syndicate // TODO UPLINK RINGTONES/SECRETS This is just a janky placeholder way of hiding uplinks from non syndicate
// players. This should really use a sort of key-code entry system that selects an account which is not directly tied to // players. This should really use a sort of key-code entry system that selects an account which is not directly tied to
@@ -113,7 +117,7 @@ namespace Content.Server.PDA
if (!TryComp<StoreComponent>(pda.Owner, out var storeComponent)) if (!TryComp<StoreComponent>(pda.Owner, out var storeComponent))
return; return;
var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument); var uplinkState = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, hasInstrument, address);
foreach (var session in ui.SubscribedSessions) foreach (var session in ui.SubscribedSessions)
{ {
@@ -122,7 +126,7 @@ namespace Content.Server.PDA
if (storeComponent.AccountOwner == user || (TryComp<MindComponent>(session.AttachedEntity, out var mindcomp) && mindcomp.Mind != null && if (storeComponent.AccountOwner == user || (TryComp<MindComponent>(session.AttachedEntity, out var mindcomp) && mindcomp.Mind != null &&
mindcomp.Mind.HasRole<TraitorRole>())) mindcomp.Mind.HasRole<TraitorRole>()))
ui.SetState(uplinkState, session); _cartridgeLoaderSystem?.UpdateUiState(pda.Owner, uplinkState, session);
} }
} }
@@ -190,9 +194,21 @@ namespace Content.Server.PDA
JobTitle = pda.ContainedID?.JobTitle JobTitle = pda.ContainedID?.JobTitle
}; };
var state = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, HasComp<InstrumentComponent>(pda.Owner)); var state = new PDAUpdateState(pda.FlashlightOn, pda.PenSlot.HasItem, ownerInfo, pda.StationName, true, HasComp<InstrumentComponent>(pda.Owner), GetDeviceNetAddress(pda.Owner));
ui.SetState(state, args.Session); _cartridgeLoaderSystem?.UpdateUiState(uid, state, args.Session);
}
private string? GetDeviceNetAddress(EntityUid uid)
{
string? address = null;
if (TryComp(uid, out DeviceNetworkComponent? deviceNetworkComponent))
{
address = deviceNetworkComponent?.Address;
}
return address;
} }
} }
} }

View File

@@ -0,0 +1,35 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.CartridgeLoader;
/// <summary>
/// This is used for defining values used for displaying in the program ui in yaml
/// </summary>
[NetworkedComponent]
[RegisterComponent]
public sealed class CartridgeComponent : Component
{
[DataField("programName", required: true)]
public string ProgramName = "default-program-name";
[DataField("icon")]
public SpriteSpecifier? Icon;
public InstallationStatus InstallationStatus = InstallationStatus.Cartridge;
}
[Serializable, NetSerializable]
public sealed class CartridgeComponentState : ComponentState
{
public InstallationStatus InstallationStatus;
}
[Serializable, NetSerializable]
public enum InstallationStatus
{
Cartridge,
Installed,
Readonly
}

View File

@@ -0,0 +1,26 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader;
[Serializable, NetSerializable]
public sealed class CartridgeLoaderUiMessage : BoundUserInterfaceMessage
{
public readonly EntityUid CartridgeUid;
public readonly CartridgeUiMessageAction Action;
public CartridgeLoaderUiMessage(EntityUid cartridgeUid, CartridgeUiMessageAction action)
{
CartridgeUid = cartridgeUid;
Action = action;
}
}
[Serializable, NetSerializable]
public enum CartridgeUiMessageAction
{
Activate,
Deactivate,
Install,
Uninstall,
UIReady
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Immutable;
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader;
[Virtual]
[Serializable, NetSerializable]
public class CartridgeLoaderUiState : BoundUserInterfaceState
{
public EntityUid? ActiveUI;
public List<EntityUid> Programs = new();
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader;
[Serializable, NetSerializable]
public enum CartridgeLoaderVisuals
{
CartridgeInserted
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader;
[Serializable, NetSerializable]
public sealed class CartridgeUiMessage : BoundUserInterfaceMessage
{
public CartridgeMessageEvent MessageEvent;
public CartridgeUiMessage(CartridgeMessageEvent messageEvent)
{
MessageEvent = messageEvent;
}
}
[Serializable, NetSerializable]
public abstract class CartridgeMessageEvent : EntityEventArgs
{
public EntityUid LoaderUid;
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class NotekeeperUiMessageEvent : CartridgeMessageEvent
{
public readonly NotekeeperUiAction Action;
public readonly string Note;
public NotekeeperUiMessageEvent(NotekeeperUiAction action, string note)
{
Action = action;
Note = note;
}
}
[Serializable, NetSerializable]
public enum NotekeeperUiAction
{
Add,
Remove
}

View File

@@ -0,0 +1,14 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class NotekeeperUiState : BoundUserInterfaceState
{
public List<String> Notes;
public NotekeeperUiState(List<string> notes)
{
Notes = notes;
}
}

View File

@@ -0,0 +1,36 @@
using Content.Shared.Containers.ItemSlots;
namespace Content.Shared.CartridgeLoader;
[Access(typeof(SharedCartridgeLoaderSystem))]
public abstract class SharedCartridgeLoaderComponent : Component
{
public const string CartridgeSlotId = "Cartridge-Slot";
[DataField("cartridgeSlot")]
public ItemSlot CartridgeSlot = new();
/// <summary>
/// List of programs that come preinstalled with this cartridge loader
/// </summary>
[DataField("preinstalled")]
public List<string> PreinstalledPrograms = new();
/// <summary>
/// The currently running program that has its ui showing
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
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 list of program entities that are spawned into the cartridge loaders program container
/// </summary>
[DataField("installedCartridges")]
public List<EntityUid> InstalledPrograms = new();
}

View File

@@ -0,0 +1,147 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
namespace Content.Shared.CartridgeLoader;
public abstract class SharedCartridgeLoaderSystem : EntitySystem
{
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedCartridgeLoaderComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SharedCartridgeLoaderComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<SharedCartridgeLoaderComponent, EntInsertedIntoContainerMessage>(OnItemInserted);
SubscribeLocalEvent<SharedCartridgeLoaderComponent, EntRemovedFromContainerMessage>(OnItemRemoved);
SubscribeLocalEvent<CartridgeComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<CartridgeComponent, ComponentHandleState>(OnHandleState);
}
private void OnComponentInit(EntityUid uid, SharedCartridgeLoaderComponent loader, ComponentInit args)
{
_itemSlotsSystem.AddItemSlot(uid, SharedCartridgeLoaderComponent.CartridgeSlotId, loader.CartridgeSlot);
}
/// <summary>
/// Marks installed program entities for deletion when the component gets removed
/// </summary>
private void OnComponentRemove(EntityUid uid, SharedCartridgeLoaderComponent loader, ComponentRemove args)
{
_itemSlotsSystem.RemoveItemSlot(uid, loader.CartridgeSlot);
foreach (var program in loader.InstalledPrograms)
{
EntityManager.QueueDeleteEntity(program);
}
}
protected virtual void OnItemInserted(EntityUid uid, SharedCartridgeLoaderComponent loader, EntInsertedIntoContainerMessage args)
{
UpdateAppearanceData(uid, loader);
}
protected virtual void OnItemRemoved(EntityUid uid, SharedCartridgeLoaderComponent loader, EntRemovedFromContainerMessage args)
{
UpdateAppearanceData(uid, loader);
}
private void OnGetState(EntityUid uid, CartridgeComponent component, ref ComponentGetState args)
{
var state = new CartridgeComponentState();
state.InstallationStatus = component.InstallationStatus;
args.State = state;
}
private void OnHandleState(EntityUid uid, CartridgeComponent component, ref ComponentHandleState args)
{
if (args.Current is not CartridgeComponentState state)
return;
component.InstallationStatus = state.InstallationStatus;
}
private void UpdateAppearanceData(EntityUid uid, SharedCartridgeLoaderComponent 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;
}
}

View File

@@ -1,10 +1,11 @@
using Content.Shared.CartridgeLoader;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.PDA namespace Content.Shared.PDA
{ {
[Serializable, NetSerializable] [Serializable, NetSerializable]
public sealed class PDAUpdateState : BoundUserInterfaceState public sealed class PDAUpdateState : CartridgeLoaderUiState
{ {
public bool FlashlightEnabled; public bool FlashlightEnabled;
public bool HasPen; public bool HasPen;
@@ -12,15 +13,17 @@ namespace Content.Shared.PDA
public string? StationName; public string? StationName;
public bool HasUplink; public bool HasUplink;
public bool CanPlayMusic; public bool CanPlayMusic;
public string? Address;
public PDAUpdateState(bool flashlightEnabled, bool hasPen, PDAIdInfoText pDAOwnerInfo, string? stationName, bool hasUplink = false, bool canPlayMusic = false) public PDAUpdateState(bool flashlightEnabled, bool hasPen, PDAIdInfoText pdaOwnerInfo, string? stationName, bool hasUplink = false, bool canPlayMusic = false, string? address = null)
{ {
FlashlightEnabled = flashlightEnabled; FlashlightEnabled = flashlightEnabled;
HasPen = hasPen; HasPen = hasPen;
PDAOwnerInfo = pDAOwnerInfo; PDAOwnerInfo = pdaOwnerInfo;
HasUplink = hasUplink; HasUplink = hasUplink;
CanPlayMusic = canPlayMusic; CanPlayMusic = canPlayMusic;
StationName = stationName; StationName = stationName;
Address = address;
} }
} }

View File

@@ -0,0 +1,2 @@
cartridge-bound-user-interface-install-button = Install
cartridge-bound-user-interface-uninstall-button = Remove

View File

@@ -0,0 +1,6 @@
default-program-name = Program
ent-notekeeper-cartridge = notekeeper cartridge
.desc = A program for keeping notes
notekeeper-program-name = Notekeeper

View File

@@ -1,3 +1,4 @@
crew-manifest-window-title = Crew Manifest crew-manifest-window-title = Crew Manifest
crew-manifest-button-label = Crew Manifest crew-manifest-button-label = Crew Manifest
crew-manifest-button-description = Show a list of your fellow crewmembers
crew-manifest-no-valid-station = Invalid station, or empty manifest! crew-manifest-no-valid-station = Invalid station, or empty manifest!

View File

@@ -3,6 +3,7 @@ device-frequency-prototype-name-atmos = Atmospheric Devices
device-frequency-prototype-name-suit-sensors = Suit Sensors device-frequency-prototype-name-suit-sensors = Suit Sensors
device-frequency-prototype-name-lights = Smart Lights device-frequency-prototype-name-lights = Smart Lights
device-frequency-prototype-name-mailing-units = Mailing Units device-frequency-prototype-name-mailing-units = Mailing Units
device-frequency-prototype-name-pdas = PDAs
## camera frequencies ## camera frequencies
device-frequency-prototype-name-surveillance-camera-test = Subnet Test device-frequency-prototype-name-surveillance-camera-test = Subnet Test
@@ -20,6 +21,9 @@ device-frequency-prototype-name-surveillance-camera-entertainment = Entertainmen
device-address-prefix-vent = Vnt- device-address-prefix-vent = Vnt-
device-address-prefix-scrubber = Scr- device-address-prefix-scrubber = Scr-
device-address-prefix-sensor = Sns- device-address-prefix-sensor = Sns-
#PDAs and terminals
device-address-prefix-console = Cls-
device-address-prefix-fire-alarm = Fir- device-address-prefix-fire-alarm = Fir-
device-address-prefix-air-alarm = Air- device-address-prefix-air-alarm = Air-

View File

@@ -8,10 +8,18 @@ comp-pda-ui-blank = ID:
comp-pda-ui-owner = Owner: [color=white]{$ActualOwnerName}[/color] comp-pda-ui-owner = Owner: [color=white]{$ActualOwnerName}[/color]
pda-bound-user-interface-main-menu-tab-title = Main Menu comp-pda-io-program-list-button = Programs
comp-pda-io-settings-button = Settings
comp-pda-io-program-fallback-title = Program
comp-pda-io-no-programs-available = No Programs Available
pda-bound-user-interface-uplink-tab-title = Uplink pda-bound-user-interface-uplink-tab-title = Uplink
pda-bound-user-interface-uplink-tab-description = Access your uplink
comp-pda-ui-menu-title = PDA comp-pda-ui-menu-title = PDA
comp-pda-ui-station = Station: [color=white]{$Station}[/color] comp-pda-ui-station = Station: [color=white]{$Station}[/color]
@@ -22,10 +30,14 @@ comp-pda-ui-eject-pen-button = Eject Pen
comp-pda-ui-ringtone-button = Ringtone comp-pda-ui-ringtone-button = Ringtone
comp-pda-ui-ringtone-button-description = Change your PDA's ringtone
comp-pda-ui-toggle-flashlight-button = Toggle Flashlight comp-pda-ui-toggle-flashlight-button = Toggle Flashlight
pda-bound-user-interface-music-button = Music Instrument pda-bound-user-interface-music-button = Music Instrument
pda-bound-user-interface-music-button-description = Play music on your PDA
comp-pda-ui-unknown = Unknown comp-pda-ui-unknown = Unknown
comp-pda-ui-unassigned = Unassigned comp-pda-ui-unassigned = Unassigned

View File

@@ -69,3 +69,8 @@
id: MailingUnit id: MailingUnit
name: device-frequency-prototype-name-mailing-units name: device-frequency-prototype-name-mailing-units
frequency: 2300 frequency: 2300
- type: deviceFrequency
id: PDA
name: device-frequency-prototype-name-pdas
frequency: 2202

View File

@@ -0,0 +1,21 @@
- type: entity
parent: BaseItem
id: NotekeeperCartridge
components:
- type: Sprite
sprite: Objects/Devices/cartridge.rsi
state: cart-y
netsync: false
- type: Icon
sprite: Objects/Devices/cartridge.rsi
state: cart-y
- type: CartridgeUi
ui: !type:NotekeeperUi
- type: Cartridge
programName: notekeeper-program-name
icon:
sprite: Objects/Misc/books.rsi
state: book6
- type: NotekeeperCartridge

View File

@@ -21,6 +21,8 @@
containers: containers:
PDA-id: !type:ContainerSlot {} PDA-id: !type:ContainerSlot {}
PDA-pen: !type:ContainerSlot {} PDA-pen: !type:ContainerSlot {}
Cartridge-Slot: !type:ContainerSlot {}
program-container: !type:Container
- type: ItemSlots - type: ItemSlots
- type: Clothing - type: Clothing
quickEquip: false quickEquip: false
@@ -41,6 +43,24 @@
mask: /Textures/Effects/LightMasks/cone.png mask: /Textures/Effects/LightMasks/cone.png
autoRot: true autoRot: true
- type: Ringer - type: Ringer
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: PDA
prefix: device-address-prefix-console
- type: WirelessNetworkConnection
range: 500
- type: CartridgeLoader
uiKey: enum.PDAUiKey.Key
preinstalled:
- NotekeeperCartridge
cartridgeSlot:
priority: -1
name: Cartridge
ejectSound: /Audio/Machines/id_swipe.ogg
insertSound: /Audio/Weapons/Guns/MagIn/batrifle_magin.ogg
whitelist:
components:
- Cartridge
- type: ActivatableUI - type: ActivatableUI
key: enum.PDAUiKey.Key key: enum.PDAUiKey.Key
singleUser: true singleUser: true
@@ -87,6 +107,8 @@
components: components:
- type: PDA - type: PDA
id: PassengerIDCard id: PassengerIDCard
- type: PDABorderColor
borderColor: "#717059"
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@@ -162,6 +184,8 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-cook state: pda-cook
- type: PDABorderColor
borderColor: "#d7d7d0"
- type: Icon - type: Icon
state: pda-cook state: pda-cook
@@ -200,6 +224,8 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-clown state: pda-clown
- type: PDABorderColor
borderColor: "#C18199"
- type: Icon - type: Icon
state: pda-clown state: pda-clown
- type: Slippery - type: Slippery
@@ -258,6 +284,8 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-chaplain state: pda-chaplain
- type: PDABorderColor
borderColor: "#333333"
- type: Icon - type: Icon
state: pda-chaplain state: pda-chaplain
@@ -363,6 +391,8 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-janitor state: pda-janitor
- type: PDABorderColor
borderColor: "#5D2D56"
- type: Icon - type: Icon
state: pda-janitor state: pda-janitor
@@ -384,6 +414,8 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-captain state: pda-captain
- type: PDABorderColor
borderColor: "#7C5D00"
- type: Icon - type: Icon
state: pda-captain state: pda-captain
@@ -416,6 +448,9 @@
components: components:
- type: PDA - type: PDA
id: CEIDCard id: CEIDCard
- type: PDABorderColor
borderColor: "#949137"
accentHColor: "#447987"
- type: Appearance - type: Appearance
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
@@ -450,6 +485,10 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-cmo state: pda-cmo
- type: PDABorderColor
borderColor: "#d7d7d0"
accentHColor: "#447987"
accentVColor: "#447987"
- type: Icon - type: Icon
state: pda-cmo state: pda-cmo
- type: HealthAnalyzer - type: HealthAnalyzer
@@ -467,6 +506,9 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-medical state: pda-medical
- type: PDABorderColor
borderColor: "#d7d7d0"
accentVColor: "#447987"
- type: Icon - type: Icon
state: pda-medical state: pda-medical
- type: HealthAnalyzer - type: HealthAnalyzer
@@ -484,6 +526,9 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-chemistry state: pda-chemistry
- type: PDABorderColor
borderColor: "#d7d7d0"
accentVColor: "#B34200"
- type: Icon - type: Icon
state: pda-chemistry state: pda-chemistry
@@ -529,6 +574,9 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-hos state: pda-hos
- type: PDABorderColor
borderColor: "#A32D26"
accentHColor: "#447987"
- type: Icon - type: Icon
state: pda-hos state: pda-hos
@@ -642,6 +690,9 @@
components: components:
- type: PDA - type: PDA
id: ERTLeaderIDCard id: ERTLeaderIDCard
- type: PDABorderColor
borderColor: "#A32D26"
accentHColor: "#447987"
- type: Appearance - type: Appearance
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
@@ -657,6 +708,9 @@
components: components:
- type: PDA - type: PDA
id: CBURNIDcard id: CBURNIDcard
- type: PDABorderColor
borderColor: "#A32D26"
accentHColor: "#447987"
- type: entity - type: entity
parent: BasePDA parent: BasePDA
@@ -670,6 +724,9 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-medical state: pda-medical
- type: PDABorderColor
borderColor: "#d7d7d0"
accentVColor: "#447987"
- type: Icon - type: Icon
state: pda-medical state: pda-medical
@@ -715,6 +772,9 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-boxer state: pda-boxer
- type: PDABorderColor
borderColor: "#333333"
borderVColor: "#390504"
- type: Icon - type: Icon
state: pda-boxer state: pda-boxer
@@ -730,5 +790,7 @@
visuals: visuals:
- type: PDAVisualizer - type: PDAVisualizer
state: pda-detective state: pda-detective
- type: PDABorderColor
borderColor: "#774705"
- type: Icon - type: Icon
state: pda-detective state: pda-detective

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.3499998 6.3499999"
version="1.1"
id="svg1055"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="geometric_panel_border.svg"
inkscape:export-filename="geometric_panel_border.svg.96dpi.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs1049" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="32"
inkscape:cx="6.140625"
inkscape:cy="12.15625"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:pagecheckerboard="true"
inkscape:window-width="1920"
inkscape:window-height="1057"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
units="px"
inkscape:snap-page="true"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1"
showguides="true">
<inkscape:grid
type="xygrid"
id="grid4206"
originx="0"
originy="0"
color="#3f3fff"
opacity="0.57254902"
empcolor="#3f3fff"
empopacity="0.75686275"
enabled="true"
dotted="false" />
<sodipodi:guide
position="0.26458333,2.6458333"
orientation="1,0"
id="guide14206"
inkscape:locked="false" />
<sodipodi:guide
position="0,2.6458333"
orientation="0,-1"
id="guide14208"
inkscape:locked="false" />
<inkscape:grid
type="axonomgrid"
id="grid16025"
spacingy="0.37438541"
gridanglex="45"
gridanglez="45"
units="px"
enabled="true"
color="#ff2439"
opacity="0.4745098"
empcolor="#870011"
empopacity="1"
originx="-0.039687499"
originy="0"
snapvisiblegridlinesonly="false" />
<sodipodi:guide
position="0.79374998,2.9752396"
orientation="0,-1"
id="guide454"
inkscape:locked="false" />
<sodipodi:guide
position="2.9752396,0.79374998"
orientation="1,0"
id="guide456"
inkscape:locked="false" />
<sodipodi:guide
position="3.3747603,5.5562499"
orientation="0,-1"
id="guide458"
inkscape:locked="false" />
<sodipodi:guide
position="5.5562499,3.3747603"
orientation="0,-1"
id="guide460"
inkscape:locked="false" />
<inkscape:grid
type="axonomgrid"
id="grid1070"
gridanglex="45"
gridanglez="45"
spacingy="0.37438541"
color="#288e00"
opacity="0.96078431"
empcolor="#3fa600"
empopacity="0.97647059"
originy="-0.039687499"
enabled="true"
originx="0"
snapvisiblegridlinesonly="false"
empspacing="9"
units="px" />
</sodipodi:namedview>
<metadata
id="metadata1052">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-79.848503,-133.93878)">
<path
id="rect1684"
style="opacity:1;fill:#000000;fill-opacity:0.25;fill-rule:nonzero;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.247312;paint-order:normal"
d="m 83.55267,133.93878 2.645833,2.64583 v 3.70417 h -3.704167 l -2.645833,-2.64583 v -3.70417 z m -2.910417,3.37476 2.18149,2.18149 2.58101,0 v -2.58101 l -2.18149,-2.18149 -2.58101,0 z"
sodipodi:nodetypes="cccccccccccccc"
inkscape:label="rect1684" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" fill="none" viewBox="0 0 21 20">
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m3.224 7.5 7.258-5.833L17.739 7.5v9.167c0 .442-.17.866-.473 1.178a1.586 1.586 0 0 1-1.14.488H4.837c-.428 0-.838-.175-1.14-.488a1.695 1.695 0 0 1-.473-1.178V7.5Z"/>
<path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.063 18.333V10H12.9v8.333"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="18" fill="none" viewBox="0 0 19 18">
<path fill="#FFFBFB" d="M15.167 2.455 1 15.545V17h1.417L16.583 3.91l-1.416-1.455Z"/>
<path stroke="#fff" d="m18 5.364-5.667 5.818M16.583 1 18 2.455M1 15.545l14.167-13.09 1.416 1.454L2.417 17H1v-1.454Z"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

View File

@@ -69,6 +69,9 @@
}, },
{ {
"name": "cart-tox" "name": "cart-tox"
},
{
"name": "cart-y"
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -0,0 +1,77 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/0c15d9dbcf0f2beb230eba5d9d889ef2d1945bb8",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "insert_overlay"
},
{
"name": "cart"
},
{
"name": "cart-e"
},
{
"name": "cart-m"
},
{
"name": "cart-chem"
},
{
"name": "cart-c"
},
{
"name": "cart-h"
},
{
"name": "cart-s"
},
{
"name": "cart-clown"
},
{
"name": "cart-a"
},
{
"name": "cart-j"
},
{
"name": "cart-q"
},
{
"name": "cart-ord"
},
{
"name": "cart-tear"
},
{
"name": "cart-b"
},
{
"name": "cart-lib"
},
{
"name": "cart-eye"
},
{
"name": "cart-mi"
},
{
"name": "cart-hos"
},
{
"name": "cart-ce"
},
{
"name": "cart-cmo"
},
{
"name": "cart-rd"
}
]
}