Nomina
Table of Contents
Nomina is a GZDoom add-on that helps with enemy names (tags).
Features:
- 1.2: if an enemy doesn't have a tag, Nomina assigns to beautify its class name and use it is a tag.
- 1.3: use
na_rename <Class> <NewName>console command to assign a tag to a class manually. 1.4: load
na_data.jsonJSON lump (file) that contains class-tag pairs. See examples in tests below.TODO: add command to export current in-game enemy tag corrections to nadata.json.
Nomina is a part of DoomToolbox.
1. Source code
version 4.14 #include "zscript/nat_Actors.zs" class nat_Test : Clematis {} class nat_EventHandler : StaticEventHandler { override void worldLoaded(WorldEvent event) { mTest = new ("nat_Test"); mTest.Describe("Nomina tests"); mExpectedNames = Dictionary.create(); // Setting expectations goes here.
version 4.14
1.1. Event handler
GameInfo { EventHandlers = "na_EventHandler" }
class na_EventHandler : StaticEventHandler { // Renames the spawned thing, if needed. override void worldThingSpawned(WorldEvent event) { Actor thing = event.thing; if (thing == NULL || !(thing.bIsMonster || thing is "Weapon")) return; { let[found, name] = findNameInConfig(thing.getClassName()); if (found) { thing.setTag(name); return; } } { let[found, name] = findNameInData(thing.getClassName()); if (found) { thing.setTag(name); return; } } bool hasTag = thing.getTag(".") != "."; if (hasTag) return; string name = makeAutoName(thing.getClassName()); thing.setTag(name); }
class nat_Imp : DoomImp {}
mExpectedNames.insert("DoomImp", "Imp"); mExpectedNames.insert("nat_Imp", "Imp");
1.2. Automatic tags
/// Turns '_' to spaces, capitalizes words, trims and removes duplicate spaces, /// splits camelCase. private static string makeAutoName(string className) { className.replace("_", " "); Array<string> words; className.split(words, " ", TOK_SkipEmpty); string result; foreach (word : words) { // It seems that non-latin characters are not allowed in class names. Overkill? let[firstChar, firstLength] = word.getNextCodepoint(0); string split = string.format("%c", string.charUpper(firstChar)); for (uint i = firstLength; i < word.length();) { let[letter, next] = word.getNextCodepoint(i); bool mustSplit = isUpper(letter) && uint(next) < word.length() && isLower(word.getNextCodepoint(next)); split.appendFormat(mustSplit ? " %c" : "%c", letter); i = next; } bool isFirst = result.length() == 0; result.appendFormat(isFirst ? "%s" : " %s", split); } return result; } private static bool isUpper(int letter) { return string.charLower(letter) != letter; } private static bool isLower(int letter) { return string.charUpper(letter) != letter; }
class _nat__nameless1_ : nat_Monster {} class nat_CamelCaseEnemy : nat_Monster {} class nat_BFGZombie : nat_Monster {} class NAT_BFG9000 : nat_Monster {}
mExpectedNames.insert("_nat__nameless1_", "Nat Nameless1"); mExpectedNames.insert("nat_CamelCaseEnemy", "Nat Camel Case Enemy"); mExpectedNames.insert("nat_BFGZombie", "Nat BFG Zombie"); mExpectedNames.insert("NAT_BFG9000", "NAT BFG9000");
1.3. In-game enemy tag correction
1.3.1. na_config Cvar
server nosave string na_config = "";
private static bool, string findNameInConfig(string className) { let config = Dictionary.fromString(na_config); string newTag = config.at(className); return newTag.length() != 0, newTag; }
class nat_NamelessByConfig : nat_Monster {}
// Cannot set a string value with " in the console, have to do it programmatically. Cvar.getCvar("na_config").setString( "{\"nat_NamelessByConfig\":\"TestName\", \"Zombieman\":\"TestZombie\"}"); mExpectedNames.insert("nat_NamelessByConfig", "TestName"); mExpectedNames.insert("Zombieman", "TestZombie");
1.3.2. na_rename console command
// Limited to 10 words. For more words, use the external name data (na_data.json). Alias na_rename "netevent na_rename:%1:%2:%3:%4:%5:%6:%7:%8:%9:%10"
[enu default] NA_USAGE = "Usage"; [ru] NA_USAGE = "Использование";
// Handles `na_rename` command. override void networkProcess(ConsoleEvent event) { Array<string> parts; event.name.split(parts, ":"); if (parts.size() == 0 || parts[0] != "na_rename") return; string className = parts[1]; string newTag = parts[2]; for (int i = 3; i < parts.size(); ++i) if (parts[i].length() != 0) newTag.appendFormat(" %s", parts[i]); if (parts.size() < 3 || className.length() == 0 || newTag.length() == 0) { Console.printf("%s:\nna_rename ClassName NewTag", StringTable.localize("$NA_USAGE")); return; } let config = Dictionary.fromString(na_config); config.insert(className, newTag); Cvar.getCvar("na_config").setString(config.toString()); let i = ThinkerIterator.create(className); for (Actor anActor = Actor(i.next()); anActor != NULL; anActor = Actor(i.next())) anActor.setTag(newTag); }
class nat_NamelessToRename : Actor { Default { Monster; } }
mExpectedNames.insert("nat_NamelessToRename", "Renamed To Several Words");
1.4. Tag databases
private bool, string findNameInData(string className) { string newTag = mData.at(className); return newTag.length() != 0, newTag; } // Initializes the event handler. override void OnEngineInitialize() { mData = Dictionary.create(); string dataLump = "na_data"; for (int i = Wads.findLump(dataLump, 0, Wads.AnyNamespace); i != -1; i = Wads.findLump(dataLump, i + 1, Wads.AnyNamespace)) { let data = Dictionary.fromString(Wads.readLump(i)); for (let i = DictionaryIterator.create(data); i.next();) { mData.insert(i.key(), i.value()); } } } private Dictionary mData; }
class nat_NamelessByData1: nat_Monster {} class nat_NamelessByData2: nat_Monster {} class nat_NamelessByData3: nat_Monster {}
{
"nat_NamelessByData1": "TestData1",
"nat_NamelessByData3": "TestData3"
}
{
"nat_NamelessByData2": "TestData2",
"nat_NamelessByData3": "TestData3-2",
"nat_NamelessByData4": "TestData4"
}
mExpectedNames.insert("nat_NamelessByData1", "TestData1"); mExpectedNames.insert("nat_NamelessByData2", "TestData2"); mExpectedNames.insert("nat_NamelessByData3", "TestData3-2");
2. Tests
GameInfo { EventHandlers = "nat_EventHandler" }
class nat_Monster : Actor { Default { Monster; } }
#include "zscript/nat_Actors.zs" class nat_Test : Clematis {} class nat_EventHandler : StaticEventHandler { override void worldLoaded(WorldEvent event) { mTest = new ("nat_Test"); mTest.Describe("Nomina tests"); mExpectedNames = Dictionary.create(); // Setting expectations goes here.
vector3 spawnPoint = players[consolePlayer].mo.pos + (100, 0, 0); for (let i = DictionaryIterator.create(mExpectedNames); i.next();) Actor.Spawn(i.key(), spawnPoint); mExpectedNames.insert("nat_NamelessToRename", "Renamed To Several Words"); } override void worldThingSpawned(WorldEvent event) { Actor thing = event.thing; if (thing == NULL || !(thing.bIsMonster || thing is "Weapon")) return; string className = thing.getClassName(); if (mExpectedNames.at(className).length() == 0) return; string actual = thing.getTag(); string expected = mExpectedNames.at(className); bool isExpected = actual == expected; mTest.it(className, mTest.assert(isExpected)); if (!isExpected) Console.printf("Actual: %s, expected: %s", actual, expected); } override void OnUnregister() { mTest.EndDescribe(); } private Clematis mTest; private Dictionary mExpectedNames; }
3. Run tests
(setq wait-delay 0) ""
(setq wait-delay (+ new-delay wait-delay)) (format "wait %d" wait-delay)
wait 2; map map01 wait 4; na_rename nat_NamelessToRename Renamed To Several Words wait 6; summon nat_NamelessToRename wait 15; quit