1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
7 // This program is free software: you can redistribute it and/or modify
8 // it under the terms of the GNU Affero General Public License as
9 // published by the Free Software Foundation, either version 3 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU Affero General Public License for more details.
17 // You should have received a copy of the GNU Affero General Public License
18 // along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #include "nel/misc/types_nl.h"
24 #include "nel/misc/file.h"
25 #include "nel/misc/config_file.h"
33 using namespace NLMISC
;
35 // define macro for outputone code line
39 fo.serialBuffer( (uint8 *) const_cast< char * >(out.c_str()), (uint)out.size() );\
44 static set
<string
> Codes
;
49 string NormalizedSkillName
;
50 CSkill
*ParentSkillPtr
;
56 string SecondaryCategory
;
57 vector
<CSkill
*> Children
;
59 CSkill() : ParentSkillPtr(NULL
),MaxValue(0),StageType(0)
62 CSkill(const CSkill
&skill
)
67 CSkill
&operator=(const CSkill
&skill
)
69 SkillName
= skill
.SkillName
;
70 NormalizedSkillName
= skill
.NormalizedSkillName
;
71 ParentSkillPtr
= skill
.ParentSkillPtr
;
72 ParentSkill
= skill
.ParentSkill
;
73 MaxValue
= skill
.MaxValue
;
75 StageType
= skill
.StageType
;
76 MainCategory
= skill
.MainCategory
;
77 SecondaryCategory
= skill
.SecondaryCategory
;
78 Children
= skill
.Children
;
83 void skillName(string name
)
86 /* // fast correction to avoid strange cases when names end with a ' '
87 sint i = SkillName.size();
90 if ( SkillName[i-1] != ' ' )
93 SkillName.resize( i );
97 c[0] = SkillName.substr( 0, 1).c_str()[0] - 32;
99 NormalizedSkillName = string( c ) + SkillName.substr( 1 );
100 while( ( idxSpace = NormalizedSkillName.find(" ") ) != string::npos )
102 string skillNameTmp = NormalizedSkillName.substr( 0, idxSpace );
103 if( idxSpace < ( SkillName.size() - 1 ) )
105 c[0] = (*NormalizedSkillName.substr( idxSpace + 1, 1 ).c_str());
106 if( c[0] >= 'a' ) c[0] -= 32;
107 skillNameTmp = skillNameTmp + string( c ) + NormalizedSkillName.substr( idxSpace + 2 );
109 NormalizedSkillName = skillNameTmp;
112 NormalizedSkillName
= SkillName
;
117 if (ParentSkillPtr
!= NULL
)
118 Code
= ParentSkillPtr
->Code
+ Code
;
120 Codes
.insert( Code
);
122 for (uint i
= 0 ; i
< Children
.size() ; ++i
)
123 Children
[i
]->buildCode();
126 void writeInSheet(COFile
&fo
)
129 <STRUCT Name="AccurateBleedingShot">
130 <ATOM Name="Skill" Value="acurate bleeding shot "/>
131 <ATOM Name="SkillCode" Value="none"/>
132 <ATOM Name="MaxSkillValue" Value="50"/>
133 <ARRAY Name="ChildSkills">
134 <ATOM Name="AccurateBreathlessShot" Value="acurate breathless shot"/>
135 <ATOM Name="AnimalSlideSlip" Value="animal slideslip"/>
140 out
= string(" <STRUCT Name=\"")+ NormalizedSkillName
+ string("\">\n");
141 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
142 out
= string(" <ATOM Name=\"Skill\" Value=\"")+ SkillName
+ string("\"/>\n");
143 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
144 out
= string(" <ATOM Name=\"SkillCode\" Value=\"")+ Code
+ string("\"/>\n");
145 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
146 out
= string(" <ATOM Name=\"MaxSkillValue\" Value=\"")+ toString(MaxValue
) + string("\"/>\n");
147 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
148 out
= string(" <ATOM Name=\"Type of Stage\" Value=\"")+ toString(StageType
) + string("\"/>\n");
149 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
151 if (ParentSkillPtr
!= NULL
)
153 out
= string(" <ATOM Name=\"ParentSkill\" Value=\"")+ ParentSkill
+ string("\"/>\n");
157 out
= string(" <ATOM Name=\"ParentSkill\" Value=\"\"/>\n");
159 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
161 if( !Children
.empty())
163 out
= string(" <ARRAY Name=\"ChildSkills\">\n");
164 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
165 for (uint i
= 0 ; i
< Children
.size() ; ++i
)
167 out
= string(" <ATOM Name=\"") + Children
[i
]->NormalizedSkillName
+ string("\" Value=\"")+ Children
[i
]->SkillName
+ string("\"/>\n");
168 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
170 out
= string(" </ARRAY>\n");
171 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
173 out
= string(" </STRUCT>\n");
174 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
176 for (uint i
= 0 ; i
< Children
.size() ; ++i
)
177 Children
[i
]->writeInSheet(fo
);
185 for (uint i
= 0 ; i
< RootSkills
.size() ; ++i
)
186 RootSkills
[i
]->buildCode();
189 vector
<CSkill
*> RootSkills
;
192 static CSkillTree SkillTree
;
193 static map
< string
, CSkill
> SkillNameToStruct
;
194 char separators
[] = ";,";
197 //-----------------------------------------------
200 //-----------------------------------------------
201 sint
main( sint argc
, char ** argv
)
203 /////////////////////////////////////////////////////////////////////////////////////
204 // Somes working variables
207 // vector contained selector category
208 vector
< string
> selector
;
210 /////////////////////////////////////////////////////////////////////////////////////
211 // Check number of arguments
214 printf("Create a file .typ for george contained a subset of skills\n\n");
215 printf("SKILL_EXTRACTOR <output file> <input file skills> <[<selector> ...]\n");
216 printf(" param 1 : create tree (yes) or no (no)\n");
217 printf(" if output file have .typ extension, generate .typ george file format\n");
218 printf(" if output file have .dfn extension, generate .dfn george file format\n");
219 printf(" param 3 is sheet2 of SkillCategory.xls exported in csv format\n");
220 printf(" Selector is category present in input file, selectors begins by '+' for or operation and by '.' for and operation\n");
221 printf(" the first selector must be or operation (begin by '+' character)\n");
225 // parse the config file
226 CConfigFile configFile
;
229 configFile
.load( "skill_extractor.cfg" );
231 catch(const Exception
&e
)
233 nlwarning("<CShopTypeManager::initShopBase> skill_extractor.cfg %s",e
.what());
238 CConfigFile::CVar
* cfgVar
= configFile
.getVarPtr("CsvDir");
241 printf("var 'CsvDir' not found in the skill_extractor.cfg");
244 const string
& CSVDir
= cfgVar
->asString() + string("/");
246 //get the path for the generated source files
247 cfgVar
= configFile
.getVarPtr("SrcDir");
250 printf("var 'SrcDir' not found in the skill_extractor.cfg");
253 const string
& srcDir
= cfgVar
->asString() + string("/");
255 //get the path for the generated source files
256 cfgVar
= configFile
.getVarPtr("PdsDir");
259 printf("var 'PdsDir' not found in the skill_extractor.cfg");
262 const string
& pdsDir
= cfgVar
->asString() + string("/");
264 //get the path for the generated dfn
265 cfgVar
= configFile
.getVarPtr("DfnDir");
268 printf("var 'DfnDir' not found in the skill_extractor.cfg");
271 const string
& dfnDir
= cfgVar
->asString() + string("/");
273 //get the path for the generated skill tree
274 cfgVar
= configFile
.getVarPtr("SkillTreeDir");
277 printf("var 'DfnDir' not found in the skill_extractor.cfg");
280 const string
& treeDir
= cfgVar
->asString() + string("/");
283 /////////////////////////////////////////////////////////////////////////////////////
284 // Export .typ and .dfn file
287 if( ! f
.open( CSVDir
+ string( argv
[3] ) ) )
289 nlwarning( "File %s open failed", argv
[3] );
293 // read all input file
297 map
< string
, CSkill
>::const_iterator itSkillStruct
;
301 f
.getline( buffer
, 4096 );
303 ptr
= strtok( buffer
, separators
);
305 while( ptr
&& string( ptr
) != string(" ") )
309 case 0: // skill name
311 skillName
= strupr( string( ptr
) );
312 vector
< string
> emptyVectorOfString
;
313 skill
.skillName(skillName
);
317 skill
.Code
= toUpperAscii(string( ptr
));
319 case 2: // parent skill
320 skill
.ParentSkill
= toUpperAscii(string( ptr
));
322 case 3: // max skill value
323 NLMISC::fromString(std::string(ptr
), skill
.MaxValue
);
325 case 4: // stage type
326 NLMISC::fromString(std::string(ptr
), skill
.StageType
);
328 case 5: // main category
329 skill
.MainCategory
= string( ptr
);
331 case 6: // secondary category
332 skill
.SecondaryCategory
= string( ptr
);
339 ptr
= strtok( 0, separators
);
342 if ( !skill
.SkillName
.empty())
344 // insert skill in the Map
345 //pair< map< string, CSkill>::const_iterator, bool> skillInsert = SkillNameToStruct.insert( make_pair(skill.SkillName, skill) );
346 //nlinfo("Insert skill %s, parent %s", skill.SkillName.c_str(), skill.ParentSkill.c_str() );
347 SkillNameToStruct
.insert( make_pair(skill
.SkillName
, skill
) );
353 map
< string
, CSkill
>::iterator itSkill
;
354 map
< string
, CSkill
>::iterator itSkillEnd
= SkillNameToStruct
.end();
356 for ( itSkill
= SkillNameToStruct
.begin() ; itSkill
!= itSkillEnd
; ++itSkill
)
359 if ( (*itSkill
).second
.ParentSkill
== string("NONE") || (*itSkill
).second
.ParentSkill
.empty() )
361 SkillTree
.RootSkills
.push_back(&((*itSkill
).second
));
365 map
< string
, CSkill
>::iterator its
= SkillNameToStruct
.find((*itSkill
).second
.ParentSkill
);
366 if (its
== itSkillEnd
)
368 nlwarning("ERROR : cannot find the parent skill %s for skill %s (skill %u)", (*itSkill
).second
.ParentSkill
.c_str(), (*itSkill
).second
.SkillName
.c_str(), count
);
371 (*itSkill
).second
.ParentSkillPtr
= &((*its
).second
);
372 (*its
).second
.Children
.push_back(&((*itSkill
).second
));
376 SkillTree
.buildCode();
379 // create the skill tree if first param == yes
380 if ( string( argv
[1] ) == string("yes") )
382 if( ! fo
.open( treeDir
+ string("skills.skill_tree"), false ) )
384 nlwarning(" Can't open file skills.skill_tree for writing");
388 string
out("<?xml version=\"1.0\"?>\n");
389 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
390 out
= string("<FORM Version=\"0.2\" State=\"modified\">\n");
391 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
392 out
= string(" <STRUCT>\n");
393 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
394 out
= string(" <ARRAY Name=\"SkillData\">\n");
395 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
397 for ( vector
<CSkill
*>::const_iterator itTree
= SkillTree
.RootSkills
.begin() ; itTree
!= SkillTree
.RootSkills
.end() ; ++itTree
)
399 (*itTree
)->writeInSheet(fo
);
402 out
= string(" </ARRAY>\n");
403 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
404 out
= string(" </STRUCT>\n");
405 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
406 out
= string("</FORM>\n");
407 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
410 // create the code .typ
411 if( ! fo
.open( string("_skillsCode.typ"), false ) )
413 nlwarning(" Can't open file _skillsCode.typ for writing");
417 out
= string("<TYPE Type=\"String\" UI=\"NonEditableCombo\" Default=\"None\" Version=\"0.1\" State=\"modified\">\n");
418 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
419 out
= string(" <DEFINITION Label=\"unknown\" Value=\"unknown\"/>\n");
420 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
422 set
<string
>::const_iterator itCode
;
423 for ( itCode
= Codes
.begin() ; itCode
!= Codes
.end() ; ++itCode
)
425 out
= string(" <DEFINITION Label=\"") + (*itCode
) + string("\" Value=\"") + (*itCode
) + string("\"/>\n");
426 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
429 out
= string("</TYPE>\n");
430 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
435 // read category in command line
436 for( sint i
= 4; i
< argc
; ++i
)
438 selector
.push_back( string( argv
[ i
] ) );
441 // generate a file containing skills and associated Code
442 if( ! fo
.open( string( "skill_codes.txt" ) ) )
444 nlwarning(" Can't open file %s for writing", "skill_codes.txt" );
447 for ( itSkill
= SkillNameToStruct
.begin() ; itSkill
!= itSkillEnd
; ++itSkill
)
451 if ( (*itSkill
).second
.NormalizedSkillName
.size() < 50)
452 space
.resize( 50 - (*itSkill
).second
.NormalizedSkillName
.size(), ' ' );
453 outLine((*itSkill
).second
.NormalizedSkillName
+ space
+ string("\t") + (*itSkill
).second
.Code
+ string("\n") );
457 // generate .typ or .dfn file
458 if( ! fo
.open( dfnDir
+ string( argv
[2] ), false ) )
460 nlwarning(" Can't open file %s for writing", argv
[2] );
464 // output header of .typ or .dfn file
465 string
out("<?xml version=\"1.0\"?>\n");
466 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
467 if( string( argv
[2] ).find(".typ") != string::npos
)
469 out
= string("<TYPE Type=\"String\" UI=\"NonEditableCombo\" Default=\"unknown\" Version=\"0.1\" State=\"modified\">\n");
470 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
471 out
= string(" <DEFINITION Label=\"unknown\" Value=\"unknown\"/>\n");
472 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
476 out
= string("<DFN Version=\"0.0\" State=\"modified\">\n");
477 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
480 // parse all skills to export selected ones
481 for ( itSkillStruct
= SkillNameToStruct
.begin() ; itSkillStruct
!= SkillNameToStruct
.end() ; ++itSkillStruct
)
483 bool selected
= false;
486 const CSkill
&skill
= (*itSkillStruct
).second
;
488 for( vector
< string
>::iterator its
= selector
.begin(); its
!= selector
.end(); ++its
)
492 if ( skill
.MainCategory
== (*its
).substr(1) )
496 else if ( skill
.SecondaryCategory
== (*its
).substr(1) )
503 if( (*its
).substr( 0, 1) == string("+") ) // or operation
507 else if( (*its
).substr( 0, 1) == string(".") ) // and operation
512 else if( (*its
).substr( 0, 1) == string(".") )
520 if( string( argv
[2] ).find(".typ") != string::npos
)
522 out
= string(" <DEFINITION Label=\"") + skill
.SkillName
+ string("\" Value=\"") + skill
.NormalizedSkillName
+ string("\"/>\n");
526 out
= string(" <ELEMENT Name=\"") + skill
.NormalizedSkillName
+ string("\" Type=\"Type\" Filename=\"creature_stat.typ\"/>\n");
528 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
532 if( string( argv
[2] ).find(".typ") != string::npos
)
534 out
= string("</TYPE>\n");
538 out
= string("</DFN>\n");
540 fo
.serialBuffer( (uint8
*) const_cast< char * >(out
.c_str()), (uint
)out
.size() );
543 /////////////////////////////////////////////////////////////////////////////////////
544 // Generate skill.cpp and skill.h code file
547 // output begin skill.h file
548 if( ! fo
.open( srcDir
+ string( "skills.h" ) ) )
550 nlwarning(" Can't open file %s for writing", "skills.h" );
554 // write header of header file
555 outLine("/** \\file skills.h\n");
556 outLine(" * skills enumeration: generated by skill extractor program\n");
560 outLine("#ifndef RY_SKILLS_H\n");
561 outLine("#define RY_SKILLS_H\n");
563 outLine("#include \"nel/misc/types_nl.h\"\n");
565 outLine("#include <string.h>\n");
567 outLine( string("// NbSkills in enum : ") + toString( SkillNameToStruct
.size() ) + string(" Report this in database.xml \n\n") );
568 outLine("namespace SKILLS\n");
570 outLine(" enum ESkills\n");
573 itSkill
= SkillNameToStruct
.begin();
574 if (itSkill
!= itSkillEnd
)
576 outLine(string(" ") + (*itSkill
).second
.NormalizedSkillName
+ string(" = 0,\n") );
577 for ( ++itSkill
; itSkill
!= itSkillEnd
; ++itSkill
)
579 outLine(string(" ") + (*itSkill
).second
.NormalizedSkillName
+ string(",\n") );
582 // output end skill enum and skill type enum and skill api
584 outLine(" NUM_SKILLS,\n");
585 outLine(" unknown,\n");
590 /* for( it = skillsAndSelector.begin(); it != skillsAndSelector.end(); ++it )
594 while( ( idxSpace = out.find(" ") ) != string::npos )
596 string tmp = out.substr( 0, idxSpace );
597 if( idxSpace < ( out.size() - 1 ) )
599 tmp = tmp + string("_") + out.substr( idxSpace + 1 );
603 outLine( string(" ") + out + string(",\n") );
606 // output end skill enum and skill type enum and skill api
608 outLine(" NUM_SKILLS,\n");
609 outLine(" unknown\n");
613 outLine(" enum ESkillType\n");
615 outLine(" skill,\n");
616 outLine(" specialized_skill,\n");
617 outLine(" training_characteristic,\n");
618 outLine(" training_resist,\n");
619 outLine(" training_score,\n");
621 outLine(" unknown_skill_type\n");
626 outLine(" * get the right skill enum from the input string\n");
627 outLine(" * \\param str the input string\n");
628 outLine(" * \\return the ESkills associated to this string (Unknown if the string cannot be interpreted)\n");
630 outLine(" ESkills toSkill ( const std::string &str );\n");
633 outLine(" * get the right skill string from the gived enum\n");
634 outLine(" * \\param skill the skill to convert\n");
635 outLine(" * \\return the string associated to this enum number (Unknown if the enum number not exist)\n");
637 outLine(" const std::string& toString( uint16 skill );\n");
640 outLine(" * get the skill category name\n");
641 outLine(" * \\param s is the enum number\n");
642 outLine(" * \\return the string name of skill type (Unknown if the enum number not exist)\n");
644 outLine(" const std::string& getSkillCategoryName( uint16 s );\n");
646 outLine("}; // SKILLS\n");
648 outLine("#endif // RY_SKILLS_H\n");
649 outLine("/* End of skills.h */\n");
651 /////////////////////////////////////////////////////////////////////////////////////
652 // begin output skill.cpp file
653 if( ! fo
.open( srcDir
+ string( "skills.cpp" ) ) )
655 nlwarning(" Can't open file skills.cpp for writing");
659 outLine("/** \\file skills.cpp\n");
663 outLine("#include \"stdpch.h\"\n");
665 outLine("#include \"nel/misc/debug.h\"\n");
666 outLine("#include \"skills.h\"\n");
667 outLine("#include \"nel/misc/string_conversion.h\"\n");
669 outLine("using namespace std;\n");
670 outLine("using namespace NLMISC;\n");
672 outLine("namespace SKILLS\n");
675 outLine("static string UnknownString(\"Unknown\");\n");
677 outLine("\tNL_BEGIN_STRING_CONVERSION_TABLE (ESkills)\n");
679 // parser all skills and init the conversion map
680 for ( itSkill
= SkillNameToStruct
.begin() ; itSkill
!= itSkillEnd
; ++itSkill
)
682 outLine (string ("\t NL_STRING_CONVERSION_TABLE_ENTRY(") + (*itSkill
).second
.NormalizedSkillName
+ string(")\n") );
684 //outLine (string (" { \"unknown\", unknown },\n" ) );
685 outLine (string("\t NL_STRING_CONVERSION_TABLE_ENTRY(unknown)\n") );
687 outLine("\tNL_END_STRING_CONVERSION_TABLE(ESkills, SkillsConversion, unknown)\n");
690 outLine("\tESkills toSkill( const std::string &str )\n");
692 outLine("\t return SkillsConversion.fromString(str);\n");
695 outLine("\tconst std::string& toString( uint16 skill )\n");
697 outLine("\t return SkillsConversion.toString((ESkills)skill);\n");
701 outLine("\tconst std::string& getSkillCategoryName( uint16 s )\n");
703 /*outLine(" if( s < sizeof(SkillCategoryStrings)/sizeof(SkillCategoryStrings[0]) )\n");
705 outLine(" return SkillCategoryStrings[ s ];\n");
707 outLine(" else return UnknownString;\n");
709 outLine("\t return UnknownString;\n");
712 outLine("}; // SKILLS\n");
715 /////////////////////////////////////////////////////////////////////////////////////
716 // Generate skills.pds script file
718 if( ! fo
.open( pdsDir
+ string( "skills.pds" ) ) )
720 nlwarning(" Can't open file %s for writing", "skills.pds" );
724 outLine( string("// NbSkills in enum : ") + toString( SkillNameToStruct
.size() ) + string(" Report this in database.xml \n\n") );
725 outLine("file \"skills.h\"\n");
727 outLine("\tenum TSkill\n");
729 outLine("\t\tBeginSkill\n");
732 itSkill
= SkillNameToStruct
.begin();
733 if (itSkill
!= itSkillEnd
)
735 outLine(string("\t\t\t") + (*itSkill
).second
.NormalizedSkillName
);
736 for ( ++itSkill
; itSkill
!= itSkillEnd
; ++itSkill
)
739 outLine(string("\t\t\t") + (*itSkill
).second
.NormalizedSkillName
);
742 outLine("\n\t\t}\n");
743 outLine("\t} EndSkill\n");
747 nlinfo("job finish");