]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
NanoTask (#34095)
authorpathetic meowmeow <uhhadd@gmail.com>
Sat, 15 Mar 2025 16:24:23 +0000 (12:24 -0400)
committerGitHub <noreply@github.com>
Sat, 15 Mar 2025 16:24:23 +0000 (09:24 -0700)
20 files changed:
Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml [new file with mode: 0644]
Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs [new file with mode: 0644]
Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/NanoTaskCartridgeComponent.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/NanoTaskPrintedComponent.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiMessageEvent.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiState.cs [new file with mode: 0644]
Content.Shared/CartridgeLoader/Cartridges/SharedNanoTaskCartridgeSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/cartridge-loader/cartridges.ftl
Resources/Prototypes/Entities/Objects/Devices/cartridges.yml
Resources/Prototypes/Entities/Objects/Devices/pda.yml
Resources/Prototypes/Entities/Objects/Misc/paper.yml
Resources/Textures/Interface/Misc/program_icons.rsi/meta.json
Resources/Textures/Interface/Misc/program_icons.rsi/nano_task.png [new file with mode: 0644]

diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
new file mode 100644 (file)
index 0000000..85a5659
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml
new file mode 100644 (file)
index 0000000..7d7c635
--- /dev/null
@@ -0,0 +1,32 @@
+<Control xmlns="https://spacestation14.io" xmlns:system="clr-namespace:System;assembly=System.Runtime">
+    <BoxContainer Name="MainContainer"
+                  Orientation="Horizontal"
+                  SetWidth="250">
+        <Button Name="MainButton"
+                HorizontalExpand="True"
+                VerticalExpand="True"
+                StyleClasses="ButtonSquare"
+                Margin="-1 0 0 0">
+            <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+                <BoxContainer Orientation="Vertical"
+                              VerticalExpand="True"
+                              HorizontalExpand="True"
+                              Margin="-5 0 0 0">
+                    <Label Name="TaskLabel"
+                           StyleClasses="LabelSubText" />
+                    <Label Name="TaskForLabel"
+                           StyleClasses="LabelSubText"
+                           Margin="0 -5 0 0" />
+                </BoxContainer>
+            </BoxContainer>
+        </Button>
+        <Button Name="DoneButton"
+                VerticalExpand="True"
+                Text="{Loc 'nano-task-ui-done'}">
+            <Button.StyleClasses>
+                <system:String>ButtonSmall</system:String>
+                <system:String>OpenLeft</system:String>
+            </Button.StyleClasses>
+        </Button>
+    </BoxContainer>
+</Control>
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs
new file mode 100644 (file)
index 0000000..5edbee9
--- /dev/null
@@ -0,0 +1,33 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Maths;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Represents a single control for a single NanoTask item
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskItemControl : Control
+{
+    public Action<int>? OnMainPressed;
+    public Action<int>? OnDonePressed;
+
+    public NanoTaskItemControl(NanoTaskItemAndId item)
+    {
+        RobustXamlLoader.Load(this);
+
+        TaskLabel.Text = item.Data.Description;
+        TaskLabel.FontColorOverride = Color.White;
+        TaskForLabel.Text = item.Data.TaskIsFor;
+
+        MainButton.OnPressed += _ => OnMainPressed?.Invoke(item.Id);
+        DoneButton.OnPressed += _ => OnDonePressed?.Invoke(item.Id);
+
+        MainButton.Disabled = item.Data.IsTaskDone;
+        DoneButton.Text = item.Data.IsTaskDone ? Loc.GetString("nano-task-ui-revert-done") : Loc.GetString("nano-task-ui-done");
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml
new file mode 100644 (file)
index 0000000..ad72df0
--- /dev/null
@@ -0,0 +1,67 @@
+<DefaultWindow xmlns="https://spacestation14.io"
+               Title="{Loc nano-task-ui-item-title}"
+               MinSize="300 300">
+    <PanelContainer StyleClasses="AngleRect">
+        <BoxContainer Orientation="Vertical" Margin="4">
+            <!-- Task Description Input -->
+            <BoxContainer Orientation="Vertical" Margin="0 4">
+                <Label Text="{Loc nano-task-ui-description-label}"
+                       StyleClasses="LabelHeading" />
+                <PanelContainer StyleClasses="ButtonSquare">
+                    <LineEdit Name="DescriptionInput"
+                              PlaceHolder="{Loc nano-task-ui-description-placeholder}" />
+                </PanelContainer>
+            </BoxContainer>
+
+            <!-- Task Requester Input -->
+            <BoxContainer Orientation="Vertical" Margin="0 4">
+                <Label Text="{Loc nano-task-ui-requester-label}"
+                       StyleClasses="LabelHeading" />
+                <PanelContainer StyleClasses="ButtonSquare">
+                    <LineEdit Name="RequesterInput"
+                              PlaceHolder="{Loc nano-task-ui-requester-placeholder}" />
+                </PanelContainer>
+            </BoxContainer>
+
+            <!-- Severity Buttons -->
+            <BoxContainer Orientation="Horizontal"
+                          HorizontalAlignment="Center"
+                          Margin="0 8 0 0">
+                <Button Name="LowButton"
+                        Text="{Loc nano-task-ui-priority-low}"
+                        StyleClasses="OpenRight"
+                        MinSize="60 0" />
+                <Button Name="MediumButton"
+                        Text="{Loc nano-task-ui-priority-medium}"
+                        StyleClasses="ButtonSquare"
+                        MinSize="60 0" />
+                <Button Name="HighButton"
+                        Text="{Loc nano-task-ui-priority-high}"
+                        StyleClasses="OpenLeft"
+                        MinSize="60 0" />
+            </BoxContainer>
+
+            <!-- Verb Buttons -->
+            <BoxContainer Orientation="Horizontal"
+                          HorizontalAlignment="Right"
+                          Margin="0 8 0 0">
+                <Button Name="CancelButton"
+                        Text="{Loc nano-task-ui-cancel}"
+                        StyleClasses="OpenRight"
+                        MinSize="60 0" />
+                <Button Name="DeleteButton"
+                        Text="{Loc nano-task-ui-delete}"
+                        StyleClasses="ButtonSquare"
+                        MinSize="60 0" />
+                <Button Name="PrintButton"
+                        Text="{Loc nano-task-ui-print}"
+                        StyleClasses="ButtonSquare"
+                        MinSize="60 0" />
+                <Button Name="SaveButton"
+                        Text="{Loc nano-task-ui-save}"
+                        StyleClasses="OpenLeft"
+                        MinSize="60 0" />
+            </BoxContainer>
+        </BoxContainer>
+    </PanelContainer>
+</DefaultWindow>
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs
new file mode 100644 (file)
index 0000000..124b7b7
--- /dev/null
@@ -0,0 +1,109 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Popup displayed to edit a NanoTask item
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskItemPopup : DefaultWindow
+{
+    private readonly ButtonGroup _priorityGroup = new();
+    private int? _editingTaskId = null;
+
+    public Action<int, NanoTaskItem>? TaskSaved;
+    public Action<int>? TaskDeleted;
+    public Action<NanoTaskItem>? TaskCreated;
+    public Action<NanoTaskItem>? TaskPrinted;
+
+    private NanoTaskItem MakeItem()
+    {
+        return new(
+            description: DescriptionInput.Text,
+            taskIsFor: RequesterInput.Text,
+            isTaskDone: false,
+            priority: _priorityGroup.Pressed switch {
+                var item when item == LowButton => NanoTaskPriority.Low,
+                var item when item == MediumButton => NanoTaskPriority.Medium,
+                var item when item == HighButton => NanoTaskPriority.High,
+                _ => NanoTaskPriority.Medium,
+            }
+        );
+    }
+
+    public NanoTaskItemPopup()
+    {
+        RobustXamlLoader.Load(this);
+
+        LowButton.Group = _priorityGroup;
+        MediumButton.Group = _priorityGroup;
+        HighButton.Group = _priorityGroup;
+
+        CancelButton.OnPressed += _ => Close();
+        DeleteButton.OnPressed += _ =>
+        {
+            if (_editingTaskId is int id)
+            {
+                TaskDeleted?.Invoke(id);
+            }
+        };
+        PrintButton.OnPressed += _ =>
+        {
+            TaskPrinted?.Invoke(MakeItem());
+        };
+        SaveButton.OnPressed += _ =>
+        {
+            if (_editingTaskId is int id)
+            {
+                TaskSaved?.Invoke(id, MakeItem());
+            }
+            else
+            {
+                TaskCreated?.Invoke(MakeItem());
+            }
+        };
+
+        DescriptionInput.OnTextChanged += args =>
+        {
+            if (args.Text.Length > NanoTaskItem.MaximumStringLength)
+                DescriptionInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
+        };
+        RequesterInput.OnTextChanged += args =>
+        {
+            if (args.Text.Length > NanoTaskItem.MaximumStringLength)
+                RequesterInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
+        };
+    }
+
+    public void SetEditingTaskId(int? id)
+    {
+        _editingTaskId = id;
+        DeleteButton.Visible = id is not null;
+    }
+
+    public void ResetInputs(NanoTaskItem? item)
+    {
+        if (item is NanoTaskItem task)
+        {
+            var button = task.Priority switch {
+                NanoTaskPriority.High => HighButton,
+                NanoTaskPriority.Medium => MediumButton,
+                NanoTaskPriority.Low => LowButton,
+            };
+            button.Pressed = true;
+            DescriptionInput.Text = task.Description;
+            RequesterInput.Text = task.TaskIsFor;
+        }
+        else
+        {
+            MediumButton.Pressed = true;
+            DescriptionInput.Text = "";
+            RequesterInput.Text = "";
+        }
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs
new file mode 100644 (file)
index 0000000..ac08051
--- /dev/null
@@ -0,0 +1,82 @@
+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;
+
+/// <summary>
+///     UI fragment responsible for displaying NanoTask controls in a PDA and coordinating with the NanoTaskCartridgeSystem for state
+/// </summary>
+public sealed partial class NanoTaskUi : UIFragment
+{
+    private NanoTaskUiFragment? _fragment;
+    private NanoTaskItemPopup? _popup;
+
+    public override Control GetUIFragmentRoot()
+    {
+        return _fragment!;
+    }
+
+    public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+    {
+        _fragment = new NanoTaskUiFragment();
+        _popup = new NanoTaskItemPopup();
+        _fragment.NewTask += () =>
+        {
+            _popup.ResetInputs(null);
+            _popup.SetEditingTaskId(null);
+            _popup.OpenCentered();
+        };
+        _fragment.OpenTask += id =>
+        {
+            if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
+                return;
+
+            _popup.ResetInputs(task.Data);
+            _popup.SetEditingTaskId(task.Id);
+            _popup.OpenCentered();
+        };
+        _fragment.ToggleTaskCompletion += id =>
+        {
+            if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
+                return;
+
+            userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
+                description: task.Data.Description,
+                taskIsFor: task.Data.TaskIsFor,
+                isTaskDone: !task.Data.IsTaskDone,
+                priority: task.Data.Priority
+            ))))));
+        };
+        _popup.TaskSaved += (id, data) =>
+        {
+            userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
+            _popup.Close();
+        };
+        _popup.TaskDeleted += id =>
+        {
+            userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
+            _popup.Close();
+        };
+        _popup.TaskCreated += data =>
+        {
+            userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
+            _popup.Close();
+        };
+        _popup.TaskPrinted += data =>
+        {
+            userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
+        };
+    }
+
+    public override void UpdateState(BoundUserInterfaceState state)
+    {
+        if (state is not NanoTaskUiState nanoTaskState)
+            return;
+
+        _fragment?.UpdateState(nanoTaskState.Tasks);
+    }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml
new file mode 100644 (file)
index 0000000..f362080
--- /dev/null
@@ -0,0 +1,58 @@
+<cartridges:NanoTaskUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
+                               xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+                               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">
+            <BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical" Margin="8" SeparationOverride="8">
+                <!-- Heading for High Priority Items -->
+                <BoxContainer Orientation="Horizontal">
+                    <PanelContainer SetWidth="7" Margin="0 0 8 0">
+                        <PanelContainer.PanelOverride>
+                            <gfx:StyleBoxFlat BackgroundColor="#e93d58"/>
+                        </PanelContainer.PanelOverride>
+                    </PanelContainer>
+                    <Label Name="HighPriority" StyleClasses="LabelHeading"/>
+                </BoxContainer>
+                <!-- Location for High Priority Items -->
+                <GridContainer Name="HighContainer"
+                               HorizontalExpand="True"
+                               Access="Public"
+                               Columns="2" />
+
+                <!-- Heading for Medium Priority Items -->
+                <BoxContainer Orientation="Horizontal">
+                    <PanelContainer SetWidth="7" Margin="0 0 8 0">
+                        <PanelContainer.PanelOverride>
+                            <gfx:StyleBoxFlat BackgroundColor="#ef973c"/>
+                        </PanelContainer.PanelOverride>
+                    </PanelContainer>
+                    <Label Name="MediumPriority" StyleClasses="LabelHeading"/>
+                </BoxContainer>
+                <!-- Location for Medium Priority Items -->
+                <GridContainer Name="MediumContainer"
+                               HorizontalExpand="True"
+                               Access="Public"
+                               Columns="2" />
+
+                <!-- Location for Low Priority Items -->
+                <BoxContainer Orientation="Horizontal">
+                    <PanelContainer SetWidth="7" Margin="0 0 8 0">
+                        <PanelContainer.PanelOverride>
+                            <gfx:StyleBoxFlat BackgroundColor="#3dd425"/>
+                        </PanelContainer.PanelOverride>
+                    </PanelContainer>
+                    <Label Name="LowPriority" StyleClasses="LabelHeading"/>
+                </BoxContainer>
+                <!-- Location for Low Priority Items -->
+                <GridContainer Name="LowContainer"
+                               HorizontalExpand="True"
+                               Access="Public"
+                               Columns="2" />
+
+                <Button Name="NewTaskButton" Text="{Loc 'nano-task-ui-new-task'}" HorizontalAlignment="Right"/>
+            </BoxContainer>
+        </ScrollContainer>
+    </BoxContainer>
+</cartridges:NanoTaskUiFragment>
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs
new file mode 100644 (file)
index 0000000..38897d6
--- /dev/null
@@ -0,0 +1,52 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Class displaying the main UI of NanoTask
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskUiFragment : BoxContainer
+{
+    public Action<int>? OpenTask;
+    public Action<int>? ToggleTaskCompletion;
+    public Action? NewTask;
+    public List<NanoTaskItemAndId> Tasks = new();
+
+    public NanoTaskUiFragment()
+    {
+        RobustXamlLoader.Load(this);
+        Orientation = LayoutOrientation.Vertical;
+        HorizontalExpand = true;
+        VerticalExpand = true;
+        NewTaskButton.OnPressed += _ => NewTask?.Invoke();
+    }
+    public void UpdateState(List<NanoTaskItemAndId> tasks)
+    {
+        Tasks = tasks;
+        HighContainer.RemoveAllChildren();
+        MediumContainer.RemoveAllChildren();
+        LowContainer.RemoveAllChildren();
+
+        HighPriority.Text = Loc.GetString("nano-task-ui-heading-high-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.High)));
+        MediumPriority.Text = Loc.GetString("nano-task-ui-heading-medium-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Medium)));
+        LowPriority.Text = Loc.GetString("nano-task-ui-heading-low-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Low)));
+
+        foreach (var task in tasks)
+        {
+            var container = task.Data.Priority switch {
+                NanoTaskPriority.High => HighContainer,
+                NanoTaskPriority.Medium => MediumContainer,
+                NanoTaskPriority.Low => LowContainer,
+            };
+            var control = new NanoTaskItemControl(task);
+            container.AddChild(control);
+            control.OnMainPressed += id => OpenTask?.Invoke(id);
+            control.OnDonePressed += id => ToggleTaskCompletion?.Invoke(id);
+        }
+    }
+}
diff --git a/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
new file mode 100644 (file)
index 0000000..8118ca8
--- /dev/null
@@ -0,0 +1,151 @@
+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.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Server-side class implementing the core UI logic of NanoTask
+/// </summary>
+public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
+{
+    [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly PaperSystem _paper = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
+        SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
+
+        SubscribeLocalEvent<NanoTaskCartridgeComponent, CartridgeRemovedEvent>(OnCartridgeRemoved);
+
+        SubscribeLocalEvent<NanoTaskInteractionComponent, InteractUsingEvent>(OnInteractUsing);
+    }
+
+    private void OnCartridgeRemoved(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeRemovedEvent args)
+    {
+        if (!_cartridgeLoader.HasProgram<NanoTaskCartridgeComponent>(args.Loader))
+        {
+            RemComp<NanoTaskInteractionComponent>(args.Loader);
+        }
+    }
+
+    private void OnInteractUsing(Entity<NanoTaskInteractionComponent> ent, ref InteractUsingEvent args)
+    {
+        if (!_cartridgeLoader.TryGetProgram<NanoTaskCartridgeComponent>(ent.Owner, out var uid, out var program))
+        {
+            return;
+        }
+        if (!EntityManager.TryGetComponent<NanoTaskPrintedComponent>(args.Used, out var printed))
+        {
+            return;
+        }
+        if (printed.Task is NanoTaskItem item)
+        {
+            program.Tasks.Add(new(program.Counter++, printed.Task));
+            args.Handled = true;
+            EntityManager.DeleteEntity(args.Used);
+            UpdateUiState(new Entity<NanoTaskCartridgeComponent>(uid.Value, program), ent.Owner);
+        }
+    }
+
+    /// <summary>
+    /// This gets called when the ui fragment needs to be updated for the first time after activating
+    /// </summary>
+    private void OnUiReady(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeUiReadyEvent args)
+    {
+        UpdateUiState(ent, args.Loader);
+    }
+
+    private void SetupPrintedTask(EntityUid uid, NanoTaskItem item)
+    {
+        PaperComponent? paper = null;
+        NanoTaskPrintedComponent? printed = null;
+        if (!Resolve(uid, ref paper, ref printed))
+            return;
+
+        printed.Task = item;
+        var msg = new FormattedMessage();
+        msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
+        msg.PushNewline();
+        msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
+        msg.PushNewline();
+        msg.AddText(item.Priority switch {
+            NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
+            NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
+            NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),
+            _ => "",
+        });
+
+        _paper.SetContent((uid, paper), msg.ToMarkup());
+    }
+
+    /// <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(Entity<NanoTaskCartridgeComponent> ent, ref CartridgeMessageEvent args)
+    {
+        if (args is not NanoTaskUiMessageEvent message)
+            return;
+
+        switch (message.Payload)
+        {
+            case NanoTaskAddTask task:
+                if (!task.Item.Validate())
+                    return;
+
+                ent.Comp.Tasks.Add(new(ent.Comp.Counter++, task.Item));
+                break;
+            case NanoTaskUpdateTask task:
+            {
+                if (!task.Item.Data.Validate())
+                    return;
+
+                var idx = ent.Comp.Tasks.FindIndex(t => t.Id == task.Item.Id);
+                if (idx != -1)
+                    ent.Comp.Tasks[idx] = task.Item;
+                break;
+            }
+            case NanoTaskDeleteTask task:
+                ent.Comp.Tasks.RemoveAll(t => t.Id == task.Id);
+                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);
+                _hands.PickupOrDrop(message.Actor, printed);
+                _audio.PlayPvs(new SoundPathSpecifier("/Audio/Machines/printer.ogg"), ent.Owner);
+                SetupPrintedTask(printed, task.Item);
+                break;
+            }
+        }
+
+        UpdateUiState(ent, GetEntity(args.LoaderUid));
+    }
+
+
+    private void UpdateUiState(Entity<NanoTaskCartridgeComponent> ent, EntityUid loaderUid)
+    {
+        var state = new NanoTaskUiState(ent.Comp.Tasks);
+        _cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
+    }
+}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NanoTaskCartridgeComponent.cs b/Content.Shared/CartridgeLoader/Cartridges/NanoTaskCartridgeComponent.cs
new file mode 100644 (file)
index 0000000..31169ae
--- /dev/null
@@ -0,0 +1,42 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Component that indicates a PDA cartridge as containing the NanoTask program
+/// </summary>
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class NanoTaskCartridgeComponent : Component
+{
+    /// <summary>
+    /// The list of tasks
+    /// </summary>
+    [DataField]
+    public List<NanoTaskItemAndId> Tasks = new();
+
+    /// <summary>
+    /// counter for generating task IDs
+    /// </summary>
+    [DataField]
+    public int Counter = 1;
+
+    /// <summary>
+    /// When the user can print again
+    /// </summary>
+    [DataField, AutoPausedField]
+    public TimeSpan NextPrintAllowedAfter = TimeSpan.Zero;
+
+    /// <summary>
+    /// How long in between each time the user can print out a task
+    /// </summary>
+    [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
+{
+}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NanoTaskPrintedComponent.cs b/Content.Shared/CartridgeLoader/Cartridges/NanoTaskPrintedComponent.cs
new file mode 100644 (file)
index 0000000..a020e46
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+
+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]
+public sealed partial class NanoTaskPrintedComponent : Component
+{
+    /// <summary>
+    /// The task that this item holds
+    /// </summary>
+    [DataField]
+    public NanoTaskItem? Task;
+}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiMessageEvent.cs b/Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiMessageEvent.cs
new file mode 100644 (file)
index 0000000..51a34aa
--- /dev/null
@@ -0,0 +1,91 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     Base UI message for NanoTask interactions
+/// </summary>
+public interface INanoTaskUiMessagePayload
+{
+}
+
+/// <summary>
+///     Dispatched when a new task is created
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskAddTask : INanoTaskUiMessagePayload
+{
+    /// <summary>
+    ///     The newly created task
+    /// </summary>
+    public readonly NanoTaskItem Item;
+
+    public NanoTaskAddTask(NanoTaskItem item)
+    {
+        Item = item;
+    }
+}
+
+/// <summary>
+///     Dispatched when an existing task is modified
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskUpdateTask : INanoTaskUiMessagePayload
+{
+    /// <summary>
+    ///     The task that was updated and its ID
+    /// </summary>
+    public readonly NanoTaskItemAndId Item;
+
+    public NanoTaskUpdateTask(NanoTaskItemAndId item)
+    {
+        Item = item;
+    }
+}
+
+/// <summary>
+///     Dispatched when an existing task is deleted
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskDeleteTask : INanoTaskUiMessagePayload
+{
+    /// <summary>
+    ///     The ID of the task to delete
+    /// </summary>
+    public readonly int Id;
+
+    public NanoTaskDeleteTask(int id)
+    {
+        Id = id;
+    }
+}
+
+/// <summary>
+///     Dispatched when a task is requested to be printed
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskPrintTask : INanoTaskUiMessagePayload
+{
+    /// <summary>
+    ///     The NanoTask to print
+    /// </summary>
+    public readonly NanoTaskItem Item;
+
+    public NanoTaskPrintTask(NanoTaskItem item)
+    {
+        Item = item;
+    }
+}
+
+/// <summary>
+///     Cartridge message event carrying the NanoTask UI messages
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class NanoTaskUiMessageEvent : CartridgeMessageEvent
+{
+    public readonly INanoTaskUiMessagePayload Payload;
+    public NanoTaskUiMessageEvent(INanoTaskUiMessagePayload payload)
+    {
+        Payload = payload;
+    }
+}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiState.cs b/Content.Shared/CartridgeLoader/Cartridges/NanoTaskUiState.cs
new file mode 100644 (file)
index 0000000..9f27067
--- /dev/null
@@ -0,0 +1,88 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+/// <summary>
+///     The priority assigned to a NanoTask item
+/// </summary>
+[Serializable, NetSerializable]
+public enum NanoTaskPriority : byte
+{
+    High,
+    Medium,
+    Low,
+};
+
+/// <summary>
+///     The data relating to a single NanoTask item, but not its identifier
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskItem
+{
+    /// <summary>
+    ///     The maximum length of the Description and TaskIsFor fields
+    /// </summary>
+    public static int MaximumStringLength = 30;
+
+    /// <summary>
+    ///     The task description, i.e. "Bake a cake"
+    /// </summary>
+    public readonly string Description;
+
+    /// <summary>
+    ///     Who the task is for, i.e. "Cargo"
+    /// </summary>
+    public readonly string TaskIsFor;
+
+    /// <summary>
+    ///     If the task is marked as done or not
+    /// </summary>
+    public readonly bool IsTaskDone;
+
+    /// <summary>
+    ///     The task's marked priority
+    /// </summary>
+    public readonly NanoTaskPriority Priority;
+
+    public NanoTaskItem(string description, string taskIsFor, bool isTaskDone, NanoTaskPriority priority)
+    {
+        Description = description;
+        TaskIsFor = taskIsFor;
+        IsTaskDone = isTaskDone;
+        Priority = priority;
+    }
+    public bool Validate()
+    {
+        return Description.Length <= MaximumStringLength && TaskIsFor.Length <= MaximumStringLength;
+    }
+};
+
+/// <summary>
+///     Pairs a NanoTask item and its identifier
+/// </summary>
+[Serializable, NetSerializable, DataRecord]
+public sealed class NanoTaskItemAndId
+{
+    public readonly int Id;
+    public readonly NanoTaskItem Data;
+
+    public NanoTaskItemAndId(int id, NanoTaskItem data)
+    {
+        Id = id;
+        Data = data;
+    }
+};
+
+/// <summary>
+///     The UI state of the NanoTask
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class NanoTaskUiState : BoundUserInterfaceState
+{
+    public List<NanoTaskItemAndId> Tasks;
+
+    public NanoTaskUiState(List<NanoTaskItemAndId> tasks)
+    {
+        Tasks = tasks;
+    }
+}
diff --git a/Content.Shared/CartridgeLoader/Cartridges/SharedNanoTaskCartridgeSystem.cs b/Content.Shared/CartridgeLoader/Cartridges/SharedNanoTaskCartridgeSystem.cs
new file mode 100644 (file)
index 0000000..444e377
--- /dev/null
@@ -0,0 +1,19 @@
+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);
+    }
+}
index 278e70286ebc24cb0c4a6715aeafaf8cd23182c8..5da0af441b8a72fa4bed0519ba49df432c6ae986 100644 (file)
@@ -2,6 +2,7 @@ device-pda-slot-component-slot-name-cartridge = Cartridge
 
 default-program-name = Program
 notekeeper-program-name = Notekeeper
+nano-task-program-name = NanoTask
 news-read-program-name = Station news
 
 crew-manifest-program-name = Crew manifest
@@ -28,6 +29,47 @@ astro-nav-program-name = AstroNav
 
 med-tek-program-name = MedTek
 
+# NanoTask cartridge
+
+nano-task-ui-heading-high-priority-tasks =
+    { $amount ->
+        [zero] No High Priority Tasks
+        [one] 1 High Priority Task
+       *[other] {$amount} High Priority Tasks
+    }
+nano-task-ui-heading-medium-priority-tasks =
+    { $amount ->
+        [zero] No Medium Priority Tasks
+        [one] 1 Medium Priority Task
+       *[other] {$amount} Medium Priority Tasks
+    }
+nano-task-ui-heading-low-priority-tasks =
+    { $amount ->
+        [zero] No Low Priority Tasks
+        [one] 1 Low Priority Task
+       *[other] {$amount} Low Priority Tasks
+    }
+nano-task-ui-done = Done
+nano-task-ui-revert-done = Undo
+nano-task-ui-priority-low = Low
+nano-task-ui-priority-medium = Medium
+nano-task-ui-priority-high = High
+nano-task-ui-cancel = Cancel
+nano-task-ui-print = Print
+nano-task-ui-delete = Delete
+nano-task-ui-save = Save
+nano-task-ui-new-task = New Task
+nano-task-ui-description-label = Description:
+nano-task-ui-description-placeholder = Get something important
+nano-task-ui-requester-label = Requester:
+nano-task-ui-requester-placeholder = John Nanotrasen
+nano-task-ui-item-title = Edit Task
+nano-task-printed-description = Description: {$description}
+nano-task-printed-requester = Requester: {$requester}
+nano-task-printed-high-priority = Priority: High
+nano-task-printed-medium-priority = Priority: Medium
+nano-task-printed-low-priority = Priority: Low
+
 # Wanted list cartridge
 wanted-list-program-name = Wanted list
 wanted-list-label-no-records = It's all right, cowboy
index 0ab0b687dcf2d7a38e2b72dc1a39600a3c5b9dfc..494bda50d16b30338a3dea972e056e5a10e0ebec 100644 (file)
       state: book_icon
   - type: NotekeeperCartridge
 
+
+- type: entity
+  parent: BaseItem
+  id: NanoTaskCartridge
+  name: NanoTask cartridge
+  description: A program that allows you to keep a list of tasks to do.
+  components:
+  - type: Sprite
+    sprite: Objects/Devices/cartridge.rsi
+    state: cart-nav
+  - type: Cartridge
+    programName: nano-task-program-name
+    icon:
+      sprite: Interface/Misc/program_icons.rsi
+      state: nano_task
+  - type: UIFragment
+    ui: !type:NanoTaskUi
+  - type: NanoTaskCartridge
+
 - type: entity
   parent: BaseItem
   id: NewsReaderCartridge
index 3ef18d1b2e5f9990af98564332aa296d752f9e71..fa9b31359054e6c5ab187f942d347df04fe0ed10 100644 (file)
@@ -80,6 +80,7 @@
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
     cartridgeSlot:
       priority: -1
     preinstalled:
     - CrewManifestCartridge
     - NotekeeperCartridge
+    - NanoTaskCartridge
     - NewsReaderCartridge
     - WantedListCartridge
 
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
       - MedTekCartridge
 
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
       - AstroNavCartridge
 
     preinstalled:
     - CrewManifestCartridge
     - NotekeeperCartridge
+    - NanoTaskCartridge
     - NewsReaderCartridge
     - WantedListCartridge
     - LogProbeCartridge
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
       - MedTekCartridge
       - WantedListCartridge
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
       - LogProbeCartridge
       - WantedListCartridge
     uiKey: enum.PdaUiKey.Key
     preinstalled:
       - NotekeeperCartridge
+      - NanoTaskCartridge
 
 - type: entity
   parent: BaseSecurityPDA
     preinstalled:
       - CrewManifestCartridge
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - NewsReaderCartridge
       - MedTekCartridge
       - WantedListCartridge
     preinstalled:
     - CrewManifestCartridge
     - NotekeeperCartridge
+    - NanoTaskCartridge
     - NewsReaderCartridge
     - WantedListCartridge
     - LogProbeCartridge
     preinstalled:
     - CrewManifestCartridge
     - NotekeeperCartridge
+    - NanoTaskCartridge
     - NewsReaderCartridge
     - WantedListCartridge
     - MedTekCartridge
     uiKey: enum.PdaUiKey.Key
     preinstalled:
       - NotekeeperCartridge
+      - NanoTaskCartridge
       - MedTekCartridge
 
 - type: entity
index a8b5bf0cabf3dcd5b07cdb0d837800307568f2bb..de4e38cc7a20f1f6cbed6c3c721302893cf52072 100644 (file)
     headerImagePath: "/Textures/Interface/Paper/paper_heading_cargo_invoice.svg.96dpi.png"
     headerMargin: 0.0, 12.0, 0.0, 0.0
 
+- type: entity
+  id: PaperNanoTaskItem
+  parent: Paper
+  name: NanoTask item
+  description: A printed NanoTask item. Can be inserted into your PDA to add it to your tasks.
+  components:
+  - type: Tag
+    tags:
+    - Document
+    - Trash
+    - Paper
+  - type: StaticPrice
+    price: 0
+  - type: NanoTaskPrinted
+
 - type: entity
   id: PaperCargoBountyManifest
   parent: PaperCargoInvoice
index 4c2a45ad768b39f2d1b9c0af6f055cf0f90c8fa4..f31c73710cfffdb70c10b4d240b8d97fe5e31b9c 100644 (file)
@@ -1,7 +1,7 @@
 {
     "version": 1,
     "license": "CC-BY-SA-3.0",
-    "copyright": "news_read by Misha_Unity, crew_manifest by Phill101",
+    "copyright": "news_read by Misha_Unity, crew_manifest by Phill101, nano_task by Janet Blackquill <uhhadd@gmail.com>",
     "size": {
       "x": 32,
       "y": 32
@@ -12,6 +12,9 @@
       },
       {
         "name": "crew_manifest"
+      },
+      {
+        "name": "nano_task"
       }
     ]
 }
diff --git a/Resources/Textures/Interface/Misc/program_icons.rsi/nano_task.png b/Resources/Textures/Interface/Misc/program_icons.rsi/nano_task.png
new file mode 100644 (file)
index 0000000..bcbfbfc
Binary files /dev/null and b/Resources/Textures/Interface/Misc/program_icons.rsi/nano_task.png differ