using Content.Shared.GameTicking; using Content.Shared.NameIdentifier; using Content.Shared.NameModifier.EntitySystems; using Robust.Shared.Collections; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.NameIdentifier; /// public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem { [Dependency] private IRobustRandom _robustRandom = default!; [Dependency] private NameModifierSystem _nameModifier = default!; /// /// Free IDs available per . /// [ViewVariables] public readonly Dictionary> CurrentIds = []; /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(CleanupIds); SubscribeLocalEvent(OnReloadPrototypes); InitialSetupPrototypes(); } private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.Group is null) return; if (!CurrentIds.TryGetValue(ent.Comp.Group, out var ids)) { _nameModifier.RefreshNameModifiers(ent.Owner); return; } // Not a valid value if (ent.Comp.Identifier == -1) return; if (ids.Count > 0) { // Avoid inserting the value right back at the end or shuffling in place: // just pick a random spot to put it and then move that one to the end. var randomIndex = _robustRandom.Next(ids.Count); var random = ids[randomIndex]; ids[randomIndex] = ent.Comp.Identifier; ids.Add(random); } else { ids.Add(ent.Comp.Identifier); } _nameModifier.RefreshNameModifiers(ent.Owner); } /// /// /// /// /// This overload resolves the ProtoId of the NameIdentifierGroupPrototype first. /// /// A ProtoId that will be resolved and passed on. /// The index value of the randomly selected modifier. public string GenerateUniqueNameModifier(ProtoId proto, out int randomVal) { return GenerateUniqueNameModifier(ProtoMan.Index(proto), out randomVal); } /// /// Generates a new unique name modifier for a given entity and removes it from /// but does not set the entity's name. /// /// The prototype to retrieve from. /// The index value of the randomly selected modifier. /// A formatted and/or localized modifier. Empty string if invalid. public string GenerateUniqueNameModifier(NameIdentifierGroupPrototype proto, out int randomVal) { randomVal = -1; if (!CurrentIds.TryGetValue(proto.ID, out var set)) return string.Empty; if (set.Count == 0) { // Oh jeez. We're outta numbers. return string.Empty; } randomVal = set[^1]; set.RemoveAt(set.Count - 1); return FormatAndLocalize(randomVal, proto); } /// /// Format and localize the provided integer against the prototype. /// /// The selected value to process. /// The prototype that defines optional localization and formatting. /// A formatted and/or localized string. private string FormatAndLocalize(int value, NameIdentifierGroupPrototype proto) { var formatted = value.ToString(); if (proto.IdentifierDataset is not null) { var identifiers = ProtoMan.Index(proto.IdentifierDataset); formatted = Loc.GetString(identifiers.Values.Prefix+formatted); } return proto.Format is not null ? Loc.GetString(proto.Format, ("number", formatted)) : formatted; } /// /// Initializes the component when initialized on the map. /// This will use the existing identifier, if present, or generate a new one and update the component appropriately. /// /// The entity component tuple being initialized. /// The arguments for the event. Unused. private void OnMapInit(Entity ent, ref MapInitEvent args) { if (ent.Comp.Group is null) return; if (!ProtoMan.Resolve(ent.Comp.Group, out var group)) return; // If it has an existing valid identifier then use that, otherwise generate a new one. if (ent.Comp.Identifier != -1 && CurrentIds.TryGetValue(ent.Comp.Group, out var ids) && ids.Remove(ent.Comp.Identifier)) { ent.Comp.FullIdentifier = FormatAndLocalize(ent.Comp.Identifier, group); } else { ent.Comp.FullIdentifier = GenerateUniqueNameModifier(group, out ent.Comp.Identifier); } Dirty(ent); _nameModifier.RefreshNameModifiers(ent.Owner); } private void InitialSetupPrototypes() { EnsureIds(); } /// /// Fill a provided list with a range of numbers corresponding to a prototype's defined range. /// /// The prototype to retrieve from. /// Reference to the list where values should be placed. private void FillGroup(NameIdentifierGroupPrototype proto, List values) { values.Clear(); var (max, min) = (proto.MaxValue, proto.MinValue); if (proto.IdentifierDataset is not null) { max = ProtoMan.Index(proto.IdentifierDataset).Values.Count; min = 1; } for (var i = min; i <= max; i++) { values.Add(i); } _robustRandom.Shuffle(values); } /// /// Retrieve the appropriate list or create a new one if one does not already exist. /// /// The prototype to retrieve from. /// The list corresponding to the prototype, a new empty list if not already extant. private List GetOrCreateIdList(NameIdentifierGroupPrototype proto) { // The ID list already exists. if (CurrentIds.TryGetValue(proto.ID, out var ids)) return ids; // If we're using a dataset, grab the count. Otherwise, use (max - min). ids = new List(proto.IdentifierDataset is null ? proto.MaxValue - proto.MinValue : ProtoMan.Index(proto.IdentifierDataset).Values.Count); CurrentIds.Add(proto.ID, ids); return ids; } private void EnsureIds() { foreach (var proto in ProtoMan.EnumeratePrototypes()) { var ids = GetOrCreateIdList(proto); FillGroup(proto, ids); } } private void OnReloadPrototypes(PrototypesReloadedEventArgs ev) { if (!ev.ByType.TryGetValue(typeof(NameIdentifierGroupPrototype), out var set)) return; var toRemove = new ValueList(); foreach (var proto in CurrentIds.Keys) { if (!ProtoMan.HasIndex(proto)) { toRemove.Add(proto); } } foreach (var proto in toRemove) { CurrentIds.Remove(proto); } foreach (var proto in set.Modified.Values) { var nameProto = (NameIdentifierGroupPrototype)proto; // Only bother adding new ones. if (CurrentIds.ContainsKey(proto.ID)) continue; var ids = GetOrCreateIdList(nameProto); FillGroup(nameProto, ids); } } private void CleanupIds(RoundRestartCleanupEvent ev) { EnsureIds(); } }