From ba8b495ec0165dcc62ba2de50524ec03ced8d064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Aguilera=20Puerto?= <6766154+Zumorica@users.noreply.github.com> Date: Tue, 3 Sep 2019 22:51:19 +0200 Subject: [PATCH] Adds Research, unlockable technologies, Protolathes... (#264) * Work on Research so far More work on UI... Fix ResearchClient and Protolathe UI stuff. Fix infinite select -> request state -> select -> ... loop Add UI to ResearchClient, etc. Technology Database states, and a bit of work on the research console ui A bit of work on Research Console UI Protolathe sync Stuff that actually does things Protolathe databases yay Alright got my motivation back Yeah, no. It's almost 3 AM already Fix serialization bug again More work on stuff Stuff Adds files for most new components/systems. * Protolathes actually work now * Research. Just Research. * Adds icons from Eris. * Address reviews * Change LatheMenu resize behaviour * Update Content.Client/GameObjects/Components/Research/ResearchConsoleBoundUserInterface.cs Co-Authored-By: Pieter-Jan Briers * Update Content.Client/Research/ResearchConsoleMenu.cs Co-Authored-By: Pieter-Jan Briers * Move IoC Resolve out of for loop * Address review * Localize stuff --- Content.Client/EntryPoint.cs | 5 + .../Research/LatheBoundUserInterface.cs | 24 +- .../Research/ProtolatheDatabaseComponent.cs | 38 +++ .../ResearchClientBoundUserInterface.cs | 46 +++ .../ResearchClientServerSelectionMenu.cs | 86 +++++ .../ResearchConsoleBoundUserInterface.cs | 80 +++++ .../Research/TechnologyDatabaseComponent.cs | 35 ++ Content.Client/Research/LatheMenu.cs | 33 +- .../Research/ResearchConsoleMenu.cs | 316 ++++++++++++++++++ Content.Server/EntryPoint.cs | 2 +- .../Components/Research/LatheComponent.cs | 27 +- .../Research/LatheDatabaseComponent.cs | 2 + .../Research/ProtolatheDatabaseComponent.cs | 59 ++++ .../Research/ResearchClientComponent.cs | 109 ++++++ .../Research/ResearchConsoleComponent.cs | 94 ++++++ .../Research/ResearchPointSourceComponent.cs | 40 +++ .../Research/ResearchServerComponent.cs | 162 +++++++++ .../Research/TechnologyDatabaseComponent.cs | 77 +++++ .../EntitySystems/ResearchSystem.cs | 89 +++++ .../Research/SharedLatheComponent.cs | 24 ++ .../Research/SharedLatheDatabaseComponent.cs | 7 +- .../SharedProtolatheDatabaseComponent.cs | 72 ++++ .../Research/SharedResearchClientComponent.cs | 73 ++++ .../SharedResearchConsoleComponent.cs | 55 +++ .../SharedTechnologyDatabaseComponent.cs | 124 +++++++ Content.Shared/GameObjects/ContentNetIDs.cs | 5 +- .../Research/LatheRecipePrototype.cs | 10 +- .../Research/TechnologyPrototype.cs | 76 +++++ Resources/Prototypes/Entities/Research.yml | 40 +++ .../Entities/buildings/computers.yml | 10 +- .../Prototypes/Entities/buildings/lathe.yml | 60 +++- Resources/Prototypes/Technologies/sheet.yml | 26 ++ .../research.rsi/circuit_imprinter.png | Bin 0 -> 603 bytes .../research.rsi/circuit_imprinter_ani.png | Bin 0 -> 1743 bytes .../research.rsi/circuit_imprinter_t.png | Bin 0 -> 664 bytes .../Buildings/research.rsi/d_analyzer.png | Bin 0 -> 592 bytes .../Buildings/research.rsi/d_analyzer_l.png | Bin 0 -> 623 bytes .../Buildings/research.rsi/d_analyzer_la.png | Bin 0 -> 1450 bytes .../research.rsi/d_analyzer_process.png | Bin 0 -> 3352 bytes .../Buildings/research.rsi/d_analyzer_t.png | Bin 0 -> 631 bytes .../Textures/Buildings/research.rsi/meta.json | 1 + .../Buildings/research.rsi/protolathe.png | Bin 0 -> 879 bytes .../research.rsi/protolathe_adamantine.png | Bin 0 -> 526 bytes .../research.rsi/protolathe_bananium.png | Bin 0 -> 521 bytes .../research.rsi/protolathe_diamond.png | Bin 0 -> 532 bytes .../research.rsi/protolathe_glass.png | Bin 0 -> 521 bytes .../research.rsi/protolathe_gold.png | Bin 0 -> 567 bytes .../research.rsi/protolathe_metal.png | Bin 0 -> 524 bytes .../Buildings/research.rsi/protolathe_n.png | Bin 0 -> 3119 bytes .../research.rsi/protolathe_silver.png | Bin 0 -> 534 bytes .../research.rsi/protolathe_solid plasma.png | Bin 0 -> 538 bytes .../Buildings/research.rsi/protolathe_t.png | Bin 0 -> 1066 bytes .../research.rsi/protolathe_uranium.png | Bin 0 -> 509 bytes .../Buildings/research.rsi/server-nopower.png | Bin 0 -> 487 bytes .../Buildings/research.rsi/server-off.png | Bin 0 -> 528 bytes .../Buildings/research.rsi/server-on.png | Bin 0 -> 529 bytes .../Buildings/research.rsi/server.png | Bin 0 -> 448 bytes .../Buildings/research.rsi/server_o.png | Bin 0 -> 559 bytes .../research.rsi/tdoppler-broken.png | Bin 0 -> 2213 bytes .../Buildings/research.rsi/tdoppler-off.png | Bin 0 -> 1113 bytes .../Buildings/research.rsi/tdoppler.png | Bin 0 -> 1621 bytes 61 files changed, 1884 insertions(+), 23 deletions(-) create mode 100644 Content.Client/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs create mode 100644 Content.Client/GameObjects/Components/Research/ResearchClientBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/Research/ResearchClientServerSelectionMenu.cs create mode 100644 Content.Client/GameObjects/Components/Research/ResearchConsoleBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/Research/TechnologyDatabaseComponent.cs create mode 100644 Content.Client/Research/ResearchConsoleMenu.cs create mode 100644 Content.Server/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs create mode 100644 Content.Server/GameObjects/Components/Research/ResearchClientComponent.cs create mode 100644 Content.Server/GameObjects/Components/Research/ResearchConsoleComponent.cs create mode 100644 Content.Server/GameObjects/Components/Research/ResearchPointSourceComponent.cs create mode 100644 Content.Server/GameObjects/Components/Research/ResearchServerComponent.cs create mode 100644 Content.Server/GameObjects/Components/Research/TechnologyDatabaseComponent.cs create mode 100644 Content.Server/GameObjects/EntitySystems/ResearchSystem.cs create mode 100644 Content.Shared/GameObjects/Components/Research/SharedProtolatheDatabaseComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Research/SharedResearchClientComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Research/SharedResearchConsoleComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Research/SharedTechnologyDatabaseComponent.cs create mode 100644 Content.Shared/Research/TechnologyPrototype.cs create mode 100644 Resources/Prototypes/Entities/Research.yml create mode 100644 Resources/Prototypes/Technologies/sheet.yml create mode 100644 Resources/Textures/Buildings/research.rsi/circuit_imprinter.png create mode 100644 Resources/Textures/Buildings/research.rsi/circuit_imprinter_ani.png create mode 100644 Resources/Textures/Buildings/research.rsi/circuit_imprinter_t.png create mode 100644 Resources/Textures/Buildings/research.rsi/d_analyzer.png create mode 100644 Resources/Textures/Buildings/research.rsi/d_analyzer_l.png create mode 100644 Resources/Textures/Buildings/research.rsi/d_analyzer_la.png create mode 100644 Resources/Textures/Buildings/research.rsi/d_analyzer_process.png create mode 100644 Resources/Textures/Buildings/research.rsi/d_analyzer_t.png create mode 100644 Resources/Textures/Buildings/research.rsi/meta.json create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_adamantine.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_bananium.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_diamond.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_glass.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_gold.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_metal.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_n.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_silver.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_solid plasma.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_t.png create mode 100644 Resources/Textures/Buildings/research.rsi/protolathe_uranium.png create mode 100644 Resources/Textures/Buildings/research.rsi/server-nopower.png create mode 100644 Resources/Textures/Buildings/research.rsi/server-off.png create mode 100644 Resources/Textures/Buildings/research.rsi/server-on.png create mode 100644 Resources/Textures/Buildings/research.rsi/server.png create mode 100644 Resources/Textures/Buildings/research.rsi/server_o.png create mode 100644 Resources/Textures/Buildings/research.rsi/tdoppler-broken.png create mode 100644 Resources/Textures/Buildings/research.rsi/tdoppler-off.png create mode 100644 Resources/Textures/Buildings/research.rsi/tdoppler.png diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index be324065ca..33597d77ac 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -100,6 +100,9 @@ namespace Content.Client "PlayerInputMover", "Computer", "AsteroidRock", + "ResearchServer", + "ResearchPointSource", + "ResearchClient", "IdCard", "Access", "AccessReader", @@ -110,8 +113,10 @@ namespace Content.Client factory.RegisterIgnore(ignoreName); } + factory.Register(); factory.Register(); factory.Register(); + factory.Register(); factory.Register(); diff --git a/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs b/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs index 8dd27a498c..571c5f9e6b 100644 --- a/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs +++ b/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs @@ -23,7 +23,7 @@ namespace Content.Client.GameObjects.Components.Research public MaterialStorageComponent Storage { get; private set; } public SharedLatheComponent Lathe { get; private set; } - public LatheDatabaseComponent Database { get; private set; } + public SharedLatheDatabaseComponent Database { get; private set; } [ViewVariables] public Queue QueuedRecipes => _queuedRecipes; @@ -41,13 +41,15 @@ namespace Content.Client.GameObjects.Components.Research if (!Owner.Owner.TryGetComponent(out MaterialStorageComponent storage) || !Owner.Owner.TryGetComponent(out SharedLatheComponent lathe) - || !Owner.Owner.TryGetComponent(out LatheDatabaseComponent database)) return; + || !Owner.Owner.TryGetComponent(out SharedLatheDatabaseComponent database)) return; + + Storage = storage; Lathe = lathe; Database = database; - menu = new LatheMenu {Owner = this}; + menu = new LatheMenu(this); queueMenu = new LatheQueueMenu { Owner = this }; menu.OnClose += Close; @@ -57,6 +59,16 @@ namespace Content.Client.GameObjects.Components.Research menu.QueueButton.OnPressed += (args) => { queueMenu.OpenCentered(); }; + menu.ServerConnectButton.OnPressed += (args) => + { + SendMessage(new SharedLatheComponent.LatheServerSelectionMessage()); + }; + + menu.ServerSyncButton.OnPressed += (args) => + { + SendMessage(new SharedLatheComponent.LatheServerSyncMessage()); + }; + storage.OnMaterialStorageChanged += menu.PopulateDisabled; storage.OnMaterialStorageChanged += menu.PopulateMaterials; @@ -74,10 +86,10 @@ namespace Content.Client.GameObjects.Components.Research { case SharedLatheComponent.LatheProducingRecipeMessage msg: if (!_prototypeManager.TryIndex(msg.ID, out LatheRecipePrototype recipe)) break; - queueMenu.SetInfo(recipe); + queueMenu?.SetInfo(recipe); break; case SharedLatheComponent.LatheStoppedProducingRecipeMessage msg: - queueMenu.ClearInfo(); + queueMenu?.ClearInfo(); break; case SharedLatheComponent.LatheFullQueueMessage msg: _queuedRecipes.Clear(); @@ -86,7 +98,7 @@ namespace Content.Client.GameObjects.Components.Research if (!_prototypeManager.TryIndex(id, out LatheRecipePrototype recipePrototype)) break; _queuedRecipes.Enqueue(recipePrototype); } - queueMenu.PopulateList(); + queueMenu?.PopulateList(); break; } } diff --git a/Content.Client/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs b/Content.Client/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs new file mode 100644 index 0000000000..71aeb4411f --- /dev/null +++ b/Content.Client/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs @@ -0,0 +1,38 @@ +using System; +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Content.Client.GameObjects.Components.Research +{ + [RegisterComponent] + [ComponentReference(typeof(SharedLatheDatabaseComponent))] + public class ProtolatheDatabaseComponent : SharedProtolatheDatabaseComponent + { +#pragma warning disable CS0649 + [Dependency] + private IPrototypeManager _prototypeManager; +#pragma warning restore + + /// + /// Invoked when the database gets updated. + /// + public event Action OnDatabaseUpdated; + + public override void HandleComponentState(ComponentState curState, ComponentState nextState) + { + base.HandleComponentState(curState, nextState); + if (!(curState is ProtolatheDatabaseState state)) return; + Clear(); + foreach (var ID in state.Recipes) + { + if(!_prototypeManager.TryIndex(ID, out LatheRecipePrototype recipe)) continue; + AddRecipe(recipe); + } + + OnDatabaseUpdated?.Invoke(); + } + } +} diff --git a/Content.Client/GameObjects/Components/Research/ResearchClientBoundUserInterface.cs b/Content.Client/GameObjects/Components/Research/ResearchClientBoundUserInterface.cs new file mode 100644 index 0000000000..544418a123 --- /dev/null +++ b/Content.Client/GameObjects/Components/Research/ResearchClientBoundUserInterface.cs @@ -0,0 +1,46 @@ +using Content.Shared.GameObjects.Components.Research; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects.Components.UserInterface; + +namespace Content.Client.GameObjects.Components.Research +{ + public class ResearchClientBoundUserInterface : BoundUserInterface + { + private ResearchClientServerSelectionMenu _menu; + + public ResearchClientBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + SendMessage(new SharedResearchClientComponent.ResearchClientSyncMessage()); + } + + protected override void Open() + { + base.Open(); + + _menu = new ResearchClientServerSelectionMenu() { Owner = this }; + + _menu.OnClose += Close; + + _menu.OpenCentered(); + } + + public void SelectServer(int serverId) + { + SendMessage(new SharedResearchClientComponent.ResearchClientServerSelectedMessage(serverId)); + } + + public void DeselectServer() + { + SendMessage(new SharedResearchClientComponent.ResearchClientServerDeselectedMessage()); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + if (!(state is SharedResearchClientComponent.ResearchClientBoundInterfaceState rstate)) return; + _menu.Populate(rstate.ServerCount, rstate.ServerNames, rstate.ServerIds, rstate.SelectedServerId); + + } + } +} diff --git a/Content.Client/GameObjects/Components/Research/ResearchClientServerSelectionMenu.cs b/Content.Client/GameObjects/Components/Research/ResearchClientServerSelectionMenu.cs new file mode 100644 index 0000000000..e32ccfa625 --- /dev/null +++ b/Content.Client/GameObjects/Components/Research/ResearchClientServerSelectionMenu.cs @@ -0,0 +1,86 @@ +using System; +using Content.Shared.GameObjects.Components.Research; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components.Research +{ + public class ResearchClientServerSelectionMenu : SS14Window + { + private ItemList _servers; + private int _serverCount = 0; + private string[] _serverNames = new string[]{}; + private int[] _serverIds = new int[]{}; + private int _selectedServerId = -1; + +#pragma warning disable 649 + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + protected override Vector2? CustomSize => (300, 300); + public ResearchClientBoundUserInterface Owner { get; set; } + + public ResearchClientServerSelectionMenu() + { + IoCManager.InjectDependencies(this); + + Title = _localizationManager.GetString("Research Server Selection"); + + _servers = new ItemList() {SelectMode = ItemList.ItemListSelectMode.Single}; + + _servers.OnItemSelected += OnItemSelected; + _servers.OnItemDeselected += OnItemDeselected; + + var margin = new MarginContainer() + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.FillExpand, + MarginTop = 5f, + MarginLeft = 5f, + MarginRight = -5f, + MarginBottom = -5f, + }; + + margin.AddChild(_servers); + + Contents.AddChild(margin); + } + + public void OnItemSelected(ItemList.ItemListSelectedEventArgs itemListSelectedEventArgs) + { + Owner.SelectServer(_serverIds[itemListSelectedEventArgs.ItemIndex]); + } + + public void OnItemDeselected(ItemList.ItemListDeselectedEventArgs itemListDeselectedEventArgs) + { + Owner.DeselectServer(); + } + + public void Populate(int serverCount, string[] serverNames, int[] serverIds, int selectedServerId) + { + _serverCount = serverCount; + _serverNames = serverNames; + _serverIds = serverIds; + _selectedServerId = selectedServerId; + + // Disable so we can select the new selected server without triggering a new sync request. + _servers.OnItemSelected -= OnItemSelected; + _servers.OnItemDeselected -= OnItemDeselected; + + _servers.Clear(); + for (var i = 0; i < _serverCount; i++) + { + var id = _serverIds[i]; + _servers.AddItem($"ID: {id} || {_serverNames[i]}"); + if(id == _selectedServerId) + _servers.Select(i); + } + + _servers.OnItemSelected += OnItemSelected; + _servers.OnItemDeselected += OnItemDeselected; + } + } +} diff --git a/Content.Client/GameObjects/Components/Research/ResearchConsoleBoundUserInterface.cs b/Content.Client/GameObjects/Components/Research/ResearchConsoleBoundUserInterface.cs new file mode 100644 index 0000000000..9f6723e6db --- /dev/null +++ b/Content.Client/GameObjects/Components/Research/ResearchConsoleBoundUserInterface.cs @@ -0,0 +1,80 @@ +using Content.Client.Research; +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Log; + +namespace Content.Client.GameObjects.Components.Research +{ + public class ResearchConsoleBoundUserInterface : BoundUserInterface + { + public int Points { get; private set; } = 0; + public int PointsPerSecond { get; private set; } = 0; + private ResearchConsoleMenu _consoleMenu; + private TechnologyDatabaseComponent TechnologyDatabase; + + + public ResearchConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + SendMessage(new SharedResearchConsoleComponent.ConsoleServerSyncMessage()); + } + + protected override void Open() + { + base.Open(); + + if (!Owner.Owner.TryGetComponent(out TechnologyDatabase)) return; + + _consoleMenu = new ResearchConsoleMenu(this); + + _consoleMenu.ServerSyncButton.OnPressed += (args) => + { + SendMessage(new SharedResearchConsoleComponent.ConsoleServerSyncMessage()); + }; + + _consoleMenu.ServerSelectionButton.OnPressed += (args) => + { + SendMessage(new SharedResearchConsoleComponent.ConsoleServerSelectionMessage()); + }; + + _consoleMenu.UnlockButton.OnPressed += (args) => + { + SendMessage(new SharedResearchConsoleComponent.ConsoleUnlockTechnologyMessage(_consoleMenu.TechnologySelected.ID)); + }; + + _consoleMenu.OpenCentered(); + + TechnologyDatabase.OnDatabaseUpdated += _consoleMenu.Populate; + } + + public bool IsTechnologyUnlocked(TechnologyPrototype technology) + { + return TechnologyDatabase.IsTechnologyUnlocked(technology); + } + + public bool CanUnlockTechnology(TechnologyPrototype technology) + { + return TechnologyDatabase.CanUnlockTechnology(technology); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (SharedResearchConsoleComponent.ResearchConsoleBoundInterfaceState)state; + Points = castState.Points; + PointsPerSecond = castState.PointsPerSecond; + // We update the user interface here. + _consoleMenu?.PopulatePoints(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + _consoleMenu?.Dispose(); + } + } +} diff --git a/Content.Client/GameObjects/Components/Research/TechnologyDatabaseComponent.cs b/Content.Client/GameObjects/Components/Research/TechnologyDatabaseComponent.cs new file mode 100644 index 0000000000..98efd87227 --- /dev/null +++ b/Content.Client/GameObjects/Components/Research/TechnologyDatabaseComponent.cs @@ -0,0 +1,35 @@ +using System; +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using SixLabors.ImageSharp.Processing; + +namespace Content.Client.GameObjects.Components.Research +{ + [RegisterComponent] + public 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 TechnologyDatabaseState state)) return; + _technologies.Clear(); + var protoManager = IoCManager.Resolve(); + foreach (var techID in state.Technologies) + { + if (!protoManager.TryIndex(techID, out TechnologyPrototype technology)) continue; + _technologies.Add(technology); + } + + OnDatabaseUpdated?.Invoke(); + + } + } +} diff --git a/Content.Client/Research/LatheMenu.cs b/Content.Client/Research/LatheMenu.cs index 4a0ffbae6e..fec807a8f2 100644 --- a/Content.Client/Research/LatheMenu.cs +++ b/Content.Client/Research/LatheMenu.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Content.Client.GameObjects.Components.Research; +using Content.Shared.GameObjects.Components.Research; using Content.Shared.Materials; using Content.Shared.Research; using Robust.Client.UserInterface; @@ -25,6 +26,8 @@ namespace Content.Client.Research private LineEdit AmountLineEdit; private LineEdit SearchBar; public Button QueueButton; + public Button ServerConnectButton; + public Button ServerSyncButton; protected override Vector2? CustomSize => (300, 450); public LatheBoundUserInterface Owner { get; set; } @@ -32,10 +35,12 @@ namespace Content.Client.Research private List _recipes = new List(); private List _shownRecipes = new List(); - public LatheMenu() + public LatheMenu(LatheBoundUserInterface owner = null) { IoCManager.InjectDependencies(this); + Owner = owner; + Title = "Lathe Menu"; var margin = new MarginContainer() @@ -69,7 +74,23 @@ namespace Content.Client.Research { Text = "Queue", TextAlign = Button.AlignMode.Center, - SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.Fill, + SizeFlagsStretchRatio = 1, + }; + + ServerConnectButton = new Button() + { + Text = "Server list", + TextAlign = Button.AlignMode.Center, + SizeFlagsHorizontal = SizeFlags.Fill, + SizeFlagsStretchRatio = 1, + }; + + ServerSyncButton = new Button() + { + Text = "Sync", + TextAlign = Button.AlignMode.Center, + SizeFlagsHorizontal = SizeFlags.Fill, SizeFlagsStretchRatio = 1, }; @@ -101,7 +122,7 @@ namespace Content.Client.Research { Text = "Filter", TextAlign = Button.AlignMode.Center, - SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.Fill, SizeFlagsStretchRatio = 1, Disabled = true, }; @@ -130,6 +151,12 @@ namespace Content.Client.Research }; hBoxButtons.AddChild(spacer); + if (Owner?.Database is ProtolatheDatabaseComponent database) + { + hBoxButtons.AddChild(ServerConnectButton); + hBoxButtons.AddChild(ServerSyncButton); + database.OnDatabaseUpdated += Populate; + } hBoxButtons.AddChild(QueueButton); hBoxFilter.AddChild(SearchBar); diff --git a/Content.Client/Research/ResearchConsoleMenu.cs b/Content.Client/Research/ResearchConsoleMenu.cs new file mode 100644 index 0000000000..aed3b4e3af --- /dev/null +++ b/Content.Client/Research/ResearchConsoleMenu.cs @@ -0,0 +1,316 @@ +using System.Collections.Generic; +using Content.Client.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.Utility; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; + +namespace Content.Client.Research +{ + public class ResearchConsoleMenu : SS14Window + { + public ResearchConsoleBoundUserInterface Owner { get; set; } + + protected override Vector2? CustomSize => (800, 400); + + private List _unlockedTechnologyPrototypes = new List(); + private List _unlockableTechnologyPrototypes = new List(); + private List _futureTechnologyPrototypes = new List(); + + private Label _pointLabel; + private Label _pointsPerSecondLabel; + private Label _technologyName; + private Label _technologyDescription; + private Label _technologyRequirements; + private TextureRect _technologyIcon; + private ItemList _unlockedTechnologies; + private ItemList _unlockableTechnologies; + private ItemList _futureTechnologies; + +#pragma warning disable 649 + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + public Button UnlockButton { get; private set; } + public Button ServerSelectionButton { get; private set; } + public Button ServerSyncButton { get; private set; } + + public TechnologyPrototype TechnologySelected; + + public ResearchConsoleMenu(ResearchConsoleBoundUserInterface owner = null) + { + IoCManager.InjectDependencies(this); + + Title = _localizationManager.GetString("R&D Console"); + + Owner = owner; + + _unlockedTechnologies = new ItemList() + { + SelectMode = ItemList.ItemListSelectMode.Single, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + }; + + _unlockedTechnologies.OnItemSelected += UnlockedTechnologySelected; + + _unlockableTechnologies = new ItemList() + { + SelectMode = ItemList.ItemListSelectMode.Single, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + }; + + _unlockableTechnologies.OnItemSelected += UnlockableTechnologySelected; + + _futureTechnologies = new ItemList() + { + SelectMode = ItemList.ItemListSelectMode.Single, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + }; + + _futureTechnologies.OnItemSelected += FutureTechnologySelected; + + var vbox = new VBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + }; + + var hboxTechnologies = new HBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 2, + SeparationOverride = 10, + }; + + var hboxSelected = new HBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 1 + }; + + var vboxPoints = new VBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 1, + }; + + var vboxTechInfo = new VBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 3, + }; + + _pointLabel = new Label() { Text = _localizationManager.GetString("Research Points") + ": 0" }; + _pointsPerSecondLabel = new Label() { Text = _localizationManager.GetString("Points per Second") + ": 0" }; + + var vboxPointsButtons = new VBoxContainer() + { + Align = BoxContainer.AlignMode.End, + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + }; + + ServerSelectionButton = new Button() { Text = _localizationManager.GetString("Server list") }; + ServerSyncButton = new Button() { Text = _localizationManager.GetString("Sync")}; + UnlockButton = new Button() { Text = _localizationManager.GetString("Unlock"), Disabled = true }; + + + vboxPointsButtons.AddChild(ServerSelectionButton); + vboxPointsButtons.AddChild(ServerSyncButton); + vboxPointsButtons.AddChild(UnlockButton); + + vboxPoints.AddChild(_pointLabel); + vboxPoints.AddChild(_pointsPerSecondLabel); + vboxPoints.AddChild(vboxPointsButtons); + + _technologyIcon = new TextureRect() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 1, + Stretch = TextureRect.StretchMode.KeepAspectCentered, + }; + _technologyName = new Label(); + _technologyDescription = new Label(); + _technologyRequirements = new Label(); + + vboxTechInfo.AddChild(_technologyName); + vboxTechInfo.AddChild(_technologyDescription); + vboxTechInfo.AddChild(_technologyRequirements); + + hboxSelected.AddChild(_technologyIcon); + hboxSelected.AddChild(vboxTechInfo); + hboxSelected.AddChild(vboxPoints); + + hboxTechnologies.AddChild(_unlockedTechnologies); + hboxTechnologies.AddChild(_unlockableTechnologies); + hboxTechnologies.AddChild(_futureTechnologies); + + vbox.AddChild(hboxTechnologies); + vbox.AddChild(hboxSelected); + + Contents.AddChild(vbox); + + UnlockButton.OnPressed += (args) => + { + CleanSelectedTechnology(); + }; + + Populate(); + } + + /// + /// Cleans the selected technology controls to blank. + /// + private void CleanSelectedTechnology() + { + UnlockButton.Disabled = true; + _technologyIcon.Texture = Texture.Transparent; + _technologyName.Text = ""; + _technologyDescription.Text = ""; + _technologyRequirements.Text = ""; + } + + /// + /// Called when an unlocked technology is selected. + /// + private void UnlockedTechnologySelected(ItemList.ItemListSelectedEventArgs obj) + { + TechnologySelected = _unlockedTechnologyPrototypes[obj.ItemIndex]; + + _unlockedTechnologies.Unselect(obj.ItemIndex); + + UnlockButton.Disabled = true; + + PopulateSelectedTechnology(); + } + + /// + /// Called when an unlockable technology is selected. + /// + private void UnlockableTechnologySelected(ItemList.ItemListSelectedEventArgs obj) + { + TechnologySelected = _unlockableTechnologyPrototypes[obj.ItemIndex]; + + _unlockableTechnologies.Unselect(obj.ItemIndex); + + UnlockButton.Disabled = Owner.Points < TechnologySelected.RequiredPoints; + + PopulateSelectedTechnology(); + } + + /// + /// Called when a future technology is selected + /// + private void FutureTechnologySelected(ItemList.ItemListSelectedEventArgs obj) + { + TechnologySelected = _futureTechnologyPrototypes[obj.ItemIndex]; + + _futureTechnologies.Unselect(obj.ItemIndex); + + UnlockButton.Disabled = true; + + PopulateSelectedTechnology(); + } + + /// + /// Populate all technologies in the ItemLists. + /// + public void PopulateItemLists() + { + _unlockedTechnologies.Clear(); + _unlockableTechnologies.Clear(); + _futureTechnologies.Clear(); + + _unlockedTechnologyPrototypes.Clear(); + _unlockableTechnologyPrototypes.Clear(); + _futureTechnologyPrototypes.Clear(); + + var prototypeMan = IoCManager.Resolve(); + + // For now, we retrieve all technologies. In the future, this should be changed. + foreach (var tech in prototypeMan.EnumeratePrototypes()) + { + if (Owner.IsTechnologyUnlocked(tech)) + { + _unlockedTechnologies.AddItem(tech.Name, tech.Icon.Frame0()); + _unlockedTechnologyPrototypes.Add(tech); + } + else if (Owner.CanUnlockTechnology(tech)) + { + _unlockableTechnologies.AddItem(tech.Name, tech.Icon.Frame0()); + _unlockableTechnologyPrototypes.Add(tech); + } + else + { + _futureTechnologies.AddItem(tech.Name, tech.Icon.Frame0());; + _futureTechnologyPrototypes.Add(tech); + } + } + } + + /// + /// Fills the selected technology controls with details. + /// + public void PopulateSelectedTechnology() + { + if (TechnologySelected == null) + { + _technologyName.Text = ""; + _technologyDescription.Text = ""; + _technologyRequirements.Text = ""; + return; + } + + _technologyIcon.Texture = TechnologySelected.Icon.Frame0(); + _technologyName.Text = TechnologySelected.Name; + _technologyDescription.Text = TechnologySelected.Description+$"\n{TechnologySelected.RequiredPoints} " + _localizationManager.GetString("research points"); + _technologyRequirements.Text = _localizationManager.GetString("No technology requirements."); + + var prototypeMan = IoCManager.Resolve(); + + for (var i = 0; i < TechnologySelected.RequiredTechnologies.Count; i++) + { + var requiredId = TechnologySelected.RequiredTechnologies[i]; + if (!prototypeMan.TryIndex(requiredId, out TechnologyPrototype prototype)) continue; + if (i == 0) + _technologyRequirements.Text = _localizationManager.GetString("Requires") + $": {prototype.Name}"; + else + _technologyRequirements.Text += $", {prototype.Name}"; + } + } + + /// + /// Updates the research point labels. + /// + public void PopulatePoints() + { + _pointLabel.Text = _localizationManager.GetString("Research Points") + $": {Owner.Points}"; + _pointsPerSecondLabel.Text = _localizationManager.GetString("Points per second") + $": {Owner.PointsPerSecond}"; + } + + /// + /// Updates the whole user interface. + /// + public void Populate() + { + PopulatePoints(); + PopulateSelectedTechnology(); + PopulateItemLists(); + } + } +} diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index fef9a48bca..201659a0fe 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -35,7 +35,7 @@ namespace Content.Server "SubFloorHide", "LowWall", "Window", - "CharacterInfo" + "CharacterInfo", }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Server/GameObjects/Components/Research/LatheComponent.cs b/Content.Server/GameObjects/Components/Research/LatheComponent.cs index 705c2015e8..548ef3b368 100644 --- a/Content.Server/GameObjects/Components/Research/LatheComponent.cs +++ b/Content.Server/GameObjects/Components/Research/LatheComponent.cs @@ -6,6 +6,7 @@ using Content.Shared.GameObjects.Components.Research; using Content.Shared.Research; using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.UserInterface; using Robust.Shared.Timers; @@ -37,10 +38,9 @@ namespace Content.Server.GameObjects.Components.Research _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; } - private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg) + private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) { - var message = serverMsg.Message; - switch (message) + switch (message.Message) { case LatheQueueRecipeMessage msg: _prototypeManager.TryIndex(msg.ID, out LatheRecipePrototype recipe); @@ -57,6 +57,20 @@ namespace Content.Server.GameObjects.Components.Research if (_producingRecipe != null) _userInterface.SendMessage(new LatheProducingRecipeMessage(_producingRecipe.ID)); break; + + case LatheServerSelectionMessage msg: + if (!Owner.TryGetComponent(out ResearchClientComponent researchClient)) return; + researchClient.OpenUserInterface(message.Session); + break; + + case LatheServerSyncMessage msg: + if (!Owner.TryGetComponent(out TechnologyDatabaseComponent database) + || !Owner.TryGetComponent(out ProtolatheDatabaseComponent protoDatabase)) return; + + if(database.SyncWithServer()) + protoDatabase.Sync(); + + break; } } @@ -88,12 +102,17 @@ namespace Content.Server.GameObjects.Components.Research return true; } + public void OpenUserInterface(IPlayerSession session) + { + _userInterface.Open(session); + } + void IActivate.Activate(ActivateEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) return; - _userInterface.Open(actor.playerSession); + OpenUserInterface(actor.playerSession); return; } bool IAttackBy.AttackBy(AttackByEventArgs eventArgs) diff --git a/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs b/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs index d43d5e052f..955bd935d1 100644 --- a/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs +++ b/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs @@ -30,12 +30,14 @@ namespace Content.Server.GameObjects.Components.Research public override void Clear() { if (Static) return; + base.Clear(); Dirty(); } public override void AddRecipe(LatheRecipePrototype recipe) { if (Static) return; + base.AddRecipe(recipe); Dirty(); } diff --git a/Content.Server/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs b/Content.Server/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs new file mode 100644 index 0000000000..d14e953120 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/ProtolatheDatabaseComponent.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + [ComponentReference(typeof(SharedLatheDatabaseComponent))] + public class ProtolatheDatabaseComponent : SharedProtolatheDatabaseComponent + { + public override string Name => "ProtolatheDatabase"; + + public override ComponentState GetComponentState() + { + return new ProtolatheDatabaseState(GetRecipeIdList()); + } + + /// + /// Adds unlocked recipes from technologies to the database. + /// + public void Sync() + { + if (!Owner.TryGetComponent(out TechnologyDatabaseComponent database)) return; + + var prototypeManager = IoCManager.Resolve(); + + foreach (var technology in database.Technologies) + { + foreach (var id in technology.UnlockedRecipes) + { + var recipe = (LatheRecipePrototype)prototypeManager.Index(typeof(LatheRecipePrototype), id); + UnlockRecipe(recipe); + } + } + + Dirty(); + } + + /// + /// Unlocks a recipe but only if it's one of the allowed recipes on this protolathe. + /// + /// The recipe + /// Whether it could add it or not. + public bool UnlockRecipe(LatheRecipePrototype recipe) + { + if (!ProtolatheRecipes.Contains(recipe)) return false; + + AddRecipe(recipe); + + return true; + } + } +} diff --git a/Content.Server/GameObjects/Components/Research/ResearchClientComponent.cs b/Content.Server/GameObjects/Components/Research/ResearchClientComponent.cs new file mode 100644 index 0000000000..ee74335ad7 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/ResearchClientComponent.cs @@ -0,0 +1,109 @@ +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Components.Research; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Systems; +using Robust.Shared.IoC; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + public class ResearchClientComponent : SharedResearchClientComponent, IActivate + { + // TODO: Create GUI for changing RD server. + + private BoundUserInterface _userInterface; + +#pragma warning disable 649 + [Dependency] private readonly IEntitySystemManager _entitySystemManager; +#pragma warning restore 649 + + public bool ConnectedToServer => Server != null; + + [ViewVariables(VVAccess.ReadOnly)] + public ResearchServerComponent Server { get; set; } + + public bool RegisterServer(ResearchServerComponent server) + { + var result = server != null && server.RegisterClient(this); + return result; + } + + public void UnregisterFromServer() + { + Server?.UnregisterClient(this); + } + + public override void Initialize() + { + base.Initialize(); + // For now it just registers on the first server it can find. + var servers = _entitySystemManager.GetEntitySystem().Servers; + if(servers.Count > 0) + RegisterServer(servers[0]); + _userInterface = Owner.GetComponent().GetBoundUserInterface(ResearchClientUiKey.Key); + _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; + } + + public void OpenUserInterface(IPlayerSession session) + { + UpdateUserInterface(); + _userInterface.Open(session); + } + + void IActivate.Activate(ActivateEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) + return; + + OpenUserInterface(actor.playerSession); + return; + } + + public void UpdateUserInterface() + { + _userInterface?.SetState(GetNewUiState()); + } + + private ResearchClientBoundInterfaceState GetNewUiState() + { + var rd = _entitySystemManager.GetEntitySystem(); + + return new ResearchClientBoundInterfaceState(rd.Servers.Count, rd.GetServerNames(), + rd.GetServerIds(), ConnectedToServer ? Server.Id : -1); + } + + private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage msg) + { + switch (msg.Message) + { + case ResearchClientSyncMessage _: + UpdateUserInterface(); + break; + + case ResearchClientServerSelectedMessage selectedMessage: + UnregisterFromServer(); + RegisterServer(_entitySystemManager.GetEntitySystem().GetServerById(selectedMessage.ServerId)); + UpdateUserInterface(); + break; + + case ResearchClientServerDeselectedMessage _: + UnregisterFromServer(); + UpdateUserInterface(); + break; + } + } + + public override void Shutdown() + { + base.Shutdown(); + UnregisterFromServer(); + + } + } +} diff --git a/Content.Server/GameObjects/Components/Research/ResearchConsoleComponent.cs b/Content.Server/GameObjects/Components/Research/ResearchConsoleComponent.cs new file mode 100644 index 0000000000..63e083c653 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/ResearchConsoleComponent.cs @@ -0,0 +1,94 @@ +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + public class ResearchConsoleComponent : SharedResearchConsoleComponent, IActivate + { + private BoundUserInterface _userInterface; + private ResearchClientComponent _client; + public override void Initialize() + { + base.Initialize(); + _userInterface = Owner.GetComponent().GetBoundUserInterface(ResearchConsoleUiKey.Key); + _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage; + _client = Owner.GetComponent(); + } + + private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) + { + if (!Owner.TryGetComponent(out TechnologyDatabaseComponent database)) return; + + switch (message.Message) + { + case ConsoleUnlockTechnologyMessage msg: + var protoMan = IoCManager.Resolve(); + if (!protoMan.TryIndex(msg.Id, out TechnologyPrototype tech)) break; + if(!_client.Server.CanUnlockTechnology(tech)) break; + if (_client.Server.UnlockTechnology(tech)) + { + database.SyncWithServer(); + database.Dirty(); + UpdateUserInterface(); + } + + break; + + case ConsoleServerSyncMessage msg: + database.SyncWithServer(); + UpdateUserInterface(); + break; + + case ConsoleServerSelectionMessage msg: + if (!Owner.TryGetComponent(out ResearchClientComponent client)) break; + client.OpenUserInterface(message.Session); + break; + } + } + + /// + /// Method to update the user interface on the clients. + /// + public void UpdateUserInterface() + { + _userInterface.SetState(GetNewUiState()); + } + + private ResearchConsoleBoundInterfaceState GetNewUiState() + { + var points = _client.ConnectedToServer ? _client.Server.Point : 0; + var pointsPerSecond = _client.ConnectedToServer ? _client.Server.PointsPerSecond : 0; + + return new ResearchConsoleBoundInterfaceState(points, pointsPerSecond); + } + + /// + /// Open the user interface on a certain player session. + /// + /// Session where the UI will be shown + public void OpenUserInterface(IPlayerSession session) + { + _userInterface.Open(session); + } + + void IActivate.Activate(ActivateEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) + return; + + OpenUserInterface(actor.playerSession); + return; + } + } +} diff --git a/Content.Server/GameObjects/Components/Research/ResearchPointSourceComponent.cs b/Content.Server/GameObjects/Components/Research/ResearchPointSourceComponent.cs new file mode 100644 index 0000000000..905f2ef8f4 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/ResearchPointSourceComponent.cs @@ -0,0 +1,40 @@ +using Content.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + public class ResearchPointSourceComponent : ResearchClientComponent + { + public override string Name => "ResearchPointSource"; + + private int _pointsPerSecond; + private bool _active; + + [ViewVariables] + public int PointsPerSecond + { + get => _pointsPerSecond; + set => value = _pointsPerSecond; + } + + [ViewVariables] + public bool Active + { + get => _active; + set => value = _active; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _pointsPerSecond, "pointspersecond", 0); + serializer.DataField(ref _active, "active", false); + } + } +} diff --git a/Content.Server/GameObjects/Components/Research/ResearchServerComponent.cs b/Content.Server/GameObjects/Components/Research/ResearchServerComponent.cs new file mode 100644 index 0000000000..7c0faf5e22 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/ResearchServerComponent.cs @@ -0,0 +1,162 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices.WindowsRuntime; +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + public class ResearchServerComponent : Component + { + public static int ServerCount = 0; + + public override string Name => "ResearchServer"; + + [ViewVariables(VVAccess.ReadWrite)] + public string ServerName => _serverName; + + private string _serverName = "RDSERVER"; + private float _timer = 0f; + public TechnologyDatabaseComponent Database { get; private set; } + + [ViewVariables(VVAccess.ReadWrite)] + private int _points = 0; + + [ViewVariables(VVAccess.ReadOnly)] + public int Id { get; private set; } + + // You could optimize research by keeping a list of unlocked recipes too. + [ViewVariables(VVAccess.ReadOnly)] + public IReadOnlyList UnlockedTechnologies => Database.Technologies; + [ViewVariables(VVAccess.ReadOnly)] + public List PointSources { get; } = new List(); + [ViewVariables(VVAccess.ReadOnly)] + public List Clients { get; } = new List(); + + public int Point => _points; + + /// + /// How many points per second this R&D server gets. + /// The value is calculated from all point sources connected to it. + /// + [ViewVariables(VVAccess.ReadOnly)] + public int PointsPerSecond + { + // This could be changed to PointsPerMinute quite easily for optimization. + get + { + var points = 0; + + foreach (var source in PointSources) + { + if (source.Active) points += source.PointsPerSecond; + } + + return points; + } + } + + public override void Initialize() + { + base.Initialize(); + Id = ServerCount++; + IoCManager.Resolve()?.GetEntitySystem()?.RegisterServer(this); + Database = Owner.GetComponent(); + } + + public override void Shutdown() + { + base.Shutdown(); + IoCManager.Resolve()?.GetEntitySystem()?.UnregisterServer(this); + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _serverName, "servername", "RDSERVER"); + serializer.DataField(ref _points, "points", 0); + } + + public bool CanUnlockTechnology(TechnologyPrototype technology) + { + if (!Database.CanUnlockTechnology(technology) || _points < technology.RequiredPoints || Database.IsTechnologyUnlocked(technology)) return false; + return true; + } + + /// + /// Unlocks a technology, but only if there are enough research points for it. + /// If there are, it subtracts the amount of points from the total. + /// + /// + /// + public bool UnlockTechnology(TechnologyPrototype technology) + { + if (!CanUnlockTechnology(technology)) return false; + var result = Database.UnlockTechnology(technology); + if(result) + _points -= technology.RequiredPoints; + return result; + } + + /// + /// Check whether a technology is unlocked or not. + /// + /// + /// + public bool IsTechnologyUnlocked(TechnologyPrototype technology) + { + return Database.IsTechnologyUnlocked(technology); + } + + /// + /// Registers a remote client on this research server. + /// + /// + /// + public bool RegisterClient(ResearchClientComponent client) + { + if (client is ResearchPointSourceComponent source) + { + if (PointSources.Contains(source)) return false; + PointSources.Add(source); + source.Server = this; + return true; + } + + if (Clients.Contains(client)) return false; + Clients.Add(client); + client.Server = this; + return true; + } + + /// + /// Unregisters a remote client from this server. + /// + /// + public void UnregisterClient(ResearchClientComponent client) + { + if (client is ResearchPointSourceComponent source) + { + PointSources.Remove(source); + return; + } + + Clients.Remove(client); + } + + public void Update(float frameTime) + { + _timer += frameTime; + if (_timer < 1f) return; + _timer = 0f; + _points += PointsPerSecond; + } + } +} diff --git a/Content.Server/GameObjects/Components/Research/TechnologyDatabaseComponent.cs b/Content.Server/GameObjects/Components/Research/TechnologyDatabaseComponent.cs new file mode 100644 index 0000000000..ccdc0eaf46 --- /dev/null +++ b/Content.Server/GameObjects/Components/Research/TechnologyDatabaseComponent.cs @@ -0,0 +1,77 @@ +using Content.Shared.GameObjects.Components.Research; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameObjects.Components.Research +{ + [RegisterComponent] + public class TechnologyDatabaseComponent : SharedTechnologyDatabaseComponent + { + public override ComponentState GetComponentState() + { + return new TechnologyDatabaseState(_technologies); + } + + /// + /// Synchronizes this database against other, + /// adding all technologies from the other that + /// this one doesn't have. + /// + /// The other database + /// Whether the other database should be synced against this one too or not. + public void Sync(TechnologyDatabaseComponent otherDatabase, bool twoway = true) + { + foreach (var tech in otherDatabase.Technologies) + { + if (!IsTechnologyUnlocked(tech)) AddTechnology(tech); + } + + if(twoway) + otherDatabase.Sync(this, false); + + Dirty(); + } + + /// + /// If there's a research client component attached to the owner entity, + /// and the research client is connected to a research server, this method + /// syncs against the research server, and the server against the local database. + /// + /// Whether it could sync or not + public bool SyncWithServer() + { + if (!Owner.TryGetComponent(out ResearchClientComponent client)) return false; + if (!client.ConnectedToServer) return false; + + Sync(client.Server.Database); + + return true; + } + + /// + /// If possible, unlocks a technology on this database. + /// + /// + /// + public bool UnlockTechnology(TechnologyPrototype technology) + { + if (!CanUnlockTechnology(technology)) return false; + + AddTechnology(technology); + Dirty(); + return true; + } + + /// + /// Adds a technology to the database without checking if it could be unlocked. + /// + /// + public void AddTechnology(TechnologyPrototype technology) + { + _technologies.Add(technology); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/ResearchSystem.cs b/Content.Server/GameObjects/EntitySystems/ResearchSystem.cs new file mode 100644 index 0000000000..6e4734139b --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/ResearchSystem.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class ResearchSystem : EntitySystem + { + public const float ResearchConsoleUIUpdateTime = 30f; + + private float _timer = ResearchConsoleUIUpdateTime; + private readonly List _servers = new List(); + private readonly IEntityQuery ConsoleQuery; + public IReadOnlyList Servers => _servers; + + public ResearchSystem() + { + ConsoleQuery = new TypeEntityQuery(typeof(ResearchConsoleComponent)); + } + + public bool RegisterServer(ResearchServerComponent server) + { + if (_servers.Contains(server)) return false; + _servers.Add(server); + return true; + } + + public void UnregisterServer(ResearchServerComponent server) + { + _servers.Remove(server); + } + + public ResearchServerComponent GetServerById(int id) + { + foreach (var server in Servers) + { + if (server.Id == id) return server; + } + + return null; + } + + public string[] GetServerNames() + { + var list = new string[Servers.Count]; + + for (var i = 0; i < Servers.Count; i++) + { + list[i] = Servers[i].ServerName; + } + + return list; + } + + public int[] GetServerIds() + { + var list = new int[Servers.Count]; + + for (var i = 0; i < Servers.Count; i++) + { + list[i] = Servers[i].Id; + } + + return list; + } + + public override void Update(float frameTime) + { + _timer += frameTime; + + foreach (var server in _servers) + { + server.Update(frameTime); + } + + if (_timer >= ResearchConsoleUIUpdateTime) + { + foreach (var console in EntityManager.GetEntities(ConsoleQuery)) + { + console.GetComponent().UpdateUserInterface(); + } + + _timer = 0f; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Research/SharedLatheComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedLatheComponent.cs index c5ab7f76e2..fd35b3cffc 100644 --- a/Content.Shared/GameObjects/Components/Research/SharedLatheComponent.cs +++ b/Content.Shared/GameObjects/Components/Research/SharedLatheComponent.cs @@ -54,6 +54,30 @@ namespace Content.Shared.GameObjects.Components.Research } } + + + /// + /// Sent to the server to sync the lathe's technology database with the research server. + /// + [Serializable, NetSerializable] + public class LatheServerSyncMessage : BoundUserInterfaceMessage + { + public LatheServerSyncMessage() + { + } + } + + /// + /// Sent to the server to open the ResearchClient UI. + /// + [Serializable, NetSerializable] + public class LatheServerSelectionMessage : BoundUserInterfaceMessage + { + public LatheServerSelectionMessage() + { + } + } + /// /// Sent to the client when the lathe is producing a recipe. /// diff --git a/Content.Shared/GameObjects/Components/Research/SharedLatheDatabaseComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedLatheDatabaseComponent.cs index 91ae1c1737..67e5f56701 100644 --- a/Content.Shared/GameObjects/Components/Research/SharedLatheDatabaseComponent.cs +++ b/Content.Shared/GameObjects/Components/Research/SharedLatheDatabaseComponent.cs @@ -13,8 +13,8 @@ namespace Content.Shared.GameObjects.Components.Research public class SharedLatheDatabaseComponent : Component, IEnumerable { public override string Name => "LatheDatabase"; - public sealed override uint? NetID => ContentNetIDs.LATHE_DATABASE; - public sealed override Type StateType => typeof(LatheDatabaseState); + public override uint? NetID => ContentNetIDs.LATHE_DATABASE; + public override Type StateType => typeof(LatheDatabaseState); private List _recipes = new List(); @@ -34,7 +34,8 @@ namespace Content.Shared.GameObjects.Components.Research /// Whether it could be added or not public virtual void AddRecipe(LatheRecipePrototype recipe) { - _recipes.Add(recipe); + if(!Contains(recipe)) + _recipes.Add(recipe); } /// diff --git a/Content.Shared/GameObjects/Components/Research/SharedProtolatheDatabaseComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedProtolatheDatabaseComponent.cs new file mode 100644 index 0000000000..a0694f87ef --- /dev/null +++ b/Content.Shared/GameObjects/Components/Research/SharedProtolatheDatabaseComponent.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Research +{ + + [ComponentReference(typeof(SharedLatheDatabaseComponent))] + public class SharedProtolatheDatabaseComponent : SharedLatheDatabaseComponent + { + public override string Name => "ProtolatheDatabase"; + public sealed override uint? NetID => ContentNetIDs.PROTOLATHE_DATABASE; + public sealed override Type StateType => typeof(ProtolatheDatabaseState); + + private List _protolatheRecipes = new List(); + + /// + /// A full list of recipes this protolathe can print. + /// + public List ProtolatheRecipes => _protolatheRecipes; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + if (serializer.Reading) + { + var recipes = serializer.ReadDataField("protolatherecipes", new List()); + var prototypeManager = IoCManager.Resolve(); + foreach (var id in recipes) + { + if (!prototypeManager.TryIndex(id, out LatheRecipePrototype recipe)) continue; + _protolatheRecipes.Add(recipe); + } + } else if (serializer.Writing) + { + var recipes = GetProtolatheRecipeIdList(); + serializer.DataField(ref recipes, "protolatherecipes", new List()); + } + } + + /// + /// Returns a list of the allowed protolathe recipe IDs. + /// + /// A list of recipe IDs allowed + public List GetProtolatheRecipeIdList() + { + var list = new List(); + + foreach (var recipe in ProtolatheRecipes) + { + list.Add(recipe.ID); + } + + return list; + } + } + + [NetSerializable, Serializable] + public class ProtolatheDatabaseState : ComponentState + { + public readonly List Recipes; + public ProtolatheDatabaseState(List recipes) : base(ContentNetIDs.PROTOLATHE_DATABASE) + { + Recipes = recipes; + } + } +} diff --git a/Content.Shared/GameObjects/Components/Research/SharedResearchClientComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedResearchClientComponent.cs new file mode 100644 index 0000000000..7551b7ddd4 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Research/SharedResearchClientComponent.cs @@ -0,0 +1,73 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Research +{ + public class SharedResearchClientComponent : Component + { + public override string Name => "ResearchClient"; + + /// + /// Request that the server updates the client. + /// + [Serializable, NetSerializable] + public class ResearchClientSyncMessage : BoundUserInterfaceMessage + { + + public ResearchClientSyncMessage() + { + } + } + + /// + /// Sent to the server when the client chooses a research server. + /// + [Serializable, NetSerializable] + public class ResearchClientServerSelectedMessage : BoundUserInterfaceMessage + { + public int ServerId; + + public ResearchClientServerSelectedMessage(int serverId) + { + ServerId = serverId; + } + } + + /// + /// Sent to the server when the client deselects a research server. + /// + [Serializable, NetSerializable] + public class ResearchClientServerDeselectedMessage : BoundUserInterfaceMessage + { + public ResearchClientServerDeselectedMessage() + { + } + } + + [NetSerializable, Serializable] + public enum ResearchClientUiKey + { + Key, + } + + [Serializable, NetSerializable] + public sealed class ResearchClientBoundInterfaceState : BoundUserInterfaceState + { + public int ServerCount; + public string[] ServerNames; + public int[] ServerIds; + public int SelectedServerId; + + public ResearchClientBoundInterfaceState(int serverCount, string[] serverNames, int[] serverIds, int selectedServerId = -1) + { + ServerCount = serverCount; + ServerNames = serverNames; + ServerIds = serverIds; + SelectedServerId = selectedServerId; + } + } + } + +} diff --git a/Content.Shared/GameObjects/Components/Research/SharedResearchConsoleComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedResearchConsoleComponent.cs new file mode 100644 index 0000000000..433f145198 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Research/SharedResearchConsoleComponent.cs @@ -0,0 +1,55 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Research +{ + public class SharedResearchConsoleComponent : Component + { + public override string Name => "ResearchConsole"; + public override uint? NetID => ContentNetIDs.RESEARCH_CONSOLE; + + [NetSerializable, Serializable] + public enum ResearchConsoleUiKey + { + Key, + } + + [Serializable, NetSerializable] + public class ConsoleUnlockTechnologyMessage : BoundUserInterfaceMessage + { + public string Id; + public ConsoleUnlockTechnologyMessage(string id) + { + Id = id; + } + } + + [Serializable, NetSerializable] + public class ConsoleServerSyncMessage : BoundUserInterfaceMessage + { + public ConsoleServerSyncMessage() + {} + } + + [Serializable, NetSerializable] + public class ConsoleServerSelectionMessage : BoundUserInterfaceMessage + { + public ConsoleServerSelectionMessage() + {} + } + + [Serializable, NetSerializable] + public sealed class ResearchConsoleBoundInterfaceState : BoundUserInterfaceState + { + public int Points; + public int PointsPerSecond; + public ResearchConsoleBoundInterfaceState(int points, int pointsPerSecond) + { + Points = points; + PointsPerSecond = pointsPerSecond; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Research/SharedTechnologyDatabaseComponent.cs b/Content.Shared/GameObjects/Components/Research/SharedTechnologyDatabaseComponent.cs new file mode 100644 index 0000000000..12f0af4052 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Research/SharedTechnologyDatabaseComponent.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Content.Shared.Research; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Research +{ + public class SharedTechnologyDatabaseComponent : Component, IEnumerable + { + public override string Name => "TechnologyDatabase"; + public override uint? NetID => ContentNetIDs.TECHNOLOGY_DATABASE; + public override Type StateType => typeof(TechnologyDatabaseState); + + protected List _technologies = new List(); + + /// + /// A read-only list of unlocked technologies. + /// + public IReadOnlyList Technologies => _technologies; + + public IEnumerator GetEnumerator() + { + return Technologies.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Returns a list with the IDs of all unlocked technologies. + /// + /// A list of technology IDs + public List GetTechnologyIdList() + { + List techIds = new List(); + + foreach (var tech in _technologies) + { + techIds.Add(tech.ID); + } + + return techIds; + } + + /// + /// 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(TechnologyPrototype technology) + { + return _technologies.Contains(technology); + } + + /// + /// 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 (technology == null || IsTechnologyUnlocked(technology)) 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)) + return false; + } + return true; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + if (serializer.Reading) + { + var techs = serializer.ReadDataField("technologies", new List()); + var prototypeManager = IoCManager.Resolve(); + foreach (var id in techs) + { + if (!prototypeManager.TryIndex(id, out TechnologyPrototype tech)) continue; + _technologies.Add(tech); + } + } else if (serializer.Writing) + { + var techs = GetTechnologyIdList(); + serializer.DataField(ref techs, "technologies", new List()); + } + } + } + + [Serializable, NetSerializable] + public class TechnologyDatabaseState : ComponentState + { + public List Technologies; + public TechnologyDatabaseState(List technologies) : base(ContentNetIDs.TECHNOLOGY_DATABASE) + { + technologies = technologies; + } + + public TechnologyDatabaseState(List technologies) : base(ContentNetIDs.TECHNOLOGY_DATABASE) + { + Technologies = new List(); + foreach (var technology in technologies) + { + Technologies.Add(technology.ID); + } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index dcb8609547..28d43f282a 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -24,6 +24,9 @@ public const uint MATERIAL_STORAGE = 1018; public const uint HAND_TELEPORTER = 1019; public const uint VENDING_MACHINE = 1020; - public const uint WIRES = 1021; + public const uint PROTOLATHE_DATABASE = 1021; + public const uint TECHNOLOGY_DATABASE = 1022; + public const uint RESEARCH_CONSOLE = 1023; + public const uint WIRES = 1024; } } diff --git a/Content.Shared/Research/LatheRecipePrototype.cs b/Content.Shared/Research/LatheRecipePrototype.cs index 2f82b046f0..8ae61746e0 100644 --- a/Content.Shared/Research/LatheRecipePrototype.cs +++ b/Content.Shared/Research/LatheRecipePrototype.cs @@ -6,6 +6,7 @@ using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; using YamlDotNet.RepresentationModel; namespace Content.Shared.Research @@ -21,11 +22,13 @@ namespace Content.Shared.Research private int _completeTime; private Dictionary _requiredMaterials; + [ViewVariables] public string ID => _id; /// /// Name displayed in the lathe GUI. /// + [ViewVariables] public string Name { get @@ -43,6 +46,7 @@ namespace Content.Shared.Research /// /// Short description displayed in the lathe GUI. /// + [ViewVariables] public string Description { get @@ -60,17 +64,20 @@ namespace Content.Shared.Research /// /// Texture path used in the lathe GUI. /// + [ViewVariables] public SpriteSpecifier Icon => _icon; /// /// The prototype name of the resulting entity when the recipe is printed. /// + [ViewVariables] public string Result => _result; /// /// The materials required to produce this recipe. /// Takes a material ID as string. /// + [ViewVariables] public Dictionary RequiredMaterials { get => _requiredMaterials; @@ -78,10 +85,11 @@ namespace Content.Shared.Research } - /// + /// /// How many milliseconds it'll take for the lathe to finish this recipe. /// Might lower depending on the lathe's upgrade level. /// + [ViewVariables] public int CompleteTime => _completeTime; public void LoadFrom(YamlMappingNode mapping) diff --git a/Content.Shared/Research/TechnologyPrototype.cs b/Content.Shared/Research/TechnologyPrototype.cs new file mode 100644 index 0000000000..bd41e2016e --- /dev/null +++ b/Content.Shared/Research/TechnologyPrototype.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.Research +{ + [NetSerializable, Serializable, Prototype("technology")] + public class TechnologyPrototype : IPrototype, IIndexedPrototype + { + private string _name; + private string _id; + private SpriteSpecifier _icon; + private string _description; + private int _requiredPoints; + private List _requiredTechnologies; + private List _unlockedRecipes; + + /// + /// The ID of this technology prototype. + /// + [ViewVariables] + public string ID => _id; + + /// + /// The name this technology will have on user interfaces. + /// + [ViewVariables] + public string Name => _name; + + /// + /// An icon that represent this technology. + /// + public SpriteSpecifier Icon => _icon; + + /// + /// A short description of the technology. + /// + [ViewVariables] + public string Description => _description; + + /// + /// The required research points to unlock this technology. + /// + [ViewVariables] + public int RequiredPoints => _requiredPoints; + + /// + /// A list of technology IDs required to unlock this technology. + /// + [ViewVariables] + public List RequiredTechnologies => _requiredTechnologies; + + /// + /// A list of recipe IDs this technology unlocks. + /// + [ViewVariables] + public List UnlockedRecipes => _unlockedRecipes; + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + + serializer.DataField(ref _name, "name", string.Empty); + serializer.DataField(ref _id, "id", string.Empty); + serializer.DataField(ref _description, "description", string.Empty); + serializer.DataField(ref _icon, "icon", SpriteSpecifier.Invalid); + serializer.DataField(ref _requiredPoints, "requiredpoints", 0); + serializer.DataField(ref _requiredTechnologies, "requiredtechnologies", new List()); + serializer.DataField(ref _unlockedRecipes, "unlockedrecipes", new List()); + } + } +} diff --git a/Resources/Prototypes/Entities/Research.yml b/Resources/Prototypes/Entities/Research.yml new file mode 100644 index 0000000000..3b0aa90fc3 --- /dev/null +++ b/Resources/Prototypes/Entities/Research.yml @@ -0,0 +1,40 @@ +- type: entity + id: researchAndDevelopmentServer + name: "R&D Server" + components: + - type: Sprite + sprite: Buildings/research.rsi + state: server + - type: Icon + sprite: Buildings/research.rsi + state: server + - type: Clickable + - type: BoundingBox + - type: Collidable + - type: SnapGrid + offset: Center + - type: ResearchServer + - type: TechnologyDatabase + +- type: entity + id: baseResearchAndDevelopmentPointSource + name: "Base R&D Point Source" + components: + - type: Sprite + sprite: Buildings/research.rsi + state: tdoppler + - type: Icon + sprite: Buildings/research.rsi + state: tdoppler + - type: Clickable + - type: BoundingBox + - type: Collidable + - type: SnapGrid + offset: Center + - type: ResearchPointSource + pointspersecond: 1000 + active: true + - type: UserInterface + interfaces: + - key: enum.ResearchClientUiKey.Key + type: ResearchClientBoundUserInterface \ No newline at end of file diff --git a/Resources/Prototypes/Entities/buildings/computers.yml b/Resources/Prototypes/Entities/buildings/computers.yml index 9de6036e27..9b7e604a1f 100644 --- a/Resources/Prototypes/Entities/buildings/computers.yml +++ b/Resources/Prototypes/Entities/buildings/computers.yml @@ -10,7 +10,6 @@ - type: Icon sprite: Buildings/computer.rsi state: computer - - type: Computer - type: PowerDevice priority: High @@ -94,6 +93,15 @@ - type: ComputerVisualizer2D key: rd_key screen: rdcomp + - type: ResearchClient + - type: ResearchConsole + - type: TechnologyDatabase + - type: UserInterface + interfaces: + - key: enum.ResearchConsoleUiKey.Key + type: ResearchConsoleBoundUserInterface + - key: enum.ResearchClientUiKey.Key + type: ResearchClientBoundUserInterface - type: entity diff --git a/Resources/Prototypes/Entities/buildings/lathe.yml b/Resources/Prototypes/Entities/buildings/lathe.yml index d17cd313be..a96a6d2953 100644 --- a/Resources/Prototypes/Entities/buildings/lathe.yml +++ b/Resources/Prototypes/Entities/buildings/lathe.yml @@ -1,8 +1,25 @@ - type: entity + id: BaseLathe + name: "Lathe" + components: + - type: Clickable + - type: BoundingBox + - type: Collidable + - type: SnapGrid + offset: Center + - type: Lathe + - type: MaterialStorage + - type: UserInterface + interfaces: + - key: enum.LatheUiKey.Key + type: LatheBoundUserInterface + + +- type: entity + parent: BaseLathe id: autolathe name: "Autolathe" components: - - type: Clickable - type: Sprite sprite: Buildings/autolathe.rsi state: idle @@ -33,8 +50,45 @@ - CableStack - Crowbar - Multitool - - type: MaterialStorage + +- type: entity + parent: BaseLathe + id: protolathe + name: "Protolathe" + components: + - type: Sprite + sprite: Buildings/research.rsi + state: protolathe + - type: Icon + sprite: Buildings/research.rsi + state: protolathe + - type: Collidable + shapes: + - !type:PhysShapeAabb + mask: 19 + layer: 16 + - type: SnapGrid + offset: Center + - type: ResearchClient + - type: TechnologyDatabase + - type: ProtolatheDatabase + protolatherecipes: + - Brutepack + - Ointment + - LightTube + - LightBulb + - MetalStack + - GlassStack + - Wirecutter + - Screwdriver + - Welder + - Wrench + - CableStack + - Crowbar + - Multitool - type: UserInterface interfaces: - key: enum.LatheUiKey.Key - type: LatheBoundUserInterface \ No newline at end of file + type: LatheBoundUserInterface + - key: enum.ResearchClientUiKey.Key + type: ResearchClientBoundUserInterface \ No newline at end of file diff --git a/Resources/Prototypes/Technologies/sheet.yml b/Resources/Prototypes/Technologies/sheet.yml new file mode 100644 index 0000000000..8106312e76 --- /dev/null +++ b/Resources/Prototypes/Technologies/sheet.yml @@ -0,0 +1,26 @@ +- type: technology + name: "Test Technology 1" + id: Parent1 + description: Parent technology needed for sheets + icon: Objects/sheet_glass.png + requiredpoints: 5000 + +- type: technology + name: "Test Technology 2" + id: Parent2 + description: Parent technology 2 needed for sheets + icon: Objects/sheet_glass.png + requiredpoints: 5000 + +- type: technology + name: Material sheet printing + id: Sheets + description: Print those sheets! + icon: Objects/sheet_metal.png + requiredpoints: 500 + requiredtechnologies: + - Parent1 + - Parent2 + unlockedrecipes: + - MetalStack + - GlassStack \ No newline at end of file diff --git a/Resources/Textures/Buildings/research.rsi/circuit_imprinter.png b/Resources/Textures/Buildings/research.rsi/circuit_imprinter.png new file mode 100644 index 0000000000000000000000000000000000000000..c147a0743a631d3c696d3f339670571ecb406933 GIT binary patch literal 603 zcmV-h0;K(kP)a z(=c3m@xt1le4U2lbl(7~g*k z6tYI1&E)_n77M<&Vf8t&*W>jYw_Ou(VWB$E^f0WhHhH`L(q9KUO50vWn$~4|`$H-L zyad((UDs1H^lFC|B5UnyS{HS%O?h(0cPy30SzB8PHIwBUn$~sIk;-{S2ft~U@BA`DqhR{OmKZpY^)FPi74XO1KsMn`E|0|g@HKu2k( z62ME?4@u=racZ+%o(4cw6{Ng?VHg-c1+4ZUF7Ivu@bqE7cwO4KV)v=4!k5px^FYgL zi`%NL%7=REfa9kn-?CiX^&wKm7 pY;$M(Q3;&`|I8tnR?7PE`3(sdoOyp&!I1y}002ovPDHLkV1nfi9Z3KH literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/circuit_imprinter_ani.png b/Resources/Textures/Buildings/research.rsi/circuit_imprinter_ani.png new file mode 100644 index 0000000000000000000000000000000000000000..c598e271b27653a69a96d522afb8bd490b5cb1e8 GIT binary patch literal 1743 zcmchYdoa{%7{@uHq9wT3$1CdDRzJ?GWyT-7kUuu!dAu2GidHkV-JRPgg!uuF$FcM(m}-9Hw}>We zflfA;d_-E=eG1KJI@ zUHU@ekfP2mOpNNF>@s$wycR<+Whm;L@&wA*1f-<7Aft2;iKEF(umGffPU&^b_@ly= zq_MPax7sKPBVn2;{4ZjpC(Z`mp99Q|GUB)C;-xx&59bEx3V@JV*N8g4gb zmFZq;`M6{AS{-pnL&Ju4EOu9_1iDw|0fy1THHI#=83fv5Q(t;vn*?CCfAOXlbkXdKka2P4I6(jlp#Ni=oY} z*5G#{=r4r&H=R&v{rWI%1JE$2#e1uq&*w>Vi|#;LX#GOt;bXo8JozyD41+dE1{_SnWmsA7QnN4sT49hOi5ps zpUhlF1PssjgR_S62BLLGL+atA`m6SM?T0aEWHM^YgCRT>WD`~;$x2C|x!br36AxFv z6XwTA8(&Fsa*1LHbUTZV+5l!%dL5;jM=W&KyF^}|F$!xl4x2I=M@8^l#!>IysKFh4 z@jbdZTm`G=du)bFRlx87C)9;$;pmr-b;D@m-vq`sa0hST11!cmI1=iX4SiBN9Sc(sNR5V{T%ZL9y!8aT!n;jtZjrAa5K}0L`kd}D0 zS17N;Z9&fP>1|;6Kz84umPZoJ;~e1HLg>&x-Kkub*TJ`m(XHod+;uc%3k!Sr3aa9` z>V#a1KQ#f?x*Sdmp5ot$jpo9@jJF$;_HE|=t0nVXy7^)z$P2%hRe7@d*7~B7LmqQ; JBp(fq{~NP|qW=H@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/circuit_imprinter_t.png b/Resources/Textures/Buildings/research.rsi/circuit_imprinter_t.png new file mode 100644 index 0000000000000000000000000000000000000000..917771771525a89cf5dd71696e63aaad950b7213 GIT binary patch literal 664 zcmV;J0%!e+P)(w-)lx|kL_Xw@M_9mI5yKtdrIHG+j`Km;fA4|H{L_62l^xEZLF zN{42X+7KM<5NYFNV>Qr2>Zv%yA$=~FbE&hXJ#BA0F|eC_%@89UGWUXY03a-T>gymi2ynG63K{;%@mrIeErWK&P}A$iK`3jVbVWy=2l? z0nmQ@>={UWxyS1TAQ13(jD~14qVLCOEY-3AXJ!&TMfbw|e4f_}xz2OoLwQZ#M%C&j z#p2t(7-+}z98guYZ-T0-5{;$mVifjSy{ITbeg9WB)=W!92~sMpSpG!+#km}{<2e@R za(a9*<@yMG+;}$x)Q|Z`JIoA^jGC5$_MHQ;^SyZwgda9bA*d)pqOnxHWeU2Pup%Ds zefAeK4e??IF5GR_D@w3)4G0sGBq{;mI5}}jUmsQpN#tl?d^r@F0H9JSBl}!vnuhkvg{U{<;`%B84{kROuk%Zn^tMW+ z%-7HB(?CI7d|j_&<4}2gAw0fD5M#X&RaJR2KD8@s9!DUOlAig1;M;ZuP169EEyH%{ yYM@kF(VuJEzjbkA{oi7_2L73XIfXvQx6dEiJH6-P`bJFv0000?mx)% z!X(~U&nW;An>_R6F+!N3kgIA8ptrG}Q)WV8^g%?%>2qfF&;*xq&kip@Rbvz%KH{ek z(u6B3_Z;?{%qq(-o{wB!sSE(bRaMF_pMO2pIB5aa?}nu3EUD@{0Cq243G|lUgo#7~ z%d!H?aUm_B)oKxqMgz-nvA+Zh3rSb!a9ecltW?0R$A&!^d)STu7>3TvS9#BXH6dtQ zZLr!>0``6hKE?+Gb)6)5#x@N7uK*zg<#LHi<&(on)6&dZZPZi!MD3X-Cu1?+dUxi8 zqrGLdG0G*E05Qmv>lEBAZ1w|X*D!QlYSxp$C7_#Kn>y)80h*Q$W!A$>9U0YO3z`P` zkLE7bYE=P`??ADzM6s~M&0DvpLVoRP%F%Y@*YS}Hkh+Y%H=6*Mrb(yM8LNK!%(?wI znwI8m$+^#50-k2$4*NrL5m=O(jTgQF+K{bjX(|<1o=^3Pg-s>!7*69P^hNYD5x4|9 z+glS8UcYf?WS_5qo$W2(!Lsm+69H({Yp5fBo$Om{QXGuasMmnO!{q#h%b`9>Y<&Il eUJ3R(?)U?yl;ox;S%*#l0000A$L5J`+?HN*re0TGRxi+T``dhsCiLkJ=sJk}gq$R`NWvq+&g{QxN*dT9v+ zQvxbNf{GLho25wuYrF0SFY(ZBx=HrWY*TVjpUcc--hF?+ePUAB)jwEX>l-1 z;nkJ01Q4;si!WcG>&8sPWjO&bSY0VgXJ(@4LqsI#YjNSk0{QZ*lN*rb1odan`Kjx| zg7fo_N5<9S0{7p%_FP_}3IHT!Stzbjc^~RIVFT8$j$mYz8{a+x@cri9z-Zx3m`bIv zEGw`Li?D%SuSYBv3v9zOT!L&iGcoD>EO!2iP{Q9X8+JUma~uKCv^H@lmb!9M80@fr~|`5Rn?5{cDsQFolF>5DlLafFfAhn z6s6$#)~AVcsUuz!wjnpdmU&zwdUl^`wR(IVJE_<10kBv~`tAWQ{LHmlP3E%aaE1tf z6^n|5CA0zO5xbB`xb{!oKzDBs0K+io_xqvxmo8r&=24UatyU9tXWQGrVktQ?{+!7H zIpH@NX9rLmV|!!Ivm|B4nk8TSAH002ov JPDHLkV1m`18HoS@ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/d_analyzer_la.png b/Resources/Textures/Buildings/research.rsi/d_analyzer_la.png new file mode 100644 index 0000000000000000000000000000000000000000..54342922f8361d0990cd7f7bc7c20b43aae2f600 GIT binary patch literal 1450 zcmZ`(doq4BJd#>|9HrKDC;UQ;WN zvPv~I9x&_ukLvbMC!#z}rJt+fW+- zpo{f%Kd6p0wFMzG)xJ9+0Smyl(O7p^e0=UyZddNHusHv;Fk?r(kV*<~lxHjJ z7HuDrCrY;t+Na@1Dt}o8ym=XSs8Py6{FM>T?>@cb>4l=cq)LB6`1FsgMCv4H9DU-a zQs^j#?VDePbDbqMTebwSyCYfevIXPJ>BHGD5M-WpsE>H+qg5F+j=DlP0~n^$*2LYZ zc1#KkC}cg=4=xl_T_~eDFPoaMjGxQ$2`61yCahUd!WFGCG7`zUSIzfJ=oF2Sxz4+p zg6XuH`3D`a^z}r)B(`|;5A+_YCa}pgQOpd7jlf#2CWn+RVPx;Kii#f?kX|ZI;+uHE zud^TGPm^2bkqvYhU`LlfmVm;JqR0YFuxUZrhL|h|gZkitbC-J~CTArxmp5j$6tf!{ zF(naBh&$C|nePc*xm@m6m*^!6AhD^|Be$9eFPocF zSF*T#$%RM4o9J=z;%hG2A%=GJ@#17#U0@Ga00%t3{BgOdndg1RB`XgqTO9fdHF_HI zFWku)SzRWpt_)ClDKV2uXmOYq$Rg!#RN2^Dw}k)|f+r9>FI-w#pg2+0xlqahu7572 zZHkQr4(V1Z5q|}PeF)5UU`-W(9Y+I(^@%`{Z^uI62=Zw1YMlT+V`?s8rlVQ z!WaC>sdD*9sbltqw9aZ7=8043vh(= zDaxgS4uMy}Xj&9n4oi{uBF%Vh6fj8nZxx!<(ia~VYqd8O!58La(ZoYaqD&D;%%GS^ zGb?v@A+wu4!Q$p7e0t{@vl2ef0RmVkT3k|c6U$mcIu7pT?JuU5e`#qX(i-12dhdFA zdX6|k3b)AnkStSB6b`6ubt1bSWc#KZakGbq9*7hAvL6kdhKkhV!~^I9cxM)hx|}IO z)~XtY2Kk{tE?cy)b;&07--V}YjI5NW(aV`=xr1Y5d^@T zEVL4JP7U=7n9zUmwz)g!sTcc)6HG_aS1#To&5YFyT=@UAkU|S`ChW}7yA$dhP(R%| zW)+RvWmv*YkXsh;kwFft>2D%izFYRS$vZW9d11JbIr;4m!?UvG>fAFMr^OkaJH-n` zM;hecH9=AZv66eqJ>dIH996&@TUH1&+e4@3`-rskZ?t7;s?~X|7N#8KVy~9t?d`^5 gWZ!n4qM;0b_#ra{vGU literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/d_analyzer_process.png b/Resources/Textures/Buildings/research.rsi/d_analyzer_process.png new file mode 100644 index 0000000000000000000000000000000000000000..c18ad48f9a33d27fd90fe174f1750f53fe4376ce GIT binary patch literal 3352 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/d_analyzer_t.png b/Resources/Textures/Buildings/research.rsi/d_analyzer_t.png new file mode 100644 index 0000000000000000000000000000000000000000..27179ba0f7e2e0cdf9c241086dc878f4db2c12ad GIT binary patch literal 631 zcmV--0*L*IP)cSW+VnXqp1w3NnFSznPg^io0xQ=FAVpddk^P*&zy7T1ODS*hh$HlJx?^p zNxZS1lmG%YdE(Ve45K<0a#@}MXl|@0rHQdH`XC_Uw3k>OnBY!wb#MW)Jj45?B`TG# z!i2N4F^9dESmxo{nrC^1?f^hA7!>l`O@DCMm#eTAFw z%+wT(U-iD@Ng*ttc6`i4V6yLcQnX4CiCpN}>|7J=furgV-#FcN06^C@UT?m24R~Oh zSoH=R{S*_>s8bG||6?@j)J@YhwytY!0fu3a%VjAR_Z?1(5@p)5xO6cLfN7e9LZLqM zNyqy4szolB?Ga!YMq5B9o~Ba()K*r}HBBh%VbCL>w&MTf8mRz9iH;@f&Qb@wHf%wo zAU~{pL{(J@T%H4&^c`cdKG(7p zCCYBr*=G{K{K8^WQKA%!`;OY{n%2{pgP^;a?U<=JKI}B6W)LD z#BlHiF)GOyBOW^)lz}apYrX` zaM+>VpEo7(L_CA7zUYi%9YDkfGuKJ0bb+)zX2%eDx_~> RkzfD-002ovPDHLkV1gx_B6t7* literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/meta.json b/Resources/Textures/Buildings/research.rsi/meta.json new file mode 100644 index 0000000000..d30c7ab0e8 --- /dev/null +++ b/Resources/Textures/Buildings/research.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/blob/master/icons/obj/machines/excelsior/autolathe.dmi at 40b254106b46981b8ad95ccd5589deb8fa56e765", "states": [{"name": "circuit_imprinter", "directions": 1, "delays": [[1.0]]}, {"name": "circuit_imprinter_ani", "directions": 1, "delays": [[0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08]]}, {"name": "circuit_imprinter_t", "directions": 1, "delays": [[1.0]]}, {"name": "d_analyzer", "directions": 1, "delays": [[1.0]]}, {"name": "d_analyzer_l", "directions": 1, "delays": [[1.0]]}, {"name": "d_analyzer_la", "directions": 1, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "d_analyzer_process", "directions": 1, "delays": [[0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09]]}, {"name": "d_analyzer_t", "directions": 1, "delays": [[1.0]]}, {"name": "protolathe", "directions": 1, "delays": [[1.0]]}, {"name": "protolathe_adamantine", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_bananium", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_diamond", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_glass", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_gold", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_metal", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_n", "directions": 1, "delays": [[0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]]}, {"name": "protolathe_silver", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_solid plasma", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "protolathe_t", "directions": 1, "delays": [[1.0]]}, {"name": "protolathe_uranium", "directions": 1, "delays": [[0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088, 0.088]]}, {"name": "server", "directions": 1, "delays": [[1.0]]}, {"name": "server-nopower", "directions": 1, "delays": [[1.0]]}, {"name": "server-off", "directions": 1, "delays": [[1.0]]}, {"name": "server-on", "directions": 1, "delays": [[1.0]]}, {"name": "server_o", "directions": 1, "delays": [[1.0]]}, {"name": "tdoppler", "directions": 4, "delays": [[0.8, 0.2], [0.8, 0.2], [0.8, 0.2], [0.8, 0.2]]}, {"name": "tdoppler-broken", "directions": 1, "delays": [[0.1, 0.1, 0.1]]}, {"name": "tdoppler-off", "directions": 1, "delays": [[1.0]]}]} \ No newline at end of file diff --git a/Resources/Textures/Buildings/research.rsi/protolathe.png b/Resources/Textures/Buildings/research.rsi/protolathe.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb2d2ce37d5267cdd2844cec1bdb41bf65d673a GIT binary patch literal 879 zcmV-#1CacQP)u)t5b(j#-;2?)2>?Kc)daU^M3+PnSBMBsfCBq*8@x|u zRc-*l=-32|Ml)Rp0zSB$1DmnQtYjxo9L--qHryKa)A`lGv=~SKXhdmdSNarxy9NALWsTq09c=O!^rJ%e?){3{PyoujVzAY-4I(+bFn40E(V+fqLMuenhlLOci{t?4UM=r z{{HU@kmVQ-nI6I3orHI~1WW!iTn#9srgok@5szZ1=dLE%ZO;wvf|@m;klVYHSn{i5 zGhUb05ekd7OShXW)-G<36F>;TC5H_F@Ny0S2z5Y`R*;BCQCrtQpJ&%f2d&SW(bVZv zja6@(ant<}*IZ{+e5xXp{&OVs+*e38XGW2*LVV2mn(9k;p2HMl*VP`cxwedv`MDJRd|N zt8`9lpk~khWLsPNmJ$_;U~mx|pTc|%1cQs)BH%w*`~fsLFzU5AIEJ^Of}P|UM+K3i?jh+3*arv1~}^T z%@5gBGm|(Jz*%bnrkY#k5mvGQl`6eysY;b~K5J1nK$c%Ykv2e<1@LCM0Vseo3gE1@ z03Uj{%!Bv<07z0f9M+pxYfqz*YkbS~Cy%4=CKIWu*4Mu(7DY1g^0EBY(eCqjEUi{< z@h#Wi%UO5V51^~7a#-JAp18)Bq}?1+@w2@l(?#mTj}#vO00000004j;$r}`rCRDsJ zqU0>wJ7Q5b0KK6t7*Ps>5hZ7gC^^gWD=5+iX#ExB-5>86zfz^8ZrtJn00N8DpPyp6 z{^YNqNd5UKmh123psDGPm$qBAt^4zLbMT7aTPUl})%?E7iy0AluUo49H;`VFnZ<)*olzoKDF8=-;00000000ox570vQB0wMD Qk^lez07*qoM6N<$f@4VRD*ylh literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_bananium.png b/Resources/Textures/Buildings/research.rsi/protolathe_bananium.png new file mode 100644 index 0000000000000000000000000000000000000000..2ba3e3ecf5e5c14b14758f69ad1aa6fa99ec6a56 GIT binary patch literal 521 zcmV+k0`~ohP)G|SF! z;C(;M@Grbs-h}L*_p%*}xS!j$e0(*wPV6(-kJGw0h6A(ZbIPggs24!W_$O_Cc@x8d zkNDXpzzY%}R-1r}%aqgbE1G~+Fk3!-jwhp3As`%v`V)0u!8>+64_Z}Yr00000000006?Djm)z*fp z@8NPYHAzK8YFQTNdO`xkNPrj}GGfuXAnM}*01(NgKEE7Oi&ZzbCF}bKlw<1n)8f6+ z`LC?{1FD*C*rCKPM#XkKd;9iUCj9|vIuxdQY_n+CdEA(reS)QEx; z(EguPieTc+y!q48Zcg!&taYFJ23&gl zhvs|cJm25`8lRzRGgI40pRwGSS|+u zwV@HTC}qNJi&8Fv1JKs>{$?eY1Hk2%u!kP8ty1jh09e<@TgJDZ{Q!WBy$lA~)aQq2 z?tk!C(A#h*ve{%&pC6*R|49~_y83vL<5F$y`ux)ztm1b$sPzX}*T?4`9{>OV00000 z000N*kTK@=B0jUl!lWWX8~p*s7yx4ofH68`j72Zv^Gba@008>D)aQ3wZK3MsR9s#^ zpsUaCw%YL@i|twKU#|KC4xPJWmlD6IirwAQ=h0(M`U9F2;oQalIR^j$00000V7%bz;uumf=k0AnKc+x|V;}ja zP7u%%-rUQ(HGP3uSj@pw+7)ck-nn}kKQe@f%{!2hzL)n(OOoyxp&Mt^W=x%_{{FxB z31tVP&DQ)*W$9I$depD}`guM3{2!iwM*?44hfnXzGy#bfKDfm#pgWm&LLTEH%UlL7 zfi-&>j;Hr!J~^#-AWN_IT-`N|nK=z6^-V^%7&65aW<6$Gz;IB*VZ%43nmyLb9BUXD zR6JAn$LFrQm)7iT&ncgKwllJ}#Gt@nEyj({}W`&A+{O z-oC10+OQsdcbn2=xJX3j?ED2iW)9Wy0VVY{DSxiURPF2U=4Ut&?)Uen*!joi|5=}2nV5vlf`K8)EV? z6JE3Ocjw(uL)}%c+uus2KVt@l43j0qKMLXZ=P-Votl5%!4gTe~DWM4fQ3m9p literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_gold.png b/Resources/Textures/Buildings/research.rsi/protolathe_gold.png new file mode 100644 index 0000000000000000000000000000000000000000..391e8be6cf7c3124bbc3dd02dac4e1b6ee15a176 GIT binary patch literal 567 zcmV-70?7S|P)M z_l#!UnWyjnnl(zCdCUVAb|$pMVR9m6_4C1nv|P8r@7VPHVKnH-!{eJ~E(+ZK0Am!un706v`dwJLEiOlC_@io`q$ouWOcR*dIMZ3H{-ZhOcBC?)? zD}JySWa@C;`7y-@000000001Bjq?IUOcN^B7*R47>=m(CHUPb$9*ihO!drk?CRD6B z3lP(UinSo0f@0bL-m@U<{&>syS)NFii->f^YDf_P5UH5@^IbI8?>q~NsXyOEbN!8+ z>{k8pBCD<1+WqI2aI0-%>i_T2yg002ovPDHLk FV1mdD22%h4 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_metal.png b/Resources/Textures/Buildings/research.rsi/protolathe_metal.png new file mode 100644 index 0000000000000000000000000000000000000000..4f762ab3839d3c352bca771fd4b7cc867276daf2 GIT binary patch literal 524 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_silver.png b/Resources/Textures/Buildings/research.rsi/protolathe_silver.png new file mode 100644 index 0000000000000000000000000000000000000000..e48844d6beab4808960278b7acc4b72b24b8ab4d GIT binary patch literal 534 zcmV+x0_pvUP)N?C59+#>?RKlVmZL};ptAthqHKVhA^-R_ ztLs`NhXNSuEWms|*IY+f#R8<1WVhQ%N=bV?M^QF_%a@=?8^C1&tZ8ll3Sf)^80##6 z)jvVx0{}v6#^d(%s_lI;G0oS!|Kf4<%WNjH-yg4+suopo;mup~r=#7E=~U!!Fw57x z|3S{W`#u1DJvQU^_2JGmUqs}54W)dyAIS7kdhjFV0{{R30001hKgIL$NCOhqI{SjO z`s1T>`GHKNXQ0ahh%llQp81GUlmQ8AZYN}-aQhMzwL}m1efyx*7=(A34OV z00000001W#Q8MOsL}|5JnRG;GbJ?652?a1l0gN%CWUSa1M1MR007ks@=jW@oQg`!E zT+RpN{``E^&VQ=DdYyl{nGZPE_r|Uzzgkr7{qyIK?>U(dXoiS$m;d`300000000ox YH$%`AXWtZ++5i9m07*qoM6N<$g0^Y$nE(I) literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_solid plasma.png b/Resources/Textures/Buildings/research.rsi/protolathe_solid plasma.png new file mode 100644 index 0000000000000000000000000000000000000000..668c5453a6833a19a056139511b5bf8769321f88 GIT binary patch literal 538 zcmV+#0_FXQP)WYv$orumF={D$l#GGAX9o`7C(Z075d9Qn-5+lme^N~4e0wZX zn56&!@XAwveu(D!gP(#t_2-9ZuD_9krm8<)WVKaWyFY(D2dns14r=oO*8TC`;{yNy z00000003YK6Eeo4y`ftD@iAxH$0i*S+LCO}^@IW#qX5R3kTDkQ3!*CU1FEZb{HyG<*7;Xb^8w51E^JfcXQQksQ>@~ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_t.png b/Resources/Textures/Buildings/research.rsi/protolathe_t.png new file mode 100644 index 0000000000000000000000000000000000000000..06128a0926274797ed33c8404ca32f03f3fb7bf2 GIT binary patch literal 1066 zcmV+_1l9YAP)SBL)|)?3GG5bPw^t{l2*L1>98P_PF%FOY3;D>6lbAq&BeswZrm=? z4#5tQ{=qSHvs!h`>c!lg73|igX|5WEHmzcEmQ2eB8&i6*coEL$IXNfE+6}$&fy4Lb zeZTj4zW2QE;lO{4qWcx7@6hpdA)j{#4_o+58~eHf1%u>*qLlOeb8R{8pMG{8AyxqR za5N}+i;d)OA2~!q`#ldyX-?bl$Qc0UBTe*nn5XFI5UFVO~IQzJPA782Tt=H;lEZ#+%ZwFsE9I(4vp5L$@0RT6}A~(e%0N34*aQ=;>^iHkP zJGF|xJxz7^4qqQj0uWn{0Z?*zj%R8v5PYuea&aKK@#g=3E+tZ9jDx`mxJH^ocX#kv+n;JvDut9)mqKnf=?hb^&mDEVG{c<_F7JlK{uY65012PXqIjD0Obt zls4U-4Ix%?wy6<{Qc5T^V=q8YkD_=@qV)aj_h7>y6UdX_&vctMVpl}<~L-h)BjI}wm;{Q}=#zL1x^ z$=8AqBDVm5rArNz8Adzu)~BkN{MIZ2pknJbmbZj#$*XU8EPIykfDnQ^v*o7Ih-dsk z)Y{Bkt<9|O1(pn`dh|SSIjeYO|F5{5RlGm?%l#FgX$f|CKgQplW@5aEx!Jvz8qi6- zjaKqhGR~2XAzQM+fp;tyw5HW5K}(k!*uHbdG?xA3;#lw_-VD5G;xiSYrX^4m#q#KV ztEbOs)Av)!IDjqr_MMrGOpF&v(K_VcY2y>E>;5A)WqR(xl^fT8 kw$?y6eDz-u@IQ?B8v;<72!Sabw*UYD07*qoM6N<$f?6yCY5)KL literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/protolathe_uranium.png b/Resources/Textures/Buildings/research.rsi/protolathe_uranium.png new file mode 100644 index 0000000000000000000000000000000000000000..eb7a83f39066c34c985ea3a13cbaa661577a6000 GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U_9dK;uumf=j~lXzrzj!4i`BO zb%`up^pW>OVbJBkLI$aG4u=_?4vSham@ZoKgk_?1kh{n-2KQ~I-41E#X0!g!>Yb#q z`E{i0#VWU%ozJ3f9e!**bLIPk`z?I;Z4#PjzPpo$KF2koifNnDr%$$3oSPU={o&%;!g$K3!Td0<1Jj&|4Cyaf_DG#;`oX|Zleucu z+x~0wo)usJG3|AD|Kj)4y(iv%XYem{@4Bm<=KW{a_m#~T=brfYp6#8;n9fljDrbzOD-dc->W(~iO6 z^7=+PSGTP@FMs}^cAYu<=6?+DfBkszkr8Io1?Tpj%_X@j0&8n7weARUiO}9`VJ;x_ zpohg`vGcK+apEquHVh1QUMtVPcgv0{E!K1WU+-WZb$tI?d$qasqF3cFn3pCV7up|p v{q56dwI5;>|1$da6&yYKzmgAPMaL`QN`tE@&zB`#1qpb%`njxgN@xNADd^g~ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/server-nopower.png b/Resources/Textures/Buildings/research.rsi/server-nopower.png new file mode 100644 index 0000000000000000000000000000000000000000..a471835d97aa36d57f98e38d08c34df142966285 GIT binary patch literal 487 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/tdoppler-off.png b/Resources/Textures/Buildings/research.rsi/tdoppler-off.png new file mode 100644 index 0000000000000000000000000000000000000000..e8aa031b11a038d103344bcc4316de7b21a24b43 GIT binary patch literal 1113 zcmV-f1g86mP)m9iHr z!cd{14_j;Li`5p2sVhiasaB{M2ewU%=Em&OUWW_Y!g}{G@A2N;do_ZxFC5PK{W-ti z_jmr>lfeHB$4&(fHMFSOIDY$zZJv^yN_?^BywH8piE2k=TUQ{|(4t=KIgcbs04U0; zu=#vGzWVwb6NznK0WF!LtWsL~%BH@S1pI!#YNankDwPt|gc@4ZmFEjcl4Ph{kCo>O zgc@2@{VJFU`2Bvh^7UE(mKPuKYGpOci;pY=`u%=YPvT7E3qFr_0MOGL6Mmf?VPU_1 z{hA?$?H2s==L#N=NA!h|W!a>YB#FkRFz*}+(Ag1YDl$cIAjp?p-vRJoCTW`Ev`TJQ z@kWT>zT!wECh+-uWR?|rdSe6!g7oyp$Sf;lmK73-iJd=$+6~b9(VOuN{kN6CT6HSG z-0UO=D?AMLe^1?8NAz}k0%heFKFPO_B#@0?UsRq=2p zE_^QyUNGN+*0!h`{p@2j0#=923hj|F4`z}aJ9-#JS*5chPJ1NGthxlP8*NeJu$*i>{J-lHzI83)A>nbd{$~ zFGxK}*#^~;Yg?enMH5ix@;LiEsJ$zwb9o#cWjN|~@$#&UI+sW7UBTIB{x;|_HJvo{ zMG?pZ`~V`Ev(JO0ZWn+-cJ_IAd3NJ7608rEN>`ap36P7huXO0fcSd67sjYxY9) z55!nX%c5#6`P6ihrL;`{K+H6!1Ym6K?!J-Az|kYTHnJZ^);vb`uk|*v4(*Y!=%=Ue zS?Vnb412vO&0)F$x7$tb59`;g5UxFYZApYeAxpihbhnv+hJ9sajNM5Tm0S<)ky*Rr z01REbY4G{*gZFWj?q*+EnKc1PO7r7SH@2J{;Qsx423eM6?%o|Y`Dw4JB&Cf6rn6c8 zQWOzretzStqoKAoAOh-1$g)f>mot&n2q=nTB%sIqd{PA748DpAr1W|gRV|<&PTl>(C!SfX3se)@_o}R$X1Q zF@eXAf5q)~TTU(p`Z=;Jlbrp^t_ycuV*fq4xNL1AEeD00000NkvXXu0mjfNb(z2 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Buildings/research.rsi/tdoppler.png b/Resources/Textures/Buildings/research.rsi/tdoppler.png new file mode 100644 index 0000000000000000000000000000000000000000..67f4452a53160cc13a1ad9c6c7f76ebae4ad06d4 GIT binary patch literal 1621 zcmbuAdpy%?9LIl)3{6NixlKojR9J2e!^y33TjWw9Yog2MIuvG+dyY=Lieiitr@54r zb(=Yjl-qQXxie{$aWc1!ZRgkPb^bZ8*Lj_P&L7YBdA^_L`+Q#S=XpKP>&f==a8Xj! zPy_%#>4>Z2aj-x0qshyGky{jR1psndM;sk|5=!``5#efKTJ20j)l0D!X+`EyPs_DA zkFFj>CEeeV-t*oP#W%`k-BRk`7F>I&l8|gFr?x}$iIHX#i<|3i)O`PhgGQ+?5qr_$ zJW3_+kp(_(nizYvZk+EF;~DcdFnZvoq?SD|(=NU|M`6cYp6e=DY?eFY8dXT+df_Xp zj9m_t9ViZawpMy?3u4}8Z&NZyaewe(+!FkW19i}upTNVkPDTM_T2D5zjGf6wODlde zeigk47jNzjrJ*tx7T`xQ;+QE*KG|Nkyco~kwLLd`6xL^Pp)r4cr%t}+6?Ex9`-%z; z|8XHO9Vu)?>8i+wH!w`CNwOlDlUMXc@gi9S{Rse|UWi&rb^Ajwq`BdNVdBv*bjhA@ z7MDA*Il}ileZuimQ-aMe&qXGqoYQ%qZkagWaaNgC;C@(s008v%inyMd1j#8qdBX0+ zAuB1eQ|@Kio#SS^wqTKZ(Za`P6IM}4Xhc<*Pi)-MPL7&mnBR}Yjs3g2PBTI3{!m>~ z*S3^$(0g4#Z9Yl4Z-;#;31;w(8V!<#)`MnyD3#DdDc2Q zq%3{34>+?vr&KFQ>`mle{P?oTmNBfuE)VX?owkf^xtfL_?4y#>{KHzl1x^cLLQu$1 zWtBF_2aRdHS{>0WDgp1txBdt1r43n}72Le(2RJui<&?G{WxZ!HXAms{Dj+LxBlASo zyYmFa$z}ho*729pXg$;?yHmeZnH&hTN+Xh#^{cmUDkfxT(bV%`Y~drg)*|_&s*W3gY=m*2Gf`!Evzy&!C$)Tq7WSGe6n+xb?ABo z;k%#!`{7j4^FZ9f-8ixZI{y(t`Eh+f_@p56u}=%8af7F_Vee}mmDpWTYrV3~9I0X6 zwaG!BNtLOpkpXmZp(8q07kmM72%~l=tva)?WE+(3Q%UGY%hG*#!=7=>X|kBn-s&8+ z@j81GM)W+0@-~}-cJnZ-=qC!_gl{eIR`gh04As_qF{S_C@?hhTa598=9-eUhdW5(`AW7;%+^O7KDY^(SifYpN=yyU|~C z_?^?%YaBCy1$twyQjJ(mpJ%3nk@sFd1;~;{xYF8eG7tZ>!w>83lpbn}6gfd#C%so^ zc!G_cknvA-cQc4im`MI$U#-|iCqFXury3BeGFw*Sdjj*W^%{dZROQ}R@oLf8ASV@v zEbenA%K@j{n!}%? zJ{mW}i^0+(!Io3E?b@)&Vl%gAHwNZq7wzI;Tpwbd!E|0508|4*evTtFG4|OFBQJ2% zz~~08Oh!<*ZEvQrn!>o~GAx1d^!wMloNH}E$T5CEK!D+`gNe3&X}INF&sVGyL**9N z;LY)#dAY>mKAVDL@E-MRGu~+2QW!&_Mk!=uANu=! zeFNVb#N!H&@x#;)O(X#gWdcYmsRydEb_iiV%lli9y#FF|>kol&_Y(MWQx_l`?GQ{D zgE0gWi{h8A`Nd!buk#S4&of5xE69JUa^+VNzv#3jnQ%l}WH zsM@jKD1crds3~Zd0kTG-$?703i;1vzJ1>BAjti46DE2@