Author: beffy (c++programmer)

Here is a quick introduction to the Torque Conversation Editor, TGEConEd.

UPDATE 12/01/2002:
I forgot to mention the loading function for the conversation files. Make sure you follow this step!

The Conversation Editor is an in-game tool for creating and editing simple NPC dialogs.

This is how the conversations basically work... you've got one sequence per conversation file, and they are linked by either the TConNext fields for default sequences OR TConTarget[] entries for multiple choice sequences...


 
To end a dialog, put the string "end" there as value, like this:


 
In-game use

To talk to a NPC, add your AIPlayer to the game, switch to "object selection" mode, click on the bot, and the GUI will pop up (if the bot's name matches your "TConParticipant1" entry in your dialog). You can either use the mouse and click on the buttons, or you can hit "enter" to continue in the current sequence, hit "esc" to close the dialog gui, use your up- and down-arrows to scroll through long dialogs and use "page-up" and "page-down" to scroll to the beginning or end of the current dialog sequence, respectively. If an NPC doesn't have to say anything to you (anymore), he will tell you to go away ;)


 
Ok, now here is the step-by-step howto (the same as in the readme file provided with the download)...

Okay, first of all, YOU NEED THE HEAD VERSION WITH THE NEW AIPLAYER CLASS and you have to implement "object selection" into the engine. To do this, follow this tutorial: http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=2173

After that, you have to get bots/NPCs into Torque, of course if you don't have them already - you can see how to do it here: http://tork.zenkel.com/tutorials/bots/adding_bots.php

Here is how I am adding bots for test purposes: (the bot is added, its conversations are set to the default value ("BOTNAME_seq01") and he moves immediately to the current position of the player) I've added a function to initialize the conversations (which you need in any case!!) for each NPC which looks like this: Of course, the first dialog/sequence file for each NPC must have the label "<NPC_NAME>_seq01"!

// you need this function to initialize the sequences
function initializeSequences(%client, %npcName)
{
	$NPCS = %client;
	$NPCS.nextSequence[%npcName] = %npcName @ "_seq01";
	echo("CONVERSATIONS INIT:" SPC %npcName SPC $NPCS.nextSequence[%npcName]);
}

// you can add your bots however you want, but remember to call
// initializeSequences(%client, %npcName); with the current client and the NPC's name
// however, here is how I am adding bots:
$botCounter = 0;
function serverCmdAddBot(%client) {
         MissionCleanup.add($bots);
	 %npcName = "Bot" @ $botCounter;
         $bots[$botCounter] = AIPlayer::spawnPlayer(%npcName);
         MissionCleanup.add($bots[$botCounter]);
	 
	 // init the conversations for this NPC
	 // NOTE: the first conversation must have the label: BOTNAME_seq01
	 initializeSequences(%client, %npcName);
	 
         $botCounter++;
         serverCmdMoveBotsToPlayer(%client);
}
function serverCmdMoveBotsToPlayer(%client)
{
   if (isObject(%client.player))
   {
         $daPlaya = %client.player;
         for(%i = 0; %i < $botCounter; %i++)
         {
		if(isObject($bots[%i])){
			$bots[%i].setMoveDestination( %client.player.getPosition() );
			echo("Moving to current position of the player!");
		}
         }
   }
}

// add this to default.bind.cs
function addBot(%val) 
{
	if(%val)
	{
		commandToServer('AddBot');
	}
}
MoveMap.bind(keyboard, "ctrl b", addBot);

// and this also to client/config.cs
MoveMap.bind(keyboard, "ctrl b", addBot);

To toggle the editor, add this to "client/config.cs" and also to "client/scripts/default.bind.cs":

moveMap.bind(keyboard, "ctrl c", toggleConversationEditor);

IMPORTANT:

One again, the first sequence for EACH NPC who should be "talking" has to be labeled "BOTNAME_seq01", e.g. "Bot0_seq01", "Bot1_seq01", "BillyBoy_seq01", etc.!!! All the other sequences can be labeled however you want, e.g. "Ork01_askForFood" or whatever... ;)


To load all the dialogs, add this function to "client/init.cs":
function loadAllDialogues()
{
   %path = $TGEConEd::SaveFilePath @ "*.cs";
   for(%file = findFirstFile(%path); %file !$= ""; %file = findNextFile(%path))
   {
      if (isFile(%file))
      {
         echo("---> Found TGEConEdFile:" SPC %file);
         exec(%file);
      }
   }
}
and add these two lines in the function initClient() ( I've added it right after loadMainMenu(); ):
   $TGEConEd::SaveFilePath = "common/editor/dialogues/";
   loadAllDialogues();

Then, create the following folder(s) for TGEConEd:
- common/editor/dialogues
- engine/game/ai/conversation
and put tgeConEd.h and tgeConEd.cc into the latter.
Add the folder engine/game/ai/conversation and these two source files to your VStudio project, also!!

Sound:

TGEConEdGui.cs searches for a folder named "conversations" in your game directory and finds all wav files in there. If you want to use the provided example dialogs or audio files in general, create this folder:
- fps/data/conversations/audio
I've included a folder data/conversations/audio/mission1 with 3 wav test files in the zip file you've downloaded.

After that, add these files to your engine:
- common/editor/TGEConEdBrowser.gui
- common/editor/TGEConEdGui.cs
- common/editor/TGEConEdGui.gui
- common/editor/TGEConEdStartup.cs
- common/editor/coned_logo1.png
- common/help/12. Conversation Editor.hfl
(to access that help file, simply open the standard help window in-game with F1)
- fps/client/ui/MainConversationHud.gui
- fps/client/ui/MultipleChoiceConversationHud.gui
I've also included some stupid test dialogs in the "dialogues" folder in the zip file... put them into "common/editor/dialogues" if you want to use them as examples...

Then, add these lines to client/init.cs

   exec("./ui/MainConversationHud.gui");
   exec("./ui/MultipleChoiceConversationHud.gui");

and also add

   exec("./editor/TGEConEdStartup.cs");
in commom/main.cs, (at the end of the function initBaseClient()).

Now, in server/scripts/commands.cs, replace the object selection function you've added before with this:
$currentDialogNum = 1;
$currentDialogId = 0;
$continueDialogs = true;

function serverCmdSelectObject(%client, %mouseVec, %cameraPoint)
{
   //Determine how far should the picking ray extend into the world?
   %selectRange = 200;
   // scale mouseVec to the range the player is able to select with mouse
   %mouseScaled = VectorScale(%mouseVec, %selectRange);
   // cameraPoint = the world position of the camera
   // rangeEnd = camera point + length of selectable range
   %rangeEnd = VectorAdd(%cameraPoint, %mouseScaled);

   // Search for anything that is selectable below are some examples
   %searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::CorpseObjectType |
      				$TypeMasks::ItemObjectType | $TypeMasks::TriggerObjectType;

   // Search for objects within the range that fit the masks above
   // If we are in first person mode, we make sure player is not selectable by setting fourth parameter (exempt
   // from collisions) when calling ContainerRayCast
   %player = %client.player;
   if ($firstPerson)
   {
	  %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks, %player);
   }
   else //3rd person - player is selectable in this case
   {
	  %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks);
   }

   // a target in range was found so select it
   if (%scanTarg)
   {
      %targetObject = firstWord(%scanTarg);
      %client.setSelectedObj(%targetObject);
      // if it is a NPC, check if he has something to say...
      if(%targetObject.getClassName() $= "AIPlayer")
      {
         %npcName = %targetObject.getShapeName();
         $currNPC = %npcName;
         // now search conversation sequences in the RootGroup
	 %dataGroup = "RootGroup";
         for(%i = 0; %i < %dataGroup.getCount(); %i++)
         {
            %obj = %dataGroup.getObject(%i);
            if(%obj.getClassName() !$= "TGEConSequence")
            {
               continue;
            }
            if(isObject(%obj))
            {
	       if(%npcName $= %obj.TConParticipant1 && %obj.TConLabel $= $NPCS.nextSequence[%npcName] )
               {
		  $currentPlayingDialog = %obj;
		  $NPCS.nextSequence[$currNPC] = $currentPlayingDialog.TConNext;
		  error("NEXT SEQ:" SPC $NPCS.nextSequence[$currNPC]);
		  $continueDialogs = true;
		  if(%obj.getTConType() $= "DefaultSequence")
		  {
			  $tgeTextCount = 1;
			  Canvas.pushDialog( MainConversationHud );
			  TGETextField.setText("");
			  TGETextField.addText(%obj.TConText[0] @ "\n", true);
			  %currentSoundFile = %obj.TConAudioFile[0];
			  playConversationSound(%currentSoundFile);
			  break;
		  }
		  else if(%obj.getTConType() $= "MultipleChoiceSequence")
		  {
			  Canvas.pushDialog( MultipleChoiceConversationHud );
			  MultipleChoiceTGETextField.setText("");
			  MultipleChoiceTGETextField.addText(%obj.TConText[0] @ "\n", true);
			  %currentSoundFile = %obj.TConAudioFile[0];
			  playConversationSound(%currentSoundFile);
			  %answers = %obj.TConCounter;
			  for(%a=0; %a < %answers; %a++)
			  {
				  $targetArray[%a] = %obj.TConTarget[%a];
				  MultipleChoiceAnswerList.addRow(%a, %obj.TConAnswer[%a]);
				  echo("TARGET:" SPC %a SPC $targetArray[%a]);
			  }
			  break;
		  }
               }
	       // if it is a bot who doesnt have any dialogs or a bot who is finished talking to you...
	       else if(%obj.TConLabel !$= $NPCS.nextSequence[%npcName] || %npcName !$= %obj.TConParticipant1)
	       {
		  $continueDialogs = false;
		  Canvas.pushDialog( MainConversationHud );
                  TGETextField.setText("Leave me alone!\n");
	       }
            }
         }
      }
   }
}
Next, add these text profiles to client/ui/defaultGameProfiles.cs:
new GuiControlProfile ("ConversationHudMessageProfile")
{
   fontType = "Arial";
   fontSize = 15;
   fontColor = "219 200 128";
   autoSizeWidth = true;
   autoSizeHeight = true;
};

new GuiControlProfile ("ConversationScrollProfile")
{
   opaque = false;
   border = 0;
   borderThickness = 0;
   borderColor = "0 0 0";
   bitmap = "common/ui/torqueScroll";
   hasBitmapArray = true;
};
new GuiControlProfile ("ConversationMCAnswerProfile")
{
   opaque = false;
   fillColor = "128 128 128";
   fontColor = "229 220 188";
   border = true;
   fontSize = 15;
   borderColor = "0 0 0";
};
Then, open your VStudio project, go to gui/guiScrollCtrl.cc and add this:
// beffy: added for TGEConEd
ConsoleMethod(GuiScrollCtrl, scrollDelta, void, 4, 4, "(S32 deltaX, S32 deltaY) - scrolls the scroll control about the delta values.")
{
   argc; argv;
	GuiScrollCtrl* control = static_cast( object );
	control->scrollDelta(dAtoi(argv[2]), dAtoi(argv[3]));
}
I've added it right before
void GuiScrollCtrl::initPersistFields()
{
...
Finally, do a CLEAN/BUILD or REBUILD ALL ... (shouldn't be necessary, but you never know... ;)
Once in game, you can trigger the object selection with ALT-M by default and toggle the TGEConEd with ALT-C.

Further suggestions/thoughts:
- to make the sequence handling persistant, a very simple way would be to save this var to some file: $NPCS.nextSequence[$currNPC] it holds the next sequence for each bot...

I've added an (yet unused) example to TGEConEdGui.cs:
//--------------------------------------------------------------------
// example how to save the label of the next sequence to play for each NPC
// not used/tested yet!
//--------------------------------------------------------------------
function saveNextSequenceForNPCs()
{
   %count = MissionCleanup.getCount();
   for(%j = 0; %j < %count; %j++)
   {
      %client = MissionCleanup.getObject(%j);
      %bot = %client.getClassName();
      if(%bot $= "AIPlayer")
      {
	      %botName = %client.getShapeName();
	      $Game::Dialogs::NPCS.nextSequence[%botName] = $NPCS.nextSequence[%botName];
      }
   }
}
Of course, you'd have to read in these vars when the NPC enters the game the next time! (e.g. in function initializeSequences(%client, %npcName))

If there are any problems or questions, contact me at beffy@gmx.de or on IRC, of course!

If you use this little tool, it would be nice to mention me in your credits! :))

Have fun!!! :)