Give mobs amusing name prefixes (#44977)

* Functional product

* Bugfix

* Handle empty case gracefully

* Fix inserting bad values

* First set of tests

* Don't need this

* Dont add empty modifers to names

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Typo and [SubscribeLocalEvent]

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Refactor GenerateUniqueName to GenerateUniqueNameModifier

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Fix returning name when empty

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Docs

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Tests

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Fix comment

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Fix FTL test

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Add adjectives

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Amateur hour

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Simplify spawning

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Even more streamlined

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Improve comment

Signed-off-by: Connor Huffine <chuffine@gmail.com>

* Apply suggestions from code review

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>

---------

Signed-off-by: Connor Huffine <chuffine@gmail.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
This commit is contained in:
Connor Huffine 2026-08-08 20:22:15 -04:00 • committed by GitHub
parent 1807635fa0
commit 8b48748048
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 416 additions and 57 deletions

View file

@ -0,0 +1,248 @@
using System.Collections.Generic;
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.NameIdentifier;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.NameIdentifier;
[TestOf(typeof(NameIdentifierSystem))]
public sealed class NameIdentifierTest : GameTest
{
private const string MaxIds = "5";
private const int MaxIdsInt = 5;
private const string NameTest = "NameTest";
private const string NumberTestGroup = "NumberTestGroup";
private const string PrefixTestGroup = "PrefixTestGroup";
private const string PrefixTest = "PrefixTest";
private const string ParenTestEnt = "ParenTestEnt";
private const string LocTestEnt = "LocTestEnt";
[TestPrototypes]
private const string Prototypes =
$"""
- type: nameIdentifierGroup
id: {NumberTestGroup}
minValue: 1
maxValue: {MaxIds}
- type: entity
name: {NameTest}
id: {NameTest}
components:
- type: NameIdentifier
group: {NumberTestGroup}
- type: nameIdentifierGroup
parent: {NumberTestGroup}
id: {PrefixTestGroup}
prefix: true
- type: entity
name: {PrefixTest}
id: {PrefixTest}
components:
- type: NameIdentifier
group: {PrefixTestGroup}
- type: entity
name: {ParenTestEnt}
id: {ParenTestEnt}
components:
- type: NameIdentifier
group: GenericNumber
- type: localizedDataset
id: NameIdentifierTest
values:
prefix: name-identifier-test-
count: 1
- type: nameIdentifierGroup
id: Localized
identifierDataset: NameIdentifierTest
- type: entity
name: {LocTestEnt}
id: {LocTestEnt}
components:
- type: NameIdentifier
group: Localized
""";
[SidedDependency(Side.Server)] private NameIdentifierSystem _nameModifier = default!;
/// <inheritdoc/>
/// <remarks>
/// We set <see cref="PoolSettings.Connected"/> false because the client has no unique behavior.
/// Skipping client sim saves several seconds.
/// </remarks>
public override PoolSettings PoolSettings => new ()
{
Connected = false,
};
[Test]
[Description("Tests that prototypes are loaded into the identifier system properly.")]
public async Task ValidatePoolGeneration()
{
await Server.WaitAssertion(() =>
{
using (Assert.EnterMultipleScope())
{
Assert.That(_nameModifier.CurrentIds.ContainsKey(NumberTestGroup),
"Failed to load test prototypes.");
Assert.That(_nameModifier.CurrentIds[NumberTestGroup],
Has.Count.EqualTo(MaxIdsInt),
"Failed to load test prototypes.");
}
});
}
[Test]
[Description("Tests that IDs are removed from the pool and properly returned when deleted.")]
public async Task TakeAndReturn()
{
await Server.WaitAssertion(() =>
{
// Create an entity that will draw a modifier from the pool
var single = SSpawn(NameTest);
Assert.That(_nameModifier.CurrentIds[NumberTestGroup],
Has.Count.EqualTo(MaxIdsInt - 1),
"CurrentIds did not decrease.");
// Delete the entity, which should return the modifier to the pool
SDeleteNow(single);
Assert.That(_nameModifier.CurrentIds[NumberTestGroup],
Has.Count.EqualTo(MaxIdsInt),
"CurrentIds did not return to max.");
});
}
[Test]
[Description("Tests that the list of IDs behaves properly when taken to zero values.")]
public async Task ExhaustAndRefillList()
{
var entList = new List<EntityUid>();
var original = _nameModifier.CurrentIds[NumberTestGroup];
await Server.WaitAssertion(() =>
{
// Spawn as many entities as the pool should hold.
for (var i = 0; i <= MaxIdsInt; i++)
entList.Add(SSpawn(NameTest));
using (Assert.EnterMultipleScope())
{
Assert.That(_nameModifier.CurrentIds[NumberTestGroup],
Has.Count.EqualTo(0),
"CurrentIds failed to empty.");
}
// Delete all entities, returning them to the pool.
foreach (var ent in entList)
SDeleteNow(ent);
using (Assert.EnterMultipleScope())
{
Assert.That(_nameModifier.CurrentIds[NumberTestGroup],
Has.Count.EqualTo(MaxIdsInt),
"CurrentIds failed to refill.");
Assert.That(original,
Is.EqualTo(_nameModifier.CurrentIds[NumberTestGroup]),
"List of values was not returned to starting state.");
}
});
}
[Test]
[Description("Tests that IDs are not duplicated and correctly formatted.")]
public async Task ValidateDefaultGeneration()
{
var entList = new List<EntityUid>();
await Server.WaitAssertion(() =>
{
// Spawn as many entities as the pool should hold.
for (var i = 0; i < MaxIdsInt; i++)
entList.Add(SSpawn(NameTest));
var nameList = entList
.Select(p => SEntMan.GetComponent<MetaDataComponent>(p).EntityName)
.ToList();
using (Assert.EnterMultipleScope())
{
Assert.That(nameList, Is.Unique, "A duplicate name was generated.");
Assert.That(nameList, Has.All.Contain(NameTest), "The base name was not preserved.");
Assert.That(nameList, Has.All.Match(@"\d+$"), "Created an invalid name.");
}
});
}
[Test]
[Description("Tests that the core parenthesis wrapped modifier works properly.")]
public async Task ParenthesisAppend()
{
await Server.WaitAssertion(() =>
{
var single = SSpawn(ParenTestEnt);
Assert.That(
SEntMan.GetComponent<MetaDataComponent>(single).EntityName,
Does.Match($@"{ParenTestEnt} \(\d+\)$"),
"Did not create a valid parenthesis wrapped name."
);
});
}
[Test]
[Description("Tests that nothing is attached to the name when the pool of available names is empty.")]
public async Task DrawWhenEmpty()
{
await Server.WaitAssertion(() =>
{
// Spawn as many entities as the pool should hold.
for (var i = 0; i < MaxIdsInt; i++)
SSpawn(NameTest);
var emptyDraw = SSpawn(NameTest);
var name = SEntMan.GetComponent<MetaDataComponent>(emptyDraw).EntityName;
Assert.That(name, Is.EqualTo(NameTest), "Created an invalid name.");
});
}
[Test]
[Description("Tests that a localized value is properly fetched and attached.")]
public async Task LocalizedIdentifier()
{
await Server.WaitAssertion(() =>
{
var single = SSpawn(LocTestEnt);
Assert.That(
SEntMan.GetComponent<MetaDataComponent>(single).EntityName,
Is.EqualTo($"{LocTestEnt} TestValue"),
"Did not create a valid localized name."
);
});
}
[Test]
[Description("Tests that the prefix setting properly prepends a value.")]
public async Task PrefixedIdentifier()
{
await Server.WaitAssertion(() =>
{
var single = SSpawn(PrefixTest);
Assert.That(
SEntMan.GetComponent<MetaDataComponent>(single).EntityName,
Does.Match(@"^\d+"),
"Did not create a valid name with a prefix."
);
});
}
}

View file

@ -428,7 +428,7 @@ public sealed partial class CargoSystem
if (component.Bounties.Count >= component.MaxBounties)
return false;
_nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
_nameIdentifier.GenerateUniqueNameModifier(BountyNameIdentifierGroup, out var randomVal);
var newBounty = new CargoBountyData(bounty, randomVal);
// This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
if (component.Bounties.Any(b => b.Id == newBounty.Id))

View file

@ -7,6 +7,7 @@ using Robust.Shared.Random;
namespace Content.Server.NameIdentifier;
/// <inheritdoc cref="SharedNameIdentifierSystem"/>
public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
{
[Dependency] private IRobustRandom _robustRandom = default!;
@ -18,6 +19,7 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
[ViewVariables]
public readonly Dictionary<string, List<int>> CurrentIds = [];
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
@ -35,7 +37,17 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
if (ent.Comp.Group is null)
return;
if (CurrentIds.TryGetValue(ent.Comp.Group, out var ids) && ids.Count > 0)
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.
@ -44,44 +56,79 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
ids[randomIndex] = ent.Comp.Identifier;
ids.Add(random);
}
else
{
ids.Add(ent.Comp.Identifier);
}
_nameModifier.RefreshNameModifiers(ent.Owner);
}
/// <summary>
/// Generates a new unique name/suffix for a given entity and adds it to <see cref="CurrentIds"/>
/// but does not set the entity's name.
/// <inheritdoc cref="GenerateUniqueNameModifier(Content.Shared.NameIdentifier.NameIdentifierGroupPrototype,out int)" path="/summary"/>
/// </summary>
public string GenerateUniqueName(EntityUid uid, ProtoId<NameIdentifierGroupPrototype> proto, out int randomVal)
/// <remarks>
/// This overload resolves the ProtoId of the NameIdentifierGroupPrototype first.
/// </remarks>
/// <param name="proto">A ProtoId that will be resolved and passed on.</param>
/// <param name="randomVal">The index value of the randomly selected modifier.</param>
public string GenerateUniqueNameModifier(ProtoId<NameIdentifierGroupPrototype> proto, out int randomVal)
{
return GenerateUniqueName(uid, ProtoMan.Index(proto), out randomVal);
return GenerateUniqueNameModifier(ProtoMan.Index(proto), out randomVal);
}
/// <summary>
/// Generates a new unique name/suffix for a given entity and adds it to <see cref="CurrentIds"/>
/// but does not set the entity's name.
/// Generates a new unique name modifier for a given entity and removes it from <see cref="CurrentIds"/>
/// but does not set the entity's name.
/// </summary>
public string GenerateUniqueName(EntityUid uid, NameIdentifierGroupPrototype proto, out int randomVal)
/// <param name="proto">The <see cref="NameIdentifierGroupPrototype"/> prototype to retrieve from.</param>
/// <param name="randomVal">The index value of the randomly selected modifier.</param>
/// <returns>A formatted and/or localized modifier. Empty string if invalid.</returns>
public string GenerateUniqueNameModifier(NameIdentifierGroupPrototype proto, out int randomVal)
{
randomVal = 0;
var entityName = Name(uid);
randomVal = -1;
if (!CurrentIds.TryGetValue(proto.ID, out var set))
return entityName;
return string.Empty;
if (set.Count == 0)
{
// Oh jeez. We're outta numbers.
return entityName;
return string.Empty;
}
randomVal = set[^1];
set.RemoveAt(set.Count - 1);
return proto.Format is not null
? Loc.GetString(proto.Format, ("number", randomVal))
: $"{randomVal}";
return FormatAndLocalize(randomVal, proto);
}
/// <summary>
/// Format and localize the provided integer against the prototype.
/// </summary>
/// <param name="value">The selected value to process.</param>
/// <param name="proto">The prototype that defines optional localization and formatting.</param>
/// <returns>A formatted and/or localized string.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ent">The entity component tuple being initialized.</param>
/// <param name="args">The arguments for the event. Unused.</param>
private void OnMapInit(Entity<NameIdentifierComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.Group is null)
@ -90,29 +137,18 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
if (!ProtoMan.Resolve(ent.Comp.Group, out var group))
return;
int id;
string uniqueName;
// 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))
{
id = ent.Comp.Identifier;
uniqueName = group.Format is not null
? Loc.GetString(group.Format, ("number", id))
: $"{id}";
ent.Comp.FullIdentifier = FormatAndLocalize(ent.Comp.Identifier, group);
}
else
{
uniqueName = GenerateUniqueName(ent, group, out id);
ent.Comp.Identifier = id;
ent.Comp.FullIdentifier = GenerateUniqueNameModifier(group, out ent.Comp.Identifier);
}
ent.Comp.FullIdentifier = group.FullName
? uniqueName
: $"({uniqueName})";
Dirty(ent);
_nameModifier.RefreshNameModifiers(ent.Owner);
}
@ -122,10 +158,24 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
EnsureIds();
}
/// <summary>
/// Fill a provided list with a range of numbers corresponding to a prototype's defined range.
/// </summary>
/// <param name="proto">The <see cref="NameIdentifierGroupPrototype"/> prototype to retrieve from.</param>
/// <param name="values">Reference to the list where values should be placed.</param>
private void FillGroup(NameIdentifierGroupPrototype proto, List<int> values)
{
values.Clear();
for (var i = proto.MinValue; i < proto.MaxValue; i++)
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);
}
@ -133,14 +183,23 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
_robustRandom.Shuffle(values);
}
/// <summary>
/// Retrieve the appropriate list or create a new one if one does not already exist.
/// </summary>
/// <param name="proto">The <see cref="NameIdentifierGroupPrototype"/> prototype to retrieve from.</param>
/// <returns>The list corresponding to the prototype, a new empty list if not already extant.</returns>
private List<int> GetOrCreateIdList(NameIdentifierGroupPrototype proto)
{
if (!CurrentIds.TryGetValue(proto.ID, out var ids))
{
ids = new List<int>(proto.MaxValue - proto.MinValue);
CurrentIds.Add(proto.ID, ids);
}
// 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<int>(proto.IdentifierDataset is null
? proto.MaxValue - proto.MinValue
: ProtoMan.Index(proto.IdentifierDataset).Values.Count);
CurrentIds.Add(proto.ID, ids);
return ids;
}
@ -176,18 +235,17 @@ public sealed partial class NameIdentifierSystem : SharedNameIdentifierSystem
foreach (var proto in set.Modified.Values)
{
var name_proto = (NameIdentifierGroupPrototype)proto;
var nameProto = (NameIdentifierGroupPrototype)proto;
// Only bother adding new ones.
if (CurrentIds.ContainsKey(proto.ID))
continue;
var ids = GetOrCreateIdList(name_proto);
FillGroup(name_proto, ids);
var ids = GetOrCreateIdList(nameProto);
FillGroup(nameProto, ids);
}
}
private void CleanupIds(RoundRestartCleanupEvent ev)
{
EnsureIds();

View file

@ -9,6 +9,9 @@ namespace Content.Shared.NameIdentifier;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class NameIdentifierComponent : Component
{
/// <summary>
/// The group prototype that defines the behavior of the identifier.
/// </summary>
[DataField]
public ProtoId<NameIdentifierGroupPrototype>? Group;

View file

@ -1,3 +1,4 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Shared.NameIdentifier;
@ -14,6 +15,12 @@ public sealed partial class NameIdentifierGroupPrototype : IPrototype
[DataField]
public bool FullName = false;
/// <summary>
/// Should the identifier be a prefix, or a suffix?
/// </summary>
[DataField]
public bool Prefix = false;
/// <summary>
/// Optional format identifier. If set, the name will be formatted using it (e.g., "MK-500").
/// If not set, only the numeric part will be used (e.g., "500").
@ -32,4 +39,11 @@ public sealed partial class NameIdentifierGroupPrototype : IPrototype
/// </summary>
[DataField]
public int MinValue = 0;
/// <summary>
/// An optional field that will provide a list of name elements to pull from.
/// If non-null, <see cref="MinValue"/> and <see cref="MaxValue"/> will be ignored.
/// </summary>
[DataField]
public ProtoId<LocalizedDatasetPrototype>? IdentifierDataset;
}

View file

@ -7,18 +7,16 @@ namespace Content.Shared.NameIdentifier;
/// </summary>
public abstract partial class SharedNameIdentifierSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NameIdentifierComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
[SubscribeLocalEvent]
private void OnRefreshNameModifiers(Entity<NameIdentifierComponent> ent, ref RefreshNameModifiersEvent args)
{
if (ent.Comp.Group is null)
return;
// Don't add whitespace
if (string.IsNullOrWhiteSpace(ent.Comp.FullIdentifier))
return;
// Don't apply the modifier if the component is being removed
if (ent.Comp.LifeStage > ComponentLifeStage.Running)
return;
@ -26,7 +24,11 @@ public abstract partial class SharedNameIdentifierSystem : EntitySystem
if (!ProtoMan.Resolve(ent.Comp.Group, out var group))
return;
var format = group.FullName ? "name-identifier-format-full" : "name-identifier-format-append";
var format = "name-identifier-format-full";
if (!group.FullName)
format = group.Prefix ? "name-identifier-format-prepend" : "name-identifier-format-append";
// We apply the modifier with a low priority to keep it near the base name
// "Beep (Si-4562) the zombie" instead of "Beep the zombie (Si-4562)"
args.AddModifier(format, -10, ("identifier", ent.Comp.FullIdentifier));

View file

@ -9,13 +9,7 @@ public sealed partial class NameModifierSystem : EntitySystem
{
[Dependency] private MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NameModifierComponent, EntityRenamedEvent>(OnEntityRenamed);
}
[SubscribeLocalEvent]
private void OnEntityRenamed(Entity<NameModifierComponent> ent, ref EntityRenamedEvent args)
{
SetBaseName(ent, args.NewName);
@ -135,7 +129,7 @@ public sealed class RefreshNameModifiersEvent : IInventoryRelayEvent
/// </summary>
public string GetModifiedName()
{
// Start out with the entity's name name
// Start out with the entity's base name
var name = BaseName;
// Iterate through all the modifiers in priority order

View file

@ -394,3 +394,29 @@ adjectives-dataset-393 = many
adjectives-dataset-394 = numerous
adjectives-dataset-395 = substantial
adjectives-dataset-396 = capitalist
adjectives-dataset-397 = zippy
adjectives-dataset-398 = zesty
adjectives-dataset-399 = ravenous
adjectives-dataset-400 = abominable
adjectives-dataset-401 = man-eating
adjectives-dataset-402 = sly
adjectives-dataset-403 = aloof
adjectives-dataset-404 = sluggish
adjectives-dataset-405 = wise
adjectives-dataset-406 = noble
adjectives-dataset-407 = stealthy
adjectives-dataset-408 = cowardly
adjectives-dataset-409 = sniveling
adjectives-dataset-410 = wriggling
adjectives-dataset-411 = skittering
adjectives-dataset-412 = wretched
adjectives-dataset-413 = creeping
adjectives-dataset-414 = cunning
adjectives-dataset-415 = reticulated
adjectives-dataset-416 = snapping
adjectives-dataset-417 = giant
adjectives-dataset-418 = suspicious
adjectives-dataset-419 = sussy
adjectives-dataset-420 = blazed
adjectives-dataset-421 = curmudgeonly
adjectives-dataset-422 = pudgy

View file

@ -7,3 +7,4 @@ name-identifier-format-silicon = Si-{$number}
name-identifier-format-xenoborg = Xi-{$number}
name-identifier-format-station-ai = AI-{$number}
name-identifier-format-telepad = TELE-{$number}
name-identifier-format-parens = ({$number})

View file

@ -1,2 +1,5 @@
name-identifier-format-append = {$baseName} {$identifier}
name-identifier-format-prepend = {$identifier} {$baseName}
name-identifier-format-full = {$identifier}
name-identifier-test-1 = TestValue

View file

@ -2,4 +2,4 @@
id: Adjectives
values:
prefix: adjectives-dataset-
count: 396
count: 422

View file

@ -560,6 +560,8 @@
weightlessAcceleration: 1.5
weightlessFriction: 1
baseWeightlessModifier: 1
- type: NameIdentifier
group: AdjectivePrefix
- type: Damageable
damageModifierSet: Moth
- type: Bloodstream
@ -2000,6 +2002,8 @@
- type: MovementSpeedModifier
baseWalkSpeed : 2.5
baseSprintSpeed : 5
- type: NameIdentifier
group: AdjectivePrefix
- type: DamageStateVisuals
states:
Alive:

View file

@ -1,4 +1,4 @@
# Non-fungible apes, anyone?
# Non-fungible apes, anyone?
- type: nameIdentifierGroup
id: Monkey
format: name-identifier-format-monkey
@ -48,6 +48,7 @@
# Used to suffix a number to any mob to identify player controlled mob griefing.
- type: nameIdentifierGroup
format: name-identifier-format-parens
id: GenericNumber
- type: nameIdentifierGroup
@ -60,3 +61,8 @@
format: name-identifier-format-telepad
minValue: 0
maxValue: 999
- type: nameIdentifierGroup
id: AdjectivePrefix
identifierDataset: Adjectives
prefix: true