Blender V2.61 - r43446
|
00001 /* 00002 * Add steering behaviors 00003 * 00004 * 00005 * ***** BEGIN GPL LICENSE BLOCK ***** 00006 * 00007 * This program is free software; you can redistribute it and/or 00008 * modify it under the terms of the GNU General Public License 00009 * as published by the Free Software Foundation; either version 2 00010 * of the License, or (at your option) any later version. 00011 * 00012 * This program is distributed in the hope that it will be useful, 00013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00015 * GNU General Public License for more details. 00016 * 00017 * You should have received a copy of the GNU General Public License 00018 * along with this program; if not, write to the Free Software Foundation, 00019 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 00020 * 00021 * The Original Code is Copyright (C) 2001-2002 by NaN Holding BV. 00022 * All rights reserved. 00023 * 00024 * The Original Code is: all of this file. 00025 * 00026 * Contributor(s): none yet. 00027 * 00028 * ***** END GPL LICENSE BLOCK ***** 00029 */ 00030 00031 #include "BLI_math.h" 00032 #include "KX_SteeringActuator.h" 00033 #include "KX_GameObject.h" 00034 #include "KX_NavMeshObject.h" 00035 #include "KX_ObstacleSimulation.h" 00036 #include "KX_PythonInit.h" 00037 #include "KX_PyMath.h" 00038 #include "Recast.h" 00039 00040 /* ------------------------------------------------------------------------- */ 00041 /* Native functions */ 00042 /* ------------------------------------------------------------------------- */ 00043 00044 KX_SteeringActuator::KX_SteeringActuator(SCA_IObject *gameobj, 00045 int mode, 00046 KX_GameObject *target, 00047 KX_GameObject *navmesh, 00048 float distance, 00049 float velocity, 00050 float acceleration, 00051 float turnspeed, 00052 bool isSelfTerminated, 00053 int pathUpdatePeriod, 00054 KX_ObstacleSimulation* simulation, 00055 short facingmode, 00056 bool normalup, 00057 bool enableVisualization) 00058 : SCA_IActuator(gameobj, KX_ACT_STEERING), 00059 m_target(target), 00060 m_mode(mode), 00061 m_distance(distance), 00062 m_velocity(velocity), 00063 m_acceleration(acceleration), 00064 m_turnspeed(turnspeed), 00065 m_simulation(simulation), 00066 m_updateTime(0), 00067 m_obstacle(NULL), 00068 m_isActive(false), 00069 m_isSelfTerminated(isSelfTerminated), 00070 m_enableVisualization(enableVisualization), 00071 m_facingMode(facingmode), 00072 m_normalUp(normalup), 00073 m_pathLen(0), 00074 m_pathUpdatePeriod(pathUpdatePeriod), 00075 m_wayPointIdx(-1), 00076 m_steerVec(MT_Vector3(0, 0, 0)) 00077 { 00078 m_navmesh = static_cast<KX_NavMeshObject*>(navmesh); 00079 if (m_navmesh) 00080 m_navmesh->RegisterActuator(this); 00081 if (m_target) 00082 m_target->RegisterActuator(this); 00083 00084 if (m_simulation) 00085 m_obstacle = m_simulation->GetObstacle((KX_GameObject*)gameobj); 00086 KX_GameObject* parent = ((KX_GameObject*)gameobj)->GetParent(); 00087 if (m_facingMode>0 && parent) 00088 { 00089 m_parentlocalmat = parent->GetSGNode()->GetLocalOrientation(); 00090 } 00091 else 00092 m_parentlocalmat.setIdentity(); 00093 } 00094 00095 KX_SteeringActuator::~KX_SteeringActuator() 00096 { 00097 if (m_navmesh) 00098 m_navmesh->UnregisterActuator(this); 00099 if (m_target) 00100 m_target->UnregisterActuator(this); 00101 } 00102 00103 CValue* KX_SteeringActuator::GetReplica() 00104 { 00105 KX_SteeringActuator* replica = new KX_SteeringActuator(*this); 00106 // replication just copy the m_base pointer => common random generator 00107 replica->ProcessReplica(); 00108 return replica; 00109 } 00110 00111 void KX_SteeringActuator::ProcessReplica() 00112 { 00113 if (m_target) 00114 m_target->RegisterActuator(this); 00115 if (m_navmesh) 00116 m_navmesh->RegisterActuator(this); 00117 SCA_IActuator::ProcessReplica(); 00118 } 00119 00120 00121 bool KX_SteeringActuator::UnlinkObject(SCA_IObject* clientobj) 00122 { 00123 if (clientobj == m_target) 00124 { 00125 m_target = NULL; 00126 return true; 00127 } 00128 else if (clientobj == m_navmesh) 00129 { 00130 m_navmesh = NULL; 00131 return true; 00132 } 00133 return false; 00134 } 00135 00136 void KX_SteeringActuator::Relink(CTR_Map<CTR_HashedPtr, void*> *obj_map) 00137 { 00138 void **h_obj = (*obj_map)[m_target]; 00139 if (h_obj) { 00140 if (m_target) 00141 m_target->UnregisterActuator(this); 00142 m_target = (KX_GameObject*)(*h_obj); 00143 m_target->RegisterActuator(this); 00144 } 00145 00146 h_obj = (*obj_map)[m_navmesh]; 00147 if (h_obj) { 00148 if (m_navmesh) 00149 m_navmesh->UnregisterActuator(this); 00150 m_navmesh = (KX_NavMeshObject*)(*h_obj); 00151 m_navmesh->RegisterActuator(this); 00152 } 00153 } 00154 00155 bool KX_SteeringActuator::Update(double curtime, bool frame) 00156 { 00157 if (frame) 00158 { 00159 double delta = curtime - m_updateTime; 00160 m_updateTime = curtime; 00161 00162 if (m_posevent && !m_isActive) 00163 { 00164 delta = 0; 00165 m_pathUpdateTime = -1; 00166 m_updateTime = curtime; 00167 m_isActive = true; 00168 } 00169 bool bNegativeEvent = IsNegativeEvent(); 00170 if (bNegativeEvent) 00171 m_isActive = false; 00172 00173 RemoveAllEvents(); 00174 00175 if (!delta) 00176 return true; 00177 00178 if (bNegativeEvent || !m_target) 00179 return false; // do nothing on negative events 00180 00181 KX_GameObject *obj = (KX_GameObject*) GetParent(); 00182 const MT_Point3& mypos = obj->NodeGetWorldPosition(); 00183 const MT_Point3& targpos = m_target->NodeGetWorldPosition(); 00184 MT_Vector3 vectotarg = targpos - mypos; 00185 MT_Vector3 vectotarg2d = vectotarg; 00186 vectotarg2d.z() = 0; 00187 m_steerVec = MT_Vector3(0, 0, 0); 00188 bool apply_steerforce = false; 00189 bool terminate = true; 00190 00191 switch (m_mode) { 00192 case KX_STEERING_SEEK: 00193 if (vectotarg2d.length2()>m_distance*m_distance) 00194 { 00195 terminate = false; 00196 m_steerVec = vectotarg; 00197 m_steerVec.normalize(); 00198 apply_steerforce = true; 00199 } 00200 break; 00201 case KX_STEERING_FLEE: 00202 if (vectotarg2d.length2()<m_distance*m_distance) 00203 { 00204 terminate = false; 00205 m_steerVec = -vectotarg; 00206 m_steerVec.normalize(); 00207 apply_steerforce = true; 00208 } 00209 break; 00210 case KX_STEERING_PATHFOLLOWING: 00211 if (m_navmesh && vectotarg.length2()>m_distance*m_distance) 00212 { 00213 terminate = false; 00214 00215 static const MT_Scalar WAYPOINT_RADIUS(0.25); 00216 00217 if (m_pathUpdateTime<0 || (m_pathUpdatePeriod>=0 && 00218 curtime - m_pathUpdateTime>((double)m_pathUpdatePeriod/1000))) 00219 { 00220 m_pathUpdateTime = curtime; 00221 m_pathLen = m_navmesh->FindPath(mypos, targpos, m_path, MAX_PATH_LENGTH); 00222 m_wayPointIdx = m_pathLen > 1 ? 1 : -1; 00223 } 00224 00225 if (m_wayPointIdx>0) 00226 { 00227 MT_Vector3 waypoint(&m_path[3*m_wayPointIdx]); 00228 if ((waypoint-mypos).length2()<WAYPOINT_RADIUS*WAYPOINT_RADIUS) 00229 { 00230 m_wayPointIdx++; 00231 if (m_wayPointIdx>=m_pathLen) 00232 { 00233 m_wayPointIdx = -1; 00234 terminate = true; 00235 } 00236 else 00237 waypoint.setValue(&m_path[3*m_wayPointIdx]); 00238 } 00239 00240 m_steerVec = waypoint - mypos; 00241 apply_steerforce = true; 00242 00243 00244 if (m_enableVisualization) 00245 { 00246 //debug draw 00247 static const MT_Vector3 PATH_COLOR(1,0,0); 00248 m_navmesh->DrawPath(m_path, m_pathLen, PATH_COLOR); 00249 } 00250 } 00251 00252 } 00253 break; 00254 } 00255 00256 if (apply_steerforce) 00257 { 00258 bool isdyna = obj->IsDynamic(); 00259 if (isdyna) 00260 m_steerVec.z() = 0; 00261 if (!m_steerVec.fuzzyZero()) 00262 m_steerVec.normalize(); 00263 MT_Vector3 newvel = m_velocity*m_steerVec; 00264 00265 //adjust velocity to avoid obstacles 00266 if (m_simulation && m_obstacle /*&& !newvel.fuzzyZero()*/) 00267 { 00268 if (m_enableVisualization) 00269 KX_RasterizerDrawDebugLine(mypos, mypos + newvel, MT_Vector3(1.,0.,0.)); 00270 m_simulation->AdjustObstacleVelocity(m_obstacle, m_mode!=KX_STEERING_PATHFOLLOWING ? m_navmesh : NULL, 00271 newvel, m_acceleration*delta, m_turnspeed/180.0f*M_PI*delta); 00272 if (m_enableVisualization) 00273 KX_RasterizerDrawDebugLine(mypos, mypos + newvel, MT_Vector3(0.,1.,0.)); 00274 } 00275 00276 HandleActorFace(newvel); 00277 if (isdyna) 00278 { 00279 //temporary solution: set 2D steering velocity directly to obj 00280 //correct way is to apply physical force 00281 MT_Vector3 curvel = obj->GetLinearVelocity(); 00282 newvel.z() = curvel.z(); 00283 obj->setLinearVelocity(newvel, false); 00284 } 00285 else 00286 { 00287 MT_Vector3 movement = delta*newvel; 00288 obj->ApplyMovement(movement, false); 00289 } 00290 } 00291 else 00292 { 00293 if (m_simulation && m_obstacle) 00294 { 00295 m_obstacle->dvel[0] = 0.f; 00296 m_obstacle->dvel[1] = 0.f; 00297 } 00298 00299 } 00300 00301 if (terminate && m_isSelfTerminated) 00302 return false; 00303 } 00304 00305 return true; 00306 } 00307 00308 const MT_Vector3& KX_SteeringActuator::GetSteeringVec() 00309 { 00310 static MT_Vector3 ZERO_VECTOR(0, 0, 0); 00311 if (m_isActive) 00312 return m_steerVec; 00313 else 00314 return ZERO_VECTOR; 00315 } 00316 00317 inline float vdot2(const float* a, const float* b) 00318 { 00319 return a[0]*b[0] + a[2]*b[2]; 00320 } 00321 static bool barDistSqPointToTri(const float* p, const float* a, const float* b, const float* c) 00322 { 00323 float v0[3], v1[3], v2[3]; 00324 rcVsub(v0, c,a); 00325 rcVsub(v1, b,a); 00326 rcVsub(v2, p,a); 00327 00328 const float dot00 = vdot2(v0, v0); 00329 const float dot01 = vdot2(v0, v1); 00330 const float dot02 = vdot2(v0, v2); 00331 const float dot11 = vdot2(v1, v1); 00332 const float dot12 = vdot2(v1, v2); 00333 00334 // Compute barycentric coordinates 00335 float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01); 00336 float u = (dot11 * dot02 - dot01 * dot12) * invDenom; 00337 float v = (dot00 * dot12 - dot01 * dot02) * invDenom; 00338 00339 float ud = u<0.f ? -u : (u>1.f ? u-1.f : 0.f); 00340 float vd = v<0.f ? -v : (v>1.f ? v-1.f : 0.f); 00341 return ud*ud+vd*vd ; 00342 } 00343 00344 inline void flipAxes(float* vec) 00345 { 00346 std::swap(vec[1],vec[2]); 00347 } 00348 00349 static bool getNavmeshNormal(dtStatNavMesh* navmesh, const MT_Vector3& pos, MT_Vector3& normal) 00350 { 00351 static const float polyPickExt[3] = {2, 4, 2}; 00352 float spos[3]; 00353 pos.getValue(spos); 00354 flipAxes(spos); 00355 dtStatPolyRef sPolyRef = navmesh->findNearestPoly(spos, polyPickExt); 00356 if (sPolyRef == 0) 00357 return false; 00358 const dtStatPoly* p = navmesh->getPoly(sPolyRef-1); 00359 const dtStatPolyDetail* pd = navmesh->getPolyDetail(sPolyRef-1); 00360 00361 float distMin = FLT_MAX; 00362 int idxMin = -1; 00363 for (int i = 0; i < pd->ntris; ++i) 00364 { 00365 const unsigned char* t = navmesh->getDetailTri(pd->tbase+i); 00366 const float* v[3]; 00367 for (int j = 0; j < 3; ++j) 00368 { 00369 if (t[j] < p->nv) 00370 v[j] = navmesh->getVertex(p->v[t[j]]); 00371 else 00372 v[j] = navmesh->getDetailVertex(pd->vbase+(t[j]-p->nv)); 00373 } 00374 float dist = barDistSqPointToTri(spos, v[0], v[1], v[2]); 00375 if (dist<distMin) 00376 { 00377 distMin = dist; 00378 idxMin = i; 00379 } 00380 } 00381 00382 if (idxMin>=0) 00383 { 00384 const unsigned char* t = navmesh->getDetailTri(pd->tbase+idxMin); 00385 const float* v[3]; 00386 for (int j = 0; j < 3; ++j) 00387 { 00388 if (t[j] < p->nv) 00389 v[j] = navmesh->getVertex(p->v[t[j]]); 00390 else 00391 v[j] = navmesh->getDetailVertex(pd->vbase+(t[j]-p->nv)); 00392 } 00393 MT_Vector3 tri[3]; 00394 for (size_t j=0; j<3; j++) 00395 tri[j].setValue(v[j][0],v[j][2],v[j][1]); 00396 MT_Vector3 a,b; 00397 a = tri[1]-tri[0]; 00398 b = tri[2]-tri[0]; 00399 normal = b.cross(a).safe_normalized(); 00400 return true; 00401 } 00402 00403 return false; 00404 } 00405 00406 void KX_SteeringActuator::HandleActorFace(MT_Vector3& velocity) 00407 { 00408 if (m_facingMode==0 && (!m_navmesh || !m_normalUp)) 00409 return; 00410 KX_GameObject* curobj = (KX_GameObject*) GetParent(); 00411 MT_Vector3 dir = m_facingMode==0 ? curobj->NodeGetLocalOrientation().getColumn(1) : velocity; 00412 if (dir.fuzzyZero()) 00413 return; 00414 dir.normalize(); 00415 MT_Vector3 up(0,0,1); 00416 MT_Vector3 left; 00417 MT_Matrix3x3 mat; 00418 00419 if (m_navmesh && m_normalUp) 00420 { 00421 dtStatNavMesh* navmesh = m_navmesh->GetNavMesh(); 00422 MT_Vector3 normal; 00423 MT_Vector3 trpos = m_navmesh->TransformToLocalCoords(curobj->NodeGetWorldPosition()); 00424 if (getNavmeshNormal(navmesh, trpos, normal)) 00425 { 00426 00427 left = (dir.cross(up)).safe_normalized(); 00428 dir = (-left.cross(normal)).safe_normalized(); 00429 up = normal; 00430 } 00431 } 00432 00433 switch (m_facingMode) 00434 { 00435 case 1: // TRACK X 00436 { 00437 left = dir.safe_normalized(); 00438 dir = -(left.cross(up)).safe_normalized(); 00439 break; 00440 }; 00441 case 2: // TRACK Y 00442 { 00443 left = (dir.cross(up)).safe_normalized(); 00444 break; 00445 } 00446 00447 case 3: // track Z 00448 { 00449 left = up.safe_normalized(); 00450 up = dir.safe_normalized(); 00451 dir = left; 00452 left = (dir.cross(up)).safe_normalized(); 00453 break; 00454 } 00455 00456 case 4: // TRACK -X 00457 { 00458 left = -dir.safe_normalized(); 00459 dir = -(left.cross(up)).safe_normalized(); 00460 break; 00461 }; 00462 case 5: // TRACK -Y 00463 { 00464 left = (-dir.cross(up)).safe_normalized(); 00465 dir = -dir; 00466 break; 00467 } 00468 case 6: // track -Z 00469 { 00470 left = up.safe_normalized(); 00471 up = -dir.safe_normalized(); 00472 dir = left; 00473 left = (dir.cross(up)).safe_normalized(); 00474 break; 00475 } 00476 } 00477 00478 mat.setValue ( 00479 left[0], dir[0],up[0], 00480 left[1], dir[1],up[1], 00481 left[2], dir[2],up[2] 00482 ); 00483 00484 00485 00486 KX_GameObject* parentObject = curobj->GetParent(); 00487 if(parentObject) 00488 { 00489 MT_Point3 localpos; 00490 localpos = curobj->GetSGNode()->GetLocalPosition(); 00491 MT_Matrix3x3 parentmatinv; 00492 parentmatinv = parentObject->NodeGetWorldOrientation ().inverse (); 00493 mat = parentmatinv * mat; 00494 mat = m_parentlocalmat * mat; 00495 curobj->NodeSetLocalOrientation(mat); 00496 curobj->NodeSetLocalPosition(localpos); 00497 } 00498 else 00499 { 00500 curobj->NodeSetLocalOrientation(mat); 00501 } 00502 00503 } 00504 00505 #ifndef DISABLE_PYTHON 00506 00507 /* ------------------------------------------------------------------------- */ 00508 /* Python functions */ 00509 /* ------------------------------------------------------------------------- */ 00510 00511 /* Integration hooks ------------------------------------------------------- */ 00512 PyTypeObject KX_SteeringActuator::Type = { 00513 PyVarObject_HEAD_INIT(NULL, 0) 00514 "KX_SteeringActuator", 00515 sizeof(PyObjectPlus_Proxy), 00516 0, 00517 py_base_dealloc, 00518 0, 00519 0, 00520 0, 00521 0, 00522 py_base_repr, 00523 0,0,0,0,0,0,0,0,0, 00524 Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, 00525 0,0,0,0,0,0,0, 00526 Methods, 00527 0, 00528 0, 00529 &SCA_IActuator::Type, 00530 0,0,0,0,0,0, 00531 py_base_new 00532 }; 00533 00534 PyMethodDef KX_SteeringActuator::Methods[] = { 00535 {NULL,NULL} //Sentinel 00536 }; 00537 00538 PyAttributeDef KX_SteeringActuator::Attributes[] = { 00539 KX_PYATTRIBUTE_INT_RW("behaviour", KX_STEERING_NODEF+1, KX_STEERING_MAX-1, true, KX_SteeringActuator, m_mode), 00540 KX_PYATTRIBUTE_RW_FUNCTION("target", KX_SteeringActuator, pyattr_get_target, pyattr_set_target), 00541 KX_PYATTRIBUTE_RW_FUNCTION("navmesh", KX_SteeringActuator, pyattr_get_navmesh, pyattr_set_navmesh), 00542 KX_PYATTRIBUTE_FLOAT_RW("distance", 0.0f, 1000.0f, KX_SteeringActuator, m_distance), 00543 KX_PYATTRIBUTE_FLOAT_RW("velocity", 0.0f, 1000.0f, KX_SteeringActuator, m_velocity), 00544 KX_PYATTRIBUTE_FLOAT_RW("acceleration", 0.0f, 1000.0f, KX_SteeringActuator, m_acceleration), 00545 KX_PYATTRIBUTE_FLOAT_RW("turnspeed", 0.0f, 720.0f, KX_SteeringActuator, m_turnspeed), 00546 KX_PYATTRIBUTE_BOOL_RW("selfterminated", KX_SteeringActuator, m_isSelfTerminated), 00547 KX_PYATTRIBUTE_BOOL_RW("enableVisualization", KX_SteeringActuator, m_enableVisualization), 00548 KX_PYATTRIBUTE_RO_FUNCTION("steeringVec", KX_SteeringActuator, pyattr_get_steeringVec), 00549 KX_PYATTRIBUTE_SHORT_RW("facingMode", 0, 6, true, KX_SteeringActuator, m_facingMode), 00550 KX_PYATTRIBUTE_INT_RW("pathUpdatePeriod", -1, 100000, true, KX_SteeringActuator, m_pathUpdatePeriod), 00551 { NULL } //Sentinel 00552 }; 00553 00554 PyObject* KX_SteeringActuator::pyattr_get_target(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef) 00555 { 00556 KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self); 00557 if (!actuator->m_target) 00558 Py_RETURN_NONE; 00559 else 00560 return actuator->m_target->GetProxy(); 00561 } 00562 00563 int KX_SteeringActuator::pyattr_set_target(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef, PyObject *value) 00564 { 00565 KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self); 00566 KX_GameObject *gameobj; 00567 00568 if (!ConvertPythonToGameObject(value, &gameobj, true, "actuator.object = value: KX_SteeringActuator")) 00569 return PY_SET_ATTR_FAIL; // ConvertPythonToGameObject sets the error 00570 00571 if (actuator->m_target != NULL) 00572 actuator->m_target->UnregisterActuator(actuator); 00573 00574 actuator->m_target = (KX_GameObject*) gameobj; 00575 00576 if (actuator->m_target) 00577 actuator->m_target->RegisterActuator(actuator); 00578 00579 return PY_SET_ATTR_SUCCESS; 00580 } 00581 00582 PyObject* KX_SteeringActuator::pyattr_get_navmesh(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef) 00583 { 00584 KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self); 00585 if (!actuator->m_navmesh) 00586 Py_RETURN_NONE; 00587 else 00588 return actuator->m_navmesh->GetProxy(); 00589 } 00590 00591 int KX_SteeringActuator::pyattr_set_navmesh(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef, PyObject *value) 00592 { 00593 KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self); 00594 KX_GameObject *gameobj; 00595 00596 if (!ConvertPythonToGameObject(value, &gameobj, true, "actuator.object = value: KX_SteeringActuator")) 00597 return PY_SET_ATTR_FAIL; // ConvertPythonToGameObject sets the error 00598 00599 if (!PyObject_TypeCheck(value, &KX_NavMeshObject::Type)) 00600 { 00601 PyErr_Format(PyExc_TypeError, "KX_NavMeshObject is expected"); 00602 return PY_SET_ATTR_FAIL; 00603 } 00604 00605 if (actuator->m_navmesh != NULL) 00606 actuator->m_navmesh->UnregisterActuator(actuator); 00607 00608 actuator->m_navmesh = static_cast<KX_NavMeshObject*>(gameobj); 00609 00610 if (actuator->m_navmesh) 00611 actuator->m_navmesh->RegisterActuator(actuator); 00612 00613 return PY_SET_ATTR_SUCCESS; 00614 } 00615 00616 PyObject* KX_SteeringActuator::pyattr_get_steeringVec(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef) 00617 { 00618 KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self); 00619 const MT_Vector3& steeringVec = actuator->GetSteeringVec(); 00620 return PyObjectFrom(steeringVec); 00621 } 00622 00623 #endif // DISABLE_PYTHON 00624 00625 /* eof */ 00626