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