diff --git a/Content.Client/Research/ResearchSystem.cs b/Content.Client/Research/ResearchSystem.cs
new file mode 100644
index 0000000000..55d9535272
--- /dev/null
+++ b/Content.Client/Research/ResearchSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Research.Systems;
+
+namespace Content.Client.Research;
+
+public sealed class ResearchSystem : SharedResearchSystem
+{
+
+}
diff --git a/Content.Client/Research/TechnologyDatabaseComponent.cs b/Content.Client/Research/TechnologyDatabaseComponent.cs
deleted file mode 100644
index e68bd4b489..0000000000
--- a/Content.Client/Research/TechnologyDatabaseComponent.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Content.Shared.Research.Components;
-using Content.Shared.Research.Prototypes;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Research
-{
- [RegisterComponent]
- public sealed class TechnologyDatabaseComponent : SharedTechnologyDatabaseComponent
- {
- ///
- /// Event called when the database is updated.
- ///
- public event Action? OnDatabaseUpdated;
-
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- base.HandleComponentState(curState, nextState);
-
- if (curState is not TechnologyDatabaseState state) return;
-
- TechnologyIds.Clear();
-
- var protoManager = IoCManager.Resolve();
-
- foreach (var techID in state.Technologies)
- {
- if (!protoManager.HasIndex(techID)) continue;
- TechnologyIds.Add(techID);
- }
-
- OnDatabaseUpdated?.Invoke();
- }
- }
-}
diff --git a/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs b/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs
new file mode 100644
index 0000000000..da167960f9
--- /dev/null
+++ b/Content.Client/Research/UI/DiskConsoleBoundUserInterface.cs
@@ -0,0 +1,53 @@
+using Content.Shared.Lathe;
+using Content.Shared.Research;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Research.UI
+{
+ public sealed class DiskConsoleBoundUserInterface : BoundUserInterface
+ {
+ private DiskConsoleMenu? _menu;
+
+ public DiskConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
+ {
+
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new();
+
+ _menu.OnClose += Close;
+ _menu.OpenCentered();
+
+ _menu.OnServerButtonPressed += () =>
+ {
+ SendMessage(new LatheServerSelectionMessage());
+ };
+ _menu.OnPrintButtonPressed += () =>
+ {
+ SendMessage(new DiskConsolePrintDiskMessage());
+ };
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+ _menu?.Close();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not DiskConsoleBoundUserInterfaceState msg)
+ return;
+
+ _menu?.Update(msg);
+ }
+ }
+}
diff --git a/Content.Client/Research/UI/DiskConsoleMenu.xaml b/Content.Client/Research/UI/DiskConsoleMenu.xaml
new file mode 100644
index 0000000000..2a208534ef
--- /dev/null
+++ b/Content.Client/Research/UI/DiskConsoleMenu.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Research/UI/DiskConsoleMenu.xaml.cs b/Content.Client/Research/UI/DiskConsoleMenu.xaml.cs
new file mode 100644
index 0000000000..7ddc0eccf0
--- /dev/null
+++ b/Content.Client/Research/UI/DiskConsoleMenu.xaml.cs
@@ -0,0 +1,29 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Research;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Research.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DiskConsoleMenu : FancyWindow
+{
+ public event Action? OnServerButtonPressed;
+ public event Action? OnPrintButtonPressed;
+
+ public DiskConsoleMenu()
+ {
+ RobustXamlLoader.Load(this);
+
+ ServerButton.OnPressed += _ => OnServerButtonPressed?.Invoke();
+ PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
+ }
+
+ public void Update(DiskConsoleBoundUserInterfaceState state)
+ {
+ PrintButton.Disabled = !state.CanPrint;
+ TotalLabel.Text = Loc.GetString("tech-disk-ui-total-label", ("amount", state.ServerPoints));
+ CostLabel.Text = Loc.GetString("tech-disk-ui-cost-label", ("amount", state.PointCost));
+ }
+}
+
diff --git a/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs b/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs
index 0d0b31aa3b..e6a8d349ad 100644
--- a/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs
+++ b/Content.Client/Research/UI/ResearchConsoleBoundUserInterface.cs
@@ -1,5 +1,6 @@
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
+using Content.Shared.Research.Systems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -12,17 +13,22 @@ namespace Content.Client.Research.UI
public int PointsPerSecond { get; private set; }
private ResearchConsoleMenu? _consoleMenu;
private TechnologyDatabaseComponent? _technologyDatabase;
+ private readonly IEntityManager _entityManager;
+ private readonly SharedResearchSystem _research;
public ResearchConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new ConsoleServerSyncMessage());
+ _entityManager = IoCManager.Resolve();
+ _research = _entityManager.System();
}
protected override void Open()
{
base.Open();
- if (!IoCManager.Resolve().TryGetComponent(Owner.Owner, out _technologyDatabase)) return;
+ if (!_entityManager.TryGetComponent(Owner.Owner, out _technologyDatabase))
+ return;
_consoleMenu = new ResearchConsoleMenu(this);
@@ -47,18 +53,22 @@ namespace Content.Client.Research.UI
};
_consoleMenu.OpenCentered();
-
- _technologyDatabase.OnDatabaseUpdated += _consoleMenu.Populate;
}
public bool IsTechnologyUnlocked(TechnologyPrototype technology)
{
- return _technologyDatabase?.IsTechnologyUnlocked(technology.ID) ?? false;
+ if (_technologyDatabase == null)
+ return false;
+
+ return _research.IsTechnologyUnlocked(_technologyDatabase.Owner, technology, _technologyDatabase);
}
public bool CanUnlockTechnology(TechnologyPrototype technology)
{
- return _technologyDatabase?.CanUnlockTechnology(technology) ?? false;
+ if (_technologyDatabase == null)
+ return false;
+
+ return _research.CanUnlockTechnology(_technologyDatabase.Owner, technology, _technologyDatabase);
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -70,12 +80,14 @@ namespace Content.Client.Research.UI
PointsPerSecond = castState.PointsPerSecond;
// We update the user interface here.
_consoleMenu?.PopulatePoints();
+ _consoleMenu?.Populate();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (!disposing) return;
+ if (!disposing)
+ return;
_consoleMenu?.Dispose();
}
}
diff --git a/Content.Server/Lathe/LatheSystem.cs b/Content.Server/Lathe/LatheSystem.cs
index 740767d534..caeeac62ea 100644
--- a/Content.Server/Lathe/LatheSystem.cs
+++ b/Content.Server/Lathe/LatheSystem.cs
@@ -43,12 +43,12 @@ namespace Content.Server.Lathe
SubscribeLocalEvent(OnLatheQueueRecipeMessage);
SubscribeLocalEvent(OnLatheSyncRequestMessage);
- SubscribeLocalEvent(OnLatheServerSelectionMessage);
SubscribeLocalEvent((u,c,_) => UpdateUserInterfaceState(u,c));
SubscribeLocalEvent((u,c,_) => UpdateUserInterfaceState(u,c));
SubscribeLocalEvent(OnGetRecipes);
+ SubscribeLocalEvent(OnLatheServerSelectionMessage);
SubscribeLocalEvent(OnLatheServerSyncMessage);
}
@@ -202,14 +202,7 @@ namespace Content.Server.Lathe
if (uid != args.Lathe || !TryComp(uid, out var latheComponent) || latheComponent.DynamicRecipes == null)
return;
- //gets all of the techs that are unlocked and also in the DynamicRecipes list
- var allTechs = (from technology in from tech in component.TechnologyIds
- select _proto.Index(tech)
- from recipe in technology.UnlockedRecipes
- where latheComponent.DynamicRecipes.Contains(recipe)
- select recipe).ToList();
-
- args.Recipes = args.Recipes.Union(allTechs).ToList();
+ args.Recipes = args.Recipes.Union(component.RecipeIds.Where(r => latheComponent.DynamicRecipes.Contains(r))).ToList();
}
///
@@ -308,16 +301,13 @@ namespace Content.Server.Lathe
UpdateUserInterfaceState(uid, component);
}
- private void OnLatheServerSelectionMessage(EntityUid uid, LatheComponent component, LatheServerSelectionMessage args)
+ private void OnLatheServerSelectionMessage(EntityUid uid, ResearchClientComponent component, LatheServerSelectionMessage args)
{
- // TODO: one day, when you can open BUIs clientside, do that. Until then, picture Electro seething.
- if (component.DynamicRecipes != null)
- _uiSys.TryOpen(uid, ResearchClientUiKey.Key, (IPlayerSession) args.Session);
+ _uiSys.TryOpen(uid, ResearchClientUiKey.Key, (IPlayerSession) args.Session);
}
private void OnLatheServerSyncMessage(EntityUid uid, TechnologyDatabaseComponent component, LatheServerSyncMessage args)
{
- Logger.Debug("OnLatheServerSyncMessage");
_researchSys.SyncWithServer(component);
UpdateUserInterfaceState(uid);
}
diff --git a/Content.Server/Research/Components/TechnologyDatabaseComponent.cs b/Content.Server/Research/Components/TechnologyDatabaseComponent.cs
deleted file mode 100644
index 86d2bd7a45..0000000000
--- a/Content.Server/Research/Components/TechnologyDatabaseComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Research.Components;
-
-namespace Content.Server.Research.Components
-{
- [RegisterComponent]
- public sealed class TechnologyDatabaseComponent : SharedTechnologyDatabaseComponent {}
-}
diff --git a/Content.Server/Research/Systems/ResearchSystem.Server.cs b/Content.Server/Research/Systems/ResearchSystem.Server.cs
index 16f27b39e8..c69a9872fe 100644
--- a/Content.Server/Research/Systems/ResearchSystem.Server.cs
+++ b/Content.Server/Research/Systems/ResearchSystem.Server.cs
@@ -1,5 +1,6 @@
using Content.Server.Power.EntitySystems;
using Content.Server.Research.Components;
+using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
namespace Content.Server.Research.Systems;
@@ -44,7 +45,7 @@ public sealed partial class ResearchSystem
{
if (!Resolve(component.Owner, ref databaseComponent, false))
return false;
- return databaseComponent.IsTechnologyUnlocked(prototype.ID);
+ return IsTechnologyUnlocked(databaseComponent.Owner, prototype.ID, databaseComponent);
}
public bool CanUnlockTechnology(ResearchServerComponent component, TechnologyPrototype technology, TechnologyDatabaseComponent? databaseComponent = null)
@@ -52,7 +53,7 @@ public sealed partial class ResearchSystem
if (!Resolve(component.Owner, ref databaseComponent, false))
return false;
- if (!databaseComponent.CanUnlockTechnology(technology) ||
+ if (!CanUnlockTechnology(databaseComponent.Owner, technology, databaseComponent) ||
component.Points < technology.RequiredPoints ||
IsTechnologyUnlocked(component, technology, databaseComponent))
return false;
diff --git a/Content.Server/Research/Systems/ResearchSystem.Technology.cs b/Content.Server/Research/Systems/ResearchSystem.Technology.cs
index 1a82f48716..37c63c8bd4 100644
--- a/Content.Server/Research/Systems/ResearchSystem.Technology.cs
+++ b/Content.Server/Research/Systems/ResearchSystem.Technology.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Content.Server.Research.Components;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
@@ -7,16 +8,6 @@ namespace Content.Server.Research.Systems;
public sealed partial class ResearchSystem
{
- private void InitializeTechnology()
- {
- SubscribeLocalEvent(OnTechnologyGetState);
- }
-
- private void OnTechnologyGetState(EntityUid uid, TechnologyDatabaseComponent component, ref ComponentGetState args)
- {
- args.State = new TechnologyDatabaseState(component.TechnologyIds);
- }
-
///
/// Synchronizes this database against other,
/// adding all technologies from the other that
@@ -27,11 +18,8 @@ public sealed partial class ResearchSystem
/// Whether the other database should be synced against this one too or not.
public void Sync(TechnologyDatabaseComponent component, TechnologyDatabaseComponent otherDatabase, bool twoway = true)
{
- foreach (var tech in otherDatabase.TechnologyIds)
- {
- if (!component.IsTechnologyUnlocked(tech))
- AddTechnology(component, tech);
- }
+ otherDatabase.TechnologyIds = otherDatabase.TechnologyIds.Union(component.TechnologyIds).ToList();
+ otherDatabase.RecipeIds = otherDatabase.RecipeIds.Union(component.RecipeIds).ToList();
if (twoway)
Sync(otherDatabase, component, false);
@@ -65,11 +53,9 @@ public sealed partial class ResearchSystem
///
public bool UnlockTechnology(TechnologyDatabaseComponent component, TechnologyPrototype technology)
{
- if (!component.CanUnlockTechnology(technology))
- return false;
+ if (!CanUnlockTechnology(component.Owner, technology, component)) return false;
AddTechnology(component, technology.ID);
- Dirty(component);
return true;
}
@@ -80,6 +66,39 @@ public sealed partial class ResearchSystem
///
public void AddTechnology(TechnologyDatabaseComponent component, string technology)
{
- component.TechnologyIds.Add(technology);
+ if (!_prototypeManager.TryIndex(technology, out var prototype))
+ return;
+ AddTechnology(component, prototype);
+ }
+
+ public void AddTechnology(TechnologyDatabaseComponent component, TechnologyPrototype technology)
+ {
+ component.TechnologyIds.Add(technology.ID);
+ foreach (var unlock in technology.UnlockedRecipes)
+ {
+ if (component.RecipeIds.Contains(unlock))
+ continue;
+ component.RecipeIds.Add(unlock);
+ }
+ Dirty(component);
+
+ if (!TryComp(component.Owner, out var server))
+ return;
+ foreach (var client in server.Clients)
+ {
+ if (!TryComp(client, out var console))
+ continue;
+ UpdateConsoleInterface(console);
+ }
+ }
+
+ public void AddLatheRecipe(TechnologyDatabaseComponent component, string recipe, bool dirty = true)
+ {
+ if (component.RecipeIds.Contains(recipe))
+ return;
+
+ component.RecipeIds.Add(recipe);
+ if (dirty)
+ Dirty(component);
}
}
diff --git a/Content.Server/Research/Systems/ResearchSystem.cs b/Content.Server/Research/Systems/ResearchSystem.cs
index 603a003641..2486edd229 100644
--- a/Content.Server/Research/Systems/ResearchSystem.cs
+++ b/Content.Server/Research/Systems/ResearchSystem.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Research.Components;
+using Content.Shared.Research.Systems;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
@@ -8,7 +9,7 @@ using Robust.Shared.Timing;
namespace Content.Server.Research.Systems
{
[UsedImplicitly]
- public sealed partial class ResearchSystem : EntitySystem
+ public sealed partial class ResearchSystem : SharedResearchSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -22,7 +23,6 @@ namespace Content.Server.Research.Systems
InitializeClient();
InitializeConsole();
InitializeSource();
- InitializeTechnology();
}
private void OnStartup(EntityUid uid, ResearchServerComponent component, ComponentStartup args)
diff --git a/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs b/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs
new file mode 100644
index 0000000000..2f07cae3b8
--- /dev/null
+++ b/Content.Server/Research/TechnologyDisk/Components/DiskConsoleComponent.cs
@@ -0,0 +1,21 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Research.TechnologyDisk.Components;
+
+[RegisterComponent]
+public sealed class DiskConsoleComponent : Component
+{
+ [DataField("pricePerDisk"), ViewVariables(VVAccess.ReadWrite)]
+ public int PricePerDisk = 2500;
+
+ [DataField("diskPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string DiskPrototype = "TechnologyDisk";
+
+ [DataField("printDuration")]
+ public TimeSpan PrintDuration = TimeSpan.FromSeconds(1);
+
+ [DataField("printSound")]
+ public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/printer.ogg");
+}
diff --git a/Content.Server/Research/TechnologyDisk/Components/DiskConsolePrintingComponent.cs b/Content.Server/Research/TechnologyDisk/Components/DiskConsolePrintingComponent.cs
new file mode 100644
index 0000000000..3823027983
--- /dev/null
+++ b/Content.Server/Research/TechnologyDisk/Components/DiskConsolePrintingComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Research.TechnologyDisk.Components;
+
+[RegisterComponent]
+public sealed class DiskConsolePrintingComponent : Component
+{
+ public TimeSpan FinishTime;
+}
diff --git a/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs
new file mode 100644
index 0000000000..36af9dc326
--- /dev/null
+++ b/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Research.TechnologyDisk.Components;
+
+[RegisterComponent]
+public sealed class TechnologyDiskComponent : Component
+{
+ ///
+ /// The recipe that will be added. If null, one will be randomly generated
+ ///
+ [DataField("recipes")]
+ public List? Recipes;
+}
diff --git a/Content.Server/Research/TechnologyDisk/Systems/DiskConsoleSystem.cs b/Content.Server/Research/TechnologyDisk/Systems/DiskConsoleSystem.cs
new file mode 100644
index 0000000000..26c9288292
--- /dev/null
+++ b/Content.Server/Research/TechnologyDisk/Systems/DiskConsoleSystem.cs
@@ -0,0 +1,87 @@
+using Content.Server.Research.Components;
+using Content.Server.Research.Systems;
+using Content.Server.Research.TechnologyDisk.Components;
+using Content.Server.UserInterface;
+using Content.Shared.Research;
+using Robust.Server.GameObjects;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Research.TechnologyDisk.Systems;
+
+public sealed class DiskConsoleSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly ResearchSystem _research = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnPrintDisk);
+ SubscribeLocalEvent(OnPointsChanged);
+ SubscribeLocalEvent(OnBeforeUiOpen);
+
+ SubscribeLocalEvent(OnShutdown);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var (printing, console, xform) in EntityQuery())
+ {
+ if (printing.FinishTime > _timing.CurTime)
+ continue;
+
+ RemComp(printing.Owner, printing);
+ EntityManager.SpawnEntity(console.DiskPrototype, xform.Coordinates);
+ }
+ }
+
+ private void OnPrintDisk(EntityUid uid, DiskConsoleComponent component, DiskConsolePrintDiskMessage args)
+ {
+ if (!TryComp(uid, out var client) || client.Server == null)
+ return;
+
+ if (client.Server.Points < component.PricePerDisk)
+ return;
+
+ _research.ChangePointsOnServer(client.Server.Owner, -component.PricePerDisk, client.Server);
+ _audio.PlayPvs(component.PrintSound, uid);
+
+ var printing = EnsureComp(uid);
+ printing.FinishTime = _timing.CurTime + component.PrintDuration;
+ }
+
+ private void OnPointsChanged(EntityUid uid, DiskConsoleComponent component, ref ResearchServerPointsChangedEvent args)
+ {
+ UpdateUserInterface(uid, component);
+ }
+
+ private void OnBeforeUiOpen(EntityUid uid, DiskConsoleComponent component, BeforeActivatableUIOpenEvent args)
+ {
+ UpdateUserInterface(uid, component);
+ }
+
+ public void UpdateUserInterface(EntityUid uid, DiskConsoleComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false))
+ return;
+
+ var totalPoints = 0;
+ if (TryComp(uid, out var client) && client.Server != null)
+ {
+ totalPoints = client.Server.Points;
+ }
+ var canPrint = !HasComp(uid) && totalPoints >= component.PricePerDisk;
+
+ var state = new DiskConsoleBoundUserInterfaceState(totalPoints, component.PricePerDisk, canPrint);
+ _ui.TrySetUiState(uid, DiskConsoleUiKey.Key, state);
+ }
+
+ private void OnShutdown(EntityUid uid, DiskConsolePrintingComponent component, ComponentShutdown args)
+ {
+ UpdateUserInterface(uid);
+ }
+}
diff --git a/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs b/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs
new file mode 100644
index 0000000000..e098a222b9
--- /dev/null
+++ b/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs
@@ -0,0 +1,94 @@
+using System.Linq;
+using Content.Server.Popups;
+using Content.Server.Research.Components;
+using Content.Server.Research.Systems;
+using Content.Server.Research.TechnologyDisk.Components;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Research.Components;
+using Content.Shared.Research.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Research.TechnologyDisk.Systems;
+
+public sealed class TechnologyDiskSystem : EntitySystem
+{
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly ResearchSystem _research = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnMapInit);
+ }
+
+ private void OnAfterInteract(EntityUid uid, TechnologyDiskComponent component, AfterInteractEvent args)
+ {
+ if (args.Handled || !args.CanReach || args.Target is not { } target)
+ return;
+
+ if (!HasComp(target) || !TryComp(target, out var database))
+ return;
+
+ if (component.Recipes != null)
+ {
+ foreach (var recipe in component.Recipes)
+ {
+ _research.AddLatheRecipe(database, recipe, false);
+ }
+ Dirty(database);
+ }
+ _popup.PopupEntity(Loc.GetString("tech-disk-inserted"), target, args.User);
+ EntityManager.DeleteEntity(uid);
+ args.Handled = true;
+ }
+
+ private void OnExamine(EntityUid uid, TechnologyDiskComponent component, ExaminedEvent args)
+ {
+ var message = Loc.GetString("tech-disk-examine-none");
+ if (component.Recipes != null && component.Recipes.Any())
+ {
+ var prototype = _prototype.Index(component.Recipes[0]);
+ var resultPrototype = _prototype.Index(prototype.Result);
+ message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name));
+
+ if (component.Recipes.Count > 1) //idk how to do this well. sue me.
+ message += " " + Loc.GetString("tech-disk-examine-more");
+ }
+ args.PushMarkup(message);
+ }
+
+ private void OnMapInit(EntityUid uid, TechnologyDiskComponent component, MapInitEvent args)
+ {
+ if (component.Recipes != null)
+ return;
+
+ //get a list of every distinct recipe in all the technologies.
+ var allTechs = new List();
+ foreach (var tech in _prototype.EnumeratePrototypes())
+ {
+ allTechs.AddRange(tech.UnlockedRecipes);
+ }
+ allTechs = allTechs.Distinct().ToList();
+
+ //get a list of every distinct unlocked tech across all databases
+ var allUnlocked = new List();
+ foreach (var database in EntityQuery())
+ {
+ allUnlocked.AddRange(database.RecipeIds);
+ }
+ allUnlocked = allUnlocked.Distinct().ToList();
+
+ //make a list of every single non-unlocked tech
+ var validTechs = allTechs.Where(tech => !allUnlocked.Contains(tech)).ToList();
+
+ //pick one
+ component.Recipes = new();
+ component.Recipes.Add(_random.Pick(validTechs));
+ }
+}
diff --git a/Content.Shared/Research/Components/SharedTechnologyDatabaseComponent.cs b/Content.Shared/Research/Components/SharedTechnologyDatabaseComponent.cs
deleted file mode 100644
index 30e0994417..0000000000
--- a/Content.Shared/Research/Components/SharedTechnologyDatabaseComponent.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Content.Shared.Research.Prototypes;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Shared.Research.Components
-{
- [NetworkedComponent()]
- public abstract class SharedTechnologyDatabaseComponent : Component
- {
- [DataField("technologies", customTypeSerializer: typeof(PrototypeIdListSerializer))]
- public readonly List TechnologyIds = new();
-
- ///
- /// Returns whether a technology is unlocked on this database or not.
- ///
- /// The technology to be checked
- /// Whether it is unlocked or not
- public bool IsTechnologyUnlocked(string technologyId)
- {
- return TechnologyIds.Contains(technologyId);
- }
-
- ///
- /// Returns whether a technology can be unlocked on this database,
- /// taking parent technologies into account.
- ///
- /// The technology to be checked
- /// Whether it could be unlocked or not
- public bool CanUnlockTechnology(TechnologyPrototype technology)
- {
- if (IsTechnologyUnlocked(technology.ID)) return false;
- var protoMan = IoCManager.Resolve();
- foreach (var technologyId in technology.RequiredTechnologies)
- {
- protoMan.TryIndex(technologyId, out TechnologyPrototype? requiredTechnology);
- if (requiredTechnology == null)
- return false;
-
- if (!IsTechnologyUnlocked(requiredTechnology.ID))
- return false;
- }
- return true;
- }
- }
-
- [Serializable, NetSerializable]
- public sealed class TechnologyDatabaseState : ComponentState
- {
- public List Technologies;
- public TechnologyDatabaseState(List technologies)
- {
- Technologies = technologies;
- }
-
- public TechnologyDatabaseState(List technologies)
- {
- Technologies = new List();
- foreach (var technology in technologies)
- {
- Technologies.Add(technology.ID);
- }
- }
- }
-}
diff --git a/Content.Shared/Research/Components/TechnologyDatabaseComponent.cs b/Content.Shared/Research/Components/TechnologyDatabaseComponent.cs
new file mode 100644
index 0000000000..3101212b3b
--- /dev/null
+++ b/Content.Shared/Research/Components/TechnologyDatabaseComponent.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Research.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Shared.Research.Components
+{
+ [RegisterComponent, NetworkedComponent]
+ public sealed class TechnologyDatabaseComponent : Component
+ {
+ [DataField("technologyIds", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List TechnologyIds = new();
+
+ [DataField("recipeIds", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List RecipeIds = new();
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class TechnologyDatabaseState : ComponentState
+ {
+ public List Technologies;
+ public List Recipes;
+
+ public TechnologyDatabaseState(List technologies, List recipes)
+ {
+ Technologies = technologies;
+ Recipes = recipes;
+ }
+ }
+}
diff --git a/Content.Shared/Research/SharedDiskConsole.cs b/Content.Shared/Research/SharedDiskConsole.cs
new file mode 100644
index 0000000000..2b44799f99
--- /dev/null
+++ b/Content.Shared/Research/SharedDiskConsole.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Research;
+
+[Serializable, NetSerializable]
+public enum DiskConsoleUiKey : byte
+{
+ Key
+}
+
+[Serializable, NetSerializable]
+public sealed class DiskConsoleBoundUserInterfaceState : BoundUserInterfaceState
+{
+ public bool CanPrint;
+ public int PointCost;
+ public int ServerPoints;
+
+ public DiskConsoleBoundUserInterfaceState(int serverPoints, int pointCost, bool canPrint)
+ {
+ CanPrint = canPrint;
+ PointCost = pointCost;
+ ServerPoints = serverPoints;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class DiskConsolePrintDiskMessage : BoundUserInterfaceMessage
+{
+
+}
diff --git a/Content.Shared/Research/Systems/SharedResearchSystem.cs b/Content.Shared/Research/Systems/SharedResearchSystem.cs
new file mode 100644
index 0000000000..8d4355b9cb
--- /dev/null
+++ b/Content.Shared/Research/Systems/SharedResearchSystem.cs
@@ -0,0 +1,75 @@
+using Content.Shared.Research.Components;
+using Content.Shared.Research.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Research.Systems;
+
+public abstract class SharedResearchSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTechnologyGetState);
+ SubscribeLocalEvent(OnTechnologyHandleState);
+ }
+
+ private void OnTechnologyHandleState(EntityUid uid, TechnologyDatabaseComponent component, ref ComponentHandleState args)
+ {
+ if (args.Current is not TechnologyDatabaseState state)
+ return;
+ component.TechnologyIds = new (state.Technologies);
+ component.RecipeIds = new(state.Recipes);
+ }
+
+ private void OnTechnologyGetState(EntityUid uid, TechnologyDatabaseComponent component, ref ComponentGetState args)
+ {
+ args.State = new TechnologyDatabaseState(component.TechnologyIds, component.RecipeIds);
+ }
+
+ ///
+ /// Returns whether a technology is unlocked on this database or not.
+ ///
+ /// Whether it is unlocked or not
+ public bool IsTechnologyUnlocked(EntityUid uid, string technologyId, TechnologyDatabaseComponent? component = null)
+ {
+ return Resolve(uid, ref component) && component.TechnologyIds.Contains(technologyId);
+ }
+
+ ///
+ /// Returns whether a technology is unlocked on this database or not.
+ ///
+ /// Whether it is unlocked or not
+ public bool IsTechnologyUnlocked(EntityUid uid, TechnologyPrototype technologyId, TechnologyDatabaseComponent? component = null)
+ {
+ return Resolve(uid, ref component) && IsTechnologyUnlocked(uid, technologyId.ID, component);
+ }
+
+ ///
+ /// Returns whether a technology can be unlocked on this database,
+ /// taking parent technologies into account.
+ ///
+ /// Whether it could be unlocked or not
+ public bool CanUnlockTechnology(EntityUid uid, TechnologyPrototype technology, TechnologyDatabaseComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ if (IsTechnologyUnlocked(uid, technology.ID, component))
+ return false;
+
+ foreach (var technologyId in technology.RequiredTechnologies)
+ {
+ _prototypeManager.TryIndex(technologyId, out TechnologyPrototype? requiredTechnology);
+ if (requiredTechnology == null)
+ return false;
+
+ if (!IsTechnologyUnlocked(uid, requiredTechnology.ID, component))
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/Resources/Locale/en-US/research/components/technology-disk.ftl b/Resources/Locale/en-US/research/components/technology-disk.ftl
new file mode 100644
index 0000000000..4f72532010
--- /dev/null
+++ b/Resources/Locale/en-US/research/components/technology-disk.ftl
@@ -0,0 +1,9 @@
+tech-disk-inserted = You insert the disk, adding a new recipe to the server.
+tech-disk-examine-none = The label is blank.
+tech-disk-examine = The label has a small dot matrix printed image depicting a {$result}.
+tech-disk-examine-more = There are more images printed, but they're too small to discern.
+
+tech-disk-ui-name = technology disk terminal
+tech-disk-ui-total-label = There are {$amount} points on the selected server
+tech-disk-ui-cost-label = Each disk costs {$amount} points to print
+tech-disk-ui-print-button = Print Disk
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml
index 213ca87a89..f62b28da1c 100644
--- a/Resources/Prototypes/Catalog/Research/technologies.yml
+++ b/Resources/Prototypes/Catalog/Research/technologies.yml
@@ -479,6 +479,7 @@
requiredTechnologies:
- BasicResearch
unlockedRecipes:
+ - TechDiskComputerCircuitboard
- CapacitorStockPart
- MatterBinStockPart
- MicroLaserStockPart
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
index b1d37ffcc0..0258ed0027 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml
@@ -146,6 +146,17 @@
- type: ComputerBoard
prototype: ComputerAnalysisConsole
+- type: entity
+ parent: BaseComputerCircuitboard
+ id: TechDiskComputerCircuitboard
+ name: technology disk terminal board
+ description: A computer printed circuit board for a technology disk terminal.
+ components:
+ - type: Sprite
+ state: cpu_science
+ - type: ComputerBoard
+ prototype: ComputerTechnologyDiskTerminal
+
- type: entity
parent: BaseComputerCircuitboard
id: CrewMonitoringComputerCircuitboard
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
index f1464aba79..2c5bdd4250 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml
@@ -36,3 +36,21 @@
components:
- type: ResearchDisk
unlockAllTech: true
+
+- type: entity
+ parent: BaseItem
+ id: TechnologyDisk
+ name: technology disk
+ description: A disk for the R&D server containing research technology.
+ components:
+ - type: Sprite
+ sprite: Objects/Misc/module.rsi
+ layers:
+ - state: datadisk_base
+ map: ["enum.DamageStateVisualLayers.Base"]
+ - state: datadisk_label
+ - type: RandomSprite
+ available:
+ - enum.DamageStateVisualLayers.Base:
+ datadisk_base: Sixteen
+ - type: TechnologyDisk
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml
new file mode 100644
index 0000000000..6c633be9ee
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/techdiskterminal.yml
@@ -0,0 +1,41 @@
+- type: entity
+ parent: BaseComputer
+ id: ComputerTechnologyDiskTerminal
+ name: technology disk terminal
+ description: A terminal used to print out technology disks.
+ components:
+ - type: Sprite
+ netsync: false
+ noRot: true
+ sprite: Structures/Machines/tech_disk_printer.rsi
+ layers:
+ - state: icon
+ map: ["enum.ComputerVisualizer+Layers.Body"]
+ - state: unshaded
+ shader: unshaded
+ map: ["enum.ComputerVisualizer+Layers.Screen"]
+ - type: Appearance
+ visuals:
+ - type: ComputerVisualizer
+ screen: unshaded
+ key: ""
+ body: icon
+ bodyBroken: icon
+ - type: DiskConsole
+ - type: ResearchClient
+ - type: ActivatableUI
+ key: enum.DiskConsoleUiKey.Key
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ - key: enum.DiskConsoleUiKey.Key
+ type: DiskConsoleBoundUserInterface
+ - key: enum.ResearchClientUiKey.Key
+ type: ResearchClientBoundUserInterface
+ - type: ExtensionCableReceiver
+ - type: Computer
+ board: TechDiskComputerCircuitboard
+ - type: PointLight
+ radius: 0.8
+ energy: 0.5
+ color: "#b53ca1"
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
index fd30b6f033..8b06f91dca 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml
@@ -292,6 +292,7 @@
- ShuttleConsoleCircuitboard
- RadarConsoleCircuitboard
- CircuitImprinterMachineCircuitboard
+ - TechDiskComputerCircuitboard
- DawInstrumentMachineCircuitboard
- CloningConsoleComputerCircuitboard
- StasisBedMachineCircuitboard
diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml
index 54e5d7218a..9d7b811a93 100644
--- a/Resources/Prototypes/Recipes/Lathes/electronics.yml
+++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml
@@ -233,6 +233,16 @@
Glass: 900
Gold: 100
+- type: latheRecipe
+ id: TechDiskComputerCircuitboard
+ icon: { sprite: Objects/Misc/module.rsi, state: cpu_science }
+ result: TechDiskComputerCircuitboard
+ completetime: 4
+ materials:
+ Steel: 100
+ Glass: 900
+ Gold: 100
+
- type: latheRecipe
id: CrewMonitoringComputerCircuitboard
icon: { sprite: Objects/Misc/module.rsi, state: id_mod }
diff --git a/Resources/Textures/Objects/Misc/module.rsi/datadisk_base.png b/Resources/Textures/Objects/Misc/module.rsi/datadisk_base.png
new file mode 100644
index 0000000000..320aea1dfc
Binary files /dev/null and b/Resources/Textures/Objects/Misc/module.rsi/datadisk_base.png differ
diff --git a/Resources/Textures/Objects/Misc/module.rsi/datadisk_label.png b/Resources/Textures/Objects/Misc/module.rsi/datadisk_label.png
new file mode 100644
index 0000000000..1f78f1ad4b
Binary files /dev/null and b/Resources/Textures/Objects/Misc/module.rsi/datadisk_label.png differ
diff --git a/Resources/Textures/Objects/Misc/module.rsi/meta.json b/Resources/Textures/Objects/Misc/module.rsi/meta.json
index 298cad6f60..e87fbf1e1e 100644
--- a/Resources/Textures/Objects/Misc/module.rsi/meta.json
+++ b/Resources/Textures/Objects/Misc/module.rsi/meta.json
@@ -1 +1 @@
-{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/tgstation/tgstation at 0d9c9a8233dfc3fc55edc538955a761a6328bee0, generic, service, command, science, security, medical, supply, and engineering taken from shiptest at https://github.com/shiptest-ss13/Shiptest/pull/1473, additional sprites created by EmoGarbage404", "states": [{"name": "abductor_mod"}, {"name": "airalarm_electronics"}, {"name": "ash_plating"}, {"name": "beaker_holder"}, {"name": "blank_mod"}, {"name": "bluespacearray"}, {"name": "boris"}, {"name": "boris_recharging", "delays": [[1.0, 1.0]]}, {"name": "card_mini"}, {"name": "card_mod"}, {"name": "cargodisk"}, {"name": "cart_connector"}, {"name": "cddrive"}, {"name": "cell"}, {"name": "cell_con"}, {"name": "cell_con_micro"}, {"name": "cell_micro"}, {"name": "cell_mini"}, {"name": "charger_APC"}, {"name": "charger_lambda"}, {"name": "charger_pda"}, {"name": "charger_wire"}, {"name": "clock_mod", "delays": [[0.6, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "command"}, {"name": "cpu"}, {"name": "cpu_adv"}, {"name": "cpu_command"}, {"name": "cpu_engineering"}, {"name": "cpu_medical"}, {"name": "cpu_science"}, {"name": "cpu_security"}, {"name": "cpu_service"}, {"name": "cpu_super", "delays": [[0.1, 0.1]]}, {"name": "cpu_supply"}, {"name": "cpuboard"}, {"name": "cpuboard_adv"}, {"name": "cpuboard_super", "delays": [[0.1, 0.1]]}, {"name": "cyborg_upgrade"}, {"name": "cyborg_upgrade1"}, {"name": "cyborg_upgrade2"}, {"name": "cyborg_upgrade3"}, {"name": "cyborg_upgrade4"}, {"name": "cyborg_upgrade5"}, {"name": "datadisk0"}, {"name": "datadisk1"}, {"name": "datadisk2"}, {"name": "datadisk3"}, {"name": "datadisk4"}, {"name": "datadisk5"}, {"name": "datadisk6"}, {"name": "datadisk_gene", "delays": [[0.1, 0.1, 0.1]]}, {"name": "datadisk_hydro", "delays": [[0.1, 0.1, 0.1]]}, {"name": "depositbox"}, {"name": "door_electronics"}, {"name": "engineering"}, {"name": "flopdrive"}, {"name": "generic"}, {"name": "harddisk"}, {"name": "harddisk_micro"}, {"name": "harddisk_mini"}, {"name": "holodisk", "delays": [[0.1, 0.1]]}, {"name": "id_mod"}, {"name": "mainboard"}, {"name": "mcontroller"}, {"name": "medical"}, {"name": "net_wired"}, {"name": "nucleardisk", "delays": [[0.1, 0.1]]}, {"name": "power_mod"}, {"name": "printer"}, {"name": "printer_mini"}, {"name": "prizevendor"}, {"name": "radio"}, {"name": "radio_micro"}, {"name": "radio_mini"}, {"name": "ram"}, {"name": "rndmajordisk"}, {"name": "science"}, {"name": "secmodschematic"}, {"name": "security"}, {"name": "selfrepair_off"}, {"name": "selfrepair_on", "delays": [[0.1, 0.1, 0.1, 0.1]]}, {"name": "service"}, {"name": "servo"}, {"name": "ssd"}, {"name": "ssd_large"}, {"name": "ssd_micro"}, {"name": "ssd_mini"}, {"name": "std_mod"}, {"name": "supply"}]}
\ No newline at end of file
+{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/tgstation/tgstation at 0d9c9a8233dfc3fc55edc538955a761a6328bee0, generic, service, command, science, security, medical, supply, and engineering taken from shiptest at https://github.com/shiptest-ss13/Shiptest/pull/1473, additional sprites created by EmoGarbage404", "states": [{"name": "abductor_mod"}, {"name": "airalarm_electronics"}, {"name": "ash_plating"}, {"name": "beaker_holder"}, {"name": "blank_mod"}, {"name": "bluespacearray"}, {"name": "boris"}, {"name": "boris_recharging", "delays": [[1.0, 1.0]]}, {"name": "card_mini"}, {"name": "card_mod"}, {"name": "cargodisk"}, {"name": "cart_connector"}, {"name": "cddrive"}, {"name": "cell"}, {"name": "cell_con"}, {"name": "cell_con_micro"}, {"name": "cell_micro"}, {"name": "cell_mini"}, {"name": "charger_APC"}, {"name": "charger_lambda"}, {"name": "charger_pda"}, {"name": "charger_wire"}, {"name": "clock_mod", "delays": [[0.6, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "command"}, {"name": "cpu"}, {"name": "cpu_adv"}, {"name": "cpu_command"}, {"name": "cpu_engineering"}, {"name": "cpu_medical"}, {"name": "cpu_science"}, {"name": "cpu_security"}, {"name": "cpu_service"}, {"name": "cpu_super", "delays": [[0.1, 0.1]]}, {"name": "cpu_supply"}, {"name": "cpuboard"}, {"name": "cpuboard_adv"}, {"name": "cpuboard_super", "delays": [[0.1, 0.1]]}, {"name": "cyborg_upgrade"}, {"name": "cyborg_upgrade1"}, {"name": "cyborg_upgrade2"}, {"name": "cyborg_upgrade3"}, {"name": "cyborg_upgrade4"}, {"name": "cyborg_upgrade5"}, {"name": "datadisk_base"}, {"name": "datadisk0"}, {"name": "datadisk1"}, {"name": "datadisk2"}, {"name": "datadisk3"}, {"name": "datadisk4"}, {"name": "datadisk5"}, {"name": "datadisk6"}, {"name": "datadisk_gene", "delays": [[0.1, 0.1, 0.1]]}, {"name": "datadisk_hydro", "delays": [[0.1, 0.1, 0.1]]}, {"name": "datadisk_label"}, {"name": "depositbox"}, {"name": "door_electronics"}, {"name": "engineering"}, {"name": "flopdrive"}, {"name": "generic"}, {"name": "harddisk"}, {"name": "harddisk_micro"}, {"name": "harddisk_mini"}, {"name": "holodisk", "delays": [[0.1, 0.1]]}, {"name": "id_mod"}, {"name": "mainboard"}, {"name": "mcontroller"}, {"name": "medical"}, {"name": "net_wired"}, {"name": "nucleardisk", "delays": [[0.1, 0.1]]}, {"name": "power_mod"}, {"name": "printer"}, {"name": "printer_mini"}, {"name": "prizevendor"}, {"name": "radio"}, {"name": "radio_micro"}, {"name": "radio_mini"}, {"name": "ram"}, {"name": "rndmajordisk"}, {"name": "science"}, {"name": "secmodschematic"}, {"name": "security"}, {"name": "selfrepair_off"}, {"name": "selfrepair_on", "delays": [[0.1, 0.1, 0.1, 0.1]]}, {"name": "service"}, {"name": "servo"}, {"name": "ssd"}, {"name": "ssd_large"}, {"name": "ssd_micro"}, {"name": "ssd_mini"}, {"name": "std_mod"}, {"name": "supply"}]}
\ No newline at end of file
diff --git a/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/icon.png b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/icon.png
new file mode 100644
index 0000000000..d21713efdd
Binary files /dev/null and b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/icon.png differ
diff --git a/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/meta.json b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/meta.json
new file mode 100644
index 0000000000..5959f53100
--- /dev/null
+++ b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/meta.json
@@ -0,0 +1,21 @@
+{
+ "version": 1,
+ "license": "CC0-1.0",
+ "copyright": "Created by EmoGarbage",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "printing",
+ "delays": [[0.25, 0.25, 0.25, 0.25]]
+ },
+ {
+ "name": "unshaded"
+ }
+ ]
+}
diff --git a/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/printing.png b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/printing.png
new file mode 100644
index 0000000000..15bd1978d7
Binary files /dev/null and b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/printing.png differ
diff --git a/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/unshaded.png b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/unshaded.png
new file mode 100644
index 0000000000..bfb67b28f3
Binary files /dev/null and b/Resources/Textures/Structures/Machines/tech_disk_printer.rsi/unshaded.png differ