using System.Linq; using Content.Shared.Lathe; using Content.Shared.Research.Components; using Content.Shared.Research.Prototypes; 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); } private void OnMapInit(EntityUid uid, TechnologyDatabaseComponent component, MapInitEvent args) { UpdateTechnologyCards(uid, component); } 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.Discipline == discipline); 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; if (!component.SupportedDisciplines.Contains(tech.Discipline)) return false; if (tech.Tier > disciplineTiers[tech.Discipline]) 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.Discipline == techDiscipline.ID && !p.Hidden).ToList(); var allUnlocked = new List(); foreach (var recipe in component.UnlockedTechnologies) { var proto = PrototypeManager.Index(recipe); if (proto.Discipline != techDiscipline.ID) 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.Discipline == techDiscipline.ID && p.Tier == tier - 1).ToList(); 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; } /// /// 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) { 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); } /// /// 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); } }