Author Topic: Null the Gull, or How Does This Work?  (Read 2711 times)

AbbeyRoad

  • Underling
  • *
  • Posts: 6
  • woof
Null the Gull, or How Does This Work?
« on: January 26, 2016, 04:07:46 AM »
I'm curious as to how you guys got Null the Gull to work. As far as I can tell, he's the only NPC other than the tailors that does stuff, and that's pretty cool, and I'm interested from a technical standpoint on how he works.

Codewalker

  • Hero of the City
  • Titan Network Admin
  • Elite Boss
  • *****
  • Posts: 2,740
  • Moar Dots!
Re: Null the Gull, or How Does This Work?
« Reply #1 on: January 27, 2016, 03:58:04 PM »
TL;DR version: Hardcoded hacks

Long version: The map files are stored in geobin/maps/* (inside various pigg files, but those are unimportant) in a custom binary format. The binary format is the same basic format as just about all of the *.bin files the game uses. Each bin has a different format, but similar structure. Technically they are serializations of the game's internal structures, one of two forms that can be loaded and saved by what Cryptic called the textparser. As you might guess from the name, the other format is plain text files and that's apparently what all development was done using. Around 5 years ago I figured out how to extract the tables from the client exe that are used to load and parse these structures in both text and binary form, and created what I call the "Rosetta Stone" of COH data formats.

The maps themselves are a hierarchical structure of groups. After Paragon Chat loads a map, it traverses the hierarchy and looks for groups that contain a property called "PersistentNPC". This marks a location for spawning a fixed NPC that never moves -- i.e. contacts and non-combat background NPCs that stand still. It reads the ID from the map and looks it up in another bin file, npcs_client.bin. This file contains information about the NPC, such as its human-readable name and which model to use. That file doesn't contain all of the information about the NPC -- some of it was only stored serverside -- but Paragon Chat attempts to guess at the properties of the NPC that it doesn't know. That missing information is why several NPCs that had animation loops on the live servers instead just stand in the Ready pose.

Inside of this loop, Paragon Chat keeps an eye out for several special NPCs that it has hardcoded behavior for, and sets a flag on the entity that is created. Null is one of those:

Code: [Select]
if (!strcmp(clientnpc->id, "Null_Gull"))
newentity->specialEntity = SPECIALENT_NULLGULL;

When you click on an NPC, the game client sends a client input packet with a message id of 5, which according to the debug messages left in the client means CLIENTINP_TOUCH_NPC. It also sends the entity ID of the touched NPC, encoded using a variable-length bit packed scheme that ranges from 12-68 bits.

The "server", or in this case Paragon Chat finds the entity based on the ID and sees that it has specialEntity set. So it calls the specialEntityTouched() function defined in hacks/specialent.c, which is just a big switch statement that ends up calling nullClick(player, npc);

nullClick just sends back a command to open a contact dialog:

Code: [Select]
q = getOutboundQueue(player);
if (!q->started) {
    bsAppendPacked(q->bs, 1, 15); // SERVER_GAME_CMDS
    q->started = true;
}
bsAppendPacked(q->bs, 1, 53); // SERVER_CONTACT_DIALOG_OPEN
bsAppendPacked(q->bs, 1, 0); // something to do with ouroboros crystals?
bsAppendString(q->bs, "<img align=left src=npc:7365>" // index of seagull model
"<font scale=1.2 face=body color=red>Hey!</font><br><br>"
"Okay, that's better.<br><br>"
[ ... ] );
bsAppendPacked(q->bs, 1, 2); // number of responses
// RESPONSE 1
bsAppendPacked(q->bs, 1, 20); // some arbitrary value
bsAppendInt(q->bs, 0); // unknown
bsAppendString(q->bs, "What kind of change?");
bsAppendString q->bs, ""); // shows up on the right
// RESPONSE 2
bsAppendPacked(q->bs, 1, 3); // close when clicked
bsAppendInt(q->bs, 0);
bsAppendString(q->bs, "No, thanks.");
bsAppendString q->bs, "");

Rinse and repeat for CLIENTINP_CONTACT_DIALOG_RESPONSE, which is sent when the player clicks on one of the options. It calls specialEntityDialog, which calls nullDialog, which sends a second box (omitted for brevity) if the selected item ID is 20, populating it with options 50-56. If one of those is chosen, it uses these tables to determine what to do based off the ID-50:

Code: [Select]
static const char *anames[] = { "a Hero", "a Villain", "a Rogue", "a Vigilante", "a Loyalist", "with the Resistance", "Neutral" };
static sqlAlignment aalign[] = { A_Hero, A_Villain, A_Rogue, A_Vigilante, A_Loyalist, A_Resistance, A_Neutral };

It builds some text using the name for the chosen option, sends it as another contact dialog, updates the player's alignment in the database, and translates that to new player Type, Subtype, and PraetorianProgress values to send to the client in an entity update message.

For a real game you'd want to have all of this generalized into data files or loadable scripts or something instead of being hardcoded, but for a concept test and tech demo to wow people it works well enough. :)

AbbeyRoad

  • Underling
  • *
  • Posts: 6
  • woof
Re: Null the Gull, or How Does This Work?
« Reply #2 on: January 28, 2016, 04:36:19 AM »
This is really cool stuff, honestly. I like reading the technical details on how things work, it helps me get a better understanding of the game world. Thanks!