////////////////////////////////////////////////////////////////////// // JMSAgentObj.cpp - a NeatTools module that controls // a Microsoft Agent character. // You can control its location and // the speech it synthesizes. // Voice-recognition has not been implemented yet. // ver. 99 July 28 // // REQUIRED COMPONENTS: (also available on the MSDN CDs) // // 1. http://activex.microsoft.com/activex/controls/agent2/MSagent.exe // 2. http://activex.microsoft.com/activex/controls/agent2/tv_enua.exe // 3. http://msdn.microsoft.com/workshop/imedia/agent/agentdl.asp#character // // more information is available at // http://msdn.microsoft.com/msagent/default.asp // http://msdn.microsoft.com/workshop/imedia/agent/documentation.asp // ** available languages: Arabic, French, German, Hebrew, // Italian, Japanese, Korean, Spanish, and Chinese // // // Language: Microsoft Visual C++, ver 6.0 // Authors: Tav Hawkins (taviare@physics.syr.edu) // Rob Salgado (salgado@physics.syr.edu) // // Credits: Special thanks to Yuh-Jye Chang for help // with threads. // // This module was partially generated by // Rob Salgado's ModuleMaker utility. // ////////////////////////////////////////////////////////////////////// #include "JMSAgentObj.h" #include "JLinkObj.h" /// #include "JInteger.h" #include "JIntegerData.h" #include "JIntegerProperty.h" #include "JIntegerListProperty.h" /// #include "JString.h" #include "JStringData.h" #include "JStringProperty.h" // #include "JFileProperty.h" // // #include #include #include ///////////////////////////////////////////////////////////////////////// // // THREAD METHODS // ////for threads void JMSAgentObj::startup() { //start the thread thread = JThread(this); thread.start(); } ////for threads void JMSAgentObj::run() { //the thread has been started already int k; //dummy variable //initialize stuff needed if ( InitializeMSAgentServer()==1 ) { if ( InitializeMSAgentChar()==1 ) { //initialization successful repaint(); valid = true; while(valid) //while the thread is running { while (!(textUpdated==1 || positionUpdated==1) && valid) { JThread::sleep(50); //wait 50ms for the state to change //(shorter sleep means faster response from system //at the cost of more CPU usage) AgentCharGetPosition(); //refresh module-variables in response //to external movement of the character } if (!valid) {break;} if (positionUpdated==1) { k=AgentCharSetPosition(); broadcast(TRIGGER_OUTPUT);} if (textUpdated==1) { k=AgentCharSpeak(); broadcast(TRIGGER_OUTPUT);} } } else { if (MSAgentCharLoaded!=1) {moduleColor=JColor::magenta;} } } else { if (MSAgentServerLoaded!=1) {moduleColor=JColor::red;} } //un-initialize stuff needed //the thread will end now if (MSAgentCharLoaded==1) { ShutdownMSAgentChar(); } if (MSAgentServerLoaded==1) { ShutdownMSAgentServer(); } repaint(); }// ////for threads, called by copy-constructor... I believe run() is then called void JMSAgentObj::reset() { valid = false; } ////for threads, begin to shut down the thread void JMSAgentObj::close() { if (!valid) { return;} valid = false; thread.waitFor(); //wait for thread to end } ///////////////////////////////////////////////////////////////////////// // // METHODS THAT INTERFACE TO THE MICROSOFT AGENT // int JMSAgentObj::InitializeMSAgentServer(void) { //return -1 upon failure //return +1 upon success //sets MSAgentServerLoaded // Initialize COM if (FAILED(CoInitialize(NULL))) { //MessageBox(NULL, _T("There was an error initializing COM."), // kpszAppTitle, MB_OK | MB_ICONERROR); MSAgentServerLoaded=-1; //failure return MSAgentServerLoaded; } // Create an instance of the Agent 2.0 server. hRes = CoCreateInstance(CLSID_AgentServer, NULL, CLSCTX_SERVER, IID_IAgentEx, (LPVOID *)&pAgentEx); if (FAILED(hRes)) { //wsprintf(szError, _T("There was an error initializing Microsoft Agent, code = 0x%x"), hRes); //MessageBox(NULL, szError, // kpszAppTitle, MB_OK | MB_ICONERROR | MB_TOPMOST); CoUninitialize(); MSAgentServerLoaded=-1;//failure } else { MSAgentServerLoaded=1;//success } return MSAgentServerLoaded; } int JMSAgentObj::InitializeMSAgentChar(void) { //return -1 upon failure //return +1 upon success //sets MSAgentCharLoaded if (! (&vPathDefault) ) { VariantInit(&vPathDefault); (vPathDefault).vt = VT_EMPTY; } __try { //Attempt to load the character named by the filename // This implements a hack to convert the JString to a BSTR. bszSpeak = SysAllocStringByteLen((char *) filename, 2*filename.length() ); for (int ii=0; iiLoad(vPath, &lCharID, &lRequestID); if (FAILED(hRes)) { // Try to load the default character VariantCopy(&vPath,&vPathDefault); hRes = (pAgentEx)->Load(vPath, &lCharID, &lRequestID); if (FAILED(hRes)) __leave; } // Get the IAgentCharEx interface hRes = (pAgentEx)->GetCharacterEx(lCharID, &pCharacterEx); if (FAILED(hRes)) __leave; // Set the language of the character hRes = (pCharacterEx)->SetLanguageID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)); if (FAILED(hRes)) __leave; // Set the initial position of the character hRes = (pCharacterEx)->SetPosition((long)posX, (long)posY); if (FAILED(hRes)) __leave; // Start an animated entrance. //(A quiet, non-animated entrance may not display the character at this time.) if (startState) { hRes = (pCharacterEx)->Show(FALSE, &lRequestID); } else { hRes = (pCharacterEx)->Show(TRUE, &lRequestID); } if (FAILED(hRes)) __leave; // Set the idle processing of the character hRes = (pCharacterEx)->SetIdleOn(FALSE); if (FAILED(hRes)) __leave; MSAgentCharLoaded=1; //success } __finally { if (FAILED(hRes)) { //wsprintf(szError, _T("(INIT CHAR) An error occurred in Microsoft Agent, code = 0x%x"), hRes); //MessageBox(NULL, szError, // kpszAppTitle, MB_OK | MB_ICONERROR | MB_TOPMOST); close(); MSAgentCharLoaded=-1; //failure } } return MSAgentCharLoaded; } //////////////////////////////////////////////////////////// // // position the character according to the module's posX and posY // int JMSAgentObj::AgentCharSetPosition(void) { posX=min(max(posX,MINX),MAXX); posY=min(max(posY,MINY),MAXY); positionUpdated=0; __try { hRes = (pCharacterEx)->SetPosition((long)posX, (long)posY); if (FAILED(hRes)) __leave; hRes = (pCharacterEx)->Show(TRUE, &lRequestID); if (FAILED(hRes)) __leave; } __finally { if (FAILED(hRes)) { //wsprintf(szError, _T("(POSITION) An error occurred in Microsoft Agent, code = 0x%x"), hRes); //MessageBox(NULL, szError, // kpszAppTitle, MB_OK | MB_ICONERROR | MB_TOPMOST); close(); return -1;//failure } } return 1; } //////////////////////////////////////////////////////////// // // update the posX and posY (in case the character is moved manually) // int JMSAgentObj::AgentCharGetPosition(void) { long newX, newY; hRes = (pCharacterEx)->GetPosition(&newX, &newY); if (FAILED(hRes)) { //wsprintf(szError, _T("(GET POSITION) An error occurred in Microsoft Agent, code = 0x%x"), hRes); //MessageBox(NULL, szError, // kpszAppTitle, MB_OK | MB_ICONERROR | MB_TOPMOST); close(); return -1;//failure } if (posX != newX || posY != newY) { posX=newX; posY=newY; broadcast(X_OUTPUT);broadcast(Y_OUTPUT); return 1; } else { return 0; } } //////////////////////////////////////////////////////////// // // make the character speak (speech-synthesized text) // int JMSAgentObj::AgentCharSpeak(void) { textUpdated=0; __try { hRes = (pCharacterEx)->Show(TRUE, &lRequestID); if (FAILED(hRes)) __leave; // This implements a hack to convert the JString to a BSTR. bszSpeak = SysAllocStringByteLen((char *) speechText, 2*speechText.length() ); for (int ii=0; iiSpeak(bszSpeak, NULL, &lRequestID); SysFreeString(bszSpeak); //clean up of string if (FAILED(hRes)) __leave; } __finally { if (FAILED(hRes)) { //wsprintf(szError, _T("(SPEAK) An error occurred in Microsoft Agent, code = 0x%x"), hRes); //MessageBox(NULL, szError, // kpszAppTitle, MB_OK | MB_ICONERROR | MB_TOPMOST); close(); return -1;//failure } } return 1; } //////////////////////////////////////////////////////////// void JMSAgentObj::ShutdownMSAgentChar(void) { MSAgentCharLoaded=0; if (pCharacterEx) { (pCharacterEx)->Release(); // Release the character interface (pAgentEx)->Unload(lCharID); // Unload the character. } } //////////////////////////////////////////////////////////// void JMSAgentObj::ShutdownMSAgentServer(void) { MSAgentServerLoaded=0; if (pAgentEx) { (pAgentEx)->Release(); // Release the Agent } VariantClear(&vPathDefault); VariantClear(&vPath); CoUninitialize(); } // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // // Register() registers the module with NeatTools // char* theJMSAgentObj = JMSAgentObj().Register(); // ------------------------------------------------------------------------ // // // className() returns the name of the module // const char* JMSAgentObj::className() const { return "JMSAgentObj";} // ------------------------------------------------------------------------ // // // clone() is used to instantiate copies of the module // JObject* JMSAgentObj::clone() const { return new JMSAgentObj(*this);} // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // // PERSISTENCY METHODS to record and recover "the STATE of this module" // // ------------------------------------------------------------------------ // // void JMSAgentObj::writeContent(JOutputStream& os) { //call base-class writeContent() first JAddObj::writeContent(os); //write a table of "persistent variables", //which comprise the "state" of this instance of the module // format: // put_x_(os, INTERNAL_VARIABLE_TAG, PERSISTENT_VARIABLE) putInteger(os, "posX", posX); putInteger(os, "posY", posY); putInteger(os, "startState", startState); //use JObject for JStrings putObject(os, "preText", preText); putObject(os, "nickname", nickname); putObject(os, "filename", filename); } // ------------------------------------------------------------------------ // // void JMSAgentObj::readContent(JDictionary& dict) { //apparently called soon after constructor //used to rebuild the "persistent state" //call base-class readContent() first JAddObj::readContent(dict); //read a table of "persistent variables", //which comprise the "state" of this instance of the module posX= getInteger(dict, "posX"); posY= getInteger(dict, "posY"); startState= getInteger(dict, "startState"); //use JObject for JStrings JObject *obj; obj = getObject(dict, "preText"); if (obj) { preText = *(JString*)obj;} ///if (obj) test avoids NULL pointer problem obj = getObject(dict, "nickname"); if (obj) { nickname = *(JString*)obj;} obj = getObject(dict, "filename"); if (obj) { filename = *(JString*)obj;} } // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // // Constructor method: called only ONCE! // when the external module is loaded into NeatTools // // JMSAgentObj::JMSAgentObj() { //making a clever use of enums defined the header file... igm = NUMBER_OF_INPUTS; // tell NeatTools the number of inputs (declared in JModuleObj) ogm = NUMBER_OF_OUTPUTS; // tell NeatTools the number of outputs (declared in JModuleObj) doRepaint = false; moduleColor=JColor::blue.brighter(); //see JColor.h for definitions posX=0; //initial value of posX posY=0; //initial value of posY theText=JString(" "); //initial value of theText preText=JString(""); //initial value of preText MSAgentServerLoaded=0; MSAgentCharLoaded=0; filename = JString("C:\\Windows\\MSAgent\\Chars\\genie.acs"); //default prefix=filename; nickname="name?"; startState=0; //start hidden } // ------------------------------------------------------------------------ // // // Copy Constructor method: called when instantiating the module for the toolbar // or for the desktop // in general, there's no need to supply your own // (since the compiler generates one for you) // I did it here to give me more control as I learned // more about writing modules and using threads. // // In particular, I want to make sure that the AgentServer // is initialized. // JMSAgentObj::JMSAgentObj(const JMSAgentObj& src):JAddObj(*(JAddObj*)& src) { /// use your own copy constructor /// and NOT the compiler-generated one /// to do memberwise-copy of arrays and to do special initializations posX=src.posX; //initial value of posX posY=src.posY; //initial value of posY startState=src.startState; //initial value of startState theText=src.theText; //initial value of theText preText=src.preText; //initial value of theText prefix=src.prefix; //initial value //type=src.type; //initial value filename=src.filename; //initial value prefix=filename; nickname=src.nickname; MSAgentServerLoaded=0; //! MSAgentCharLoaded=0; //! } // ------------------------------------------------------------------------ // // Destructor method // in general, there's no need to supply your own // (since the compiler generates one for you) // here, I use it to make sure the thread is killed. // JMSAgentObj::~JMSAgentObj() { close(); //needed to kill the thread } // ------------------------------------------------------------------------ // // // engine() is called when an input-event is detected // void JMSAgentObj::engine(int n, JLinkObj& link) { int nv; JString nstr; switch(n) { case X_INPUT: //for JIntegers link.access(JIntegerData(nv)); //ask module-of-input-link for JInteger posX=nv; posX=min(max(posX,MINX),MAXX); positionUpdated=1; broadcast(X_OUTPUT); //tell clients we have moved broadcast(Y_OUTPUT); //tell clients we have moved //broadcast(NICKNAME_OUTPUT); break; case Y_INPUT: //for JIntegers link.access(JIntegerData(nv)); //ask module-of-input-link for JInteger posY=nv; posY=min(max(posY,MINY),MAXY); positionUpdated=1; broadcast(X_OUTPUT); //tell clients we have moved broadcast(Y_OUTPUT); //tell clients we have moved //broadcast(NICKNAME_OUTPUT); break; case STRING_INPUT: //for JStrings link.access(JStringData(nstr)); //ask module-of-input-link for JString theText=nstr; // intentionally left //if (textUpdated==0 && MSAgentServerLoaded==1 && MSAgentCharLoaded==1 && theText.length()>0) if (textUpdated==0 && theText.length()>0) { textUpdated=1; speechText=preText+theText; } broadcast(STRING_OUTPUT); broadcast(NICKNAME_OUTPUT); break; case TRIGGER_INPUT: //for later use default: broadcast(TRIGGER_OUTPUT); if (theText.length()>0) { textUpdated=1; speechText=preText+theText; } broadcast(NICKNAME_OUTPUT); break; } } // ------------------------------------------------------------------------ // // // access() may be called by an output-link engine() method // when we do a broadcast() // void JMSAgentObj::access(int n, JLinkObj& link, const JDataType& data) { switch(n) { case TRIGGER_OUTPUT: ////How to return a JInteger INT(data)=TRUE; break; case NICKNAME_OUTPUT: ////How to return a JString JSTRING(data)=nickname; break; case X_OUTPUT: ////How to return a JInteger INT(data)=posX; break; case Y_OUTPUT: ////How to return a JInteger INT(data)=posY; break; case STRING_OUTPUT: default: ////How to return a JString JSTRING(data)=preText+theText; break; ////How to return a JBlock //// ////data.assign( JBlock(*buffer)(0,numbytes) ); //// ////in general, ////give value (suitably casted) in response to my output ping } } // ------------------------------------------------------------------------ // // // paint() method is called by the system // void JMSAgentObj::paint(JGraphics g, double dx, double dy, JRegion& rgn, double scale) { //determine the size or extent of the module JRect rect = getIExtent(dx, dy, scale); //the next "write" to screenbuffer g is the appropriate color g.setJColor(moduleColor); //draw a box with a 3D-look that fills the module extent g.fill3DRect(rect.x, rect.y, rect.width, rect.height, 3); int ddh = rect.height/3; int ddy = rect.height/12; JRect rect1(rect.x, rect.y, rect.width, ddh); JRect rect2(rect.x, rect.y+ddh, rect.width, ddh*2); if (!valid) { g.setJColor(JColor::gray); } else { g.setJColor(JColor::yellow); } drawText(g, "MSAgent", rect1); drawText(g, nickname, rect); //some additional examples of graphics // //int xx[] = { rect.x, rect.x+rect.width-6, rect.x, rect.x}; //int yy[] = { rect.y, rect.y+(rect.height-1)/2, rect.y+rect.height-1, rect.y}; //g.drawPolygon(xx, yy, 4); //g.drawOval(rect.x+rect.width-5, rect.y+(rect.height-1)/2-2, 5, 5); //g.drawLine(rect.x+rect.width, rect.y, rect.x, rect.y+rect.height); //drawText(g,label,rect); } // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // INPUT / OUTPUT INTERFACES // // // ------------------------------------------------------------------------ // int JMSAgentObj::inputType(int n) { //what data-types are expected? switch(n) { case TRIGGER_INPUT: case X_INPUT: case Y_INPUT: default: return JIntegerData::id; case STRING_INPUT: return JStringData::id; } ///for JBytes: // return JBytesData::id; ///for JString: // return JStringData::id; ///for JIntegers: // return JIntegerData::id; } int JMSAgentObj::outputType(int n) { //what data-types are expected? switch(n) { case TRIGGER_OUTPUT: case X_OUTPUT: case Y_OUTPUT: default: return JIntegerData::id; case NICKNAME_OUTPUT: case STRING_OUTPUT: return JStringData::id; } ///for JBytes: // return JBytesData::id; ///for JString: // return JStringData::id; ///for JIntegers: // return JIntegerData::id; } // ------------------------------------------------------------------------ // boolean JMSAgentObj::inputAllowed(int n){ //"for any port" since we don't test return !inputSet(n).last(); //accept only one link per input port //return true; //accept as many links as needed } boolean JMSAgentObj::outputAllowed(int n){ //"for any port" since we don't test //return !outputSet(n).last(); //accept only one link per output port return true; //accept as many links as needed } // ------------------------------------------------------------------------ // JString JMSAgentObj::inputTag(int n) { //friendly labels char* tag[] = { "trigger", "X", "Y", "text"}; //label inputs //char* tag[] = { "input"}; //boring "input" label for all inputs return tag[n]; } JString JMSAgentObj::outputTag(int n) { //friendly labels char* tag[] = { "triggered", "nickname", "X", "Y", "text"}; //label inputs //char* tag[] = { "output"}; //boring "output" label for all outputs return tag[n]; } // ------------------------------------------------------------------------ // int JMSAgentObj::inputFace(int n) { //which face(s) for inputs? if (n==TRIGGER_INPUT) { return TOP; } else { return LEFT; } } int JMSAgentObj::outputFace(int n) { //which face(s) for outputs? if (n==TRIGGER_OUTPUT || n==NICKNAME_OUTPUT) { return BOTTOM; } else { return RIGHT; } } // ------------------------------------------------------------------------ // // inputArea, outputArea defines the area that a port will accept connections // leftArea(int portNumber, // int whichPortSlotInThisArea, // int howManyPortSlotsInThisArea, // double whereToStartAreaFraction (default 0.25), // double widthAreaFraction (default is 0.50) // double perpendicularReciprocalFactor (default is 6) ) //ex: // return leftArea(n, 0, 1); //port-0 is alone on LEFT // JFRect JMSAgentObj::inputArea(int n) { if (n==TRIGGER_INPUT) { return topArea(n, TRIGGER_INPUT,1); } else { return leftArea(n, TRIGGER_INPUT+1, NUMBER_OF_INPUTS-1); } } JFRect JMSAgentObj::outputArea(int n) { if (n==TRIGGER_OUTPUT || n==NICKNAME_OUTPUT) { return bottomArea(n, TRIGGER_OUTPUT, NICKNAME_OUTPUT-TRIGGER_OUTPUT+1); } else { return rightArea(n, TRIGGER_OUTPUT+1, NUMBER_OF_OUTPUTS-1); } } // ------------------------------------------------------------------------ // // for fine-tuned positioning (for possibly multiple links) with the areas JFPoint JMSAgentObj::inputPoint(int n, JLinkObj& link) { if (n==TRIGGER_INPUT) { return topPoint(n,link, TRIGGER_INPUT,1); } else { return leftPoint(n,link, TRIGGER_INPUT+1, NUMBER_OF_INPUTS-1); } } JFPoint JMSAgentObj::outputPoint(int n, JLinkObj& link) { if (n==TRIGGER_OUTPUT || n==NICKNAME_OUTPUT) { return bottomPoint(n,link, TRIGGER_OUTPUT,NICKNAME_OUTPUT-TRIGGER_OUTPUT+1); } else { return rightPoint(n,link, TRIGGER_OUTPUT+1, NUMBER_OF_OUTPUTS-1); } } // ------------------------------------------------------------------------ // // ------------------------------------------------------------------------ // // // PROPERTY SHEET METHODS // // ------------------------------------------------------------------------ // char* startStateTag[] = { "start hidden", "start with animation", null}; //null ends the list // ------------------------------------------------------------------------ // // // getProperties() produces a list of properties // JArray JMSAgentObj::getProperties() { JArray properties; // format: // J_x_Property(PROPERTYSHEET_VARIABLE_TAG, PERSISTENT_VARIABLE) //IntegerProperties need a range to be specified // if (MSAgentServerLoaded !=1) { properties.append( JStringProperty("ERROR", JString("COM/AgentServer failed to load.")) ); } else if (MSAgentCharLoaded !=1) { properties.append( JStringProperty("ERROR", JString("AgentCharacter failed to load.")) ); } properties.append( JIntegerProperty("X", posX, MINX, MAXX) ); properties.append( JIntegerProperty("Y", posY, MINY, MAXY) ); properties.append(JIntegerListProperty("startState", startState, startStateTag)); properties.append( JStringProperty("preText", preText) ); properties.append( JStringProperty("nickname", nickname) ); properties.append(JFileProperty("filename", filename, type)); //I think it's okay to use repeated labels as long as one does not //want to edit its property field ///ROB return properties; } // ------------------------------------------------------------------------ // // // updateProperty() updates a given property, as specified by VARIABLE_LABEL // boolean JMSAgentObj::updateProperty(JProperty& prop) { if (prop.getName() == JString("X")) { posX = ((JIntegerProperty*)&prop)->value; //do update posX=min(max(posX,MINX),MAXX); broadcast(X_OUTPUT); //ping X_OUTPUT...because it might want to know broadcast(NICKNAME_OUTPUT); return true; //leave and announce that the property was found } if (prop.getName() == JString("Y")) { posY = ((JIntegerProperty*)&prop)->value; //do update posY=min(max(posY,MINY),MAXY); broadcast(Y_OUTPUT); //ping Y_OUTPUT...because it might want to know broadcast(NICKNAME_OUTPUT); return true; //leave and announce that the property was found } if (prop.getName() == JString("startState")) { startState = ((JIntegerListProperty*)&prop)->value; //do update repaint(); return true; } if (prop.getName() == JString("preText")) { preText = ((JStringProperty*)&prop)->value; //do update repaint(); //you might want to repaint() broadcast(STRING_OUTPUT); //ping STRING_OUTPUT...because it might want to know broadcast(NICKNAME_OUTPUT); return true; //leave and announce that the property was found } if (prop.getName() == JString("nickname")) { nickname = ((JStringProperty*)&prop)->value; //do update repaint(); //you might want to repaint() broadcast(STRING_OUTPUT); //ping STRING_OUTPUT...because it might want to know broadcast(NICKNAME_OUTPUT); return true; //leave and announce that the property was found } if (prop.getName() == JString("filename")) { int sequence = parseName(((JFileProperty*)&prop)->value, prefix); filename = composit(prefix, sequence); close(); startup(); return true; } return false; //leave and announce that the property was not found } // ------------------------------------------------------------------------ // // // support methods for handling filenames (ripped from some example code) // /* Composit the Prefix and Sequence into Filename Stream */ JString JMSAgentObj::composit(JString prefix, int sequence) { if (sequence) return prefix+"_"+JInteger::toJString(sequence)+type(1); return prefix+type(1); } /* Parse filename into prefix and sequence number */ int JMSAgentObj::parseName(JString filename, JString& prefix) { int start = filename.lastIndexOf('_'); int end = filename.lastIndexOf('.'); if (end == -1) end = filename.length(); if (start > -1) { prefix = filename(0, start); return (int)JInteger(filename(start+1, end)); } else prefix = filename(0, end); return 0; } // ------------------------------------------------------------------------ //