using System.Linq; using Content.Shared.Examine; using Content.Shared.Lathe; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; using Content.Shared.Storage; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Shared.Research.Systems; public abstract class SharedResearchSystem : EntitySystem { [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedLatheSystem _lathe = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnServerExamined); // Frontier } private void OnMapInit(EntityUid uid, TechnologyDatabaseComponent component, MapInitEvent args) { UpdateTechnologyCards(uid, component); } // Frontier: print server ID on examine private void OnServerExamined(Entity ent, ref ExaminedEvent args) { if (args.IsInDetailsRange) args.PushMarkup(Loc.GetString("research-server-examine-id", ("id", ent.Comp.Id))); } // End Frontier: print server ID on examine public void UpdateTechnologyCards(EntityUid uid, TechnologyDatabaseComponent? component = null) { if (!Resolve(uid, ref component)) return; var availableTechnology = GetAvailableTechnologies(uid, component); _random.Shuffle(availableTechnology); component.CurrentTechnologyCards.Clear(); foreach (var discipline in component.SupportedDisciplines) { var selected = availableTechnology.FirstOrDefault(p => p.HasDiscipline(discipline)); // Frontier: Updated to support dual-discipline technologies if (selected == null) continue; component.CurrentTechnologyCards.Add(selected.ID); } Dirty(uid, component); } public List GetAvailableTechnologies(EntityUid uid, TechnologyDatabaseComponent? component = null) { if (!Resolve(uid, ref component, false)) return new List(); var availableTechnologies = new List(); var disciplineTiers = GetDisciplineTiers(component); foreach (var tech in PrototypeManager.EnumeratePrototypes()) { if (IsTechnologyAvailable(component, tech, disciplineTiers)) availableTechnologies.Add(tech); } return availableTechnologies; } public bool IsTechnologyAvailable(TechnologyDatabaseComponent component, TechnologyPrototype tech, Dictionary? disciplineTiers = null) { disciplineTiers ??= GetDisciplineTiers(component); if (tech.Hidden) return false; var techDisciplines = tech.GetAllDisciplines(); // Frontier: Updated to support dual-discipline technologies - tech is available if ANY of its disciplines are supported if (!techDisciplines.Any(discipline => component.SupportedDisciplines.Contains(discipline))) return false; // if (tech.Tier > disciplineTiers[tech.Discipline]) // Goobstation R&D Console rework - removed main discipline checks // return false; if (component.UnlockedTechnologies.Contains(tech.ID)) return false; foreach (var prereq in tech.TechnologyPrerequisites) { if (!component.UnlockedTechnologies.Contains(prereq)) return false; } return true; } public Dictionary GetDisciplineTiers(TechnologyDatabaseComponent component) { var tiers = new Dictionary(); foreach (var discipline in component.SupportedDisciplines) { tiers.Add(discipline, GetHighestDisciplineTier(component, discipline)); } return tiers; } public int GetHighestDisciplineTier(TechnologyDatabaseComponent component, string disciplineId) { return GetHighestDisciplineTier(component, PrototypeManager.Index(disciplineId)); } public int GetHighestDisciplineTier(TechnologyDatabaseComponent component, TechDisciplinePrototype techDiscipline) { var allTech = PrototypeManager.EnumeratePrototypes() .Where(p => p.HasDiscipline(techDiscipline.ID) && !p.Hidden).ToList(); // Frontier: Updated to support dual-discipline technologies var allUnlocked = new List(); foreach (var recipe in component.UnlockedTechnologies) { var proto = PrototypeManager.Index(recipe); if (!proto.HasDiscipline(techDiscipline.ID)) // Frontier: Updated to support dual-discipline technologies continue; allUnlocked.Add(proto); } var highestTier = techDiscipline.TierPrerequisites.Keys.Max(); var tier = 2; //tier 1 is always given // todo this might break if you have hidden technologies. i'm not sure while (tier <= highestTier) { // we need to get the tech for the tier 1 below because that's // what the percentage in TierPrerequisites is referring to. var unlockedTierTech = allUnlocked.Where(p => p.Tier == tier - 1).ToList(); var allTierTech = allTech.Where(p => p.HasDiscipline(techDiscipline.ID) && p.Tier == tier - 1).ToList(); // Frontier: Updated to support dual-discipline technologies if (allTierTech.Count == 0) break; var percent = (float) unlockedTierTech.Count / allTierTech.Count; if (percent < techDiscipline.TierPrerequisites[tier]) break; if (tier >= techDiscipline.LockoutTier && component.MainDiscipline != null && techDiscipline.ID != component.MainDiscipline) break; tier++; } return tier - 1; } public FormattedMessage GetTechnologyDescription( TechnologyPrototype technology, bool includeCost = true, bool includeTier = true, bool includePrereqs = false, TechDisciplinePrototype? disciplinePrototype = null) { var description = new FormattedMessage(); if (includeTier) { disciplinePrototype ??= PrototypeManager.Index(technology.Discipline); description.AddMarkupOrThrow(Loc.GetString("research-console-tier-discipline-info", ("tier", technology.Tier), ("color", disciplinePrototype.Color), ("discipline", Loc.GetString(disciplinePrototype.Name)))); description.PushNewline(); } if (includeCost) { description.AddMarkupOrThrow(Loc.GetString("research-console-cost", ("amount", technology.Cost))); description.PushNewline(); } if (includePrereqs && technology.TechnologyPrerequisites.Any()) { description.AddMarkupOrThrow(Loc.GetString("research-console-prereqs-list-start")); foreach (var recipe in technology.TechnologyPrerequisites) { var techProto = PrototypeManager.Index(recipe); description.PushNewline(); description.AddMarkupOrThrow(Loc.GetString("research-console-prereqs-list-entry", ("text", Loc.GetString(techProto.Name)))); } description.PushNewline(); } description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-start")); foreach (var recipe in technology.RecipeUnlocks) { var recipeProto = PrototypeManager.Index(recipe); description.PushNewline(); description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-entry", ("name", _lathe.GetRecipeName(recipeProto)))); } foreach (var generic in technology.GenericUnlocks) { description.PushNewline(); description.AddMarkupOrThrow(Loc.GetString("research-console-unlocks-list-entry-generic", ("text", Loc.GetString(generic.UnlockDescription)))); } return description; } // Horizon start public FormattedMessage GetTechNeededItemList(EntityUid? serverUid, TechnologyPrototype technology) { var itemList = new FormattedMessage(); if (technology.ResearchTargets is null || !TryComp(serverUid, out var storage)) return itemList; itemList.AddMarkupOrThrow(Loc.GetString("research-console-need-items-list")); itemList.PushNewline(); foreach (var id in technology.ResearchTargets) { var markup = Color.Red; if (TryGetTarget(id, storage)) markup = Color.Green; itemList.AddMarkupOrThrow(Loc.GetString("research-console-research-target", ("target", PrototypeManager.Index(id).Name), ("markup", markup))); itemList.PushNewline(); } return itemList; } public bool TryGetAllTargets(EntityUid? serverUid, TechnologyPrototype tech) { if (tech.ResearchTargets is null) return true; if (!TryComp(serverUid, out var storageComp)) return false; var count = tech.ResearchTargets.Count; foreach (var id in tech.ResearchTargets) { foreach (var (uid, _) in storageComp.StoredItems) { var item = MetaData(uid).EntityPrototype?.ID; if (item == id) count--; } } return count == 0; } private bool TryGetTarget(string id, StorageComponent storage) { foreach (var (uid, _) in storage.StoredItems) { var protoId = MetaData(uid).EntityPrototype?.ID; if (protoId == id) return true; } return false; } // Horizon end /// /// Returns whether a technology is unlocked on this database or not. /// /// Whether it is unlocked or not public bool IsTechnologyUnlocked(EntityUid uid, TechnologyPrototype technology, TechnologyDatabaseComponent? component = null) { return Resolve(uid, ref component) && IsTechnologyUnlocked(uid, technology.ID, component); } /// /// 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, false) && component.UnlockedTechnologies.Contains(technologyId); } public void TrySetMainDiscipline(TechnologyPrototype prototype, EntityUid uid, TechnologyDatabaseComponent? component = null) { return; // Frontier: allow unlocking all disciplines /* if (!Resolve(uid, ref component)) return; var discipline = PrototypeManager.Index(prototype.Discipline); if (prototype.Tier < discipline.LockoutTier) return; component.MainDiscipline = prototype.Discipline; Dirty(uid, component); var ev = new TechnologyDatabaseModifiedEvent(); RaiseLocalEvent(uid, ref ev); */ // End Frontier: allow unlocking all disciplines } /// /// Removes a technology and its recipes from a technology database. /// public bool TryRemoveTechnology(Entity entity, ProtoId tech) { return TryRemoveTechnology(entity, PrototypeManager.Index(tech)); } /// /// Removes a technology and its recipes from a technology database. /// [PublicAPI] public bool TryRemoveTechnology(Entity entity, TechnologyPrototype tech) { if (!entity.Comp.UnlockedTechnologies.Remove(tech.ID)) return false; // check to make sure we didn't somehow get the recipe from another tech. // unlikely, but whatever var recipes = tech.RecipeUnlocks; foreach (var recipe in recipes) { var hasTechElsewhere = false; foreach (var unlockedTech in entity.Comp.UnlockedTechnologies) { var unlockedTechProto = PrototypeManager.Index(unlockedTech); if (!unlockedTechProto.RecipeUnlocks.Contains(recipe)) continue; hasTechElsewhere = true; break; } if (!hasTechElsewhere) entity.Comp.UnlockedRecipes.Remove(recipe); } Dirty(entity, entity.Comp); UpdateTechnologyCards(entity, entity); return true; } /// /// Clear all unlocked technologies from the database. /// [PublicAPI] public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null) { if (!Resolve(uid, ref comp) || comp.UnlockedTechnologies.Count == 0) return; comp.UnlockedTechnologies.Clear(); Dirty(uid, comp); } /// /// Adds a lathe recipe to the specified technology database /// without checking if it can be unlocked. /// public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component.UnlockedRecipes.Contains(recipe)) return; component.UnlockedRecipes.Add(recipe); Dirty(uid, component); var ev = new TechnologyDatabaseModifiedEvent(new List { recipe }); RaiseLocalEvent(uid, ref ev); } }