Making conversation

Completely rewrote the conversation system – actually that’s not as big a deal as it might sound. Conversations are basically a scoping mechanic for actors.

By default, when an actor speaks in a scene, that speech is conveyed to all other actors in the same location. It turned out early on that that wasn’t nearly good enough.

There are times you want to have more complex situations in a scene. Actors could whisper to each-other or have other private forms of conversation. Or an actor might have ways to communicate with another actor in a remote location (ever hear of the telephone?).

Initially, I extended the “actors in the same location hear what is said” model with some simple scoping extensions. I could add other locations as additional scopes for speech-targeting.

Not good enough.

The code for that is still there, but fairly soon after it was written, I prototyped the conversation system. Specify a list of actors who are having the conversation, and when an actor in that conversation speaks, only the other members of the conversation are speech-targets. Location independent.

Problem was, the design was kind of sloppy. I was initially just focused on something that would work well enough to let me keep on writing and testing the story.

This led to a system where I had a master list of conversations (expressed as strings which contained the current scene ID and the identifiers of the conversation participants), and each conversation participant was tagged with that string so that the system would know they were in a conversation, and could look up the master list to find other participants.

It worked.

It wasn’t superlative, but it worked well enough.

And then along came recursive, nestable scenes, and I was writing some scenes with fairly complex scoping for multiple simultaneous conversations. The former sat on my bug-board for a while.

Was it a feature or a bug that you couldn’t risk adding an actor in a nested scene to a conversation (if they were already in one, that conversation would be terminated)? By “feature” in this case, I mean “Watch out for that, this isn’t a bug exactly, but it is something you should avoid doing.”

I eventually decided it was a misfeature, and decided to rewrite the system from scratch. Handling actors speaking is a surprisingly heavy-weight area of the code (though considerably less so, since I updated the way actors learn things).

Every time an actor speaks, there’s a certain amount of work to do. We have to assemble a list of all the actors that are valid speech-targets. Approximate age-group and gender of the speaker are learned by those listeners, and the speech is tagged with the identity of the speaker as each listener knows or thinks it to be.

Each listener might have a different notion of identity for a speaker. To one, it might just be “Man” – the speaker is identified as an adult male, neither particularly young nor elderly.

A second listener might instead identify the speaker as “Mister Sendak” – they know his family name, and assume he holds no special rank or title.

A third might identify him as “Heer Sendak” if they know that he comes from the nation of Kolturn, and thus Heer and Fraa are used instead of Mister or Miss.

And I haven’t even tossed his given name into the mix, let alone actual military ranks or civilian titles.

So, there’s just a bit going on in the code where I wanted to make the rewrite.

The core part of the code (central tracking of conversations) was fairly easy. A proper class to track the specifics of conversation participants and scene scope, and a container to stuff them in.

That led to tracking on the actor side of things, where the most effective model seemed to be another container of indexes into the master conversation container. Terminated conversations are marked invalid, and anytime there are no valid ones at all, we clear() the whole lot.

This allows an actor to actually be in multiple conversations at once, but only the most recent one is actually used for calculating speech targets. This has the happy side-effect of allowing us to nest conversations, and just as easily unroll the conversation stack. Recursive scenes are no longer a conversation-menace.

There was an unanticipated issue with clearing the conversations away. They just plain didn’t want to go.

Here’s what didn’t work:

 for (auto it : conversations)
 {
  if (it.scene_id == scene_id && it.participants == p && it.valid)
   {
    it.invalidate(); <--- THIS!
    ...

And here’s what did:

 for (size_t it = 0; it < conversations.size();it++)
 {
 if (conversations[it].scene_id == scene_id&&conversations[it].participants == p&&conversations[it].valid)
 {
 conversations[it].invalidate(); <--- Now working!
 ...

Everything looked right in the first version, the if expression evaluated as it should, we called invalidate, which set it.valid=false.

Except that it.valid stayed true.

Stopped using the iterator and switched to using an index. No problem. Works like a treat.

I’m probably missing something quite obvious, but it’s been a long week.

In any case, after some annoying debugging trying to figure out why the first version didn’t work, I got all the code overhauled.

And then remembered I still had to Serialise/Deserialise it all. A little more annoying dog-work, because the new form isn’t sufficiently compatible with the old one to preserve save-files. So, the save-file format has been bumped up from version 12 to version 13.

Probably version 14 will be a blob-file.

Ah, blob-files. I’ll talk more about those another day.