Browse Source

Merge branch 'release/1.10.x'

rdb 5 years ago
parent
commit
e099d9e787
54 changed files with 1260 additions and 44 deletions
  1. 33 22
      direct/src/stdpy/pickle.py
  2. 3 0
      dtool/src/interrogate/functionRemap.cxx
  3. 23 10
      dtool/src/interrogate/interfaceMakerPythonNative.cxx
  4. 2 2
      makepanda/makepanda.py
  5. 46 0
      panda/src/collide/collisionHandlerEvent.cxx
  6. 9 1
      panda/src/collide/collisionHandlerEvent.h
  7. 70 0
      panda/src/collide/collisionHandlerEvent_ext.cxx
  8. 38 0
      panda/src/collide/collisionHandlerEvent_ext.h
  9. 21 8
      panda/src/collide/collisionHandlerFloor.cxx
  10. 3 0
      panda/src/collide/collisionHandlerFloor.h
  11. 29 0
      panda/src/collide/collisionHandlerGravity.cxx
  12. 3 0
      panda/src/collide/collisionHandlerGravity.h
  13. 5 0
      panda/src/collide/collisionHandlerPhysical.h
  14. 108 0
      panda/src/collide/collisionHandlerPhysical_ext.cxx
  15. 38 0
      panda/src/collide/collisionHandlerPhysical_ext.h
  16. 21 0
      panda/src/collide/collisionHandlerPusher.cxx
  17. 3 0
      panda/src/collide/collisionHandlerPusher.h
  18. 3 0
      panda/src/collide/collisionHandlerQueue.h
  19. 27 0
      panda/src/collide/collisionHandlerQueue_ext.cxx
  20. 37 0
      panda/src/collide/collisionHandlerQueue_ext.h
  21. 4 0
      panda/src/collide/collisionTraverser.h
  22. 73 0
      panda/src/collide/collisionTraverser_ext.cxx
  23. 38 0
      panda/src/collide/collisionTraverser_ext.h
  24. 4 0
      panda/src/collide/p3collide_ext_composite.cxx
  25. 2 1
      panda/src/display/graphicsStateGuardian_ext.cxx
  26. 3 0
      panda/src/egg/eggAnimPreload.h
  27. 2 0
      panda/src/egg/eggComment.h
  28. 32 0
      panda/src/egg/eggComment_ext.cxx
  29. 37 0
      panda/src/egg/eggComment_ext.h
  30. 2 0
      panda/src/egg/eggCoordinateSystem.h
  31. 32 0
      panda/src/egg/eggCoordinateSystem_ext.cxx
  32. 37 0
      panda/src/egg/eggCoordinateSystem_ext.h
  33. 2 0
      panda/src/egg/eggNode.h
  34. 93 0
      panda/src/egg/eggNode_ext.cxx
  35. 42 0
      panda/src/egg/eggNode_ext.h
  36. 10 0
      panda/src/egg/lexer.cxx.prebuilt
  37. 10 0
      panda/src/egg/lexer.lxx
  38. 1 0
      panda/src/egg/lexerDefs.h
  39. 4 0
      panda/src/egg/p3egg_ext_composite.cxx
  40. 2 0
      panda/src/egg/parser.cxx.prebuilt
  41. 2 0
      panda/src/egg/parser.yxx
  42. 4 0
      panda/src/express/pointerToArray.h
  43. 6 0
      panda/src/express/pointerToArrayBase.h
  44. 35 0
      panda/src/express/pointerToArray_ext.I
  45. 4 0
      panda/src/express/pointerToArray_ext.h
  46. 2 0
      panda/src/pgraph/loaderFileTypeRegistry.h
  47. 10 0
      panda/src/pgraph/loaderFileTypeRegistry_ext.cxx
  48. 2 0
      panda/src/pgraph/loaderFileTypeRegistry_ext.h
  49. 108 0
      tests/collide/test_collision_handlers.py
  50. 31 0
      tests/collide/test_collision_traverser.py
  51. 71 0
      tests/express/test_pointertoarray.py
  52. 9 0
      tests/pgraph/test_loader_types.py
  53. 10 0
      tests/putil/test_bitmask.py
  54. 14 0
      tests/stdpy/test_pickle.py

+ 33 - 22
direct/src/stdpy/pickle.py

@@ -21,8 +21,12 @@ shared context between all objects written by that Pickler.
 Unfortunately, cPickle cannot be supported, because it does not
 support extensions of this nature. """
 
+__all__ = ["PickleError", "PicklingError", "UnpicklingError", "Pickler",
+           "Unpickler", "dump", "dumps", "load", "loads",
+           "HIGHEST_PROTOCOL", "DEFAULT_PROTOCOL"]
+
 import sys
-from panda3d.core import BamWriter, BamReader
+from panda3d.core import BamWriter, BamReader, TypedObject
 from copyreg import dispatch_table
 
 
@@ -30,17 +34,29 @@ from copyreg import dispatch_table
 # with the local pickle.py.
 pickle = __import__('pickle')
 
+HIGHEST_PROTOCOL = pickle.HIGHEST_PROTOCOL
+DEFAULT_PROTOCOL = pickle.DEFAULT_PROTOCOL
+
+PickleError = pickle.PickleError
 PicklingError = pickle.PicklingError
+UnpicklingError = pickle.UnpicklingError
+
 BasePickler = pickle._Pickler
 BaseUnpickler = pickle._Unpickler
 
 
-class _Pickler(BasePickler):
+class Pickler(BasePickler):
 
     def __init__(self, *args, **kw):
         self.bamWriter = BamWriter()
+        self._canonical = {}
         BasePickler.__init__(self, *args, **kw)
 
+    def clear_memo(self):
+        BasePickler.clear_memo(self)
+        self._canonical.clear()
+        self.bamWriter = BamWriter()
+
     # We have to duplicate most of the save() method, so we can add
     # support for __reduce_persist__().
 
@@ -54,6 +70,21 @@ class _Pickler(BasePickler):
             self.save_pers(pid)
             return
 
+        # Check if this is a Panda type that we've already saved; if so, store
+        # a mapping to the canonical copy, so that Python's memoization system
+        # works properly.  This is needed because Python uses id(obj) for
+        # memoization, but there may be multiple Python wrappers for the same
+        # C++ pointer, and we don't want that to result in duplication.
+        t = type(obj)
+        if issubclass(t, TypedObject.__base__):
+            canonical = self._canonical.get(obj.this)
+            if canonical is not None:
+                obj = canonical
+            else:
+                # First time we're seeing this C++ pointer; save it as the
+                # "canonical" version.
+                self._canonical[obj.this] = obj
+
         # Check the memo
         x = self.memo.get(id(obj))
         if x:
@@ -61,7 +92,6 @@ class _Pickler(BasePickler):
             return
 
         # Check the type dispatch table
-        t = type(obj)
         f = self.dispatch.get(t)
         if f:
             f(self, obj) # Call unbound method with explicit self
@@ -146,25 +176,6 @@ class Unpickler(BaseUnpickler):
     BaseUnpickler.dispatch[pickle.REDUCE[0]] = load_reduce
 
 
-if sys.version_info >= (3, 8):
-    # In Python 3.8 and up, we can use the C implementation of Pickler, which
-    # supports a reducer_override method.
-    class Pickler(pickle.Pickler):
-        def __init__(self, *args, **kw):
-            self.bamWriter = BamWriter()
-            pickle.Pickler.__init__(self, *args, **kw)
-
-        def reducer_override(self, obj):
-            reduce = getattr(obj, "__reduce_persist__", None)
-            if reduce:
-                return reduce(self)
-
-            return NotImplemented
-else:
-    # Otherwise, we have to use our custom version that overrides save().
-    Pickler = _Pickler
-
-
 # Shorthands
 from io import BytesIO
 

+ 3 - 0
dtool/src/interrogate/functionRemap.cxx

@@ -920,6 +920,9 @@ setup_properties(const InterrogateFunction &ifunc, InterfaceMaker *interface_mak
             || fname == "__delattr__") {
       // Just to prevent these from getting keyword arguments.
 
+    } else if (fname == "__setstate__") {
+      _args_type = InterfaceMaker::AT_single_arg;
+
     } else {
       if (_args_type == InterfaceMaker::AT_varargs) {
         // Every other method can take keyword arguments, if they take more

+ 23 - 10
dtool/src/interrogate/interfaceMakerPythonNative.cxx

@@ -3637,18 +3637,23 @@ write_function_for_name(ostream &out, Object *obj,
     std::string ClassName = make_safe_name(obj->_itype.get_scoped_name());
     std::string cClassName = obj->_itype.get_true_name();
     // string class_name = remap->_cpptype->get_simple_name();
+    CPPStructType *struct_type = obj->_itype._cpptype->as_struct_type();
 
     // If this is a non-static __setstate__, we run the default constructor.
-    if (remap->_cppfunc->get_local_name() == "__setstate__") {
-      out << "  if (DtoolInstance_VOID_PTR(self) != nullptr) {\n"
-          << "    Dtool_Raise_TypeError(\"C++ object is already constructed.\");\n";
-      error_return(out, 4, return_flags);
-      out << "  }\n"
-          << "  " << cClassName << " *local_this = new " << cClassName << ";\n"
-          << "  DTool_PyInit_Finalize(self, local_this, &Dtool_" << ClassName
-          << ", false, false);\n"
-          << "  if (local_this == nullptr) {\n"
-          << "    PyErr_NoMemory();\n";
+    if (remap->_cppfunc->get_local_name() == "__setstate__" &&
+        !struct_type->is_abstract()) {
+      out << "  " << cClassName << " *local_this = nullptr;\n"
+          << "  if (DtoolInstance_VOID_PTR(self) == nullptr) {\n"
+          << "    local_this = new " << cClassName << ";\n"
+          << "    DTool_PyInit_Finalize(self, local_this, &Dtool_" << ClassName
+          << ", true, false);\n"
+          << "    if (local_this == nullptr) {\n"
+          << "      PyErr_NoMemory();\n";
+      error_return(out, 6, return_flags);
+      out << "    }\n"
+          << "  } else if (!Dtool_Call_ExtractThisPointer_NonConst(self, Dtool_" << ClassName << ", "
+          << "(void **)&local_this, \"" << classNameFromCppName(cClassName, false)
+          << "." << methodNameFromCppName(remap, cClassName, false) << "\")) {\n";
     }
     else if (all_nonconst) {
       // All remaps are non-const.  Also check that this object isn't const.
@@ -3685,6 +3690,14 @@ write_function_for_name(ostream &out, Object *obj,
     args_type = AT_varargs;
   }
 
+  // If this is a __setstate__ taking multiple arguments, and we're given a
+  // tuple as argument, unpack it.
+  if (args_type == AT_single_arg && max_required_args > 1 &&
+      remap->_cppfunc->get_local_name() == "__setstate__") {
+    out << "  PyObject *args = arg;\n";
+    args_type = AT_varargs;
+  }
+
   if (args_type == AT_keyword_args || args_type == AT_varargs) {
     max_required_args = collapse_default_remaps(map_sets, max_required_args);
   }

+ 2 - 2
makepanda/makepanda.py

@@ -4397,7 +4397,7 @@ if not PkgSkip("EGG"):
         IGATEFILES.remove("parser.h")
     TargetAdd('libp3egg.in', opts=OPTS, input=IGATEFILES)
     TargetAdd('libp3egg.in', opts=['IMOD:panda3d.egg', 'ILIB:libp3egg', 'SRCDIR:panda/src/egg'])
-    PyTargetAdd('p3egg_eggGroupNode_ext.obj', opts=OPTS, input='eggGroupNode_ext.cxx')
+    PyTargetAdd('p3egg_ext_composite.obj', opts=OPTS, input='p3egg_ext_composite.cxx')
 
 #
 # DIRECTORY: panda/src/egg2pg/
@@ -4486,7 +4486,7 @@ if not PkgSkip("EGG"):
     PyTargetAdd('egg_module.obj', opts=['IMOD:panda3d.egg', 'ILIB:egg', 'IMPORT:panda3d.core'])
 
     PyTargetAdd('egg.pyd', input='egg_module.obj')
-    PyTargetAdd('egg.pyd', input='p3egg_eggGroupNode_ext.obj')
+    PyTargetAdd('egg.pyd', input='p3egg_ext_composite.obj')
     PyTargetAdd('egg.pyd', input='libp3egg_igate.obj')
     PyTargetAdd('egg.pyd', input='libp3egg2pg_igate.obj')
     PyTargetAdd('egg.pyd', input='libpandaegg.dll')

+ 46 - 0
panda/src/collide/collisionHandlerEvent.cxx

@@ -153,6 +153,52 @@ flush() {
   end_group();
 }
 
+/**
+ * Serializes this object, to implement pickle support.
+ */
+void CollisionHandlerEvent::
+write_datagram(Datagram &dg) const {
+  dg.add_uint32(_in_patterns.size());
+  for (const std::string &pattern : _in_patterns) {
+    dg.add_string(pattern);
+  }
+
+  dg.add_uint32(_again_patterns.size());
+  for (const std::string &pattern : _again_patterns) {
+    dg.add_string(pattern);
+  }
+
+  dg.add_uint32(_out_patterns.size());
+  for (const std::string &pattern : _out_patterns) {
+    dg.add_string(pattern);
+  }
+}
+
+/**
+ * Restores the object state from the given datagram, previously obtained using
+ * __getstate__.
+ */
+void CollisionHandlerEvent::
+read_datagram(DatagramIterator &scan) {
+  _in_patterns.clear();
+  size_t num_in_patterns = scan.get_uint32();
+  for (size_t i = 0; i < num_in_patterns; ++i) {
+    add_in_pattern(scan.get_string());
+  }
+
+  _again_patterns.clear();
+  size_t num_again_patterns = scan.get_uint32();
+  for (size_t i = 0; i < num_again_patterns; ++i) {
+    add_again_pattern(scan.get_string());
+  }
+
+  _out_patterns.clear();
+  size_t num_out_patterns = scan.get_uint32();
+  for (size_t i = 0; i < num_out_patterns; ++i) {
+    add_out_pattern(scan.get_string());
+  }
+}
+
 /**
  * Throws whatever events are suggested by the list of patterns.
  */

+ 9 - 1
panda/src/collide/collisionHandlerEvent.h

@@ -22,6 +22,7 @@
 
 #include "vector_string.h"
 #include "pointerTo.h"
+#include "extension.h"
 
 /**
  * A specialized kind of CollisionHandler that throws an event for each
@@ -61,12 +62,19 @@ PUBLISHED:
   MAKE_SEQ(get_out_patterns, get_num_out_patterns, get_out_pattern);
 
   MAKE_SEQ_PROPERTY(in_patterns, get_num_in_patterns, get_in_pattern);
-  MAKE_SEQ_PROPERTY(again_patterns, get_num_again_patterns, get_out_pattern);
+  MAKE_SEQ_PROPERTY(again_patterns, get_num_again_patterns, get_again_pattern);
   MAKE_SEQ_PROPERTY(out_patterns, get_num_out_patterns, get_out_pattern);
 
   void clear();
   void flush();
 
+  // These help implement Python pickle support.
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+  EXTENSION(void __setstate__(PyObject *self, vector_uchar data));
+
+  void write_datagram(Datagram &destination) const;
+  void read_datagram(DatagramIterator &source);
+
 protected:
   void throw_event_for(const vector_string &patterns, CollisionEntry *entry);
   void throw_event_pattern(const std::string &pattern, CollisionEntry *entry);

+ 70 - 0
panda/src/collide/collisionHandlerEvent_ext.cxx

@@ -0,0 +1,70 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandlerEvent_ext.cxx
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#include "collisionHandlerEvent_ext.h"
+#include "collisionHandlerFloor.h"
+#include "collisionHandlerGravity.h"
+#include "collisionHandlerPusher.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickling behavior.
+ */
+PyObject *Extension<CollisionHandlerEvent>::
+__reduce__(PyObject *self) const {
+  extern struct Dtool_PyTypedObject Dtool_Datagram;
+
+  // Call the write_datagram method via Python, since it's not a virtual method
+  // on the C++ end.
+  PyObject *method_name = PyUnicode_FromString("write_datagram");
+
+  Datagram dg;
+  PyObject *destination = DTool_CreatePyInstance(&dg, Dtool_Datagram, false, false);
+
+  PyObject *retval = PyObject_CallMethodOneArg(self, method_name, destination);
+  Py_DECREF(method_name);
+  Py_DECREF(destination);
+  if (retval == nullptr) {
+    return nullptr;
+  }
+  Py_DECREF(retval);
+
+  const char *data = (const char *)dg.get_data();
+  Py_ssize_t size = dg.get_length();
+  return Py_BuildValue("O()y#", Py_TYPE(self), data, size);
+}
+
+/**
+ * Takes the value returned by __getstate__ and uses it to freshly initialize
+ * this CollisionHandlerEvent object.
+ */
+void Extension<CollisionHandlerEvent>::
+__setstate__(PyObject *self, vector_uchar data) {
+  extern struct Dtool_PyTypedObject Dtool_DatagramIterator;
+
+  // Call the read_datagram method via Python, since it's not a virtual method
+  // on the C++ end.
+  PyObject *method_name = PyUnicode_FromString("read_datagram");
+
+  Datagram dg(std::move(data));
+  DatagramIterator scan(dg);
+  PyObject *source = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false);
+
+  PyObject *retval = PyObject_CallMethodOneArg(self, method_name, source);
+  Py_DECREF(method_name);
+  Py_DECREF(source);
+  Py_XDECREF(retval);
+}
+
+#endif

+ 38 - 0
panda/src/collide/collisionHandlerEvent_ext.h

@@ -0,0 +1,38 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandlerEvent_ext.h
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#ifndef COLLISIONHANDLEREVENT_EXT_H
+#define COLLISIONHANDLEREVENT_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionHandlerEvent.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionHandlerEvent, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<CollisionHandlerEvent> : public ExtensionBase<CollisionHandlerEvent> {
+public:
+  PyObject *__reduce__(PyObject *self) const;
+  void __setstate__(PyObject *self, vector_uchar data);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // COLLISIONHANDLEREVENT_EXT_H

+ 21 - 8
panda/src/collide/collisionHandlerFloor.cxx

@@ -41,18 +41,31 @@ CollisionHandlerFloor::
 }
 
 /**
- *
-
- *
-
- *
+ * Serializes this object, to implement pickle support.
+ */
+void CollisionHandlerFloor::
+write_datagram(Datagram &dg) const {
+  CollisionHandlerPhysical::write_datagram(dg);
 
- *
+  dg.add_float64(_offset);
+  dg.add_float64(_reach);
+  dg.add_float64(_max_velocity);
+}
 
- *
+/**
+ * Restores the object state from the given datagram, previously obtained using
+ * __getstate__.
+ */
+void CollisionHandlerFloor::
+read_datagram(DatagramIterator &scan) {
+  CollisionHandlerPhysical::read_datagram(scan);
 
- *
+  _offset = scan.get_float64();
+  _reach = scan.get_float64();
+  _max_velocity = scan.get_float64();
+}
 
+/**
  *
  */
 PN_stdfloat CollisionHandlerFloor::

+ 3 - 0
panda/src/collide/collisionHandlerFloor.h

@@ -43,6 +43,9 @@ PUBLISHED:
   MAKE_PROPERTY(reach, get_reach, set_reach);
   MAKE_PROPERTY(max_velocity, get_max_velocity, set_max_velocity);
 
+  void write_datagram(Datagram &destination) const;
+  void read_datagram(DatagramIterator &source);
+
 protected:
   PN_stdfloat set_highest_collision(const NodePath &target_node_path, const NodePath &from_node_path, const Entries &entries);
   virtual bool handle_entries();

+ 29 - 0
panda/src/collide/collisionHandlerGravity.cxx

@@ -46,6 +46,35 @@ CollisionHandlerGravity::
 ~CollisionHandlerGravity() {
 }
 
+/**
+ * Serializes this object, to implement pickle support.
+ */
+void CollisionHandlerGravity::
+write_datagram(Datagram &dg) const {
+  CollisionHandlerPhysical::write_datagram(dg);
+
+  dg.add_float64(_offset);
+  dg.add_float64(_reach);
+  dg.add_float64(_max_velocity);
+  dg.add_float64(_gravity);
+  dg.add_bool(_legacy_mode);
+}
+
+/**
+ * Restores the object state from the given datagram, previously obtained using
+ * __getstate__.
+ */
+void CollisionHandlerGravity::
+read_datagram(DatagramIterator &scan) {
+  CollisionHandlerPhysical::read_datagram(scan);
+
+  _offset = scan.get_float64();
+  _reach = scan.get_float64();
+  _max_velocity = scan.get_float64();
+  _gravity = scan.get_float64();
+  _legacy_mode = scan.get_bool();
+}
+
 /**
  *
  */

+ 3 - 0
panda/src/collide/collisionHandlerGravity.h

@@ -64,6 +64,9 @@ PUBLISHED:
   MAKE_PROPERTY(gravity, get_gravity, set_gravity);
   MAKE_PROPERTY(max_velocity, get_max_velocity, set_max_velocity);
 
+  void write_datagram(Datagram &destination) const;
+  void read_datagram(DatagramIterator &source);
+
 protected:
   PN_stdfloat set_highest_collision(const NodePath &target_node_path, const NodePath &from_node_path, const Entries &entries);
   virtual bool handle_entries();

+ 5 - 0
panda/src/collide/collisionHandlerPhysical.h

@@ -54,6 +54,9 @@ PUBLISHED:
 PUBLISHED:
   MAKE_PROPERTY2(center, has_center, get_center, set_center, clear_center);
 
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+  EXTENSION(void __setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths));
+
 protected:
   bool _has_contact; // Are we in contact with anything?
 
@@ -83,6 +86,8 @@ protected:
 
   NodePath _center;
 
+  friend class Extension<CollisionHandlerPhysical>;
+
 public:
   static TypeHandle get_class_type() {
     return _type_handle;

+ 108 - 0
panda/src/collide/collisionHandlerPhysical_ext.cxx

@@ -0,0 +1,108 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandlerPhysical_ext.cxx
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#include "collisionHandlerPhysical_ext.h"
+#include "collisionHandlerEvent_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickling behavior.
+ */
+PyObject *Extension<CollisionHandlerPhysical>::
+__reduce__(PyObject *self) const {
+  extern struct Dtool_PyTypedObject Dtool_Datagram;
+  extern struct Dtool_PyTypedObject Dtool_NodePath;
+
+  // Create a tuple with all the NodePath pointers.
+  PyObject *nodepaths = PyTuple_New(_this->_colliders.size() * 2 + 1);
+  Py_ssize_t i = 0;
+
+  if (_this->has_center()) {
+    const NodePath *center = &(_this->get_center());
+    PyTuple_SET_ITEM(nodepaths, i++,
+      DTool_CreatePyInstance((void *)center, Dtool_NodePath, false, true));
+  } else {
+    PyTuple_SET_ITEM(nodepaths, i++, Py_None);
+    Py_INCREF(Py_None);
+  }
+
+  CollisionHandlerPhysical::Colliders::const_iterator it;
+  for (it = _this->_colliders.begin(); it != _this->_colliders.end(); ++it) {
+    const NodePath *collider = &(it->first);
+    const NodePath *target = &(it->second._target);
+    PyTuple_SET_ITEM(nodepaths, i++,
+      DTool_CreatePyInstance((void *)collider, Dtool_NodePath, false, true));
+    PyTuple_SET_ITEM(nodepaths, i++,
+      DTool_CreatePyInstance((void *)target, Dtool_NodePath, false, true));
+  }
+
+  // Call the write_datagram method via Python, since it's not a virtual method
+  // on the C++ end.
+  PyObject *method_name = PyUnicode_FromString("write_datagram");
+
+  Datagram dg;
+  PyObject *destination = DTool_CreatePyInstance(&dg, Dtool_Datagram, false, false);
+
+  PyObject *retval = PyObject_CallMethodOneArg(self, method_name, destination);
+  Py_DECREF(method_name);
+  Py_DECREF(destination);
+  if (retval == nullptr) {
+    return nullptr;
+  }
+  Py_DECREF(retval);
+
+  const char *data = (const char *)dg.get_data();
+  Py_ssize_t size = dg.get_length();
+  return Py_BuildValue("O()(y#N)", Py_TYPE(self), data, size, nodepaths);
+}
+
+/**
+ * Takes the value returned by __getstate__ and uses it to freshly initialize
+ * this CollisionHandlerPhysical object.
+ */
+void Extension<CollisionHandlerPhysical>::
+__setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths) {
+  extern struct Dtool_PyTypedObject Dtool_DatagramIterator;
+
+  // Call the read_datagram method via Python, since it's not a virtual method
+  // on the C++ end.
+  PyObject *method_name = PyUnicode_FromString("read_datagram");
+
+  {
+    Datagram dg(std::move(data));
+    DatagramIterator scan(dg);
+    PyObject *source = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false);
+
+    PyObject *retval = PyObject_CallMethodOneArg(self, method_name, source);
+    Py_DECREF(method_name);
+    Py_DECREF(source);
+    Py_XDECREF(retval);
+  }
+
+  PyObject *center = PyTuple_GET_ITEM(nodepaths, 0);
+  if (center != Py_None) {
+    _this->set_center(*(NodePath *)DtoolInstance_VOID_PTR(center));
+  } else {
+    _this->clear_center();
+  }
+
+  size_t num_nodepaths = Py_SIZE(nodepaths);
+  for (size_t i = 1; i < num_nodepaths;) {
+    NodePath *collider = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(nodepaths, i++));
+    NodePath *target = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(nodepaths, i++));
+    _this->add_collider(*collider, *target);
+  }
+}
+
+#endif

+ 38 - 0
panda/src/collide/collisionHandlerPhysical_ext.h

@@ -0,0 +1,38 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandlerPhysical_ext.h
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#ifndef COLLISIONHANDLERPHYSICAL_EXT_H
+#define COLLISIONHANDLERPHYSICAL_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionHandlerPhysical.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionHandlerPhysical, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<CollisionHandlerPhysical> : public ExtensionBase<CollisionHandlerPhysical> {
+public:
+  PyObject *__reduce__(PyObject *self) const;
+  void __setstate__(PyObject *self, vector_uchar data, PyObject *nodepaths);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // COLLISIONHANDLERPHYSICAL_EXT_H

+ 21 - 0
panda/src/collide/collisionHandlerPusher.cxx

@@ -49,6 +49,27 @@ CollisionHandlerPusher::
 ~CollisionHandlerPusher() {
 }
 
+/**
+ * Serializes this object, to implement pickle support.
+ */
+void CollisionHandlerPusher::
+write_datagram(Datagram &dg) const {
+  CollisionHandlerPhysical::write_datagram(dg);
+
+  dg.add_bool(_horizontal);
+}
+
+/**
+ * Restores the object state from the given datagram, previously obtained using
+ * __getstate__.
+ */
+void CollisionHandlerPusher::
+read_datagram(DatagramIterator &scan) {
+  CollisionHandlerPhysical::read_datagram(scan);
+
+  _horizontal = scan.get_bool();
+}
+
 /**
  * Called by the parent class after all collisions have been detected, this
  * manages the various collisions and moves around the nodes as necessary.

+ 3 - 0
panda/src/collide/collisionHandlerPusher.h

@@ -34,6 +34,9 @@ PUBLISHED:
 PUBLISHED:
   MAKE_PROPERTY(horizontal, get_horizontal, set_horizontal);
 
+  void write_datagram(Datagram &destination) const;
+  void read_datagram(DatagramIterator &source);
+
 protected:
   virtual bool handle_entries();
   virtual void apply_net_shove(

+ 3 - 0
panda/src/collide/collisionHandlerQueue.h

@@ -18,6 +18,7 @@
 
 #include "collisionHandler.h"
 #include "collisionEntry.h"
+#include "extension.h"
 
 /**
  * A special kind of CollisionHandler that does nothing except remember the
@@ -45,6 +46,8 @@ PUBLISHED:
   void output(std::ostream &out) const;
   void write(std::ostream &out, int indent_level = 0) const;
 
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+
 private:
   typedef pvector< PT(CollisionEntry) > Entries;
   Entries _entries;

+ 27 - 0
panda/src/collide/collisionHandlerQueue_ext.cxx

@@ -0,0 +1,27 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandlerQueue_ext.cxx
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#include "collisionHandlerQueue_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickling behavior.
+ */
+PyObject *Extension<CollisionHandlerQueue>::
+__reduce__(PyObject *self) const {
+  // CollisionHandlerQueue has no interesting properties.
+  return Py_BuildValue("(O())", Py_TYPE(self));
+}
+
+#endif

+ 37 - 0
panda/src/collide/collisionHandlerQueue_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionHandler_ext.h
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#ifndef COLLISIONHANDLERQUEUE_EXT_H
+#define COLLISIONHANDLERQUEUE_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionHandlerQueue.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionHandlerQueue, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<CollisionHandlerQueue> : public ExtensionBase<CollisionHandlerQueue> {
+public:
+  PyObject *__reduce__(PyObject *self) const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // COLLISIONHANDLERQUEUE_EXT_H

+ 4 - 0
panda/src/collide/collisionTraverser.h

@@ -24,6 +24,7 @@
 
 #include "pset.h"
 #include "register_type.h"
+#include "extension.h"
 
 class CollisionNode;
 class CollisionRecorder;
@@ -79,6 +80,9 @@ PUBLISHED:
   void output(std::ostream &out) const;
   void write(std::ostream &out, int indent_level) const;
 
+  EXTENSION(PyObject *__getstate__() const);
+  EXTENSION(void __setstate__(PyObject *state));
+
 private:
   typedef pvector<CollisionLevelStateSingle> LevelStatesSingle;
   void prepare_colliders_single(LevelStatesSingle &level_states, const NodePath &root);

+ 73 - 0
panda/src/collide/collisionTraverser_ext.cxx

@@ -0,0 +1,73 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionTraverser_ext.cxx
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#include "collisionTraverser_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickling behavior.
+ */
+PyObject *Extension<CollisionTraverser>::
+__getstate__() const {
+  extern struct Dtool_PyTypedObject Dtool_CollisionHandler;
+  extern struct Dtool_PyTypedObject Dtool_CollisionTraverser;
+  extern struct Dtool_PyTypedObject Dtool_NodePath;
+
+  const std::string &name = _this->get_name();
+  size_t num_colliders = _this->get_num_colliders();
+
+  PyObject *state = PyTuple_New(num_colliders * 2 + 3);
+  PyTuple_SET_ITEM(state, 0, PyUnicode_FromStringAndSize(name.data(), name.size()));
+  PyTuple_SET_ITEM(state, 1, PyBool_FromLong(_this->get_respect_prev_transform()));
+  PyTuple_SET_ITEM(state, 2, PyLong_FromLong((long)num_colliders));
+
+  for (size_t i = 0; i < num_colliders; ++i) {
+    NodePath *collider = new NodePath(_this->get_collider(i));
+    PyTuple_SET_ITEM(state, i * 2 + 3,
+      DTool_CreatePyInstance((void *)collider, Dtool_NodePath, true, false));
+
+    PT(CollisionHandler) handler = _this->get_handler(*collider);
+    handler->ref();
+    PyTuple_SET_ITEM(state, i * 2 + 4,
+      DTool_CreatePyInstanceTyped((void *)handler.p(), Dtool_CollisionHandler, true, false, handler->get_type_index()));
+    handler.cheat() = nullptr;
+  }
+
+  return state;
+}
+
+/**
+ * Takes the value returned by __getstate__ and uses it to freshly initialize
+ * this CollisionTraverser object.
+ */
+void Extension<CollisionTraverser>::
+__setstate__(PyObject *state) {
+  _this->clear_colliders();
+
+  Py_ssize_t len = 0;
+  const char *data = PyUnicode_AsUTF8AndSize(PyTuple_GET_ITEM(state, 0), &len);
+  _this->set_name(std::string(data, len));
+
+  _this->set_respect_prev_transform(PyTuple_GET_ITEM(state, 1) != Py_False);
+  size_t num_colliders = (ssize_t)PyLong_AsLong(PyTuple_GET_ITEM(state, 2));
+
+  for (size_t i = 0; i < num_colliders; ++i) {
+    NodePath *collider = (NodePath *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(state, i * 2 + 3));
+    CollisionHandler *handler = (CollisionHandler *)DtoolInstance_VOID_PTR(PyTuple_GET_ITEM(state, i * 2 + 4));
+
+    _this->add_collider(*collider, handler);
+  }
+}
+
+#endif

+ 38 - 0
panda/src/collide/collisionTraverser_ext.h

@@ -0,0 +1,38 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file collisionTraverser_ext.h
+ * @author rdb
+ * @date 2020-12-31
+ */
+
+#ifndef COLLISIONTRAVERSER_EXT_H
+#define COLLISIONTRAVERSER_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "collisionTraverser.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for CollisionTraverser, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<CollisionTraverser> : public ExtensionBase<CollisionTraverser> {
+public:
+  PyObject *__getstate__() const;
+  void __setstate__(PyObject *state);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // COLLISIONTRAVERSER_EXT_H

+ 4 - 0
panda/src/collide/p3collide_ext_composite.cxx

@@ -1 +1,5 @@
+#include "collisionHandlerEvent_ext.cxx"
+#include "collisionHandlerPhysical_ext.cxx"
+#include "collisionHandlerQueue_ext.cxx"
 #include "collisionPolygon_ext.cxx"
+#include "collisionTraverser_ext.cxx"

+ 2 - 1
panda/src/display/graphicsStateGuardian_ext.cxx

@@ -25,10 +25,11 @@ static bool traverse_callback(TextureContext *tc, void *data) {
   PyObject *element =
     DTool_CreatePyInstanceTyped(tex, Dtool_Texture,
                                 true, false, tex->get_type_index());
-  tex->ref();
+  tex.cheat() = nullptr;
 
   PyObject *list = (PyObject *) data;
   PyList_Append(list, element);
+  Py_DECREF(element);
 
   return true;
 }

+ 3 - 0
panda/src/egg/eggAnimPreload.h

@@ -37,6 +37,9 @@ PUBLISHED:
   INLINE bool has_num_frames() const;
   INLINE int get_num_frames() const;
 
+  MAKE_PROPERTY2(fps, has_fps, get_fps, set_fps, clear_fps);
+  MAKE_PROPERTY2(num_frames, has_num_frames, get_num_frames, set_num_frames, clear_num_frames);
+
   virtual void write(std::ostream &out, int indent_level) const;
 
 private:

+ 2 - 0
panda/src/egg/eggComment.h

@@ -41,6 +41,8 @@ PUBLISHED:
 
   virtual void write(std::ostream &out, int indent_level) const;
 
+  EXTENSION(PyObject *__reduce__() const);
+
 private:
   std::string _comment;
 

+ 32 - 0
panda/src/egg/eggComment_ext.cxx

@@ -0,0 +1,32 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggComment_ext.cxx
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#include "eggComment_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<EggComment>::
+__reduce__() const {
+  extern struct Dtool_PyTypedObject Dtool_EggComment;
+
+  std::string node_name = _this->get_name();
+  std::string comment = _this->get_comment();
+  return Py_BuildValue("O(s#s#)", (PyObject *)&Dtool_EggComment,
+    node_name.data(), (Py_ssize_t)node_name.length(),
+    comment.data(), (Py_ssize_t)comment.length());
+}
+
+#endif

+ 37 - 0
panda/src/egg/eggComment_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggComment_ext.h
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#ifndef EGGCOMMENT_EXT_H
+#define EGGCOMMENT_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "eggComment.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for EggComment, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<EggComment> : public ExtensionBase<EggComment> {
+public:
+  PyObject *__reduce__() const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // EGGCOMMENT_EXT_H

+ 2 - 0
panda/src/egg/eggCoordinateSystem.h

@@ -36,6 +36,8 @@ PUBLISHED:
 
   virtual void write(std::ostream &out, int indent_level) const;
 
+  EXTENSION(PyObject *__reduce__() const);
+
 private:
   CoordinateSystem _value;
 

+ 32 - 0
panda/src/egg/eggCoordinateSystem_ext.cxx

@@ -0,0 +1,32 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggCoordinateSystem_ext.cxx
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#include "eggCoordinateSystem_ext.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<EggCoordinateSystem>::
+__reduce__() const {
+  extern struct Dtool_PyTypedObject Dtool_EggCoordinateSystem;
+
+  // We can't use the regular EggNode handling for EggCoordinateSystem, because
+  // the <CoordinateSystem> node is removed from the EggData after reading.
+  // Oh well, this is more efficient anyway.
+  int value = _this->get_value();
+  return Py_BuildValue("O(i)", (PyObject *)&Dtool_EggCoordinateSystem, value);  
+}
+
+#endif

+ 37 - 0
panda/src/egg/eggCoordinateSystem_ext.h

@@ -0,0 +1,37 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggCoordinateSystem_ext.h
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#ifndef EGGCOORDINATESYSTEM_EXT_H
+#define EGGCOORDINATESYSTEM_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "eggCoordinateSystem.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for EggCoordinateSystem, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<EggCoordinateSystem> : public ExtensionBase<EggCoordinateSystem> {
+public:
+  PyObject *__reduce__() const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // EGGCOORDINATESYSTEM_EXT_H

+ 2 - 0
panda/src/egg/eggNode.h

@@ -22,6 +22,7 @@
 #include "lmatrix.h"
 #include "pointerTo.h"
 #include "referenceCount.h"
+#include "extension.h"
 
 class EggGroupNode;
 class EggRenderMode;
@@ -90,6 +91,7 @@ PUBLISHED:
   void test_under_integrity() const { }
 #endif  // _DEBUG
 
+  EXTENSION(PyObject *__reduce__() const);
 
 protected:
   enum UnderFlags {

+ 93 - 0
panda/src/egg/eggNode_ext.cxx

@@ -0,0 +1,93 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggNode_ext.cxx
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#include "eggNode_ext.h"
+#include "eggData.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<EggNode>::
+__reduce__() const {
+  extern struct Dtool_PyTypedObject Dtool_EggNode;
+
+  // Find the parse_egg_node function in this module.
+  PyObject *sys_modules = PyImport_GetModuleDict();
+  nassertr_always(sys_modules != nullptr, nullptr);
+
+  PyObject *module_name = PyObject_GetAttrString((PyObject *)&Dtool_EggNode, "__module__");
+  nassertr_always(module_name != nullptr, nullptr);
+
+  PyObject *module = PyDict_GetItem(sys_modules, module_name);
+  Py_DECREF(module_name);
+  nassertr_always(module != nullptr, nullptr);
+
+  PyObject *func;
+  if (_this->is_of_type(EggData::get_class_type())) {
+    func = PyObject_GetAttrString(module, "parse_egg_data");
+  } else {
+    func = PyObject_GetAttrString(module, "parse_egg_node");
+  }
+  nassertr_always(func != nullptr, nullptr);
+
+  // Get the egg syntax to pass to the parse_egg_node function.
+  std::ostringstream stream;
+  _this->write(stream, INT_MIN);
+  std::string data = stream.str();
+  size_t length = data.size();
+
+  // Trim trailing whitespace.
+  while (length > 0 && isspace(data[length - 1])) {
+    --length;
+  }
+
+  return Py_BuildValue("N(s#)", func, data.data(), (Py_ssize_t)length);
+}
+
+/**
+ * Parses an EggData from the raw egg syntax.
+ */
+PT(EggData) parse_egg_data(const std::string &egg_syntax) {
+  PT(EggData) data = new EggData;
+  data->set_auto_resolve_externals(false);
+
+  std::istringstream in(egg_syntax);
+
+  if (!data->read(in)) {
+    PyErr_Format(PyExc_RuntimeError, "failed to parse egg data");
+    return nullptr;
+  }
+
+  return data;
+}
+
+/**
+ * Parses a single egg node from the raw egg syntax.
+ */
+PT(EggNode) parse_egg_node(const std::string &egg_syntax) {
+  PT(EggData) data = parse_egg_data(egg_syntax);
+  if (data == nullptr) {
+    return nullptr;
+  }
+
+  if (data->size() != 1) {
+    PyErr_Format(PyExc_RuntimeError, "expected exactly one node");
+    return nullptr;
+  }
+
+  return data->remove_child(data->get_first_child());
+} 
+
+#endif

+ 42 - 0
panda/src/egg/eggNode_ext.h

@@ -0,0 +1,42 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file eggNode_ext.h
+ * @author rdb
+ * @date 2021-01-01
+ */
+
+#ifndef EGGNODE_EXT_H
+#define EGGNODE_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "eggNode.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for EggNode, which are
+ * called instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<EggNode> : public ExtensionBase<EggNode> {
+public:
+  PyObject *__reduce__() const;
+};
+
+BEGIN_PUBLISH
+PT(EggData) parse_egg_data(const std::string &egg_syntax);
+PT(EggNode) parse_egg_node(const std::string &egg_syntax);
+END_PUBLISH
+
+#endif  // HAVE_PYTHON
+
+#endif  // EGGNODE_EXT_H

+ 10 - 0
panda/src/egg/lexer.cxx.prebuilt

@@ -971,6 +971,7 @@ extern "C" int eggyywrap(void);  // declared below.
 
 static int yyinput(void);        // declared by flex.
 
+int eggyylex_destroy(void);
 
 ////////////////////////////////////////////////////////////////////
 // Static variables
@@ -1018,6 +1019,15 @@ egg_init_lexer(istream &in, const string &filename) {
   initial_token = START_EGG;
 }
 
+void
+egg_cleanup_lexer() {
+  // Reset the lexer state.
+  eggyylex_destroy();
+
+  input_p = nullptr;
+  egg_filename.clear();
+}
+
 void
 egg_start_group_body() {
   /* Set the initial state to begin within a group_body context,

+ 10 - 0
panda/src/egg/lexer.lxx

@@ -26,6 +26,7 @@ extern "C" int eggyywrap(void);  // declared below.
 
 static int yyinput(void);        // declared by flex.
 
+int eggyylex_destroy(void);
 
 ////////////////////////////////////////////////////////////////////
 // Static variables
@@ -73,6 +74,15 @@ egg_init_lexer(istream &in, const string &filename) {
   initial_token = START_EGG;
 }
 
+void
+egg_cleanup_lexer() {
+  // Reset the lexer state.
+  yylex_destroy();
+
+  input_p = nullptr;
+  egg_filename.clear();
+}
+
 void
 egg_start_group_body() {
   /* Set the initial state to begin within a group_body context,

+ 1 - 0
panda/src/egg/lexerDefs.h

@@ -21,6 +21,7 @@
 #include <string>
 
 void egg_init_lexer(std::istream &in, const std::string &filename);
+void egg_cleanup_lexer();
 void egg_start_group_body();
 void egg_start_texture_body();
 void egg_start_primitive_body();

+ 4 - 0
panda/src/egg/p3egg_ext_composite.cxx

@@ -0,0 +1,4 @@
+#include "eggComment_ext.cxx"
+#include "eggCoordinateSystem_ext.cxx"
+#include "eggGroupNode_ext.cxx"
+#include "eggNode_ext.cxx"

+ 2 - 0
panda/src/egg/parser.cxx.prebuilt

@@ -223,6 +223,8 @@ egg_cleanup_parser() {
   textures.clear();
   materials.clear();
   groups.clear();
+
+  egg_cleanup_lexer();
 }
 
 

+ 2 - 0
panda/src/egg/parser.yxx

@@ -153,6 +153,8 @@ egg_cleanup_parser() {
   textures.clear();
   materials.clear();
   groups.clear();
+
+  egg_cleanup_lexer();
 }
 
 %}

+ 4 - 0
panda/src/express/pointerToArray.h

@@ -117,6 +117,8 @@ PUBLISHED:
   INLINE size_t count(const Element &) const;
 
 #ifdef HAVE_PYTHON
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+
   EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags));
   EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const);
 #endif
@@ -273,6 +275,8 @@ PUBLISHED:
   INLINE size_t count(const Element &) const;
 
 #ifdef HAVE_PYTHON
+  EXTENSION(PyObject *__reduce__(PyObject *self) const);
+
   EXTENSION(int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const);
   EXTENSION(void __releasebuffer__(PyObject *self, Py_buffer *view) const);
 #endif

+ 6 - 0
panda/src/express/pointerToArrayBase.h

@@ -77,6 +77,12 @@ protected:
 
 PUBLISHED:
   INLINE ~PointerToArrayBase();
+
+#ifdef CPPPARSER
+  // These are implemented in PointerToVoid, but expose them here.
+  INLINE bool operator == (const PointerToArrayBase<Element> &other) const;
+  INLINE bool operator != (const PointerToArrayBase<Element> &other) const;
+#endif
 };
 
 #include "pointerToArrayBase.I"

+ 35 - 0
panda/src/express/pointerToArray_ext.I

@@ -219,6 +219,25 @@ get_subdata(size_t n, size_t count) const {
   return PyBytes_FromStringAndSize((char *)(this->_this->p() + n), sizeof(Element) * count);
 }
 
+/**
+ * Implements pickle support.
+ */
+template<class Element>
+INLINE PyObject *Extension<PointerToArray<Element> >::
+__reduce__(PyObject *self) const {
+  // This preserves the distinction between a null vs. an empty PTA, though I'm
+  // not sure that this distinction matters to anyone.
+  if (this->_this->is_null()) {
+    return Py_BuildValue("O()", Py_TYPE(self));
+  }
+  else if (this->_this->empty()) {
+    return Py_BuildValue("O(())", Py_TYPE(self));
+  }
+  else {
+    return Py_BuildValue("O(N)", Py_TYPE(self), get_data());
+  }
+}
+
 /**
  * Same as get_element(), this returns the nth element of the array.
  */
@@ -256,6 +275,22 @@ get_subdata(size_t n, size_t count) const {
   return PyBytes_FromStringAndSize((char *)(this->_this->p() + n), sizeof(Element) * count);
 }
 
+/**
+ * Implements pickle support.
+ */
+template<class Element>
+INLINE PyObject *Extension<ConstPointerToArray<Element> >::
+__reduce__(PyObject *self) const {
+  // This preserves the distinction between a null vs. an empty PTA, though I'm
+  // not sure that this distinction matters to anyone.
+  if (!this->_this->is_null() && this->_this->empty()) {
+    return Py_BuildValue("O([])", Py_TYPE(self));
+  }
+  else {
+    return Py_BuildValue("O(N)", Py_TYPE(self), get_data());
+  }
+}
+
 /**
  * This is used to implement the buffer protocol, in order to allow efficient
  * access to the array data through a Python multiview object.

+ 4 - 0
panda/src/express/pointerToArray_ext.h

@@ -40,6 +40,8 @@ public:
   INLINE void set_data(PyObject *data);
   INLINE PyObject *get_subdata(size_t n, size_t count) const;
 
+  INLINE PyObject *__reduce__(PyObject *self) const;
+
   INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags);
   INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const;
 };
@@ -75,6 +77,8 @@ public:
   INLINE PyObject *get_data() const;
   INLINE PyObject *get_subdata(size_t n, size_t count) const;
 
+  INLINE PyObject *__reduce__(PyObject *self) const;
+
   INLINE int __getbuffer__(PyObject *self, Py_buffer *view, int flags) const;
   INLINE void __releasebuffer__(PyObject *self, Py_buffer *view) const;
 };

+ 2 - 0
panda/src/pgraph/loaderFileTypeRegistry.h

@@ -53,6 +53,8 @@ PUBLISHED:
 
   static LoaderFileTypeRegistry *get_global_ptr();
 
+  EXTENSION(PyObject *__reduce__() const);
+
 private:
   void record_extension(const std::string &extension, LoaderFileType *type);
 

+ 10 - 0
panda/src/pgraph/loaderFileTypeRegistry_ext.cxx

@@ -18,6 +18,7 @@
 #include "pythonLoaderFileType.h"
 
 extern struct Dtool_PyTypedObject Dtool_LoaderFileType;
+extern struct Dtool_PyTypedObject Dtool_LoaderFileTypeRegistry;
 
 /**
  * Registers a loader file type that is implemented in Python.
@@ -106,4 +107,13 @@ unregister_type(PyObject *type) {
   Py_XDECREF(save_func);
 }
 
+/**
+ * Implements pickle support.
+ */
+PyObject *Extension<LoaderFileTypeRegistry>::
+__reduce__() const {
+  PyObject *func = PyObject_GetAttrString((PyObject *)&Dtool_LoaderFileTypeRegistry, "get_global_ptr");
+  return Py_BuildValue("N()", func);
+}
+
 #endif

+ 2 - 0
panda/src/pgraph/loaderFileTypeRegistry_ext.h

@@ -33,6 +33,8 @@ public:
   void register_deferred_type(PyObject *entry_point);
 
   void unregister_type(PyObject *type);
+
+  PyObject *__reduce__() const;
 };
 
 #endif  // HAVE_PYTHON

+ 108 - 0
tests/collide/test_collision_handlers.py

@@ -0,0 +1,108 @@
+from direct.stdpy.pickle import dumps, loads
+from panda3d.core import NodePath, CollisionNode
+
+
+def test_collision_handler_event_pickle():
+    from panda3d.core import CollisionHandlerEvent
+
+    handler = CollisionHandlerEvent()
+    handler.add_in_pattern("abcdefg")
+    handler.add_in_pattern("test")
+    handler.add_again_pattern("again pattern")
+    handler.add_again_pattern("another again pattern")
+    handler.add_out_pattern("out pattern")
+
+    handler = loads(dumps(handler, -1))
+
+    assert tuple(handler.in_patterns) == ("abcdefg", "test")
+    assert tuple(handler.again_patterns) == ("again pattern", "another again pattern")
+    assert tuple(handler.out_patterns) == ("out pattern",)
+
+
+def test_collision_handler_queue_pickle():
+    from panda3d.core import CollisionHandlerQueue
+
+    handler = CollisionHandlerQueue()
+    handler = loads(dumps(handler, -1))
+    assert type(handler) == CollisionHandlerQueue
+
+
+def test_collision_handler_floor_pickle():
+    from panda3d.core import CollisionHandlerFloor
+
+    collider1 = NodePath(CollisionNode("collider1"))
+    collider2 = NodePath(CollisionNode("collider2"))
+    target1 = NodePath("target1")
+    target2 = NodePath("target2")
+    center = NodePath("center")
+
+    handler = CollisionHandlerFloor()
+    handler.add_out_pattern("out pattern")
+    handler.add_collider(collider1, target1)
+    handler.add_collider(collider2, target2)
+    handler.center = center
+    handler.offset = 1.0
+    handler.reach = 2.0
+    handler.max_velocity = 3.0
+
+    handler = loads(dumps(handler, -1))
+
+    assert tuple(handler.in_patterns) == ()
+    assert tuple(handler.again_patterns) == ()
+    assert tuple(handler.out_patterns) == ("out pattern",)
+    assert handler.center.name == "center"
+    assert handler.offset == 1.0
+    assert handler.reach == 2.0
+    assert handler.max_velocity == 3.0
+
+
+def test_collision_handler_gravity_pickle():
+    from panda3d.core import CollisionHandlerGravity
+
+    collider1 = NodePath(CollisionNode("collider1"))
+    collider2 = NodePath(CollisionNode("collider2"))
+    target1 = NodePath("target1")
+    target2 = NodePath("target2")
+
+    handler = CollisionHandlerGravity()
+    handler.add_out_pattern("out pattern")
+    handler.add_collider(collider1, target1)
+    handler.add_collider(collider2, target2)
+    handler.offset = 1.0
+    handler.reach = 2.0
+    handler.max_velocity = 3.0
+    handler.gravity = -4.0
+
+    handler = loads(dumps(handler, -1))
+
+    assert tuple(handler.in_patterns) == ()
+    assert tuple(handler.again_patterns) == ()
+    assert tuple(handler.out_patterns) == ("out pattern",)
+    assert handler.center == None
+    assert handler.offset == 1.0
+    assert handler.reach == 2.0
+    assert handler.max_velocity == 3.0
+    assert handler.gravity == -4.0
+
+
+def test_collision_handler_pusher_pickle():
+    from panda3d.core import CollisionHandlerPusher
+
+    collider1 = NodePath(CollisionNode("collider1"))
+    collider2 = NodePath(CollisionNode("collider2"))
+    target1 = NodePath("target1")
+    target2 = NodePath("target2")
+
+    handler = CollisionHandlerPusher()
+    handler.add_again_pattern("again pattern")
+    handler.add_collider(collider1, target1)
+    handler.add_collider(collider2, target2)
+    handler.horizontal = True
+
+    handler = loads(dumps(handler, -1))
+
+    assert tuple(handler.in_patterns) == ()
+    assert tuple(handler.again_patterns) == ("again pattern",)
+    assert tuple(handler.out_patterns) == ()
+    assert not handler.has_center()
+    assert handler.horizontal

+ 31 - 0
tests/collide/test_collision_traverser.py

@@ -0,0 +1,31 @@
+from panda3d.core import CollisionTraverser, CollisionHandlerQueue
+from panda3d.core import NodePath, CollisionNode
+
+
+
+def test_collision_traverser_pickle():
+    from direct.stdpy.pickle import dumps, loads
+
+    handler = CollisionHandlerQueue()
+
+    collider1 = NodePath(CollisionNode("collider1"))
+    collider2 = NodePath(CollisionNode("collider2"))
+
+    trav = CollisionTraverser("test123")
+    trav.respect_prev_transform = True
+    trav.add_collider(collider1, handler)
+    trav.add_collider(collider2, handler)
+
+    trav = loads(dumps(trav, -1))
+    assert trav.respect_prev_transform is True
+
+    assert trav.name == "test123"
+    assert trav.get_num_colliders() == 2
+    collider1 = trav.get_collider(0)
+    collider2 = trav.get_collider(1)
+    assert collider1.name == "collider1"
+    assert collider2.name == "collider2"
+
+    # Two colliders must still be the same object; this only works with our own
+    # version of the pickle module, in direct.stdpy.pickle.
+    assert trav.get_handler(collider1) == trav.get_handler(collider2)

+ 71 - 0
tests/express/test_pointertoarray.py

@@ -0,0 +1,71 @@
+def test_pta_float_compare():
+    from panda3d.core import PTA_float, CPTA_float
+
+    # Two null PTAs
+    assert PTA_float() == PTA_float()
+    assert not (PTA_float() != PTA_float())
+
+    # Two non-null PTAs
+    assert PTA_float([1]) != PTA_float([1])
+    assert not (PTA_float([1]) == PTA_float([1]))
+
+    # A copy of a PTA
+    pta = PTA_float([1])
+    assert pta == PTA_float(pta)
+    assert not (pta != PTA_float(pta))
+
+    # A const copy of a PTA
+    pta = PTA_float([1])
+    cpta = CPTA_float(pta)
+    assert pta == cpta
+    assert not (pta != cpta)
+
+
+def test_pta_float_pickle():
+    from panda3d.core import PTA_float
+    from direct.stdpy.pickle import dumps, loads, HIGHEST_PROTOCOL
+
+    null_pta = PTA_float()
+
+    empty_pta = PTA_float([])
+
+    data_pta = PTA_float([1.0, 2.0, 3.0])
+    data = data_pta.get_data()
+
+    for proto in range(1, HIGHEST_PROTOCOL + 1):
+        null_pta2 = loads(dumps(null_pta, proto))
+        assert null_pta2.is_null()
+        assert len(null_pta2) == 0
+
+        empty_pta2 = loads(dumps(empty_pta, proto))
+        assert not empty_pta2.is_null()
+        assert len(empty_pta2) == 0
+
+        data_pta2 = loads(dumps(data_pta, proto))
+        assert tuple(data_pta2) == (1.0, 2.0, 3.0)
+        assert data_pta2.get_data() == data_pta.get_data()
+
+
+def test_cpta_float_pickle():
+    from panda3d.core import PTA_float, CPTA_float
+    from direct.stdpy.pickle import dumps, loads, HIGHEST_PROTOCOL
+
+    null_pta = CPTA_float(PTA_float())
+
+    empty_pta = CPTA_float([])
+
+    data_pta = CPTA_float([1.0, 2.0, 3.0])
+    data = data_pta.get_data()
+
+    for proto in range(1, HIGHEST_PROTOCOL + 1):
+        null_pta2 = loads(dumps(null_pta, proto))
+        assert null_pta2.is_null()
+        assert len(null_pta2) == 0
+
+        empty_pta2 = loads(dumps(empty_pta, proto))
+        assert not empty_pta2.is_null()
+        assert len(empty_pta2) == 0
+
+        data_pta2 = loads(dumps(data_pta, proto))
+        assert tuple(data_pta2) == (1.0, 2.0, 3.0)
+        assert data_pta2.get_data() == data_pta.get_data()

+ 9 - 0
tests/pgraph/test_loader_types.py

@@ -3,6 +3,7 @@ import pytest
 import tempfile
 import os
 from contextlib import contextmanager
+import sys
 
 
 @pytest.fixture
@@ -218,3 +219,11 @@ def test_loader_ram_cache(test_filename):
         assert model1 == model2
 
         ModelPool.release_model(model2)
+
+
[email protected](sys.version_info < (3, 4), reason="Requires Python 3.4")
+def test_loader_file_type_registry_pickle():
+    from direct.stdpy.pickle import dumps, loads
+
+    registry = LoaderFileTypeRegistry.get_global_ptr()
+    assert loads(dumps(registry, -1)) == registry

+ 10 - 0
tests/putil/test_bitmask.py

@@ -86,3 +86,13 @@ def test_bitmask_pickle():
     data = pickle.dumps(mask1, -1)
     mask2 = pickle.loads(data)
     assert mask1 == mask2
+
+    mask1 = DoubleBitMaskNative(0x7fffffffffffffff)
+    data = pickle.dumps(mask1, -1)
+    mask2 = pickle.loads(data)
+    assert mask1 == mask2
+
+    mask1 = DoubleBitMaskNative(1 << (double_num_bits - 1))
+    data = pickle.dumps(mask1, -1)
+    mask2 = pickle.loads(data)
+    assert mask1 == mask2

+ 14 - 0
tests/stdpy/test_pickle.py

@@ -12,6 +12,20 @@ def test_reduce_persist():
     assert tuple(parent2.children) == (child2,)
 
 
+def test_pickle_copy():
+    from panda3d.core import PandaNode, NodePath
+
+    # Make two Python wrappers pointing to the same node
+    node1 = PandaNode("node")
+    node2 = NodePath(node1).node()
+    assert node1.this == node2.this
+    assert id(node1) != id(node2)
+
+    # Test that pickling and loading still results in the same node object.
+    node1, node2 = loads(dumps([node1, node2]))
+    assert node1 == node2
+
+
 def test_pickle_error():
     class ErroneousPickleable(object):
         def __reduce__(self):