799d214275cde856847f17dd74df53c11ecb4874
[blender-staging.git] / source / gameengine / Ketsji / KX_SteeringActuator.cpp
1 /*
2  * Add steering behaviors
3  *
4  *
5  * ***** BEGIN GPL LICENSE BLOCK *****
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
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 General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  * The Original Code is Copyright (C) 2001-2002 by NaN Holding BV.
22  * All rights reserved.
23  *
24  * The Original Code is: all of this file.
25  *
26  * Contributor(s): none yet.
27  *
28  * ***** END GPL LICENSE BLOCK *****
29  */
30
31 #include "BLI_math.h"
32 #include "KX_SteeringActuator.h"
33 #include "KX_GameObject.h"
34 #include "KX_NavMeshObject.h"
35 #include "KX_ObstacleSimulation.h"
36 #include "KX_PythonInit.h"
37 #include "KX_PyMath.h"
38 #include "Recast.h"
39
40 /* ------------------------------------------------------------------------- */
41 /* Native functions                                                          */
42 /* ------------------------------------------------------------------------- */
43
44 KX_SteeringActuator::KX_SteeringActuator(SCA_IObject *gameobj,
45                                          int mode,
46                                          KX_GameObject *target,
47                                          KX_GameObject *navmesh,
48                                          float distance,
49                                          float velocity,
50                                          float acceleration,
51                                          float turnspeed,
52                                          bool  isSelfTerminated,
53                                          int pathUpdatePeriod,
54                                          KX_ObstacleSimulation* simulation,
55                                          short facingmode,
56                                          bool normalup,
57                                          bool enableVisualization,
58                                          bool lockzvel)
59     : SCA_IActuator(gameobj, KX_ACT_STEERING),
60       m_target(target),
61       m_mode(mode),
62       m_distance(distance),
63       m_velocity(velocity),
64       m_acceleration(acceleration),
65       m_turnspeed(turnspeed),
66       m_simulation(simulation),
67       m_updateTime(0),
68       m_obstacle(NULL),
69       m_isActive(false),
70       m_isSelfTerminated(isSelfTerminated),
71       m_enableVisualization(enableVisualization),
72       m_facingMode(facingmode),
73       m_normalUp(normalup),
74       m_pathLen(0),
75       m_pathUpdatePeriod(pathUpdatePeriod),
76       m_lockzvel(lockzvel),
77       m_wayPointIdx(-1),
78       m_steerVec(MT_Vector3(0, 0, 0))
79 {
80         m_navmesh = static_cast<KX_NavMeshObject*>(navmesh);
81         if (m_navmesh)
82                 m_navmesh->RegisterActuator(this);
83         if (m_target)
84                 m_target->RegisterActuator(this);
85         
86         if (m_simulation)
87                 m_obstacle = m_simulation->GetObstacle((KX_GameObject*)gameobj);
88         KX_GameObject* parent = ((KX_GameObject*)gameobj)->GetParent();
89         if (m_facingMode>0 && parent)
90         {
91                 m_parentlocalmat = parent->GetSGNode()->GetLocalOrientation();
92         }
93         else
94                 m_parentlocalmat.setIdentity();
95
96
97 KX_SteeringActuator::~KX_SteeringActuator()
98 {
99         if (m_navmesh)
100                 m_navmesh->UnregisterActuator(this);
101         if (m_target)
102                 m_target->UnregisterActuator(this);
103
104
105 CValue* KX_SteeringActuator::GetReplica()
106 {
107         KX_SteeringActuator* replica = new KX_SteeringActuator(*this);
108         // replication just copy the m_base pointer => common random generator
109         replica->ProcessReplica();
110         return replica;
111 }
112
113 void KX_SteeringActuator::ProcessReplica()
114 {
115         if (m_target)
116                 m_target->RegisterActuator(this);
117         if (m_navmesh)
118                 m_navmesh->RegisterActuator(this);
119         SCA_IActuator::ProcessReplica();
120 }
121
122 void KX_SteeringActuator::ReParent(SCA_IObject* parent)
123 {
124         SCA_IActuator::ReParent(parent);
125         if (m_simulation)
126                 m_obstacle = m_simulation->GetObstacle((KX_GameObject*)m_gameobj);
127 }
128
129 bool KX_SteeringActuator::UnlinkObject(SCA_IObject* clientobj)
130 {
131         if (clientobj == m_target)
132         {
133                 m_target = NULL;
134                 return true;
135         }
136         else if (clientobj == m_navmesh)
137         {
138                 m_navmesh = NULL;
139                 return true;
140         }
141         return false;
142 }
143
144 void KX_SteeringActuator::Relink(CTR_Map<CTR_HashedPtr, void*> *obj_map)
145 {
146         void **h_obj = (*obj_map)[m_target];
147         if (h_obj) {
148                 if (m_target)
149                         m_target->UnregisterActuator(this);
150                 m_target = (KX_GameObject*)(*h_obj);
151                 m_target->RegisterActuator(this);
152         }
153
154         h_obj = (*obj_map)[m_navmesh];
155         if (h_obj) {
156                 if (m_navmesh)
157                         m_navmesh->UnregisterActuator(this);
158                 m_navmesh = (KX_NavMeshObject*)(*h_obj);
159                 m_navmesh->RegisterActuator(this);
160         }
161 }
162
163 bool KX_SteeringActuator::Update(double curtime, bool frame)
164 {
165         if (frame)
166         {
167                 double delta =  curtime - m_updateTime;
168                 m_updateTime = curtime;
169                 
170                 if (m_posevent && !m_isActive)
171                 {
172                         delta = 0;
173                         m_pathUpdateTime = -1;
174                         m_updateTime = curtime;
175                         m_isActive = true;
176                 }
177                 bool bNegativeEvent = IsNegativeEvent();
178                 if (bNegativeEvent)
179                         m_isActive = false;
180
181                 RemoveAllEvents();
182
183                 if (!delta)
184                         return true;
185
186                 if (bNegativeEvent || !m_target)
187                         return false; // do nothing on negative events
188
189                 KX_GameObject *obj = (KX_GameObject*) GetParent();
190                 const MT_Point3& mypos = obj->NodeGetWorldPosition();
191                 const MT_Point3& targpos = m_target->NodeGetWorldPosition();
192                 MT_Vector3 vectotarg = targpos - mypos;
193                 MT_Vector3 vectotarg2d = vectotarg;
194                 vectotarg2d.z() = 0;
195                 m_steerVec = MT_Vector3(0, 0, 0);
196                 bool apply_steerforce = false;
197                 bool terminate = true;
198
199                 switch (m_mode) {
200                         case KX_STEERING_SEEK:
201                                 if (vectotarg2d.length2()>m_distance*m_distance)
202                                 {
203                                         terminate = false;
204                                         m_steerVec = vectotarg;
205                                         m_steerVec.normalize();
206                                         apply_steerforce = true;
207                                 }
208                                 break;
209                         case KX_STEERING_FLEE:
210                                 if (vectotarg2d.length2()<m_distance*m_distance)
211                                 {
212                                         terminate = false;
213                                         m_steerVec = -vectotarg;
214                                         m_steerVec.normalize();
215                                         apply_steerforce = true;
216                                 }
217                                 break;
218                         case KX_STEERING_PATHFOLLOWING:
219                                 if (m_navmesh && vectotarg.length2()>m_distance*m_distance)
220                                 {
221                                         terminate = false;
222
223                                         static const MT_Scalar WAYPOINT_RADIUS(0.25);
224
225                                         if (m_pathUpdateTime<0 || (m_pathUpdatePeriod>=0 && 
226                                                                                                 curtime - m_pathUpdateTime>((double)m_pathUpdatePeriod/1000)))
227                                         {
228                                                 m_pathUpdateTime = curtime;
229                                                 m_pathLen = m_navmesh->FindPath(mypos, targpos, m_path, MAX_PATH_LENGTH);
230                                                 m_wayPointIdx = m_pathLen > 1 ? 1 : -1;
231                                         }
232
233                                         if (m_wayPointIdx>0)
234                                         {
235                                                 MT_Vector3 waypoint(&m_path[3*m_wayPointIdx]);
236                                                 if ((waypoint-mypos).length2()<WAYPOINT_RADIUS*WAYPOINT_RADIUS)
237                                                 {
238                                                         m_wayPointIdx++;
239                                                         if (m_wayPointIdx>=m_pathLen)
240                                                         {
241                                                                 m_wayPointIdx = -1;
242                                                                 terminate = true;
243                                                         }
244                                                         else
245                                                                 waypoint.setValue(&m_path[3*m_wayPointIdx]);
246                                                 }
247
248                                                 m_steerVec = waypoint - mypos;
249                                                 apply_steerforce = true;
250
251                                                 
252                                                 if (m_enableVisualization)
253                                                 {
254                                                         //debug draw
255                                                         static const MT_Vector3 PATH_COLOR(1,0,0);
256                                                         m_navmesh->DrawPath(m_path, m_pathLen, PATH_COLOR);
257                                                 }
258                                         }
259                                         
260                                 }
261                                 break;
262                 }
263
264                 if (apply_steerforce)
265                 {
266                         bool isdyna = obj->IsDynamic();
267                         if (isdyna)
268                                 m_steerVec.z() = 0;
269                         if (!m_steerVec.fuzzyZero())
270                                 m_steerVec.normalize();
271                         MT_Vector3 newvel = m_velocity * m_steerVec;
272
273                         //adjust velocity to avoid obstacles
274                         if (m_simulation && m_obstacle /*&& !newvel.fuzzyZero()*/)
275                         {
276                                 if (m_enableVisualization)
277                                         KX_RasterizerDrawDebugLine(mypos, mypos + newvel, MT_Vector3(1.0, 0.0, 0.0));
278                                 m_simulation->AdjustObstacleVelocity(m_obstacle, m_mode!=KX_STEERING_PATHFOLLOWING ? m_navmesh : NULL,
279                                                                 newvel, m_acceleration*delta, m_turnspeed/180.0f*M_PI*delta);
280                                 if (m_enableVisualization)
281                                         KX_RasterizerDrawDebugLine(mypos, mypos + newvel, MT_Vector3(0.0, 1.0, 0.0));
282                         }
283
284                         HandleActorFace(newvel);
285                         if (isdyna)
286                         {
287                                 //temporary solution: set 2D steering velocity directly to obj
288                                 //correct way is to apply physical force
289                                 MT_Vector3 curvel = obj->GetLinearVelocity();
290
291                                 if (m_lockzvel)
292                                         newvel.z() = 0.0f;
293                                 else
294                                         newvel.z() = curvel.z();
295
296                                 obj->setLinearVelocity(newvel, false);
297                         }
298                         else
299                         {
300                                 MT_Vector3 movement = delta*newvel;
301                                 obj->ApplyMovement(movement, false);
302                         }
303                 }
304                 else
305                 {
306                         if (m_simulation && m_obstacle)
307                         {
308                                 m_obstacle->dvel[0] = 0.f;
309                                 m_obstacle->dvel[1] = 0.f;
310                         }
311                         
312                 }
313
314                 if (terminate && m_isSelfTerminated)
315                         return false;
316         }
317
318         return true;
319 }
320
321 const MT_Vector3& KX_SteeringActuator::GetSteeringVec()
322 {
323         static MT_Vector3 ZERO_VECTOR(0, 0, 0);
324         if (m_isActive)
325                 return m_steerVec;
326         else
327                 return ZERO_VECTOR;
328 }
329
330 inline float vdot2(const float* a, const float* b)
331 {
332         return a[0]*b[0] + a[2]*b[2];
333 }
334 static bool barDistSqPointToTri(const float* p, const float* a, const float* b, const float* c)
335 {
336         float v0[3], v1[3], v2[3];
337         rcVsub(v0, c,a);
338         rcVsub(v1, b,a);
339         rcVsub(v2, p,a);
340
341         const float dot00 = vdot2(v0, v0);
342         const float dot01 = vdot2(v0, v1);
343         const float dot02 = vdot2(v0, v2);
344         const float dot11 = vdot2(v1, v1);
345         const float dot12 = vdot2(v1, v2);
346
347         // Compute barycentric coordinates
348         float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01);
349         float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
350         float v = (dot00 * dot12 - dot01 * dot02) * invDenom;
351
352         float ud = u<0.f ? -u : (u>1.f ? u-1.f : 0.f);
353         float vd = v<0.f ? -v : (v>1.f ? v-1.f : 0.f);
354         return ud * ud + vd * vd;
355 }
356
357 inline void flipAxes(float* vec)
358 {
359         std::swap(vec[1],vec[2]);
360 }
361
362 static bool getNavmeshNormal(dtStatNavMesh* navmesh, const MT_Vector3& pos, MT_Vector3& normal)
363 {
364         static const float polyPickExt[3] = {2, 4, 2};
365         float spos[3];
366         pos.getValue(spos);
367         flipAxes(spos);
368         dtStatPolyRef sPolyRef = navmesh->findNearestPoly(spos, polyPickExt);
369         if (sPolyRef == 0)
370                 return false;
371         const dtStatPoly* p = navmesh->getPoly(sPolyRef-1);
372         const dtStatPolyDetail* pd = navmesh->getPolyDetail(sPolyRef-1);
373
374         float distMin = FLT_MAX;
375         int idxMin = -1;
376         for (int i = 0; i < pd->ntris; ++i)
377         {
378                 const unsigned char* t = navmesh->getDetailTri(pd->tbase+i);
379                 const float* v[3];
380                 for (int j = 0; j < 3; ++j)
381                 {
382                         if (t[j] < p->nv)
383                                 v[j] = navmesh->getVertex(p->v[t[j]]);
384                         else
385                                 v[j] = navmesh->getDetailVertex(pd->vbase+(t[j]-p->nv));
386                 }
387                 float dist = barDistSqPointToTri(spos, v[0], v[1], v[2]);
388                 if (dist<distMin)
389                 {
390                         distMin = dist;
391                         idxMin = i;
392                 }
393         }
394
395         if (idxMin>=0)
396         {
397                 const unsigned char* t = navmesh->getDetailTri(pd->tbase+idxMin);
398                 const float* v[3];
399                 for (int j = 0; j < 3; ++j)
400                 {
401                         if (t[j] < p->nv)
402                                 v[j] = navmesh->getVertex(p->v[t[j]]);
403                         else
404                                 v[j] = navmesh->getDetailVertex(pd->vbase+(t[j]-p->nv));
405                 }
406                 MT_Vector3 tri[3];
407                 for (size_t j=0; j<3; j++)
408                         tri[j].setValue(v[j][0],v[j][2],v[j][1]);
409                 MT_Vector3 a,b;
410                 a = tri[1]-tri[0];
411                 b = tri[2]-tri[0];
412                 normal = b.cross(a).safe_normalized();
413                 return true;
414         }
415
416         return false;
417 }
418
419 void KX_SteeringActuator::HandleActorFace(MT_Vector3& velocity)
420 {
421         if (m_facingMode==0 && (!m_navmesh || !m_normalUp))
422                 return;
423         KX_GameObject* curobj = (KX_GameObject*) GetParent();
424         MT_Vector3 dir = m_facingMode==0 ?  curobj->NodeGetLocalOrientation().getColumn(1) : velocity;
425         if (dir.fuzzyZero())
426                 return;
427         dir.normalize();
428         MT_Vector3 up(0,0,1);
429         MT_Vector3 left;
430         MT_Matrix3x3 mat;
431         
432         if (m_navmesh && m_normalUp)
433         {
434                 dtStatNavMesh* navmesh =  m_navmesh->GetNavMesh();
435                 MT_Vector3 normal;
436                 MT_Vector3 trpos = m_navmesh->TransformToLocalCoords(curobj->NodeGetWorldPosition());
437                 if (getNavmeshNormal(navmesh, trpos, normal))
438                 {
439
440                         left = (dir.cross(up)).safe_normalized();
441                         dir = (-left.cross(normal)).safe_normalized();
442                         up = normal;
443                 }
444         }
445
446         switch (m_facingMode)
447         {
448         case 1: // TRACK X
449                 {
450                         left  = dir.safe_normalized();
451                         dir = -(left.cross(up)).safe_normalized();
452                         break;
453                 };
454         case 2: // TRACK Y
455                 {
456                         left  = (dir.cross(up)).safe_normalized();
457                         break;
458                 }
459
460         case 3: // track Z
461                 {
462                         left = up.safe_normalized();
463                         up = dir.safe_normalized();
464                         dir = left;
465                         left  = (dir.cross(up)).safe_normalized();
466                         break;
467                 }
468
469         case 4: // TRACK -X
470                 {
471                         left  = -dir.safe_normalized();
472                         dir = -(left.cross(up)).safe_normalized();
473                         break;
474                 };
475         case 5: // TRACK -Y
476                 {
477                         left  = (-dir.cross(up)).safe_normalized();
478                         dir = -dir;
479                         break;
480                 }
481         case 6: // track -Z
482                 {
483                         left = up.safe_normalized();
484                         up = -dir.safe_normalized();
485                         dir = left;
486                         left  = (dir.cross(up)).safe_normalized();
487                         break;
488                 }
489         }
490
491         mat.setValue (
492                 left[0], dir[0],up[0], 
493                 left[1], dir[1],up[1],
494                 left[2], dir[2],up[2]
495         );
496
497         
498         
499         KX_GameObject* parentObject = curobj->GetParent();
500         if (parentObject)
501         { 
502                 MT_Point3 localpos;
503                 localpos = curobj->GetSGNode()->GetLocalPosition();
504                 MT_Matrix3x3 parentmatinv;
505                 parentmatinv = parentObject->NodeGetWorldOrientation ().inverse ();
506                 mat = parentmatinv * mat;
507                 mat = m_parentlocalmat * mat;
508                 curobj->NodeSetLocalOrientation(mat);
509                 curobj->NodeSetLocalPosition(localpos);
510         }
511         else
512         {
513                 curobj->NodeSetLocalOrientation(mat);
514         }
515
516 }
517
518 #ifdef WITH_PYTHON
519
520 /* ------------------------------------------------------------------------- */
521 /* Python functions                                                          */
522 /* ------------------------------------------------------------------------- */
523
524 /* Integration hooks ------------------------------------------------------- */
525 PyTypeObject KX_SteeringActuator::Type = {
526         PyVarObject_HEAD_INIT(NULL, 0)
527         "KX_SteeringActuator",
528         sizeof(PyObjectPlus_Proxy),
529         0,
530         py_base_dealloc,
531         0,
532         0,
533         0,
534         0,
535         py_base_repr,
536         0,0,0,0,0,0,0,0,0,
537         Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
538         0,0,0,0,0,0,0,
539         Methods,
540         0,
541         0,
542         &SCA_IActuator::Type,
543         0,0,0,0,0,0,
544         py_base_new
545 };
546
547 PyMethodDef KX_SteeringActuator::Methods[] = {
548         {NULL,NULL} //Sentinel
549 };
550
551 PyAttributeDef KX_SteeringActuator::Attributes[] = {
552         KX_PYATTRIBUTE_INT_RW("behavior", KX_STEERING_NODEF+1, KX_STEERING_MAX-1, true, KX_SteeringActuator, m_mode),
553         KX_PYATTRIBUTE_RW_FUNCTION("target", KX_SteeringActuator, pyattr_get_target, pyattr_set_target),
554         KX_PYATTRIBUTE_RW_FUNCTION("navmesh", KX_SteeringActuator, pyattr_get_navmesh, pyattr_set_navmesh),
555         KX_PYATTRIBUTE_FLOAT_RW("distance", 0.0f, 1000.0f, KX_SteeringActuator, m_distance),
556         KX_PYATTRIBUTE_FLOAT_RW("velocity", 0.0f, 1000.0f, KX_SteeringActuator, m_velocity),
557         KX_PYATTRIBUTE_FLOAT_RW("acceleration", 0.0f, 1000.0f, KX_SteeringActuator, m_acceleration),
558         KX_PYATTRIBUTE_FLOAT_RW("turnspeed", 0.0f, 720.0f, KX_SteeringActuator, m_turnspeed),
559         KX_PYATTRIBUTE_BOOL_RW("selfterminated", KX_SteeringActuator, m_isSelfTerminated),
560         KX_PYATTRIBUTE_BOOL_RW("enableVisualization", KX_SteeringActuator, m_enableVisualization),
561         KX_PYATTRIBUTE_RO_FUNCTION("steeringVec", KX_SteeringActuator, pyattr_get_steeringVec),
562         KX_PYATTRIBUTE_SHORT_RW("facingMode", 0, 6, true, KX_SteeringActuator, m_facingMode),
563         KX_PYATTRIBUTE_INT_RW("pathUpdatePeriod", -1, 100000, true, KX_SteeringActuator, m_pathUpdatePeriod),
564         KX_PYATTRIBUTE_BOOL_RW("lockZVelocity", KX_SteeringActuator, m_lockzvel),
565         { NULL }        //Sentinel
566 };
567
568 PyObject *KX_SteeringActuator::pyattr_get_target(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef)
569 {
570         KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self);
571         if (!actuator->m_target)
572                 Py_RETURN_NONE;
573         else
574                 return actuator->m_target->GetProxy();
575 }
576
577 int KX_SteeringActuator::pyattr_set_target(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef, PyObject *value)
578 {
579         KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self);
580         KX_GameObject *gameobj;
581
582         if (!ConvertPythonToGameObject(value, &gameobj, true, "actuator.object = value: KX_SteeringActuator"))
583                 return PY_SET_ATTR_FAIL; // ConvertPythonToGameObject sets the error
584
585         if (actuator->m_target != NULL)
586                 actuator->m_target->UnregisterActuator(actuator);
587
588         actuator->m_target = (KX_GameObject*) gameobj;
589
590         if (actuator->m_target)
591                 actuator->m_target->RegisterActuator(actuator);
592
593         return PY_SET_ATTR_SUCCESS;
594 }
595
596 PyObject *KX_SteeringActuator::pyattr_get_navmesh(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef)
597 {
598         KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self);
599         if (!actuator->m_navmesh)
600                 Py_RETURN_NONE;
601         else
602                 return actuator->m_navmesh->GetProxy();
603 }
604
605 int KX_SteeringActuator::pyattr_set_navmesh(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef, PyObject *value)
606 {
607         KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self);
608         KX_GameObject *gameobj;
609
610         if (!ConvertPythonToGameObject(value, &gameobj, true, "actuator.object = value: KX_SteeringActuator"))
611                 return PY_SET_ATTR_FAIL; // ConvertPythonToGameObject sets the error
612
613     if (dynamic_cast<KX_NavMeshObject *>(gameobj) == NULL)
614         {
615                 PyErr_Format(PyExc_TypeError, "KX_NavMeshObject is expected");
616                 return PY_SET_ATTR_FAIL;
617         }
618
619         if (actuator->m_navmesh != NULL)
620                 actuator->m_navmesh->UnregisterActuator(actuator);
621
622         actuator->m_navmesh = static_cast<KX_NavMeshObject*>(gameobj);
623
624         if (actuator->m_navmesh)
625                 actuator->m_navmesh->RegisterActuator(actuator);
626
627         return PY_SET_ATTR_SUCCESS;
628 }
629
630 PyObject *KX_SteeringActuator::pyattr_get_steeringVec(void *self, const struct KX_PYATTRIBUTE_DEF *attrdef)
631 {
632         KX_SteeringActuator* actuator = static_cast<KX_SteeringActuator*>(self);
633         const MT_Vector3& steeringVec = actuator->GetSteeringVec();
634         return PyObjectFrom(steeringVec);
635 }
636
637 #endif // WITH_PYTHON
638
639 /* eof */
640