Răsfoiți Sursa

Merge branch 'master' of file:///home/rdb/panda3d into shaderpipeline

rdb 6 ani în urmă
părinte
comite
3ea66328c0
100 a modificat fișierele cu 2969 adăugiri și 5432 ștergeri
  1. 2 0
      .github/FUNDING.yml
  2. 57 0
      .github/workflows/ci.yml
  3. 3 0
      .gitignore
  4. 0 1
      .travis.yml
  5. 2 0
      BACKERS.md
  6. 10 7
      README.md
  7. 3 0
      contrib/src/ai/aiBehaviors.h
  8. 33 30
      direct/src/actor/Actor.py
  9. 2 1
      direct/src/cluster/ClusterClient.py
  10. 20 17
      direct/src/cluster/ClusterConfig.py
  11. 16 15
      direct/src/controls/ControlManager.py
  12. 8 6
      direct/src/controls/DevWalker.py
  13. 8 6
      direct/src/controls/GhostWalker.py
  14. 8 6
      direct/src/controls/GravityWalker.py
  15. 20 18
      direct/src/controls/InputState.py
  16. 8 6
      direct/src/controls/NonPhysicsWalker.py
  17. 8 6
      direct/src/controls/ObserverWalker.py
  18. 0 2
      direct/src/controls/PhysicsRoller.py
  19. 8 6
      direct/src/controls/PhysicsWalker.py
  20. 1 1
      direct/src/controls/TwoDWalker.py
  21. 5 689
      direct/src/dcparser/dcClass.cxx
  22. 58 44
      direct/src/dcparser/dcClass.h
  23. 665 0
      direct/src/dcparser/dcClass_ext.cxx
  24. 93 0
      direct/src/dcparser/dcClass_ext.h
  25. 6 313
      direct/src/dcparser/dcField.cxx
  26. 13 15
      direct/src/dcparser/dcField.h
  27. 305 0
      direct/src/dcparser/dcField_ext.cxx
  28. 48 0
      direct/src/dcparser/dcField_ext.h
  29. 0 508
      direct/src/dcparser/dcPacker.cxx
  30. 17 14
      direct/src/dcparser/dcPacker.h
  31. 3 3
      direct/src/dcparser/dcPackerInterface.I
  32. 0 1
      direct/src/dcparser/dcPackerInterface.h
  33. 508 0
      direct/src/dcparser/dcPacker_ext.cxx
  34. 45 0
      direct/src/dcparser/dcPacker_ext.h
  35. 0 1
      direct/src/dcparser/dcParserDefs.h
  36. 0 42
      direct/src/dcparser/dcPython.h
  37. 4 0
      direct/src/dcparser/dcbase.h
  38. 0 1
      direct/src/dcparser/hashGenerator.h
  39. 3 0
      direct/src/dcparser/p3dcparser_ext_composite.cxx
  40. 1 1
      direct/src/directdevices/DirectDeviceManager.py
  41. 2 2
      direct/src/directdevices/DirectFastrak.py
  42. 5 1
      direct/src/directnotify/DirectNotifyGlobal.py
  43. 2 1
      direct/src/directnotify/LoggerGlobal.py
  44. 6 6
      direct/src/directnotify/Notifier.py
  45. 15 15
      direct/src/directnotify/RotatingLog.py
  46. 0 99
      direct/src/directscripts/DetectPanda3D.js
  47. 0 132
      direct/src/directscripts/RunPanda3D.js
  48. 3 1
      direct/src/directscripts/extract_docs.py
  49. 2 0
      direct/src/directtools/DirectUtil.py
  50. 38 32
      direct/src/directutil/Verify.py
  51. 94 17
      direct/src/dist/FreezeTool.py
  52. 4 0
      direct/src/dist/__init__.py
  53. 69 2
      direct/src/dist/commands.py
  54. 269 0
      direct/src/dist/icon.py
  55. 1 1
      direct/src/distributed/AsyncRequest.py
  56. 2 1
      direct/src/distributed/DistributedObject.py
  57. 2 1
      direct/src/distributed/DistributedObjectBase.py
  58. 9 8
      direct/src/distributed/DoCollectionManager.py
  59. 8 9
      direct/src/distributed/DoHierarchy.py
  60. 1 4
      direct/src/distributed/PyDatagram.py
  61. 1 0
      direct/src/distributed/PyDatagramIterator.py
  62. 1 1
      direct/src/distributed/ServerRepository.py
  63. 4 3
      direct/src/distributed/cConnectionRepository.cxx
  64. 1 1
      direct/src/distributed/cConnectionRepository.h
  65. 4 0
      direct/src/distributed/cDistributedSmoothNodeBase.cxx
  66. 1 2
      direct/src/distributed/cDistributedSmoothNodeBase.h
  67. 4 4
      direct/src/distributed/config_distributed.h
  68. 13 12
      direct/src/fsm/ClassicFSM.py
  69. 37 33
      direct/src/fsm/FSM.py
  70. 3 6
      direct/src/fsm/FourState.py
  71. 12 14
      direct/src/fsm/FourStateAI.py
  72. 3 0
      direct/src/fsm/__init__.py
  73. 5 1
      direct/src/gui/DirectButton.py
  74. 5 1
      direct/src/gui/DirectCheckButton.py
  75. 50 45
      direct/src/gui/DirectDialog.py
  76. 14 8
      direct/src/gui/DirectEntry.py
  77. 25 3
      direct/src/gui/DirectEntryScroll.py
  78. 77 128
      direct/src/gui/DirectFrame.py
  79. 1 1
      direct/src/gui/DirectGui.py
  80. 47 39
      direct/src/gui/DirectGuiBase.py
  81. 5 6
      direct/src/gui/DirectGuiGlobals.py
  82. 5 1
      direct/src/gui/DirectLabel.py
  83. 21 9
      direct/src/gui/DirectOptionMenu.py
  84. 5 1
      direct/src/gui/DirectRadioButton.py
  85. 5 1
      direct/src/gui/DirectScrollBar.py
  86. 14 2
      direct/src/gui/DirectScrolledFrame.py
  87. 31 20
      direct/src/gui/DirectScrolledList.py
  88. 5 1
      direct/src/gui/DirectSlider.py
  89. 5 1
      direct/src/gui/DirectWaitBar.py
  90. 11 6
      direct/src/gui/OnscreenImage.py
  91. 6 2
      direct/src/gui/OnscreenText.py
  92. 1 1
      direct/src/gui/__init__.py
  93. 5 1
      direct/src/interval/ActorInterval.py
  94. 16 20
      direct/src/interval/ParticleInterval.py
  95. 2 0
      direct/src/interval/__init__.py
  96. 1 0
      direct/src/leveleditor/ActionMgr.py
  97. 0 1245
      direct/src/p3d/AppRunner.py
  98. 0 96
      direct/src/p3d/DWBPackageInstaller.py
  99. 0 1379
      direct/src/p3d/DeploymentTools.py
  100. 0 246
      direct/src/p3d/FileSpec.py

+ 2 - 0
.github/FUNDING.yml

@@ -0,0 +1,2 @@
+open_collective: panda3d
+

+ 57 - 0
.github/workflows/ci.yml

@@ -0,0 +1,57 @@
+name: Continuous Integration
+on: [push, pull_request]
+jobs:
+  makepanda:
+    strategy:
+      matrix:
+        os: [ubuntu-16.04, windows-2016, macOS-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+    - uses: actions/checkout@v1
+    - name: Install dependencies (Ubuntu)
+      if: matrix.os == 'ubuntu-16.04'
+      run: |
+        sudo apt-get install build-essential bison flex libfreetype6-dev libgl1-mesa-dev libjpeg-dev libode-dev libopenal-dev libpng-dev libssl-dev libvorbis-dev libx11-dev libxcursor-dev libxrandr-dev nvidia-cg-toolkit zlib1g-dev
+    - name: Get thirdparty packages (Windows)
+      if: runner.os == 'Windows'
+      shell: powershell
+      run: |
+        $wc = New-Object System.Net.WebClient
+        $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win64.zip", "thirdparty-tools.zip")
+        Expand-Archive -Path thirdparty-tools.zip
+        Move-Item -Path thirdparty-tools/panda3d-1.10.4.1/thirdparty -Destination .
+    - name: Get thirdparty packages (macOS)
+      if: runner.os == 'macOS'
+      run: |
+        curl -O https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz
+        tar -xf panda3d-1.10.5-tools-mac.tar.gz
+        mv panda3d-1.10.5/thirdparty thirdparty
+        rmdir panda3d-1.10.5
+        (cd thirdparty/darwin-libs-a && rm -rf rocket)
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.7
+    - name: Build Python 3.7
+      run: |
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir=$pythonLocation/include --python-libdir=$pythonLocation/lib --verbose --threads=4
+    - name: Test Python 3.7
+      shell: bash
+      run: |
+        python -m pip install pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+    - name: Set up Python 2.7
+      uses: actions/setup-python@v1
+      with:
+        python-version: 2.7
+    - name: Build Python 2.7
+      run: |
+        python makepanda/makepanda.py --no-copy-python --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir=$pythonLocation/include --python-libdir=$pythonLocation/lib --verbose --threads=4
+    - name: Test Python 2.7
+      shell: bash
+      run: |
+        python -m pip install pytest
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+    - name: Make installer
+      run: |
+        python makepanda/makepackage.py --verbose --lzma

+ 3 - 0
.gitignore

@@ -63,3 +63,6 @@ __pycache__/
 .tox/
 .cache/
 .pytest_cache/
+/.settings/
+/.cproject
+/.project

+ 0 - 1
.travis.yml

@@ -44,7 +44,6 @@ install:
     - $PYTHONV -m pip install pytest
 script:
     - $PYTHONV makepanda/makepanda.py --everything --git-commit $TRAVIS_COMMIT $FLAGS --threads 4
-    - test -n "$SKIP_TESTS" || LD_LIBRARY_PATH=built/lib PYTHONPATH=built $PYTHONV makepanda/test_imports.py
     - test -n "$SKIP_TESTS" || LD_LIBRARY_PATH=built/lib PYTHONPATH=built $PYTHONV -m pytest -v tests
 notifications:
   irc:

+ 2 - 0
BACKERS.md

@@ -23,12 +23,14 @@ This is a list of all the people who are contributing financially to Panda3D.  I
 
 * Sam Edwards
 * Max Voss
+* Will Nielsen
 
 ## Enthusiasts
 
 ![Benefactors](https://opencollective.com/panda3d/tiers/enthusiast.svg?avatarHeight=48&width=600)
 
 * Eric Thomson
+* Kyle Roach
 
 ## Backers
 

+ 10 - 7
README.md

@@ -24,7 +24,7 @@ Installing Panda3D
 ==================
 
 The latest Panda3D SDK can be downloaded from
-[this page](https://www.panda3d.org/download/sdk-1-10-3/).
+[this page](https://www.panda3d.org/download/sdk-1-10-4-1/).
 If you are familiar with installing Python packages, you can use
 the following comand:
 
@@ -64,8 +64,8 @@ depending on whether you are on a 32-bit or 64-bit system, or you can
 [click here](https://github.com/rdb/panda3d-thirdparty) for instructions on
 building them from source.
 
-https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win64.zip
-https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-win32.zip
+https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win64.zip
+https://www.panda3d.org/download/panda3d-1.10.4.1/panda3d-1.10.4.1-tools-win32.zip
 
 After acquiring these dependencies, you may simply build Panda3D from the
 command prompt using the following command.  (Change `14.1` to `14` if you are
@@ -135,7 +135,7 @@ macOS
 -----
 
 On macOS, you will need to download a set of precompiled thirdparty packages in order to
-compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.3/panda3d-1.10.3-tools-mac.tar.gz).
+compile Panda3D, which can be acquired from [here](https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:
@@ -185,16 +185,19 @@ from the Play Store.  Many of the dependencies can be installed by running the
 following command in the Termux shell:
 
 ```bash
-pkg install python-dev termux-tools ndk-stl ndk-sysroot clang libvorbis-dev libopus-dev opusfile-dev openal-soft-dev freetype-dev harfbuzz-dev libpng-dev ecj4.6 dx patchelf aapt apksigner libcrypt-dev
+pkg install python ndk-sysroot clang bison freetype harfbuzz libpng eigen openal-soft opusfile libvorbis assimp libopus ecj dx patchelf aapt apksigner libcrypt openssl pkg-config
 ```
 
-Then, you can build and install the .apk right away using these commands:
+Then, you can build the .apk using this command:
 
 ```bash
 python makepanda/makepanda.py --everything --target android-21 --no-tiff --installer
-xdg-open panda3d.apk
 ```
 
+You can install the generated panda3d.apk by browsing to the panda3d folder
+using a file manager.  You may need to copy it to `/sdcard` to be able to
+access it from other apps.
+
 To launch a Python program from Termux, you can use the `run_python.sh` script
 inside the `panda/src/android` directory.  It will launch Python in a separate
 activity, load it with the Python script you passed as argument, and use a

+ 3 - 0
contrib/src/ai/aiBehaviors.h

@@ -28,6 +28,9 @@ class PathFollow;
 class PathFind;
 class ObstacleAvoidance;
 
+#include "flee.h"
+#include "evade.h"
+
 typedef std::list<Flee, std::allocator<Flee> > ListFlee;
 typedef std::list<Evade, std::allocator<Evade> > ListEvade;
 

+ 33 - 30
direct/src/actor/Actor.py

@@ -5,6 +5,7 @@ __all__ = ['Actor']
 from panda3d.core import *
 from panda3d.core import Loader as PandaLoader
 from direct.showbase.DirectObject import DirectObject
+from direct.showbase.Loader import Loader
 from direct.directnotify import DirectNotifyGlobal
 
 
@@ -67,7 +68,7 @@ class Actor(DirectObject, NodePath):
 
         def __init__(self, filename = None, animBundle = None):
             self.filename = filename
-            self.animBundle = None
+            self.animBundle = animBundle
             self.animControl = None
 
         def makeCopy(self):
@@ -104,45 +105,43 @@ class Actor(DirectObject, NodePath):
                  lodNode = None, flattenable = True, setFinal = False,
                  mergeLODBundles = None, allowAsyncBind = None,
                  okMissing = None):
-        """__init__(self, string | string:string{}, string:string{} |
-        string:(string:string{}){}, Actor=None)
-        Actor constructor: can be used to create single or multipart
+        """Actor constructor: can be used to create single or multipart
         actors. If another Actor is supplied as an argument this
         method acts like a copy constructor. Single part actors are
         created by calling with a model and animation dictionary
-        (animName:animPath{}) as follows:
+        ``(animName:animPath{})`` as follows::
 
-           a = Actor("panda-3k.egg", {"walk":"panda-walk.egg" \
+           a = Actor("panda-3k.egg", {"walk":"panda-walk.egg",
                                       "run":"panda-run.egg"})
 
-        This could be displayed and animated as such:
+        This could be displayed and animated as such::
 
            a.reparentTo(render)
            a.loop("walk")
            a.stop()
 
         Multipart actors expect a dictionary of parts and a dictionary
-        of animation dictionaries (partName:(animName:animPath{}){}) as
-        below:
+        of animation dictionaries ``(partName:(animName:animPath{}){})``
+        as below::
 
             a = Actor(
 
                 # part dictionary
-                {"head":"char/dogMM/dogMM_Shorts-head-mod", \
-                 "torso":"char/dogMM/dogMM_Shorts-torso-mod", \
-                 "legs":"char/dogMM/dogMM_Shorts-legs-mod"}, \
+                {"head": "char/dogMM/dogMM_Shorts-head-mod",
+                 "torso": "char/dogMM/dogMM_Shorts-torso-mod",
+                 "legs": "char/dogMM/dogMM_Shorts-legs-mod"},
 
                 # dictionary of anim dictionaries
-                {"head":{"walk":"char/dogMM/dogMM_Shorts-head-walk", \
-                         "run":"char/dogMM/dogMM_Shorts-head-run"}, \
-                 "torso":{"walk":"char/dogMM/dogMM_Shorts-torso-walk", \
-                          "run":"char/dogMM/dogMM_Shorts-torso-run"}, \
-                 "legs":{"walk":"char/dogMM/dogMM_Shorts-legs-walk", \
-                         "run":"char/dogMM/dogMM_Shorts-legs-run"} \
+                {"head":{"walk": "char/dogMM/dogMM_Shorts-head-walk",
+                         "run": "char/dogMM/dogMM_Shorts-head-run"},
+                 "torso":{"walk": "char/dogMM/dogMM_Shorts-torso-walk",
+                          "run": "char/dogMM/dogMM_Shorts-torso-run"},
+                 "legs":{"walk": "char/dogMM/dogMM_Shorts-legs-walk",
+                         "run": "char/dogMM/dogMM_Shorts-legs-run"}
                  })
 
         In addition multipart actor parts need to be connected together
-        in a meaningful fashion:
+        in a meaningful fashion::
 
             a.attach("head", "torso", "joint-head")
             a.attach("torso", "legs", "joint-hips")
@@ -151,7 +150,7 @@ class Actor(DirectObject, NodePath):
         # ADD LOD COMMENT HERE!
         #
 
-        Other useful Actor class functions:
+        Other useful Actor class functions::
 
             #fix actor eye rendering
             a.drawInFront("joint-pupil?", "eyes*")
@@ -1135,7 +1134,7 @@ class Actor(DirectObject, NodePath):
     def getJoints(self, partName = None, jointName = '*', lodName = None):
         """ Returns the list of all joints, from the named part or
         from all parts, that match the indicated jointName.  The
-        jointName may include pattern characters like *. """
+        jointName may include pattern characters like \\*. """
 
         joints=[]
         pattern = GlobPattern(jointName)
@@ -1888,6 +1887,9 @@ class Actor(DirectObject, NodePath):
                 else:
                     loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFReportErrors)
 
+            # Ensure that custom Python loader hooks are initialized.
+            Loader._loadPythonFileTypes()
+
             # Pass loaderOptions to specify that we want to
             # get the skeleton model.  This only matters to model
             # files (like .mb) for which we can choose to extract
@@ -1947,6 +1949,7 @@ class Actor(DirectObject, NodePath):
                     animName = acc.getAnimName(i)
 
                     animDef = Actor.AnimDef()
+                    animDef.animBundle = animControl.getAnim()
                     animDef.animControl = animControl
                     self.__animControlDict[lodName][partName][animName] = animDef
 
@@ -2438,15 +2441,15 @@ class Actor(DirectObject, NodePath):
         return ActorInterval.ActorInterval(self, *args, **kw)
 
     def getAnimBlends(self, animName=None, partName=None, lodName=None):
-        """ Returns a list of the form:
-
-        [ (lodName, [(animName, [(partName, effect), (partName, effect), ...]),
-                     (animName, [(partName, effect), (partName, effect), ...]),
-                     ...]),
-          (lodName, [(animName, [(partName, effect), (partName, effect), ...]),
-                     (animName, [(partName, effect), (partName, effect), ...]),
-                     ...]),
-           ... ]
+        """Returns a list of the form::
+
+           [ (lodName, [(animName, [(partName, effect), (partName, effect), ...]),
+                        (animName, [(partName, effect), (partName, effect), ...]),
+                        ...]),
+             (lodName, [(animName, [(partName, effect), (partName, effect), ...]),
+                        (animName, [(partName, effect), (partName, effect), ...]),
+                        ...]),
+              ... ]
 
         This list reports the non-zero control effects for each
         partName within a particular animation and LOD. """

+ 2 - 1
direct/src/cluster/ClusterClient.py

@@ -1,4 +1,4 @@
-"""ClusterClient: Master for mutli-piping or PC clusters.  """
+"""ClusterClient: Master for multi-piping or PC clusters."""
 
 from panda3d.core import *
 from .ClusterMsgs import *
@@ -8,6 +8,7 @@ from direct.showbase import DirectObject
 from direct.task import Task
 import os
 
+
 class ClusterClient(DirectObject.DirectObject):
     notify = DirectNotifyGlobal.directNotify.newCategory("ClusterClient")
     MGR_NUM = 1000000

+ 20 - 17
direct/src/cluster/ClusterConfig.py

@@ -1,23 +1,26 @@
 
 from .ClusterClient import *
 
-# A dictionary of information for various cluster configurations.
-# Dictionary is keyed on cluster-config string
-# Each dictionary contains a list of display configurations, one for
-# each display in the cluster
-# Information that can be specified for each display:
-#      display name: Name of display (used in Configrc to specify server)
-#      display type: Used to flag client vs. server
-#      pos:   positional offset of display's camera from main cluster group
-#      hpr:   orientation offset of display's camera from main cluster group
-#      focal length: display's focal length (in mm)
-#      film size: display's film size (in inches)
-#      film offset: offset of film back (in inches)
-# Note: Note, this overrides offsets specified in DirectCamConfig.py
-# For now we only specify frustum for first display region of configuration
-# TODO: Need to handle multiple display regions per cluster node and to
-# generalize to non cluster situations
-
+#: A dictionary of information for various cluster configurations.
+#: Dictionary is keyed on cluster-config string
+#: Each dictionary contains a list of display configurations, one for
+#: each display in the cluster
+#:
+#: Information that can be specified for each display:
+#:
+#: - display name: Name of display (used in Configrc to specify server)
+#: - display type: Used to flag client vs. server
+#: - pos: positional offset of display's camera from main cluster group
+#: - hpr: orientation offset of display's camera from main cluster group
+#: - focal length: display's focal length (in mm)
+#: - film size: display's film size (in inches)
+#: - film offset: offset of film back (in inches)
+#:
+#: Note: this overrides offsets specified in DirectCamConfig.py
+#: For now we only specify frustum for first display region of configuration
+#:
+#: TODO: Need to handle multiple display regions per cluster node and to
+#: generalize to non cluster situations
 ClientConfigs = {
     'single-server':       [{'display name': 'display0',
                               'display mode': 'client',

+ 16 - 15
direct/src/controls/ControlManager.py

@@ -15,7 +15,9 @@ from direct.directnotify import DirectNotifyGlobal
 from direct.task import Task
 from panda3d.core import ConfigVariableBool
 
-CollisionHandlerRayStart = 4000.0 # This is a hack, it may be better to use a line instead of a ray.
+# This is a hack, it may be better to use a line instead of a ray.
+CollisionHandlerRayStart = 4000.0
+
 
 class ControlManager:
     notify = DirectNotifyGlobal.directNotify.newCategory("ControlManager")
@@ -52,14 +54,14 @@ class ControlManager:
         return 'ControlManager: using \'%s\'' % self.currentControlsName
 
     def add(self, controls, name="basic"):
-        """
-        controls is an avatar control system.
-        name is any key that you want to use to refer to the
-            the controls later (e.g. using the use(<name>) call).
+        """Add a control instance to the list of available control systems.
 
-        Add a control instance to the list of available control systems.
+        Args:
+            controls: an avatar control system.
+            name (str): any key that you want to use to refer to the controls
+                later (e.g. using the use(<name>) call).
 
-        See also: use().
+        See also: :meth:`use()`.
         """
         assert self.notify.debugCall(id(self))
         assert controls is not None
@@ -77,15 +79,14 @@ class ControlManager:
         return self.controls.get(name)
 
     def remove(self, name):
-        """
-        name is any key that was used to refer to the
-            the controls when they were added (e.g.
-            using the add(<controls>, <name>) call).
+        """Remove a control instance from the list of available control
+        systems.
 
-        Remove a control instance from the list of
-        available control systems.
+        Args:
+            name: any key that was used to refer to the controls when they were
+                added (e.g. using the add(<controls>, <name>) call).
 
-        See also: add().
+        See also: :meth:`add()`.
         """
         assert self.notify.debugCall(id(self))
         oldControls = self.controls.pop(name,None)
@@ -108,7 +109,7 @@ class ControlManager:
 
         Use a previously added control system.
 
-        See also: add().
+        See also: :meth:`add()`.
         """
         assert self.notify.debugCall(id(self))
         if __debug__ and hasattr(self, "ignoreUse"):

+ 8 - 6
direct/src/controls/DevWalker.py

@@ -2,15 +2,17 @@
 DevWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 

+ 8 - 6
direct/src/controls/GhostWalker.py

@@ -2,15 +2,17 @@
 GhostWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 

+ 8 - 6
direct/src/controls/GravityWalker.py

@@ -2,15 +2,17 @@
 GravityWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 from direct.directnotify.DirectNotifyGlobal import directNotify

+ 20 - 18
direct/src/controls/InputState.py

@@ -1,7 +1,6 @@
-
-
 from direct.directnotify import DirectNotifyGlobal
 from direct.showbase import DirectObject
+from direct.showbase.PythonUtil import SerialNumGen
 
 # internal class, don't create these on your own
 class InputStateToken:
@@ -136,14 +135,16 @@ class InputState(DirectObject.DirectObject):
 
     def watch(self, name, eventOn, eventOff, startState=False, inputSource=None):
         """
-        This returns a token; hold onto the token and call token.release() when you
-        no longer want to watch for these events.
-
-        # set up
-        token = inputState.watch('forward', 'w', 'w-up', inputSource=inputState.WASD)
-         ...
-        # tear down
-        token.release()
+        This returns a token; hold onto the token and call token.release() when
+        you no longer want to watch for these events.
+
+        Example::
+
+            # set up
+            token = inputState.watch('forward', 'w', 'w-up', inputSource=inputState.WASD)
+            ...
+            # tear down
+            token.release()
         """
         assert self.debugPrint(
             "watch(name=%s, eventOn=%s, eventOff=%s, startState=%s)"%(
@@ -192,15 +193,16 @@ class InputState(DirectObject.DirectObject):
         """
         Force isSet(name) to return 'value'.
 
-        This returns a token; hold onto the token and call token.release() when you
-        no longer want to force the state.
+        This returns a token; hold onto the token and call token.release() when
+        you no longer want to force the state.
+
+        Example::
 
-        example:
-        # set up
-        token=inputState.force('forward', True, inputSource='myForwardForcer')
-         ...
-        # tear down
-        token.release()
+            # set up
+            token = inputState.force('forward', True, inputSource='myForwardForcer')
+            ...
+            # tear down
+            token.release()
         """
         token = InputStateForceToken(self)
         self._token2forceInfo[token] = (name, inputSource)

+ 8 - 6
direct/src/controls/NonPhysicsWalker.py

@@ -2,15 +2,17 @@
 NonPhysicsWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 

+ 8 - 6
direct/src/controls/ObserverWalker.py

@@ -2,15 +2,17 @@
 ObserverWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 

+ 0 - 2
direct/src/controls/PhysicsRoller.py

@@ -1,2 +0,0 @@
-"""PhysicsRoller is for wheels, soccer balls, billiard balls, and other things that roll."""
-

+ 8 - 6
direct/src/controls/PhysicsWalker.py

@@ -2,15 +2,17 @@
 PhysicsWalker.py is for avatars.
 
 A walker control such as this one provides:
-    - creation of the collision nodes
-    - handling the keyboard and mouse input for avatar movement
-    - moving the avatar
+
+- creation of the collision nodes
+- handling the keyboard and mouse input for avatar movement
+- moving the avatar
 
 it does not:
-    - play sounds
-    - play animations
 
-although it does send messeges that allow a listener to play sounds or
+- play sounds
+- play animations
+
+although it does send messages that allow a listener to play sounds or
 animations based on walker events.
 """
 

+ 1 - 1
direct/src/controls/TwoDWalker.py

@@ -1,5 +1,5 @@
 """
-TwoDWalker.py is for controling the avatars in a 2D Scroller game environment.
+TwoDWalker.py is for controlling the avatars in a 2D scroller game environment.
 """
 
 from .GravityWalker import *

+ 5 - 689
direct/src/dcparser/dcClass.cxx

@@ -16,22 +16,13 @@
 #include "dcAtomicField.h"
 #include "hashGenerator.h"
 #include "dcindent.h"
-#include "dcmsgtypes.h"
 
 #include "dcClassParameter.h"
 #include <algorithm>
 
-#ifdef HAVE_PYTHON
-#include "py_panda.h"
-#endif
-
-using std::ostream;
-using std::ostringstream;
 using std::string;
 
 #ifdef WITHIN_PANDA
-#include "pStatTimer.h"
-
 #ifndef CPPPARSER
 PStatCollector DCClass::_update_pcollector("App:Show code:readerPollTask:Update");
 PStatCollector DCClass::_generate_pcollector("App:Show code:readerPollTask:Generate");
@@ -86,10 +77,7 @@ DCClass(DCFile *dc_file, const string &name, bool is_struct, bool bogus_class) :
   _number = -1;
   _constructor = nullptr;
 
-#ifdef HAVE_PYTHON
-  _class_def = nullptr;
-  _owner_class_def = nullptr;
-#endif
+  _python_class_defs = nullptr;
 }
 
 /**
@@ -105,11 +93,6 @@ DCClass::
   for (fi = _fields.begin(); fi != _fields.end(); ++fi) {
     delete (*fi);
   }
-
-#ifdef HAVE_PYTHON
-  Py_XDECREF(_class_def);
-  Py_XDECREF(_owner_class_def);
-#endif
 }
 
 /**
@@ -335,7 +318,7 @@ inherits_from_bogus_class() const {
  * Write a string representation of this instance to <out>.
  */
 void DCClass::
-output(ostream &out) const {
+output(std::ostream &out) const {
   if (_is_struct) {
     out << "struct";
   } else {
@@ -346,678 +329,11 @@ output(ostream &out) const {
   }
 }
 
-#ifdef HAVE_PYTHON
-/**
- * Returns true if the DCClass object has an associated Python class
- * definition, false otherwise.
- */
-bool DCClass::
-has_class_def() const {
-  return (_class_def != nullptr);
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Sets the class object associated with this DistributedClass.  This object
- * will be used to construct new instances of the class.
- */
-void DCClass::
-set_class_def(PyObject *class_def) {
-  Py_XINCREF(class_def);
-  Py_XDECREF(_class_def);
-  _class_def = class_def;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Returns the class object that was previously associated with this
- * DistributedClass.  This will return a new reference to the object.
- */
-PyObject *DCClass::
-get_class_def() const {
-  if (_class_def == nullptr) {
-    Py_INCREF(Py_None);
-    return Py_None;
-  }
-
-  Py_INCREF(_class_def);
-  return _class_def;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Returns true if the DCClass object has an associated Python owner class
- * definition, false otherwise.
- */
-bool DCClass::
-has_owner_class_def() const {
-  return (_owner_class_def != nullptr);
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Sets the owner class object associated with this DistributedClass.  This
- * object will be used to construct new owner instances of the class.
- */
-void DCClass::
-set_owner_class_def(PyObject *owner_class_def) {
-  Py_XINCREF(owner_class_def);
-  Py_XDECREF(_owner_class_def);
-  _owner_class_def = owner_class_def;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Returns the owner class object that was previously associated with this
- * DistributedClass.  This will return a new reference to the object.
- */
-PyObject *DCClass::
-get_owner_class_def() const {
-  if (_owner_class_def == nullptr) {
-    Py_INCREF(Py_None);
-    return Py_None;
-  }
-
-  Py_INCREF(_owner_class_def);
-  return _owner_class_def;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Extracts the update message out of the packer and applies it to the
- * indicated object by calling the appropriate method.
- */
-void DCClass::
-receive_update(PyObject *distobj, DatagramIterator &di) const {
-#ifdef WITHIN_PANDA
-  PStatTimer timer(((DCClass *)this)->_class_update_pcollector);
-#endif
-    DCPacker packer;
-    const char *data = (const char *)di.get_datagram().get_data();
-    packer.set_unpack_data(data + di.get_current_index(),
-                           di.get_remaining_size(), false);
-
-    int field_id = packer.raw_unpack_uint16();
-    DCField *field = get_field_by_index(field_id);
-    if (field == nullptr) {
-            ostringstream strm;
-            strm
-                << "Received update for field " << field_id << ", not in class "
-                << get_name();
-            nassert_raise(strm.str());
-            return;
-    }
-
-    packer.begin_unpack(field);
-    field->receive_update(packer, distobj);
-    packer.end_unpack();
-
-    di.skip_bytes(packer.get_num_unpacked_bytes());
-
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes a big datagram that includes all of the "required" fields that
- * are sent along with a normal "generate with required" message.  This is all
- * of the atomic fields that are marked "broadcast required".
- */
-void DCClass::
-receive_update_broadcast_required(PyObject *distobj, DatagramIterator &di) const {
-#ifdef WITHIN_PANDA
-  PStatTimer timer(((DCClass *)this)->_class_update_pcollector);
-#endif
-  DCPacker packer;
-  const char *data = (const char *)di.get_datagram().get_data();
-  packer.set_unpack_data(data + di.get_current_index(),
-                         di.get_remaining_size(), false);
-
-  int num_fields = get_num_inherited_fields();
-  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
-    DCField *field = get_inherited_field(i);
-    if (field->as_molecular_field() == nullptr &&
-        field->is_required() && field->is_broadcast()) {
-      packer.begin_unpack(field);
-      field->receive_update(packer, distobj);
-      if (!packer.end_unpack()) {
-        break;
-      }
-    }
-  }
-
-  di.skip_bytes(packer.get_num_unpacked_bytes());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes a big datagram that includes all of the "required" fields that
- * are sent along with a normal "generate with required" message.  This is all
- * of the atomic fields that are marked "broadcast ownrecv". Should be used
- * for 'owner-view' objects.
- */
-void DCClass::
-receive_update_broadcast_required_owner(PyObject *distobj,
-                                        DatagramIterator &di) const {
-#ifdef WITHIN_PANDA
-  PStatTimer timer(((DCClass *)this)->_class_update_pcollector);
-#endif
-  DCPacker packer;
-  const char *data = (const char *)di.get_datagram().get_data();
-  packer.set_unpack_data(data + di.get_current_index(),
-                         di.get_remaining_size(), false);
-
-  int num_fields = get_num_inherited_fields();
-  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
-    DCField *field = get_inherited_field(i);
-    if (field->as_molecular_field() == nullptr &&
-        field->is_required() && (field->is_ownrecv() || field->is_broadcast())) {
-      packer.begin_unpack(field);
-      field->receive_update(packer, distobj);
-      if (!packer.end_unpack()) {
-        break;
-      }
-    }
-  }
-
-  di.skip_bytes(packer.get_num_unpacked_bytes());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes a big datagram that includes all of the "required" fields that
- * are sent when an avatar is created.  This is all of the atomic fields that
- * are marked "required", whether they are broadcast or not.
- */
-void DCClass::
-receive_update_all_required(PyObject *distobj, DatagramIterator &di) const {
-#ifdef WITHIN_PANDA
-  PStatTimer timer(((DCClass *)this)->_class_update_pcollector);
-#endif
-  DCPacker packer;
-  const char *data = (const char *)di.get_datagram().get_data();
-  packer.set_unpack_data(data + di.get_current_index(),
-                         di.get_remaining_size(), false);
-
-  int num_fields = get_num_inherited_fields();
-  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
-    DCField *field = get_inherited_field(i);
-    if (field->as_molecular_field() == nullptr &&
-        field->is_required()) {
-      packer.begin_unpack(field);
-      field->receive_update(packer, distobj);
-      if (!packer.end_unpack()) {
-        break;
-      }
-    }
-  }
-
-  di.skip_bytes(packer.get_num_unpacked_bytes());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes a datagram that lists some additional fields that are broadcast
- * in one chunk.
- */
-void DCClass::
-receive_update_other(PyObject *distobj, DatagramIterator &di) const {
-#ifdef WITHIN_PANDA
-  PStatTimer timer(((DCClass *)this)->_class_update_pcollector);
-#endif
-  int num_fields = di.get_uint16();
-  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
-    receive_update(distobj, di);
-  }
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes an update for a named field from a packed value blob.
- */
-void DCClass::
-direct_update(PyObject *distobj, const string &field_name,
-              const vector_uchar &value_blob) {
-  DCField *field = get_field_by_name(field_name);
-  nassertv_always(field != nullptr);
-
-  DCPacker packer;
-  packer.set_unpack_data(value_blob);
-  packer.begin_unpack(field);
-  field->receive_update(packer, distobj);
-  packer.end_unpack();
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Processes an update for a named field from a packed datagram.
- */
-void DCClass::
-direct_update(PyObject *distobj, const string &field_name,
-              const Datagram &datagram) {
-  DCField *field = get_field_by_name(field_name);
-  nassertv_always(field != nullptr);
-
-  DCPacker packer;
-  packer.set_unpack_data((const char *)datagram.get_data(), datagram.get_length(), false);
-  packer.begin_unpack(field);
-  field->receive_update(packer, distobj);
-  packer.end_unpack();
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Looks up the current value of the indicated field by calling the
- * appropriate get*() function, then packs that value into the datagram.  This
- * field is presumably either a required field or a specified optional field,
- * and we are building up a datagram for the generate-with-required message.
- *
- * Returns true on success, false on failure.
- */
-bool DCClass::
-pack_required_field(Datagram &datagram, PyObject *distobj,
-                    const DCField *field) const {
-  DCPacker packer;
-  packer.begin_pack(field);
-  if (!pack_required_field(packer, distobj, field)) {
-    return false;
-  }
-  if (!packer.end_pack()) {
-    return false;
-  }
-
-  datagram.append_data(packer.get_data(), packer.get_length());
-  return true;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Looks up the current value of the indicated field by calling the
- * appropriate get*() function, then packs that value into the packer.  This
- * field is presumably either a required field or a specified optional field,
- * and we are building up a datagram for the generate-with-required message.
- *
- * Returns true on success, false on failure.
- */
-bool DCClass::
-pack_required_field(DCPacker &packer, PyObject *distobj,
-                    const DCField *field) const {
-  const DCParameter *parameter = field->as_parameter();
-  if (parameter != nullptr) {
-    // This is the easy case: to pack a parameter, we just look on the class
-    // object for the data element.
-    string field_name = field->get_name();
-
-    if (!PyObject_HasAttrString(distobj, (char *)field_name.c_str())) {
-      // If the attribute is not defined, but the field has a default value
-      // specified, quietly pack the default value.
-      if (field->has_default_value()) {
-        packer.pack_default_value();
-        return true;
-      }
-
-      // If there is no default value specified, it's an error.
-      ostringstream strm;
-      strm << "Data element " << field_name
-           << ", required by dc file for dclass " << get_name()
-           << ", not defined on object";
-      nassert_raise(strm.str());
-      return false;
-    }
-    PyObject *result =
-      PyObject_GetAttrString(distobj, (char *)field_name.c_str());
-    nassertr(result != nullptr, false);
-
-    // Now pack the value into the datagram.
-    bool pack_ok = parameter->pack_args(packer, result);
-    Py_DECREF(result);
-
-    return pack_ok;
-  }
-
-  if (field->as_molecular_field() != nullptr) {
-    ostringstream strm;
-    strm << "Cannot pack molecular field " << field->get_name()
-         << " for generate";
-    nassert_raise(strm.str());
-    return false;
-  }
-
-  const DCAtomicField *atom = field->as_atomic_field();
-  nassertr(atom != nullptr, false);
-
-  // We need to get the initial value of this field.  There isn't a good,
-  // robust way to get this; presently, we just mangle the "setFoo()" name of
-  // the required field into "getFoo()" and call that.
-  string setter_name = atom->get_name();
-
-  if (setter_name.empty()) {
-    ostringstream strm;
-    strm << "Required field is unnamed!";
-    nassert_raise(strm.str());
-    return false;
-  }
-
-  if (atom->get_num_elements() == 0) {
-    // It sure doesn't make sense to have a required field with no parameters.
-    // What data, exactly, is required?
-    ostringstream strm;
-    strm << "Required field " << setter_name << " has no parameters!";
-    nassert_raise(strm.str());
-    return false;
-  }
-
-  string getter_name = setter_name;
-  if (setter_name.substr(0, 3) == "set") {
-    // If the original method started with "set", we mangle this directly to
-    // "get".
-    getter_name[0] = 'g';
-
-  } else {
-    // Otherwise, we add a "get" prefix, and capitalize the next letter.
-    getter_name = "get" + setter_name;
-    getter_name[3] = toupper(getter_name[3]);
-  }
-
-  // Now we have to look up the getter on the distributed object and call it.
-  if (!PyObject_HasAttrString(distobj, (char *)getter_name.c_str())) {
-    // As above, if there's no getter but the field has a default value
-    // specified, quietly pack the default value.
-    if (field->has_default_value()) {
-      packer.pack_default_value();
-      return true;
-    }
-
-    // Otherwise, with no default value it's an error.
-    ostringstream strm;
-    strm << "Distributed class " << get_name()
-         << " doesn't have getter named " << getter_name
-         << " to match required field " << setter_name;
-    nassert_raise(strm.str());
-    return false;
-  }
-  PyObject *func =
-    PyObject_GetAttrString(distobj, (char *)getter_name.c_str());
-  nassertr(func != nullptr, false);
-
-  PyObject *empty_args = PyTuple_New(0);
-  PyObject *result = PyObject_CallObject(func, empty_args);
-  Py_DECREF(empty_args);
-  Py_DECREF(func);
-  if (result == nullptr) {
-    // We don't set this as an exception, since presumably the Python method
-    // itself has already triggered a Python exception.
-    std::cerr << "Error when calling " << getter_name << "\n";
-    return false;
-  }
-
-  if (atom->get_num_elements() == 1) {
-    // In this case, we expect the getter to return one object, which we wrap
-    // up in a tuple.
-    PyObject *tuple = PyTuple_New(1);
-    PyTuple_SET_ITEM(tuple, 0, result);
-    result = tuple;
-
-  } else {
-    // Otherwise, it had better already be a sequence or tuple of some sort.
-    if (!PySequence_Check(result)) {
-      ostringstream strm;
-      strm << "Since dclass " << get_name() << " method " << setter_name
-           << " is declared to have multiple parameters, Python function "
-           << getter_name << " must return a list or tuple.\n";
-      nassert_raise(strm.str());
-      return false;
-    }
-  }
-
-  // Now pack the arguments into the datagram.
-  bool pack_ok = atom->pack_args(packer, result);
-  Py_DECREF(result);
-
-  return pack_ok;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update for
- * the indicated distributed object from the client.
- */
-Datagram DCClass::
-client_format_update(const string &field_name, DOID_TYPE do_id,
-                     PyObject *args) const {
-  DCField *field = get_field_by_name(field_name);
-  if (field == nullptr) {
-    ostringstream strm;
-    strm << "No field named " << field_name << " in class " << get_name()
-         << "\n";
-    nassert_raise(strm.str());
-    return Datagram();
-  }
-
-  return field->client_format_update(do_id, args);
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update for
- * the indicated distributed object from the AI.
- */
-Datagram DCClass::
-ai_format_update(const string &field_name, DOID_TYPE do_id,
-                 CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const {
-  DCField *field = get_field_by_name(field_name);
-  if (field == nullptr) {
-    ostringstream strm;
-    strm << "No field named " << field_name << " in class " << get_name()
-         << "\n";
-    nassert_raise(strm.str());
-    return Datagram();
-  }
-
-  return field->ai_format_update(do_id, to_id, from_id, args);
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update,
- * using the indicated msg type for the indicated distributed object from the
- * AI.
- */
-Datagram DCClass::
-ai_format_update_msg_type(const string &field_name, DOID_TYPE do_id,
-                 CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, int msg_type, PyObject *args) const {
-  DCField *field = get_field_by_name(field_name);
-  if (field == nullptr) {
-    ostringstream strm;
-    strm << "No field named " << field_name << " in class " << get_name()
-         << "\n";
-    nassert_raise(strm.str());
-    return Datagram();
-  }
-
-  return field->ai_format_update_msg_type(do_id, to_id, from_id, msg_type, args);
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to generate a new
- * distributed object from the client.  This requires querying the object for
- * the initial value of its required fields.
- *
- * optional_fields is a list of fieldNames to generate in addition to the
- * normal required fields.
- *
- * This method is only called by the CMU implementation.
- */
-Datagram DCClass::
-client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
-                           ZONEID_TYPE zone_id,
-                           PyObject *optional_fields) const {
-  DCPacker packer;
-
-  packer.raw_pack_uint16(CLIENT_OBJECT_GENERATE_CMU);
-
-  packer.raw_pack_uint32(zone_id);
-  packer.raw_pack_uint16(_number);
-  packer.raw_pack_uint32(do_id);
-
-  // Specify all of the required fields.
-  int num_fields = get_num_inherited_fields();
-  for (int i = 0; i < num_fields; ++i) {
-    DCField *field = get_inherited_field(i);
-    if (field->is_required() && field->as_molecular_field() == nullptr) {
-      packer.begin_pack(field);
-      if (!pack_required_field(packer, distobj, field)) {
-        return Datagram();
-      }
-      packer.end_pack();
-    }
-  }
-
-  // Also specify the optional fields.
-  int num_optional_fields = 0;
-  if (PyObject_IsTrue(optional_fields)) {
-    num_optional_fields = PySequence_Size(optional_fields);
-  }
-  packer.raw_pack_uint16(num_optional_fields);
-
-  for (int i = 0; i < num_optional_fields; i++) {
-    PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
-#if PY_MAJOR_VERSION >= 3
-    string field_name = PyUnicode_AsUTF8(py_field_name);
-#else
-    string field_name = PyString_AsString(py_field_name);
-#endif
-    Py_XDECREF(py_field_name);
-
-    DCField *field = get_field_by_name(field_name);
-    if (field == nullptr) {
-      ostringstream strm;
-      strm << "No field named " << field_name << " in class " << get_name()
-           << "\n";
-      nassert_raise(strm.str());
-      return Datagram();
-    }
-    packer.raw_pack_uint16(field->get_number());
-    packer.begin_pack(field);
-    if (!pack_required_field(packer, distobj, field)) {
-      return Datagram();
-    }
-    packer.end_pack();
-  }
-
-  return Datagram(packer.get_data(), packer.get_length());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to generate a new
- * distributed object from the AI. This requires querying the object for the
- * initial value of its required fields.
- *
- * optional_fields is a list of fieldNames to generate in addition to the
- * normal required fields.
- */
-Datagram DCClass::
-ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
-                   DOID_TYPE parent_id, ZONEID_TYPE zone_id,
-                   CHANNEL_TYPE district_channel_id, CHANNEL_TYPE from_channel_id,
-                   PyObject *optional_fields) const {
-  DCPacker packer;
-
-  packer.raw_pack_uint8(1);
-  packer.RAW_PACK_CHANNEL(district_channel_id);
-  packer.RAW_PACK_CHANNEL(from_channel_id);
-    // packer.raw_pack_uint8('A');
-
-  bool has_optional_fields = (PyObject_IsTrue(optional_fields) != 0);
-
-  if (has_optional_fields) {
-    packer.raw_pack_uint16(STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER);
-  } else {
-    packer.raw_pack_uint16(STATESERVER_CREATE_OBJECT_WITH_REQUIRED);
-  }
-
-  packer.raw_pack_uint32(do_id);
-  // Parent is a bit overloaded; this parent is not about inheritance, this
-  // one is about the visibility container parent, i.e.  the zone parent:
-  packer.raw_pack_uint32(parent_id);
-  packer.raw_pack_uint32(zone_id);
-  packer.raw_pack_uint16(_number);
-
-  // Specify all of the required fields.
-  int num_fields = get_num_inherited_fields();
-  for (int i = 0; i < num_fields; ++i) {
-    DCField *field = get_inherited_field(i);
-    if (field->is_required() && field->as_molecular_field() == nullptr) {
-      packer.begin_pack(field);
-      if (!pack_required_field(packer, distobj, field)) {
-        return Datagram();
-      }
-      packer.end_pack();
-    }
-  }
-
-  // Also specify the optional fields.
-  if (has_optional_fields) {
-    int num_optional_fields = PySequence_Size(optional_fields);
-    packer.raw_pack_uint16(num_optional_fields);
-
-    for (int i = 0; i < num_optional_fields; ++i) {
-      PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
-#if PY_MAJOR_VERSION >= 3
-      string field_name = PyUnicode_AsUTF8(py_field_name);
-#else
-      string field_name = PyString_AsString(py_field_name);
-#endif
-      Py_XDECREF(py_field_name);
-
-      DCField *field = get_field_by_name(field_name);
-      if (field == nullptr) {
-        ostringstream strm;
-        strm << "No field named " << field_name << " in class " << get_name()
-             << "\n";
-        nassert_raise(strm.str());
-        return Datagram();
-      }
-
-      packer.raw_pack_uint16(field->get_number());
-
-      packer.begin_pack(field);
-      if (!pack_required_field(packer, distobj, field)) {
-        return Datagram();
-      }
-      packer.end_pack();
-    }
-  }
-
-  return Datagram(packer.get_data(), packer.get_length());
-}
-#endif  // HAVE_PYTHON
-
 /**
  * Write a string representation of this instance to <out>.
  */
 void DCClass::
-output(ostream &out, bool brief) const {
+output(std::ostream &out, bool brief) const {
   output_instance(out, brief, "", "", "");
 }
 
@@ -1026,7 +342,7 @@ output(ostream &out, bool brief) const {
  * stream.
  */
 void DCClass::
-write(ostream &out, bool brief, int indent_level) const {
+write(std::ostream &out, bool brief, int indent_level) const {
   indent(out, indent_level);
   if (_is_struct) {
     out << "struct";
@@ -1086,7 +402,7 @@ write(ostream &out, bool brief, int indent_level) const {
  * stream.
  */
 void DCClass::
-output_instance(ostream &out, bool brief, const string &prename,
+output_instance(std::ostream &out, bool brief, const string &prename,
                 const string &name, const string &postname) const {
   if (_is_struct) {
     out << "struct";

+ 58 - 44
direct/src/dcparser/dcClass.h

@@ -17,11 +17,12 @@
 #include "dcbase.h"
 #include "dcField.h"
 #include "dcDeclaration.h"
-#include "dcPython.h"
 
 #ifdef WITHIN_PANDA
 #include "pStatCollector.h"
 #include "configVariableBool.h"
+#include "extension.h"
+#include "datagramIterator.h"
 
 extern ConfigVariableBool dc_multiple_inheritance;
 extern ConfigVariableBool dc_virtual_inheritance;
@@ -80,44 +81,52 @@ PUBLISHED:
 
   virtual void output(std::ostream &out) const;
 
-#ifdef HAVE_PYTHON
-  bool has_class_def() const;
-  void set_class_def(PyObject *class_def);
-  PyObject *get_class_def() const;
-  bool has_owner_class_def() const;
-  void set_owner_class_def(PyObject *owner_class_def);
-  PyObject *get_owner_class_def() const;
-
-  void receive_update(PyObject *distobj, DatagramIterator &di) const;
-  void receive_update_broadcast_required(PyObject *distobj, DatagramIterator &di) const;
-  void receive_update_broadcast_required_owner(PyObject *distobj, DatagramIterator &di) const;
-  void receive_update_all_required(PyObject *distobj, DatagramIterator &di) const;
-  void receive_update_other(PyObject *distobj, DatagramIterator &di) const;
-
-  void direct_update(PyObject *distobj, const std::string &field_name,
-                     const vector_uchar &value_blob);
-  void direct_update(PyObject *distobj, const std::string &field_name,
-                     const Datagram &datagram);
-  bool pack_required_field(Datagram &datagram, PyObject *distobj,
-                           const DCField *field) const;
-  bool pack_required_field(DCPacker &packer, PyObject *distobj,
-                           const DCField *field) const;
-
-
-
-  Datagram client_format_update(const std::string &field_name,
-                                DOID_TYPE do_id, PyObject *args) const;
-  Datagram ai_format_update(const std::string &field_name, DOID_TYPE do_id,
-                            CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const;
-  Datagram ai_format_update_msg_type(const std::string &field_name, DOID_TYPE do_id,
-                            CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, int msg_type, PyObject *args) const;
-  Datagram ai_format_generate(PyObject *distobj, DOID_TYPE do_id, ZONEID_TYPE parent_id, ZONEID_TYPE zone_id,
-                              CHANNEL_TYPE district_channel_id, CHANNEL_TYPE from_channel_id,
-                              PyObject *optional_fields) const;
-  Datagram client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
-                                      ZONEID_TYPE zone_id,                                                           PyObject *optional_fields) const;
+  EXTENSION(bool has_class_def() const);
+  EXTENSION(void set_class_def(PyObject *class_def));
+  EXTENSION(PyObject *get_class_def() const);
+  EXTENSION(bool has_owner_class_def() const);
+  EXTENSION(void set_owner_class_def(PyObject *owner_class_def));
+  EXTENSION(PyObject *get_owner_class_def() const);
+
+  EXTENSION(void receive_update(PyObject *distobj, DatagramIterator &di) const);
+  EXTENSION(void receive_update_broadcast_required(PyObject *distobj, DatagramIterator &di) const);
+  EXTENSION(void receive_update_broadcast_required_owner(PyObject *distobj, DatagramIterator &di) const);
+  EXTENSION(void receive_update_all_required(PyObject *distobj, DatagramIterator &di) const);
+  EXTENSION(void receive_update_other(PyObject *distobj, DatagramIterator &di) const);
+
+  EXTENSION(void direct_update(PyObject *distobj, const std::string &field_name,
+                               const vector_uchar &value_blob));
+  EXTENSION(void direct_update(PyObject *distobj, const std::string &field_name,
+                               const Datagram &datagram));
+  EXTENSION(bool pack_required_field(Datagram &datagram, PyObject *distobj,
+                                     const DCField *field) const);
+  EXTENSION(bool pack_required_field(DCPacker &packer, PyObject *distobj,
+                                     const DCField *field) const);
+
+
+
+  EXTENSION(Datagram client_format_update(const std::string &field_name,
+                                          DOID_TYPE do_id, PyObject *args) const);
+  EXTENSION(Datagram ai_format_update(const std::string &field_name,
+                                      DOID_TYPE do_id,
+                                      CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
+                                      PyObject *args) const);
+  EXTENSION(Datagram ai_format_update_msg_type(const std::string &field_name,
+                                               DOID_TYPE do_id,
+                                               CHANNEL_TYPE to_id,
+                                               CHANNEL_TYPE from_id,
+                                               int msg_type,
+                                               PyObject *args) const);
+  EXTENSION(Datagram ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
+                                        ZONEID_TYPE parent_id,
+                                        ZONEID_TYPE zone_id,
+                                        CHANNEL_TYPE district_channel_id,
+                                        CHANNEL_TYPE from_channel_id,
+                                        PyObject *optional_fields) const);
+  EXTENSION(Datagram client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
+                                                ZONEID_TYPE zone_id,
+                                                PyObject *optional_fields) const);
 
-#endif
 
 public:
   virtual void output(std::ostream &out, bool brief) const;
@@ -136,8 +145,8 @@ private:
   void shadow_inherited_field(const std::string &name);
 
 #ifdef WITHIN_PANDA
-  PStatCollector _class_update_pcollector;
-  PStatCollector _class_generate_pcollector;
+  mutable PStatCollector _class_update_pcollector;
+  mutable PStatCollector _class_generate_pcollector;
   static PStatCollector _update_pcollector;
   static PStatCollector _generate_pcollector;
 #endif
@@ -163,12 +172,17 @@ private:
   typedef pmap<int, DCField *> FieldsByIndex;
   FieldsByIndex _fields_by_index;
 
-#ifdef HAVE_PYTHON
-  PyObject *_class_def;
-  PyObject *_owner_class_def;
-#endif
+  // See pandaNode.h for an explanation of this trick
+  class PythonClassDefs : public ReferenceCount {
+  public:
+    virtual ~PythonClassDefs() {};
+  };
+  PT(PythonClassDefs) _python_class_defs;
 
   friend class DCField;
+#ifdef WITHIN_PANDA
+  friend class Extension<DCClass>;
+#endif
 };
 
 #include "dcClass.I"

+ 665 - 0
direct/src/dcparser/dcClass_ext.cxx

@@ -0,0 +1,665 @@
+/**
+ * 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 dcClass_ext.cxx
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#include "dcClass_ext.h"
+#include "dcField_ext.h"
+#include "dcAtomicField.h"
+#include "dcPacker.h"
+#include "dcmsgtypes.h"
+
+#include "datagram.h"
+#include "datagramIterator.h"
+#include "pStatTimer.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Returns true if the DCClass object has an associated Python class
+ * definition, false otherwise.
+ */
+bool Extension<DCClass>::
+has_class_def() const {
+  return _this->_python_class_defs != nullptr
+      && ((PythonClassDefsImpl *)_this->_python_class_defs.p())->_class_def != nullptr;
+}
+
+/**
+ * Sets the class object associated with this DistributedClass.  This object
+ * will be used to construct new instances of the class.
+ */
+void Extension<DCClass>::
+set_class_def(PyObject *class_def) {
+  PythonClassDefsImpl *defs = do_get_defs();
+
+  Py_XINCREF(class_def);
+  Py_XDECREF(defs->_class_def);
+  defs->_class_def = class_def;
+}
+
+/**
+ * Returns the class object that was previously associated with this
+ * DistributedClass.  This will return a new reference to the object.
+ */
+PyObject *Extension<DCClass>::
+get_class_def() const {
+  if (!has_class_def()) {
+    Py_INCREF(Py_None);
+    return Py_None;
+  }
+
+  PythonClassDefsImpl *defs = do_get_defs();
+  Py_INCREF(defs->_class_def);
+  return defs->_class_def;
+}
+
+/**
+ * Returns true if the DCClass object has an associated Python owner class
+ * definition, false otherwise.
+ */
+bool Extension<DCClass>::
+has_owner_class_def() const {
+  return _this->_python_class_defs != nullptr
+      && ((PythonClassDefsImpl *)_this->_python_class_defs.p())->_owner_class_def != nullptr;
+}
+
+/**
+ * Sets the owner class object associated with this DistributedClass.  This
+ * object will be used to construct new owner instances of the class.
+ */
+void Extension<DCClass>::
+set_owner_class_def(PyObject *owner_class_def) {
+  PythonClassDefsImpl *defs = do_get_defs();
+
+  Py_XINCREF(owner_class_def);
+  Py_XDECREF(defs->_owner_class_def);
+  defs->_owner_class_def = owner_class_def;
+}
+
+/**
+ * Returns the owner class object that was previously associated with this
+ * DistributedClass.  This will return a new reference to the object.
+ */
+PyObject *Extension<DCClass>::
+get_owner_class_def() const {
+  if (!has_owner_class_def()) {
+    Py_INCREF(Py_None);
+    return Py_None;
+  }
+
+  PythonClassDefsImpl *defs = do_get_defs();
+  Py_INCREF(defs->_owner_class_def);
+  return defs->_owner_class_def;
+}
+
+/**
+ * Extracts the update message out of the packer and applies it to the
+ * indicated object by calling the appropriate method.
+ */
+void Extension<DCClass>::
+receive_update(PyObject *distobj, DatagramIterator &di) const {
+  PStatTimer timer(_this->_class_update_pcollector);
+  DCPacker packer;
+  const char *data = (const char *)di.get_datagram().get_data();
+  packer.set_unpack_data(data + di.get_current_index(),
+                         di.get_remaining_size(), false);
+
+  int field_id = packer.raw_unpack_uint16();
+  DCField *field = _this->get_field_by_index(field_id);
+  if (field == nullptr) {
+    ostringstream strm;
+    strm
+        << "Received update for field " << field_id << ", not in class "
+        << _this->get_name();
+    nassert_raise(strm.str());
+    return;
+  }
+
+  packer.begin_unpack(field);
+  invoke_extension(field).receive_update(packer, distobj);
+  packer.end_unpack();
+
+  di.skip_bytes(packer.get_num_unpacked_bytes());
+}
+
+/**
+ * Processes a big datagram that includes all of the "required" fields that
+ * are sent along with a normal "generate with required" message.  This is all
+ * of the atomic fields that are marked "broadcast required".
+ */
+void Extension<DCClass>::
+receive_update_broadcast_required(PyObject *distobj, DatagramIterator &di) const {
+  PStatTimer timer(_this->_class_update_pcollector);
+  DCPacker packer;
+  const char *data = (const char *)di.get_datagram().get_data();
+  packer.set_unpack_data(data + di.get_current_index(),
+                         di.get_remaining_size(), false);
+
+  int num_fields = _this->get_num_inherited_fields();
+  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
+    DCField *field = _this->get_inherited_field(i);
+    if (field->as_molecular_field() == nullptr &&
+        field->is_required() && field->is_broadcast()) {
+      packer.begin_unpack(field);
+      invoke_extension(field).receive_update(packer, distobj);
+      if (!packer.end_unpack()) {
+        break;
+      }
+    }
+  }
+
+  di.skip_bytes(packer.get_num_unpacked_bytes());
+}
+
+/**
+ * Processes a big datagram that includes all of the "required" fields that
+ * are sent along with a normal "generate with required" message.  This is all
+ * of the atomic fields that are marked "broadcast ownrecv". Should be used
+ * for 'owner-view' objects.
+ */
+void Extension<DCClass>::
+receive_update_broadcast_required_owner(PyObject *distobj,
+                                        DatagramIterator &di) const {
+  PStatTimer timer(_this->_class_update_pcollector);
+  DCPacker packer;
+  const char *data = (const char *)di.get_datagram().get_data();
+  packer.set_unpack_data(data + di.get_current_index(),
+                         di.get_remaining_size(), false);
+
+  int num_fields = _this->get_num_inherited_fields();
+  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
+    DCField *field = _this->get_inherited_field(i);
+    if (field->as_molecular_field() == nullptr &&
+        field->is_required() && (field->is_ownrecv() || field->is_broadcast())) {
+      packer.begin_unpack(field);
+      invoke_extension(field).receive_update(packer, distobj);
+      if (!packer.end_unpack()) {
+        break;
+      }
+    }
+  }
+
+  di.skip_bytes(packer.get_num_unpacked_bytes());
+}
+
+/**
+ * Processes a big datagram that includes all of the "required" fields that
+ * are sent when an avatar is created.  This is all of the atomic fields that
+ * are marked "required", whether they are broadcast or not.
+ */
+void Extension<DCClass>::
+receive_update_all_required(PyObject *distobj, DatagramIterator &di) const {
+  PStatTimer timer(_this->_class_update_pcollector);
+  DCPacker packer;
+  const char *data = (const char *)di.get_datagram().get_data();
+  packer.set_unpack_data(data + di.get_current_index(),
+                         di.get_remaining_size(), false);
+
+  int num_fields = _this->get_num_inherited_fields();
+  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
+    DCField *field = _this->get_inherited_field(i);
+    if (field->as_molecular_field() == nullptr &&
+        field->is_required()) {
+      packer.begin_unpack(field);
+      invoke_extension(field).receive_update(packer, distobj);
+      if (!packer.end_unpack()) {
+        break;
+      }
+    }
+  }
+
+  di.skip_bytes(packer.get_num_unpacked_bytes());
+}
+
+/**
+ * Processes a datagram that lists some additional fields that are broadcast
+ * in one chunk.
+ */
+void Extension<DCClass>::
+receive_update_other(PyObject *distobj, DatagramIterator &di) const {
+  PStatTimer timer(_this->_class_update_pcollector);
+  int num_fields = di.get_uint16();
+  for (int i = 0; i < num_fields && !PyErr_Occurred(); ++i) {
+    receive_update(distobj, di);
+  }
+}
+
+/**
+ * Processes an update for a named field from a packed value blob.
+ */
+void Extension<DCClass>::
+direct_update(PyObject *distobj, const std::string &field_name,
+              const vector_uchar &value_blob) {
+  DCField *field = _this->get_field_by_name(field_name);
+  nassertv_always(field != nullptr);
+
+  DCPacker packer;
+  packer.set_unpack_data(value_blob);
+  packer.begin_unpack(field);
+  invoke_extension(field).receive_update(packer, distobj);
+  packer.end_unpack();
+}
+
+/**
+ * Processes an update for a named field from a packed datagram.
+ */
+void Extension<DCClass>::
+direct_update(PyObject *distobj, const std::string &field_name,
+              const Datagram &datagram) {
+  DCField *field = _this->get_field_by_name(field_name);
+  nassertv_always(field != nullptr);
+
+  DCPacker packer;
+  packer.set_unpack_data((const char *)datagram.get_data(), datagram.get_length(), false);
+  packer.begin_unpack(field);
+  invoke_extension(field).receive_update(packer, distobj);
+  packer.end_unpack();
+}
+
+/**
+ * Looks up the current value of the indicated field by calling the
+ * appropriate get*() function, then packs that value into the datagram.  This
+ * field is presumably either a required field or a specified optional field,
+ * and we are building up a datagram for the generate-with-required message.
+ *
+ * Returns true on success, false on failure.
+ */
+bool Extension<DCClass>::
+pack_required_field(Datagram &datagram, PyObject *distobj,
+                    const DCField *field) const {
+  DCPacker packer;
+  packer.begin_pack(field);
+  if (!pack_required_field(packer, distobj, field)) {
+    return false;
+  }
+  if (!packer.end_pack()) {
+    return false;
+  }
+
+  datagram.append_data(packer.get_data(), packer.get_length());
+  return true;
+}
+
+/**
+ * Looks up the current value of the indicated field by calling the
+ * appropriate get*() function, then packs that value into the packer.  This
+ * field is presumably either a required field or a specified optional field,
+ * and we are building up a datagram for the generate-with-required message.
+ *
+ * Returns true on success, false on failure.
+ */
+bool Extension<DCClass>::
+pack_required_field(DCPacker &packer, PyObject *distobj,
+                    const DCField *field) const {
+  using std::ostringstream;
+
+  const DCParameter *parameter = field->as_parameter();
+  if (parameter != nullptr) {
+    // This is the easy case: to pack a parameter, we just look on the class
+    // object for the data element.
+    std::string field_name = field->get_name();
+
+    if (!PyObject_HasAttrString(distobj, (char *)field_name.c_str())) {
+      // If the attribute is not defined, but the field has a default value
+      // specified, quietly pack the default value.
+      if (field->has_default_value()) {
+        packer.pack_default_value();
+        return true;
+      }
+
+      // If there is no default value specified, it's an error.
+      ostringstream strm;
+      strm << "Data element " << field_name
+           << ", required by dc file for dclass " << _this->get_name()
+           << ", not defined on object";
+      nassert_raise(strm.str());
+      return false;
+    }
+    PyObject *result =
+      PyObject_GetAttrString(distobj, (char *)field_name.c_str());
+    nassertr(result != nullptr, false);
+
+    // Now pack the value into the datagram.
+    bool pack_ok = invoke_extension((DCField *)parameter).pack_args(packer, result);
+    Py_DECREF(result);
+
+    return pack_ok;
+  }
+
+  if (field->as_molecular_field() != nullptr) {
+    ostringstream strm;
+    strm << "Cannot pack molecular field " << field->get_name()
+         << " for generate";
+    nassert_raise(strm.str());
+    return false;
+  }
+
+  const DCAtomicField *atom = field->as_atomic_field();
+  nassertr(atom != nullptr, false);
+
+  // We need to get the initial value of this field.  There isn't a good,
+  // robust way to get this; presently, we just mangle the "setFoo()" name of
+  // the required field into "getFoo()" and call that.
+  std::string setter_name = atom->get_name();
+
+  if (setter_name.empty()) {
+    ostringstream strm;
+    strm << "Required field is unnamed!";
+    nassert_raise(strm.str());
+    return false;
+  }
+
+  if (atom->get_num_elements() == 0) {
+    // It sure doesn't make sense to have a required field with no parameters.
+    // What data, exactly, is required?
+    ostringstream strm;
+    strm << "Required field " << setter_name << " has no parameters!";
+    nassert_raise(strm.str());
+    return false;
+  }
+
+  std::string getter_name = setter_name;
+  if (setter_name.substr(0, 3) == "set") {
+    // If the original method started with "set", we mangle this directly to
+    // "get".
+    getter_name[0] = 'g';
+
+  } else {
+    // Otherwise, we add a "get" prefix, and capitalize the next letter.
+    getter_name = "get" + setter_name;
+    getter_name[3] = toupper(getter_name[3]);
+  }
+
+  // Now we have to look up the getter on the distributed object and call it.
+  if (!PyObject_HasAttrString(distobj, (char *)getter_name.c_str())) {
+    // As above, if there's no getter but the field has a default value
+    // specified, quietly pack the default value.
+    if (field->has_default_value()) {
+      packer.pack_default_value();
+      return true;
+    }
+
+    // Otherwise, with no default value it's an error.
+    ostringstream strm;
+    strm << "Distributed class " << _this->get_name()
+         << " doesn't have getter named " << getter_name
+         << " to match required field " << setter_name;
+    nassert_raise(strm.str());
+    return false;
+  }
+  PyObject *func =
+    PyObject_GetAttrString(distobj, (char *)getter_name.c_str());
+  nassertr(func != nullptr, false);
+
+  PyObject *empty_args = PyTuple_New(0);
+  PyObject *result = PyObject_CallObject(func, empty_args);
+  Py_DECREF(empty_args);
+  Py_DECREF(func);
+  if (result == nullptr) {
+    // We don't set this as an exception, since presumably the Python method
+    // itself has already triggered a Python exception.
+    std::cerr << "Error when calling " << getter_name << "\n";
+    return false;
+  }
+
+  if (atom->get_num_elements() == 1) {
+    // In this case, we expect the getter to return one object, which we wrap
+    // up in a tuple.
+    PyObject *tuple = PyTuple_New(1);
+    PyTuple_SET_ITEM(tuple, 0, result);
+    result = tuple;
+
+  } else {
+    // Otherwise, it had better already be a sequence or tuple of some sort.
+    if (!PySequence_Check(result)) {
+      ostringstream strm;
+      strm << "Since dclass " << _this->get_name() << " method " << setter_name
+           << " is declared to have multiple parameters, Python function "
+           << getter_name << " must return a list or tuple.\n";
+      nassert_raise(strm.str());
+      return false;
+    }
+  }
+
+  // Now pack the arguments into the datagram.
+  bool pack_ok = invoke_extension((DCField *)atom).pack_args(packer, result);
+  Py_DECREF(result);
+
+  return pack_ok;
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update for
+ * the indicated distributed object from the client.
+ */
+Datagram Extension<DCClass>::
+client_format_update(const std::string &field_name, DOID_TYPE do_id,
+                     PyObject *args) const {
+  DCField *field = _this->get_field_by_name(field_name);
+  if (field == nullptr) {
+    std::ostringstream strm;
+    strm << "No field named " << field_name << " in class " << _this->get_name()
+         << "\n";
+    nassert_raise(strm.str());
+    return Datagram();
+  }
+
+  return invoke_extension(field).client_format_update(do_id, args);
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update for
+ * the indicated distributed object from the AI.
+ */
+Datagram Extension<DCClass>::
+ai_format_update(const std::string &field_name, DOID_TYPE do_id,
+                 CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const {
+  DCField *field = _this->get_field_by_name(field_name);
+  if (field == nullptr) {
+    std::ostringstream strm;
+    strm << "No field named " << field_name << " in class " << _this->get_name()
+         << "\n";
+    nassert_raise(strm.str());
+    return Datagram();
+  }
+
+  return invoke_extension(field).ai_format_update(do_id, to_id, from_id, args);
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update,
+ * using the indicated msg type for the indicated distributed object from the
+ * AI.
+ */
+Datagram Extension<DCClass>::
+ai_format_update_msg_type(const std::string &field_name, DOID_TYPE do_id,
+                          CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
+                          int msg_type, PyObject *args) const {
+  DCField *field = _this->get_field_by_name(field_name);
+  if (field == nullptr) {
+    std::ostringstream strm;
+    strm << "No field named " << field_name << " in class " << _this->get_name()
+         << "\n";
+    nassert_raise(strm.str());
+    return Datagram();
+  }
+
+  return invoke_extension(field).ai_format_update_msg_type(do_id, to_id, from_id, msg_type, args);
+}
+
+/**
+ * Generates a datagram containing the message necessary to generate a new
+ * distributed object from the client.  This requires querying the object for
+ * the initial value of its required fields.
+ *
+ * optional_fields is a list of fieldNames to generate in addition to the
+ * normal required fields.
+ *
+ * This method is only called by the CMU implementation.
+ */
+Datagram Extension<DCClass>::
+client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
+                           ZONEID_TYPE zone_id,
+                           PyObject *optional_fields) const {
+  DCPacker packer;
+
+  packer.raw_pack_uint16(CLIENT_OBJECT_GENERATE_CMU);
+
+  packer.raw_pack_uint32(zone_id);
+  packer.raw_pack_uint16(_this->_number);
+  packer.raw_pack_uint32(do_id);
+
+  // Specify all of the required fields.
+  int num_fields = _this->get_num_inherited_fields();
+  for (int i = 0; i < num_fields; ++i) {
+    DCField *field = _this->get_inherited_field(i);
+    if (field->is_required() && field->as_molecular_field() == nullptr) {
+      packer.begin_pack(field);
+      if (!pack_required_field(packer, distobj, field)) {
+        return Datagram();
+      }
+      packer.end_pack();
+    }
+  }
+
+  // Also specify the optional fields.
+  int num_optional_fields = 0;
+  if (PyObject_IsTrue(optional_fields)) {
+    num_optional_fields = PySequence_Size(optional_fields);
+  }
+  packer.raw_pack_uint16(num_optional_fields);
+
+  for (int i = 0; i < num_optional_fields; i++) {
+    PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
+#if PY_MAJOR_VERSION >= 3
+    std::string field_name = PyUnicode_AsUTF8(py_field_name);
+#else
+    std::string field_name = PyString_AsString(py_field_name);
+#endif
+    Py_XDECREF(py_field_name);
+
+    DCField *field = _this->get_field_by_name(field_name);
+    if (field == nullptr) {
+      std::ostringstream strm;
+      strm << "No field named " << field_name << " in class " << _this->get_name()
+           << "\n";
+      nassert_raise(strm.str());
+      return Datagram();
+    }
+    packer.raw_pack_uint16(field->get_number());
+    packer.begin_pack(field);
+    if (!pack_required_field(packer, distobj, field)) {
+      return Datagram();
+    }
+    packer.end_pack();
+  }
+
+  return Datagram(packer.get_data(), packer.get_length());
+}
+
+/**
+ * Generates a datagram containing the message necessary to generate a new
+ * distributed object from the AI. This requires querying the object for the
+ * initial value of its required fields.
+ *
+ * optional_fields is a list of fieldNames to generate in addition to the
+ * normal required fields.
+ */
+Datagram Extension<DCClass>::
+ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
+                   DOID_TYPE parent_id, ZONEID_TYPE zone_id,
+                   CHANNEL_TYPE district_channel_id, CHANNEL_TYPE from_channel_id,
+                   PyObject *optional_fields) const {
+  DCPacker packer;
+
+  packer.raw_pack_uint8(1);
+  packer.RAW_PACK_CHANNEL(district_channel_id);
+  packer.RAW_PACK_CHANNEL(from_channel_id);
+    // packer.raw_pack_uint8('A');
+
+  bool has_optional_fields = (PyObject_IsTrue(optional_fields) != 0);
+
+  if (has_optional_fields) {
+    packer.raw_pack_uint16(STATESERVER_CREATE_OBJECT_WITH_REQUIRED_OTHER);
+  } else {
+    packer.raw_pack_uint16(STATESERVER_CREATE_OBJECT_WITH_REQUIRED);
+  }
+
+  packer.raw_pack_uint32(do_id);
+  // Parent is a bit overloaded; this parent is not about inheritance, this
+  // one is about the visibility container parent, i.e.  the zone parent:
+  packer.raw_pack_uint32(parent_id);
+  packer.raw_pack_uint32(zone_id);
+  packer.raw_pack_uint16(_this->_number);
+
+  // Specify all of the required fields.
+  int num_fields = _this->get_num_inherited_fields();
+  for (int i = 0; i < num_fields; ++i) {
+    DCField *field = _this->get_inherited_field(i);
+    if (field->is_required() && field->as_molecular_field() == nullptr) {
+      packer.begin_pack(field);
+      if (!pack_required_field(packer, distobj, field)) {
+        return Datagram();
+      }
+      packer.end_pack();
+    }
+  }
+
+  // Also specify the optional fields.
+  if (has_optional_fields) {
+    int num_optional_fields = PySequence_Size(optional_fields);
+    packer.raw_pack_uint16(num_optional_fields);
+
+    for (int i = 0; i < num_optional_fields; ++i) {
+      PyObject *py_field_name = PySequence_GetItem(optional_fields, i);
+#if PY_MAJOR_VERSION >= 3
+      std::string field_name = PyUnicode_AsUTF8(py_field_name);
+#else
+      std::string field_name = PyString_AsString(py_field_name);
+#endif
+      Py_XDECREF(py_field_name);
+
+      DCField *field = _this->get_field_by_name(field_name);
+      if (field == nullptr) {
+        std::ostringstream strm;
+        strm << "No field named " << field_name << " in class "
+             << _this->get_name() << "\n";
+        nassert_raise(strm.str());
+        return Datagram();
+      }
+
+      packer.raw_pack_uint16(field->get_number());
+
+      packer.begin_pack(field);
+      if (!pack_required_field(packer, distobj, field)) {
+        return Datagram();
+      }
+      packer.end_pack();
+    }
+  }
+
+  return Datagram(packer.get_data(), packer.get_length());
+}
+
+/**
+ * Returns the PythonClassDefsImpl object stored on the DCClass object,
+ * creating it if it didn't yet exist.
+ */
+Extension<DCClass>::PythonClassDefsImpl *Extension<DCClass>::
+do_get_defs() const {
+  if (!_this->_python_class_defs) {
+    _this->_python_class_defs = new PythonClassDefsImpl();
+  }
+  return (PythonClassDefsImpl *)_this->_python_class_defs.p();
+}
+
+#endif  // HAVE_PYTHON

+ 93 - 0
direct/src/dcparser/dcClass_ext.h

@@ -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 dcClass_ext.h
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#ifndef DCCLASS_EXT_H
+#define DCCLASS_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "dcClass.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for DCClass, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<DCClass> : public ExtensionBase<DCClass> {
+public:
+  bool has_class_def() const;
+  void set_class_def(PyObject *class_def);
+  PyObject *get_class_def() const;
+  bool has_owner_class_def() const;
+  void set_owner_class_def(PyObject *owner_class_def);
+  PyObject *get_owner_class_def() const;
+
+  void receive_update(PyObject *distobj, DatagramIterator &di) const;
+  void receive_update_broadcast_required(PyObject *distobj, DatagramIterator &di) const;
+  void receive_update_broadcast_required_owner(PyObject *distobj, DatagramIterator &di) const;
+  void receive_update_all_required(PyObject *distobj, DatagramIterator &di) const;
+  void receive_update_other(PyObject *distobj, DatagramIterator &di) const;
+
+  void direct_update(PyObject *distobj, const std::string &field_name,
+                     const vector_uchar &value_blob);
+  void direct_update(PyObject *distobj, const std::string &field_name,
+                     const Datagram &datagram);
+  bool pack_required_field(Datagram &datagram, PyObject *distobj,
+                           const DCField *field) const;
+  bool pack_required_field(DCPacker &packer, PyObject *distobj,
+                           const DCField *field) const;
+
+
+
+  Datagram client_format_update(const std::string &field_name,
+                                DOID_TYPE do_id, PyObject *args) const;
+  Datagram ai_format_update(const std::string &field_name, DOID_TYPE do_id,
+                            CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const;
+  Datagram ai_format_update_msg_type(const std::string &field_name, DOID_TYPE do_id,
+                            CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, int msg_type, PyObject *args) const;
+  Datagram ai_format_generate(PyObject *distobj, DOID_TYPE do_id,
+                              ZONEID_TYPE parent_id, ZONEID_TYPE zone_id,
+                              CHANNEL_TYPE district_channel_id,
+                              CHANNEL_TYPE from_channel_id,
+                              PyObject *optional_fields) const;
+  Datagram client_format_generate_CMU(PyObject *distobj, DOID_TYPE do_id,
+                                      ZONEID_TYPE zone_id,
+                                      PyObject *optional_fields) const;
+
+private:
+  /**
+   * Implementation of DCClass::PythonClassDefs which actually stores the
+   * Python pointers.  This needs to be defined here rather than on DCClass
+   * itself, since DCClass cannot include Python.h or call Python functions.
+   */
+  class PythonClassDefsImpl : public DCClass::PythonClassDefs {
+  public:
+    virtual ~PythonClassDefsImpl() {
+      Py_XDECREF(_class_def);
+      Py_XDECREF(_owner_class_def);
+    }
+
+    PyObject *_class_def = nullptr;
+    PyObject *_owner_class_def = nullptr;
+  };
+
+  PythonClassDefsImpl *do_get_defs() const;
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // DCCLASS_EXT_H

+ 6 - 313
direct/src/dcparser/dcField.cxx

@@ -18,16 +18,6 @@
 #include "hashGenerator.h"
 #include "dcmsgtypes.h"
 
-#ifdef HAVE_PYTHON
-#include "py_panda.h"
-#endif
-
-#ifdef WITHIN_PANDA
-#include "pStatTimer.h"
-#endif
-
-using std::string;
-
 /**
  *
  */
@@ -58,7 +48,7 @@ DCField() :
  *
  */
 DCField::
-DCField(const string &name, DCClass *dclass) :
+DCField(const std::string &name, DCClass *dclass) :
   DCPackerInterface(name),
   _dclass(dclass)
 #ifdef WITHIN_PANDA
@@ -161,14 +151,14 @@ as_parameter() const {
  * string formatting it for human consumption.  Returns empty string if there
  * is an error.
  */
-string DCField::
+std::string DCField::
 format_data(const vector_uchar &packed_data, bool show_field_names) {
   DCPacker packer;
   packer.set_unpack_data(packed_data);
   packer.begin_unpack(this);
-  string result = packer.unpack_and_format(show_field_names);
+  std::string result = packer.unpack_and_format(show_field_names);
   if (!packer.end_unpack()) {
-    return string();
+    return std::string();
   }
   return result;
 }
@@ -179,7 +169,7 @@ format_data(const vector_uchar &packed_data, bool show_field_names) {
  * the corresponding packed data.  Returns empty string if there is an error.
  */
 vector_uchar DCField::
-parse_string(const string &formatted_string) {
+parse_string(const std::string &formatted_string) {
   DCPacker packer;
   packer.begin_pack(this);
   if (!packer.parse_and_pack(formatted_string)) {
@@ -212,254 +202,6 @@ validate_ranges(const vector_uchar &packed_data) const {
   return (packer.get_num_unpacked_bytes() == packed_data.size());
 }
 
-#ifdef HAVE_PYTHON
-/**
- * Packs the Python arguments from the indicated tuple into the packer.
- * Returns true on success, false on failure.
- *
- * It is assumed that the packer is currently positioned on this field.
- */
-bool DCField::
-pack_args(DCPacker &packer, PyObject *sequence) const {
-  nassertr(!packer.had_error(), false);
-  nassertr(packer.get_current_field() == this, false);
-
-  packer.pack_object(sequence);
-  if (!packer.had_error()) {
-    /*
-    cerr << "pack " << get_name() << get_pystr(sequence) << "\n";
-    */
-
-    return true;
-  }
-
-  if (!Notify::ptr()->has_assert_failed()) {
-    std::ostringstream strm;
-    PyObject *exc_type = PyExc_Exception;
-
-    if (as_parameter() != nullptr) {
-      // If it's a parameter-type field, the value may or may not be a
-      // sequence.
-      if (packer.had_pack_error()) {
-        strm << "Incorrect arguments to field: " << get_name()
-             << " = " << get_pystr(sequence);
-        exc_type = PyExc_TypeError;
-      } else {
-        strm << "Value out of range on field: " << get_name()
-             << " = " << get_pystr(sequence);
-        exc_type = PyExc_ValueError;
-      }
-
-    } else {
-      // If it's a molecular or atomic field, the value should be a sequence.
-      PyObject *tuple = PySequence_Tuple(sequence);
-      if (tuple == nullptr) {
-        strm << "Value for " << get_name() << " not a sequence: " \
-             << get_pystr(sequence);
-        exc_type = PyExc_TypeError;
-
-      } else {
-        if (packer.had_pack_error()) {
-          strm << "Incorrect arguments to field: " << get_name()
-               << get_pystr(sequence);
-          exc_type = PyExc_TypeError;
-        } else {
-          strm << "Value out of range on field: " << get_name()
-               << get_pystr(sequence);
-          exc_type = PyExc_ValueError;
-        }
-
-        Py_DECREF(tuple);
-      }
-    }
-
-    string message = strm.str();
-    PyErr_SetString(exc_type, message.c_str());
-  }
-  return false;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Unpacks the values from the packer, beginning at the current point in the
- * unpack_buffer, into a Python tuple and returns the tuple.
- *
- * It is assumed that the packer is currently positioned on this field.
- */
-PyObject *DCField::
-unpack_args(DCPacker &packer) const {
-  nassertr(!packer.had_error(), nullptr);
-  nassertr(packer.get_current_field() == this, nullptr);
-
-  size_t start_byte = packer.get_num_unpacked_bytes();
-  PyObject *object = packer.unpack_object();
-
-  if (!packer.had_error()) {
-    // Successfully unpacked.
-    /*
-    cerr << "recv " << get_name() << get_pystr(object) << "\n";
-    */
-
-    return object;
-  }
-
-  if (!Notify::ptr()->has_assert_failed()) {
-    std::ostringstream strm;
-    PyObject *exc_type = PyExc_Exception;
-
-    if (packer.had_pack_error()) {
-      strm << "Data error unpacking field ";
-      output(strm, true);
-      size_t length = packer.get_unpack_length() - start_byte;
-      strm << "\nGot data (" << (int)length << " bytes):\n";
-      Datagram dg(packer.get_unpack_data() + start_byte, length);
-      dg.dump_hex(strm);
-      size_t error_byte = packer.get_num_unpacked_bytes() - start_byte;
-      strm << "Error detected on byte " << error_byte
-           << " (" << std::hex << error_byte << std::dec << " hex)";
-
-      exc_type = PyExc_RuntimeError;
-    } else {
-      strm << "Value outside specified range when unpacking field "
-           << get_name() << ": " << get_pystr(object);
-      exc_type = PyExc_ValueError;
-    }
-
-    string message = strm.str();
-    PyErr_SetString(exc_type, message.c_str());
-  }
-
-  Py_XDECREF(object);
-  return nullptr;
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Extracts the update message out of the datagram and applies it to the
- * indicated object by calling the appropriate method.
- */
-void DCField::
-receive_update(DCPacker &packer, PyObject *distobj) const {
-  if (as_parameter() != nullptr) {
-    // If it's a parameter-type field, just store a new value on the object.
-    PyObject *value = unpack_args(packer);
-    if (value != nullptr) {
-      PyObject_SetAttrString(distobj, (char *)_name.c_str(), value);
-    }
-    Py_DECREF(value);
-
-  } else {
-    // Otherwise, it must be an atomic or molecular field, so call the
-    // corresponding method.
-
-    if (!PyObject_HasAttrString(distobj, (char *)_name.c_str())) {
-      // If there's no Python method to receive this message, don't bother
-      // unpacking it to a Python tuple--just skip past the message.
-      packer.unpack_skip();
-
-    } else {
-      // Otherwise, get a Python tuple from the args and call the Python
-      // method.
-      PyObject *args = unpack_args(packer);
-
-      if (args != nullptr) {
-        PyObject *func = PyObject_GetAttrString(distobj, (char *)_name.c_str());
-        nassertv(func != nullptr);
-
-        PyObject *result;
-        {
-#ifdef WITHIN_PANDA
-          PStatTimer timer(((DCField *)this)->_field_update_pcollector);
-#endif
-          result = PyObject_CallObject(func, args);
-        }
-        Py_XDECREF(result);
-        Py_DECREF(func);
-        Py_DECREF(args);
-      }
-    }
-  }
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update for
- * the indicated distributed object from the client.
- */
-Datagram DCField::
-client_format_update(DOID_TYPE do_id, PyObject *args) const {
-  DCPacker packer;
-
-  packer.raw_pack_uint16(CLIENT_OBJECT_SET_FIELD);
-  packer.raw_pack_uint32(do_id);
-  packer.raw_pack_uint16(_number);
-
-  packer.begin_pack(this);
-  pack_args(packer, args);
-  if (!packer.end_pack()) {
-    return Datagram();
-  }
-
-  return Datagram(packer.get_data(), packer.get_length());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update for
- * the indicated distributed object from the AI.
- */
-Datagram DCField::
-ai_format_update(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const {
-  DCPacker packer;
-
-  packer.raw_pack_uint8(1);
-  packer.RAW_PACK_CHANNEL(to_id);
-  packer.RAW_PACK_CHANNEL(from_id);
-  packer.raw_pack_uint16(STATESERVER_OBJECT_SET_FIELD);
-  packer.raw_pack_uint32(do_id);
-  packer.raw_pack_uint16(_number);
-
-  packer.begin_pack(this);
-  pack_args(packer, args);
-  if (!packer.end_pack()) {
-    return Datagram();
-  }
-
-  return Datagram(packer.get_data(), packer.get_length());
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Generates a datagram containing the message necessary to send an update,
- * with the msg type, for the indicated distributed object from the AI.
- */
-Datagram DCField::
-ai_format_update_msg_type(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, int msg_type, PyObject *args) const {
-  DCPacker packer;
-
-  packer.raw_pack_uint8(1);
-  packer.RAW_PACK_CHANNEL(to_id);
-  packer.RAW_PACK_CHANNEL(from_id);
-  packer.raw_pack_uint16(msg_type);
-  packer.raw_pack_uint32(do_id);
-  packer.raw_pack_uint16(_number);
-
-  packer.begin_pack(this);
-  pack_args(packer, args);
-  if (!packer.end_pack()) {
-    return Datagram();
-  }
-
-  return Datagram(packer.get_data(), packer.get_length());
-}
-#endif  // HAVE_PYTHON
-
-
 /**
  * Accumulates the properties of this field into the hash.
  */
@@ -499,62 +241,13 @@ pack_default_value(DCPackData &pack_data, bool &) const {
  * Sets the name of this field.
  */
 void DCField::
-set_name(const string &name) {
+set_name(const std::string &name) {
   DCPackerInterface::set_name(name);
   if (_dclass != nullptr) {
     _dclass->_dc_file->mark_inherited_fields_stale();
   }
 }
 
-#ifdef HAVE_PYTHON
-/**
- * Returns the string representation of the indicated Python object.
- */
-string DCField::
-get_pystr(PyObject *value) {
-  if (value == nullptr) {
-    return "(null)";
-  }
-
-  PyObject *str = PyObject_Str(value);
-  if (str != nullptr) {
-#if PY_MAJOR_VERSION >= 3
-    string result = PyUnicode_AsUTF8(str);
-#else
-    string result = PyString_AsString(str);
-#endif
-    Py_DECREF(str);
-    return result;
-  }
-
-  PyObject *repr = PyObject_Repr(value);
-  if (repr != nullptr) {
-#if PY_MAJOR_VERSION >= 3
-    string result = PyUnicode_AsUTF8(repr);
-#else
-    string result = PyString_AsString(repr);
-#endif
-    Py_DECREF(repr);
-    return result;
-  }
-
-  if (value->ob_type != nullptr) {
-    PyObject *typestr = PyObject_Str((PyObject *)(value->ob_type));
-    if (typestr != nullptr) {
-#if PY_MAJOR_VERSION >= 3
-      string result = PyUnicode_AsUTF8(typestr);
-#else
-      string result = PyString_AsString(typestr);
-#endif
-      Py_DECREF(typestr);
-      return result;
-    }
-  }
-
-  return "(invalid object)";
-}
-#endif  // HAVE_PYTHON
-
 /**
  * Recomputes the default value of the field by repacking it.
  */

+ 13 - 15
direct/src/dcparser/dcField.h

@@ -17,10 +17,11 @@
 #include "dcbase.h"
 #include "dcPackerInterface.h"
 #include "dcKeywordList.h"
-#include "dcPython.h"
 
 #ifdef WITHIN_PANDA
 #include "pStatCollector.h"
+#include "extension.h"
+#include "datagram.h"
 #endif
 
 class DCPacker;
@@ -76,18 +77,17 @@ PUBLISHED:
   INLINE void output(std::ostream &out) const;
   INLINE void write(std::ostream &out, int indent_level) const;
 
-#ifdef HAVE_PYTHON
-  bool pack_args(DCPacker &packer, PyObject *sequence) const;
-  PyObject *unpack_args(DCPacker &packer) const;
+  EXTENSION(bool pack_args(DCPacker &packer, PyObject *sequence) const);
+  EXTENSION(PyObject *unpack_args(DCPacker &packer) const);
 
-  void receive_update(DCPacker &packer, PyObject *distobj) const;
+  EXTENSION(void receive_update(DCPacker &packer, PyObject *distobj) const);
 
-  Datagram client_format_update(DOID_TYPE do_id, PyObject *args) const;
-  Datagram ai_format_update(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
-                            PyObject *args) const;
-  Datagram ai_format_update_msg_type(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
-                            int msg_type, PyObject *args) const;
-#endif
+  EXTENSION(Datagram client_format_update(DOID_TYPE do_id, PyObject *args) const);
+  EXTENSION(Datagram ai_format_update(DOID_TYPE do_id, CHANNEL_TYPE to_id,
+                                      CHANNEL_TYPE from_id, PyObject *args) const);
+  EXTENSION(Datagram ai_format_update_msg_type(DOID_TYPE do_id, CHANNEL_TYPE to_id,
+                                               CHANNEL_TYPE from_id, int msg_type,
+                                               PyObject *args) const);
 
 public:
   virtual void output(std::ostream &out, bool brief) const=0;
@@ -100,10 +100,6 @@ public:
   INLINE void set_class(DCClass *dclass);
   INLINE void set_default_value(vector_uchar default_value);
 
-#ifdef HAVE_PYTHON
-  static std::string get_pystr(PyObject *value);
-#endif
-
 protected:
   void refresh_default_value();
 
@@ -119,6 +115,8 @@ private:
 
 #ifdef WITHIN_PANDA
   PStatCollector _field_update_pcollector;
+
+  friend class Extension<DCField>;
 #endif
 };
 

+ 305 - 0
direct/src/dcparser/dcField_ext.cxx

@@ -0,0 +1,305 @@
+/**
+ * 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 dcField_ext.cxx
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#include "dcField_ext.h"
+#include "dcPacker_ext.h"
+#include "dcmsgtypes.h"
+
+#include "datagram.h"
+#include "pStatTimer.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Packs the Python arguments from the indicated tuple into the packer.
+ * Returns true on success, false on failure.
+ *
+ * It is assumed that the packer is currently positioned on this field.
+ */
+bool Extension<DCField>::
+pack_args(DCPacker &packer, PyObject *sequence) const {
+  nassertr(!packer.had_error(), false);
+  nassertr(packer.get_current_field() == _this, false);
+
+  invoke_extension(&packer).pack_object(sequence);
+  if (!packer.had_error()) {
+    /*
+    cerr << "pack " << _this->get_name() << get_pystr(sequence) << "\n";
+    */
+
+    return true;
+  }
+
+  if (!Notify::ptr()->has_assert_failed()) {
+    std::ostringstream strm;
+    PyObject *exc_type = PyExc_Exception;
+
+    if (_this->as_parameter() != nullptr) {
+      // If it's a parameter-type field, the value may or may not be a
+      // sequence.
+      if (packer.had_pack_error()) {
+        strm << "Incorrect arguments to field: " << _this->get_name()
+             << " = " << get_pystr(sequence);
+        exc_type = PyExc_TypeError;
+      } else {
+        strm << "Value out of range on field: " << _this->get_name()
+             << " = " << get_pystr(sequence);
+        exc_type = PyExc_ValueError;
+      }
+
+    } else {
+      // If it's a molecular or atomic field, the value should be a sequence.
+      PyObject *tuple = PySequence_Tuple(sequence);
+      if (tuple == nullptr) {
+        strm << "Value for " << _this->get_name() << " not a sequence: " \
+             << get_pystr(sequence);
+        exc_type = PyExc_TypeError;
+
+      } else {
+        if (packer.had_pack_error()) {
+          strm << "Incorrect arguments to field: " << _this->get_name()
+               << get_pystr(sequence);
+          exc_type = PyExc_TypeError;
+        } else {
+          strm << "Value out of range on field: " << _this->get_name()
+               << get_pystr(sequence);
+          exc_type = PyExc_ValueError;
+        }
+
+        Py_DECREF(tuple);
+      }
+    }
+
+    std::string message = strm.str();
+    PyErr_SetString(exc_type, message.c_str());
+  }
+  return false;
+}
+
+/**
+ * Unpacks the values from the packer, beginning at the current point in the
+ * unpack_buffer, into a Python tuple and returns the tuple.
+ *
+ * It is assumed that the packer is currently positioned on this field.
+ */
+PyObject *Extension<DCField>::
+unpack_args(DCPacker &packer) const {
+  nassertr(!packer.had_error(), nullptr);
+  nassertr(packer.get_current_field() == _this, nullptr);
+
+  size_t start_byte = packer.get_num_unpacked_bytes();
+  PyObject *object = invoke_extension(&packer).unpack_object();
+
+  if (!packer.had_error()) {
+    // Successfully unpacked.
+    /*
+    cerr << "recv " << _this->get_name() << get_pystr(object) << "\n";
+    */
+
+    return object;
+  }
+
+  if (!Notify::ptr()->has_assert_failed()) {
+    std::ostringstream strm;
+    PyObject *exc_type = PyExc_Exception;
+
+    if (packer.had_pack_error()) {
+      strm << "Data error unpacking field ";
+      _this->output(strm, true);
+      size_t length = packer.get_unpack_length() - start_byte;
+      strm << "\nGot data (" << (int)length << " bytes):\n";
+      Datagram dg(packer.get_unpack_data() + start_byte, length);
+      dg.dump_hex(strm);
+      size_t error_byte = packer.get_num_unpacked_bytes() - start_byte;
+      strm << "Error detected on byte " << error_byte
+           << " (" << std::hex << error_byte << std::dec << " hex)";
+
+      exc_type = PyExc_RuntimeError;
+    } else {
+      strm << "Value outside specified range when unpacking field "
+           << _this->get_name() << ": " << get_pystr(object);
+      exc_type = PyExc_ValueError;
+    }
+
+    std::string message = strm.str();
+    PyErr_SetString(exc_type, message.c_str());
+  }
+
+  Py_XDECREF(object);
+  return nullptr;
+}
+
+/**
+ * Extracts the update message out of the datagram and applies it to the
+ * indicated object by calling the appropriate method.
+ */
+void Extension<DCField>::
+receive_update(DCPacker &packer, PyObject *distobj) const {
+  if (_this->as_parameter() != nullptr) {
+    // If it's a parameter-type field, just store a new value on the object.
+    PyObject *value = unpack_args(packer);
+    if (value != nullptr) {
+      PyObject_SetAttrString(distobj, (char *)_this->_name.c_str(), value);
+    }
+    Py_DECREF(value);
+
+  } else {
+    // Otherwise, it must be an atomic or molecular field, so call the
+    // corresponding method.
+
+    if (!PyObject_HasAttrString(distobj, (char *)_this->_name.c_str())) {
+      // If there's no Python method to receive this message, don't bother
+      // unpacking it to a Python tuple--just skip past the message.
+      packer.unpack_skip();
+
+    } else {
+      // Otherwise, get a Python tuple from the args and call the Python
+      // method.
+      PyObject *args = unpack_args(packer);
+
+      if (args != nullptr) {
+        PyObject *func = PyObject_GetAttrString(distobj, (char *)_this->_name.c_str());
+        nassertv(func != nullptr);
+
+        PyObject *result;
+        {
+#ifdef WITHIN_PANDA
+          PStatTimer timer(_this->_field_update_pcollector);
+#endif
+          result = PyObject_CallObject(func, args);
+        }
+        Py_XDECREF(result);
+        Py_DECREF(func);
+        Py_DECREF(args);
+      }
+    }
+  }
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update for
+ * the indicated distributed object from the client.
+ */
+Datagram Extension<DCField>::
+client_format_update(DOID_TYPE do_id, PyObject *args) const {
+  DCPacker packer;
+
+  packer.raw_pack_uint16(CLIENT_OBJECT_SET_FIELD);
+  packer.raw_pack_uint32(do_id);
+  packer.raw_pack_uint16(_this->_number);
+
+  packer.begin_pack(_this);
+  pack_args(packer, args);
+  if (!packer.end_pack()) {
+    return Datagram();
+  }
+
+  return Datagram(packer.get_data(), packer.get_length());
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update for
+ * the indicated distributed object from the AI.
+ */
+Datagram Extension<DCField>::
+ai_format_update(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, PyObject *args) const {
+  DCPacker packer;
+
+  packer.raw_pack_uint8(1);
+  packer.RAW_PACK_CHANNEL(to_id);
+  packer.RAW_PACK_CHANNEL(from_id);
+  packer.raw_pack_uint16(STATESERVER_OBJECT_SET_FIELD);
+  packer.raw_pack_uint32(do_id);
+  packer.raw_pack_uint16(_this->_number);
+
+  packer.begin_pack(_this);
+  pack_args(packer, args);
+  if (!packer.end_pack()) {
+    return Datagram();
+  }
+
+  return Datagram(packer.get_data(), packer.get_length());
+}
+
+/**
+ * Generates a datagram containing the message necessary to send an update,
+ * with the msg type, for the indicated distributed object from the AI.
+ */
+Datagram Extension<DCField>::
+ai_format_update_msg_type(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id, int msg_type, PyObject *args) const {
+  DCPacker packer;
+
+  packer.raw_pack_uint8(1);
+  packer.RAW_PACK_CHANNEL(to_id);
+  packer.RAW_PACK_CHANNEL(from_id);
+  packer.raw_pack_uint16(msg_type);
+  packer.raw_pack_uint32(do_id);
+  packer.raw_pack_uint16(_this->_number);
+
+  packer.begin_pack(_this);
+  pack_args(packer, args);
+  if (!packer.end_pack()) {
+    return Datagram();
+  }
+
+  return Datagram(packer.get_data(), packer.get_length());
+}
+
+/**
+ * Returns the string representation of the indicated Python object.
+ */
+std::string Extension<DCField>::
+get_pystr(PyObject *value) {
+  if (value == nullptr) {
+    return "(null)";
+  }
+
+  PyObject *str = PyObject_Str(value);
+  if (str != nullptr) {
+#if PY_MAJOR_VERSION >= 3
+    std::string result = PyUnicode_AsUTF8(str);
+#else
+    std::string result = PyString_AsString(str);
+#endif
+    Py_DECREF(str);
+    return result;
+  }
+
+  PyObject *repr = PyObject_Repr(value);
+  if (repr != nullptr) {
+#if PY_MAJOR_VERSION >= 3
+    std::string result = PyUnicode_AsUTF8(repr);
+#else
+    std::string result = PyString_AsString(repr);
+#endif
+    Py_DECREF(repr);
+    return result;
+  }
+
+  if (value->ob_type != nullptr) {
+    PyObject *typestr = PyObject_Str((PyObject *)(value->ob_type));
+    if (typestr != nullptr) {
+#if PY_MAJOR_VERSION >= 3
+      std::string result = PyUnicode_AsUTF8(typestr);
+#else
+      std::string result = PyString_AsString(typestr);
+#endif
+      Py_DECREF(typestr);
+      return result;
+    }
+  }
+
+  return "(invalid object)";
+}
+
+#endif  // HAVE_PYTHON

+ 48 - 0
direct/src/dcparser/dcField_ext.h

@@ -0,0 +1,48 @@
+/**
+ * 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 dcField_ext.h
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#ifndef DCFIELD_EXT_H
+#define DCFIELD_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "dcField.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for DCField, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<DCField> : public ExtensionBase<DCField> {
+public:
+  bool pack_args(DCPacker &packer, PyObject *sequence) const;
+  PyObject *unpack_args(DCPacker &packer) const;
+
+  void receive_update(DCPacker &packer, PyObject *distobj) const;
+
+  Datagram client_format_update(DOID_TYPE do_id, PyObject *args) const;
+  Datagram ai_format_update(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
+                            PyObject *args) const;
+  Datagram ai_format_update_msg_type(DOID_TYPE do_id, CHANNEL_TYPE to_id, CHANNEL_TYPE from_id,
+                            int msg_type, PyObject *args) const;
+
+  static std::string get_pystr(PyObject *value);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // DCFIELD_EXT_H

+ 0 - 508
direct/src/dcparser/dcPacker.cxx

@@ -19,10 +19,6 @@
 #include "dcSwitchParameter.h"
 #include "dcClass.h"
 
-#ifdef HAVE_PYTHON
-#include "py_panda.h"
-#endif
-
 using std::istream;
 using std::istringstream;
 using std::ostream;
@@ -622,335 +618,6 @@ unpack_skip() {
   }
 }
 
-#ifdef HAVE_PYTHON
-/**
- * Packs the Python object of whatever type into the packer.  Each numeric
- * object and string object maps to the corresponding pack_value() call; a
- * tuple or sequence maps to a push() followed by all of the tuple's contents
- * followed by a pop().
- */
-void DCPacker::
-pack_object(PyObject *object) {
-  nassertv(_mode == M_pack || _mode == M_repack);
-  DCPackType pack_type = get_pack_type();
-
-  // had to add this for basic 64 and unsigned data to get packed right .. Not
-  // sure if we can just do the rest this way..
-
- switch(pack_type)
-  {
-  case PT_int64:
-      if(PyLong_Check(object))
-      {
-            pack_int64(PyLong_AsLongLong(object));
-            return;
-      }
-#if PY_MAJOR_VERSION < 3
-      else if (PyInt_Check(object))
-      {
-            pack_int64(PyInt_AsLong(object));
-            return;
-      }
-#endif
-      break;
-  case PT_uint64:
-      if(PyLong_Check(object))
-      {
-            pack_uint64(PyLong_AsUnsignedLongLong(object));
-            return;
-      }
-#if PY_MAJOR_VERSION < 3
-      else if(PyInt_Check(object))
-      {
-            PyObject  *obj1 = PyNumber_Long(object);
-            pack_int(PyLong_AsUnsignedLongLong(obj1));
-            Py_DECREF(obj1);
-            return;
-      }
-#endif
-      break;
-  case PT_int:
-      if(PyLong_Check(object))
-      {
-            pack_int(PyLong_AsLong(object));
-            return;
-      }
-#if PY_MAJOR_VERSION < 3
-      else if (PyInt_Check(object))
-      {
-            pack_int(PyInt_AsLong(object));
-            return;
-      }
-#endif
-      break;
-  case PT_uint:
-      if(PyLong_Check(object))
-      {
-            pack_uint(PyLong_AsUnsignedLong(object));
-            return;
-      }
-#if PY_MAJOR_VERSION < 3
-      else if (PyInt_Check(object))
-      {
-            PyObject *obj1 = PyNumber_Long(object);
-            pack_uint(PyLong_AsUnsignedLong(obj1));
-            Py_DECREF(obj1);
-            return;
-      }
-#endif
-      break;
-  default:
-      break;
-  }
-  if (PyLong_Check(object)) {
-    pack_int(PyLong_AsLong(object));
-#if PY_MAJOR_VERSION < 3
-  } else if (PyInt_Check(object)) {
-    pack_int(PyInt_AS_LONG(object));
-#endif
-  } else if (PyFloat_Check(object)) {
-    pack_double(PyFloat_AS_DOUBLE(object));
-  } else if (PyLong_Check(object)) {
-    pack_int64(PyLong_AsLongLong(object));
-#if PY_MAJOR_VERSION >= 3
-  } else if (PyUnicode_Check(object)) {
-    const char *buffer;
-    Py_ssize_t length;
-    buffer = PyUnicode_AsUTF8AndSize(object, &length);
-    if (buffer) {
-      pack_string(string(buffer, length));
-    }
-  } else if (PyBytes_Check(object)) {
-    const unsigned char *buffer;
-    Py_ssize_t length;
-    PyBytes_AsStringAndSize(object, (char **)&buffer, &length);
-    if (buffer) {
-      pack_blob(vector_uchar(buffer, buffer + length));
-    }
-#else
-  } else if (PyString_Check(object) || PyUnicode_Check(object)) {
-    char *buffer;
-    Py_ssize_t length;
-    PyString_AsStringAndSize(object, &buffer, &length);
-    if (buffer) {
-      pack_string(string(buffer, length));
-    }
-#endif
-  } else {
-    // For some reason, PySequence_Check() is incorrectly reporting that a
-    // class instance is a sequence, even if it doesn't provide __len__, so we
-    // double-check by testing for __len__ explicitly.
-    bool is_sequence =
-      (PySequence_Check(object) != 0) &&
-      (PyObject_HasAttrString(object, "__len__") != 0);
-    bool is_instance = false;
-
-    const DCClass *dclass = nullptr;
-    const DCPackerInterface *current_field = get_current_field();
-    if (current_field != nullptr) {
-      const DCClassParameter *class_param = get_current_field()->as_class_parameter();
-      if (class_param != nullptr) {
-        dclass = class_param->get_class();
-
-        if (dclass->has_class_def()) {
-          PyObject *class_def = dclass->get_class_def();
-          is_instance = (PyObject_IsInstance(object, dclass->get_class_def()) != 0);
-          Py_DECREF(class_def);
-        }
-      }
-    }
-
-    // If dclass is not NULL, the packer is expecting a class object.  There
-    // are then two cases: (1) the user has supplied a matching class object,
-    // or (2) the user has supplied a sequence object.  Unfortunately, it may
-    // be difficult to differentiate these two cases, since a class object may
-    // also be a sequence object.
-
-    // The rule to differentiate them is:
-
-    // (1) If the supplied class object is an instance of the expected class
-    // object, it is considered to be a class object.
-
-    // (2) Otherwise, if the supplied class object has a __len__() method
-    // (i.e.  PySequence_Check() returns true), then it is considered to be a
-    // sequence.
-
-    // (3) Otherwise, it is considered to be a class object.
-
-    if (dclass != nullptr && (is_instance || !is_sequence)) {
-      // The supplied object is either an instance of the expected class
-      // object, or it is not a sequence--this is case (1) or (3).
-      pack_class_object(dclass, object);
-    } else if (is_sequence) {
-      // The supplied object is not an instance of the expected class object,
-      // but it is a sequence.  This is case (2).
-      push();
-      int size = PySequence_Size(object);
-      for (int i = 0; i < size; ++i) {
-        PyObject *element = PySequence_GetItem(object, i);
-        if (element != nullptr) {
-          pack_object(element);
-          Py_DECREF(element);
-        } else {
-          std::cerr << "Unable to extract item " << i << " from sequence.\n";
-        }
-      }
-      pop();
-    } else {
-      // The supplied object is not a sequence, and we weren't expecting a
-      // class parameter.  This is none of the above, an error.
-      ostringstream strm;
-      strm << "Don't know how to pack object: "
-           << DCField::get_pystr(object);
-      nassert_raise(strm.str());
-      _pack_error = true;
-    }
-  }
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Unpacks a Python object of the appropriate type from the stream for the
- * current field.  This may be an integer or a string for a simple field
- * object; if the current field represents a list of fields it will be a
- * tuple.
- */
-PyObject *DCPacker::
-unpack_object() {
-  PyObject *object = nullptr;
-
-  DCPackType pack_type = get_pack_type();
-
-  switch (pack_type) {
-  case PT_invalid:
-    object = Py_None;
-    Py_INCREF(object);
-    unpack_skip();
-    break;
-
-  case PT_double:
-    {
-      double value = unpack_double();
-      object = PyFloat_FromDouble(value);
-    }
-    break;
-
-  case PT_int:
-    {
-      int value = unpack_int();
-#if PY_MAJOR_VERSION >= 3
-      object = PyLong_FromLong(value);
-#else
-      object = PyInt_FromLong(value);
-#endif
-    }
-    break;
-
-  case PT_uint:
-    {
-      unsigned int value = unpack_uint();
-#if PY_MAJOR_VERSION >= 3
-      object = PyLong_FromLong(value);
-#else
-      if (value & 0x80000000) {
-        object = PyLong_FromUnsignedLong(value);
-      } else {
-        object = PyInt_FromLong(value);
-      }
-#endif
-    }
-    break;
-
-  case PT_int64:
-    {
-      int64_t value = unpack_int64();
-      object = PyLong_FromLongLong(value);
-    }
-    break;
-
-  case PT_uint64:
-    {
-      uint64_t value = unpack_uint64();
-      object = PyLong_FromUnsignedLongLong(value);
-    }
-    break;
-
-  case PT_blob:
-#if PY_MAJOR_VERSION >= 3
-    {
-      string str;
-      unpack_string(str);
-      object = PyBytes_FromStringAndSize(str.data(), str.size());
-    }
-    break;
-#endif
-    // On Python 2, fall through to below.
-
-  case PT_string:
-    {
-      string str;
-      unpack_string(str);
-#if PY_MAJOR_VERSION >= 3
-      object = PyUnicode_FromStringAndSize(str.data(), str.size());
-#else
-      object = PyString_FromStringAndSize(str.data(), str.size());
-#endif
-    }
-    break;
-
-  case PT_class:
-    {
-      const DCClassParameter *class_param = get_current_field()->as_class_parameter();
-      if (class_param != nullptr) {
-        const DCClass *dclass = class_param->get_class();
-        if (dclass->has_class_def()) {
-          // If we know what kind of class object this is and it has a valid
-          // constructor, create the class object instead of just a tuple.
-          object = unpack_class_object(dclass);
-          if (object == nullptr) {
-            std::cerr << "Unable to construct object of class "
-                 << dclass->get_name() << "\n";
-          } else {
-            break;
-          }
-        }
-      }
-    }
-    // Fall through (if no constructor)
-
-    // If we don't know what kind of class object it is, or it doesn't have a
-    // constructor, fall through and make a tuple.
-  default:
-    {
-      // First, build up a list from the nested objects.
-      object = PyList_New(0);
-
-      push();
-      while (more_nested_fields()) {
-        PyObject *element = unpack_object();
-        PyList_Append(object, element);
-        Py_DECREF(element);
-      }
-      pop();
-
-      if (pack_type != PT_array) {
-        // For these other kinds of objects, we'll convert the list into a
-        // tuple.
-        PyObject *tuple = PyList_AsTuple(object);
-        Py_DECREF(object);
-        object = tuple;
-      }
-    }
-    break;
-  }
-
-  nassertr(object != nullptr, nullptr);
-  return object;
-}
-#endif  // HAVE_PYTHON
-
-
 /**
  * Parses an object's value according to the DC file syntax (e.g.  as a
  * default value string) and packs it.  Returns true on success, false on a
@@ -1206,178 +873,3 @@ clear_stack() {
     _stack = next;
   }
 }
-
-#ifdef HAVE_PYTHON
-/**
- * Given that the current element is a ClassParameter for a Python class
- * object, try to extract the appropriate values from the class object and
- * pack in.
- */
-void DCPacker::
-pack_class_object(const DCClass *dclass, PyObject *object) {
-  push();
-  while (more_nested_fields() && !_pack_error) {
-    const DCField *field = get_current_field()->as_field();
-    nassertv(field != nullptr);
-    get_class_element(dclass, object, field);
-  }
-  pop();
-}
-#endif  // HAVE_PYTHON
-
-#ifdef HAVE_PYTHON
-/**
- * Given that the current element is a ClassParameter for a Python class for
- * which we have a valid constructor, unpack it and fill in its values.
- */
-PyObject *DCPacker::
-unpack_class_object(const DCClass *dclass) {
-  PyObject *class_def = dclass->get_class_def();
-  nassertr(class_def != nullptr, nullptr);
-
-  PyObject *object = nullptr;
-
-  if (!dclass->has_constructor()) {
-    // If the class uses a default constructor, go ahead and create the Python
-    // object for it now.
-    object = PyObject_CallObject(class_def, nullptr);
-    if (object == nullptr) {
-      return nullptr;
-    }
-  }
-
-  push();
-  if (object == nullptr && more_nested_fields()) {
-    // The first nested field will be the constructor.
-    const DCField *field = get_current_field()->as_field();
-    nassertr(field != nullptr, object);
-    nassertr(field == dclass->get_constructor(), object);
-
-    set_class_element(class_def, object, field);
-
-    // By now, the object should have been constructed.
-    if (object == nullptr) {
-      return nullptr;
-    }
-  }
-  while (more_nested_fields()) {
-    const DCField *field = get_current_field()->as_field();
-    nassertr(field != nullptr, object);
-
-    set_class_element(class_def, object, field);
-  }
-  pop();
-
-  return object;
-}
-#endif  // HAVE_PYTHON
-
-
-#ifdef HAVE_PYTHON
-/**
- * Unpacks the current element and stuffs it on the Python class object in
- * whatever way is appropriate.
- */
-void DCPacker::
-set_class_element(PyObject *class_def, PyObject *&object,
-                  const DCField *field) {
-  string field_name = field->get_name();
-  DCPackType pack_type = get_pack_type();
-
-  if (field_name.empty()) {
-    switch (pack_type) {
-    case PT_class:
-    case PT_switch:
-      // If the field has no name, but it is one of these container objects,
-      // we want to unpack its nested objects directly into the class.
-      push();
-      while (more_nested_fields()) {
-        const DCField *field = get_current_field()->as_field();
-        nassertv(field != nullptr);
-        nassertv(object != nullptr);
-        set_class_element(class_def, object, field);
-      }
-      pop();
-      break;
-
-    default:
-      // Otherwise, we just skip over the field.
-      unpack_skip();
-    }
-
-  } else {
-    // If the field does have a name, we will want to store it on the class,
-    // either by calling a method (for a PT_field pack_type) or by setting a
-    // value (for any other kind of pack_type).
-
-    PyObject *element = unpack_object();
-
-    if (pack_type == PT_field) {
-      if (object == nullptr) {
-        // If the object hasn't been constructed yet, assume this is the
-        // constructor.
-        object = PyObject_CallObject(class_def, element);
-
-      } else {
-        if (PyObject_HasAttrString(object, (char *)field_name.c_str())) {
-          PyObject *func = PyObject_GetAttrString(object, (char *)field_name.c_str());
-          if (func != nullptr) {
-            PyObject *result = PyObject_CallObject(func, element);
-            Py_XDECREF(result);
-            Py_DECREF(func);
-          }
-        }
-      }
-
-    } else {
-      nassertv(object != nullptr);
-      PyObject_SetAttrString(object, (char *)field_name.c_str(), element);
-    }
-
-    Py_DECREF(element);
-  }
-}
-#endif  // HAVE_PYTHON
-
-
-#ifdef HAVE_PYTHON
-/**
- * Gets the current element from the Python object and packs it.
- */
-void DCPacker::
-get_class_element(const DCClass *dclass, PyObject *object,
-                  const DCField *field) {
-  string field_name = field->get_name();
-  DCPackType pack_type = get_pack_type();
-
-  if (field_name.empty()) {
-    switch (pack_type) {
-    case PT_class:
-    case PT_switch:
-      // If the field has no name, but it is one of these container objects,
-      // we want to get its nested objects directly from the class.
-      push();
-      while (more_nested_fields() && !_pack_error) {
-        const DCField *field = get_current_field()->as_field();
-        nassertv(field != nullptr);
-        get_class_element(dclass, object, field);
-      }
-      pop();
-      break;
-
-    default:
-      // Otherwise, we just pack the default value.
-      pack_default_value();
-    }
-
-  } else {
-    // If the field does have a name, we will want to get it from the class
-    // and pack it.  It just so happens that there's already a method that
-    // does this on DCClass.
-
-    if (!dclass->pack_required_field(*this, object, field)) {
-      _pack_error = true;
-    }
-  }
-}
-#endif  // HAVE_PYTHON

+ 17 - 14
direct/src/dcparser/dcPacker.h

@@ -19,7 +19,10 @@
 #include "dcSubatomicType.h"
 #include "dcPackData.h"
 #include "dcPackerCatalog.h"
-#include "dcPython.h"
+
+#ifdef WITHIN_PANDA
+#include "extension.h"
+#endif
 
 class DCClass;
 class DCSwitchParameter;
@@ -104,10 +107,8 @@ public:
 
 PUBLISHED:
 
-#ifdef HAVE_PYTHON
-  void pack_object(PyObject *object);
-  PyObject *unpack_object();
-#endif
+  EXTENSION(void pack_object(PyObject *object));
+  EXTENSION(PyObject *unpack_object());
 
   bool parse_and_pack(const std::string &formatted_object);
   bool parse_and_pack(std::istream &in);
@@ -195,14 +196,12 @@ private:
   void clear();
   void clear_stack();
 
-#ifdef HAVE_PYTHON
-  void pack_class_object(const DCClass *dclass, PyObject *object);
-  PyObject *unpack_class_object(const DCClass *dclass);
-  void set_class_element(PyObject *class_def, PyObject *&object,
-                         const DCField *field);
-  void get_class_element(const DCClass *dclass, PyObject *object,
-                         const DCField *field);
-#endif
+  EXTENSION(void pack_class_object(const DCClass *dclass, PyObject *object));
+  EXTENSION(PyObject *unpack_class_object(const DCClass *dclass));
+  EXTENSION(void set_class_element(PyObject *class_def, PyObject *&object,
+                                   const DCField *field));
+  EXTENSION(void get_class_element(const DCClass *dclass, PyObject *object,
+                                   const DCField *field));
 
 private:
   enum Mode {
@@ -223,7 +222,7 @@ private:
   const DCPackerCatalog *_catalog;
   const DCPackerCatalog::LiveCatalog *_live_catalog;
 
-  class StackElement {
+  class EXPCL_DIRECT_DCPARSER StackElement {
   public:
     // As an optimization, we implement operator new and delete here to
     // minimize allocation overhead during push() and pop().
@@ -258,6 +257,10 @@ private:
   bool _parse_error;
   bool _pack_error;
   bool _range_error;
+
+#ifdef WITHIN_PANDA
+  friend class Extension<DCPacker>;
+#endif
 };
 
 #include "dcPacker.I"

+ 3 - 3
direct/src/dcparser/dcPackerInterface.I

@@ -253,8 +253,9 @@ do_unpack_int64(const char *buffer) {
                     ((uint64_t)(unsigned char)buffer[4] << 32) |
                     ((uint64_t)(unsigned char)buffer[5] << 40) |
                     ((uint64_t)(unsigned char)buffer[6] << 48) |
-                    ((int64_t)(signed char)buffer[7] << 54));
+                    ((int64_t)(signed char)buffer[7] << 56));
 }
+
 /**
  *
  */
@@ -295,10 +296,9 @@ do_unpack_uint64(const char *buffer) {
           ((uint64_t)(unsigned char)buffer[4] << 32) |
           ((uint64_t)(unsigned char)buffer[5] << 40) |
           ((uint64_t)(unsigned char)buffer[6] << 48) |
-          ((int64_t)(signed char)buffer[7] << 54));
+          ((uint64_t)(unsigned char)buffer[7] << 56));
 }
 
-
 /**
  *
  */

+ 0 - 1
direct/src/dcparser/dcPackerInterface.h

@@ -16,7 +16,6 @@
 
 #include "dcbase.h"
 #include "dcSubatomicType.h"
-#include "vector_uchar.h"
 
 class DCFile;
 class DCField;

+ 508 - 0
direct/src/dcparser/dcPacker_ext.cxx

@@ -0,0 +1,508 @@
+/**
+ * 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 dcPacker_ext.cxx
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#include "dcPacker_ext.h"
+#include "dcClass_ext.h"
+#include "dcField_ext.h"
+
+#include "dcClassParameter.h"
+
+#ifdef HAVE_PYTHON
+
+/**
+ * Packs the Python object of whatever type into the packer.  Each numeric
+ * object and string object maps to the corresponding pack_value() call; a
+ * tuple or sequence maps to a push() followed by all of the tuple's contents
+ * followed by a pop().
+ */
+void Extension<DCPacker>::
+pack_object(PyObject *object) {
+  nassertv(_this->_mode == DCPacker::Mode::M_pack ||
+           _this->_mode == DCPacker::Mode::M_repack);
+  DCPackType pack_type = _this->get_pack_type();
+
+  // had to add this for basic 64 and unsigned data to get packed right .. Not
+  // sure if we can just do the rest this way..
+
+  switch(pack_type) {
+  case PT_int64:
+    if (PyLong_Check(object)) {
+      _this->pack_int64(PyLong_AsLongLong(object));
+      return;
+    }
+#if PY_MAJOR_VERSION < 3
+    else if (PyInt_Check(object)) {
+      _this->pack_int64(PyInt_AsLong(object));
+      return;
+    }
+#endif
+    break;
+
+  case PT_uint64:
+    if (PyLong_Check(object)) {
+      _this->pack_uint64(PyLong_AsUnsignedLongLong(object));
+      return;
+    }
+#if PY_MAJOR_VERSION < 3
+    else if (PyInt_Check(object)) {
+      PyObject *obj1 = PyNumber_Long(object);
+      _this->pack_int(PyLong_AsUnsignedLongLong(obj1));
+      Py_DECREF(obj1);
+      return;
+    }
+#endif
+    break;
+
+  case PT_int:
+    if (PyLong_Check(object)) {
+      _this->pack_int(PyLong_AsLong(object));
+      return;
+    }
+#if PY_MAJOR_VERSION < 3
+    else if (PyInt_Check(object)) {
+      _this->pack_int(PyInt_AsLong(object));
+      return;
+    }
+#endif
+    break;
+
+  case PT_uint:
+    if (PyLong_Check(object)) {
+      _this->pack_uint(PyLong_AsUnsignedLong(object));
+      return;
+    }
+#if PY_MAJOR_VERSION < 3
+    else if (PyInt_Check(object)) {
+      PyObject *obj1 = PyNumber_Long(object);
+      _this->pack_uint(PyLong_AsUnsignedLong(obj1));
+      Py_DECREF(obj1);
+      return;
+    }
+#endif
+    break;
+
+  default:
+    break;
+  }
+
+  if (PyLong_Check(object)) {
+    _this->pack_int(PyLong_AsLong(object));
+#if PY_MAJOR_VERSION < 3
+  } else if (PyInt_Check(object)) {
+    _this->pack_int(PyInt_AS_LONG(object));
+#endif
+  } else if (PyFloat_Check(object)) {
+    _this->pack_double(PyFloat_AS_DOUBLE(object));
+  } else if (PyLong_Check(object)) {
+    _this->pack_int64(PyLong_AsLongLong(object));
+#if PY_MAJOR_VERSION >= 3
+  } else if (PyUnicode_Check(object)) {
+    const char *buffer;
+    Py_ssize_t length;
+    buffer = PyUnicode_AsUTF8AndSize(object, &length);
+    if (buffer) {
+      _this->pack_string(std::string(buffer, length));
+    }
+  } else if (PyBytes_Check(object)) {
+    const unsigned char *buffer;
+    Py_ssize_t length;
+    PyBytes_AsStringAndSize(object, (char **)&buffer, &length);
+    if (buffer) {
+      _this->pack_blob(vector_uchar(buffer, buffer + length));
+    }
+#else
+  } else if (PyString_Check(object) || PyUnicode_Check(object)) {
+    char *buffer;
+    Py_ssize_t length;
+    PyString_AsStringAndSize(object, &buffer, &length);
+    if (buffer) {
+      _this->pack_string(std::string(buffer, length));
+    }
+#endif
+  } else {
+    // For some reason, PySequence_Check() is incorrectly reporting that a
+    // class instance is a sequence, even if it doesn't provide __len__, so we
+    // double-check by testing for __len__ explicitly.
+    bool is_sequence =
+      (PySequence_Check(object) != 0) &&
+      (PyObject_HasAttrString(object, "__len__") != 0);
+    bool is_instance = false;
+
+    const DCClass *dclass = nullptr;
+    const DCPackerInterface *current_field = _this->get_current_field();
+    if (current_field != nullptr) {
+      const DCClassParameter *class_param = _this->get_current_field()->as_class_parameter();
+      if (class_param != nullptr) {
+        dclass = class_param->get_class();
+
+        if (invoke_extension(dclass).has_class_def()) {
+          PyObject *class_def = invoke_extension(dclass).get_class_def();
+          is_instance = (PyObject_IsInstance(object, invoke_extension(dclass).get_class_def()) != 0);
+          Py_DECREF(class_def);
+        }
+      }
+    }
+
+    // If dclass is not NULL, the packer is expecting a class object.  There
+    // are then two cases: (1) the user has supplied a matching class object,
+    // or (2) the user has supplied a sequence object.  Unfortunately, it may
+    // be difficult to differentiate these two cases, since a class object may
+    // also be a sequence object.
+
+    // The rule to differentiate them is:
+
+    // (1) If the supplied class object is an instance of the expected class
+    // object, it is considered to be a class object.
+
+    // (2) Otherwise, if the supplied class object has a __len__() method
+    // (i.e.  PySequence_Check() returns true), then it is considered to be a
+    // sequence.
+
+    // (3) Otherwise, it is considered to be a class object.
+
+    if (dclass != nullptr && (is_instance || !is_sequence)) {
+      // The supplied object is either an instance of the expected class
+      // object, or it is not a sequence--this is case (1) or (3).
+      pack_class_object(dclass, object);
+    } else if (is_sequence) {
+      // The supplied object is not an instance of the expected class object,
+      // but it is a sequence.  This is case (2).
+      _this->push();
+      int size = PySequence_Size(object);
+      for (int i = 0; i < size; ++i) {
+        PyObject *element = PySequence_GetItem(object, i);
+        if (element != nullptr) {
+          pack_object(element);
+          Py_DECREF(element);
+        } else {
+          std::cerr << "Unable to extract item " << i << " from sequence.\n";
+        }
+      }
+      _this->pop();
+    } else {
+      // The supplied object is not a sequence, and we weren't expecting a
+      // class parameter.  This is none of the above, an error.
+      std::ostringstream strm;
+      strm << "Don't know how to pack object: "
+           << Extension<DCField>::get_pystr(object);
+      nassert_raise(strm.str());
+      _this->_pack_error = true;
+    }
+  }
+}
+
+/**
+ * Unpacks a Python object of the appropriate type from the stream for the
+ * current field.  This may be an integer or a string for a simple field
+ * object; if the current field represents a list of fields it will be a
+ * tuple.
+ */
+PyObject *Extension<DCPacker>::
+unpack_object() {
+  PyObject *object = nullptr;
+
+  DCPackType pack_type = _this->get_pack_type();
+
+  switch (pack_type) {
+  case PT_invalid:
+    object = Py_None;
+    Py_INCREF(object);
+    _this->unpack_skip();
+    break;
+
+  case PT_double:
+    {
+      double value = _this->unpack_double();
+      object = PyFloat_FromDouble(value);
+    }
+    break;
+
+  case PT_int:
+    {
+      int value = _this->unpack_int();
+#if PY_MAJOR_VERSION >= 3
+      object = PyLong_FromLong(value);
+#else
+      object = PyInt_FromLong(value);
+#endif
+    }
+    break;
+
+  case PT_uint:
+    {
+      unsigned int value = _this->unpack_uint();
+#if PY_MAJOR_VERSION >= 3
+      object = PyLong_FromLong(value);
+#else
+      if (value & 0x80000000) {
+        object = PyLong_FromUnsignedLong(value);
+      } else {
+        object = PyInt_FromLong(value);
+      }
+#endif
+    }
+    break;
+
+  case PT_int64:
+    {
+      int64_t value = _this->unpack_int64();
+      object = PyLong_FromLongLong(value);
+    }
+    break;
+
+  case PT_uint64:
+    {
+      uint64_t value = _this->unpack_uint64();
+      object = PyLong_FromUnsignedLongLong(value);
+    }
+    break;
+
+  case PT_blob:
+#if PY_MAJOR_VERSION >= 3
+    {
+      std::string str;
+      _this->unpack_string(str);
+      object = PyBytes_FromStringAndSize(str.data(), str.size());
+    }
+    break;
+#endif
+    // On Python 2, fall through to below.
+
+  case PT_string:
+    {
+      std::string str;
+      _this->unpack_string(str);
+#if PY_MAJOR_VERSION >= 3
+      object = PyUnicode_FromStringAndSize(str.data(), str.size());
+#else
+      object = PyString_FromStringAndSize(str.data(), str.size());
+#endif
+    }
+    break;
+
+  case PT_class:
+    {
+      const DCClassParameter *class_param = _this->get_current_field()->as_class_parameter();
+      if (class_param != nullptr) {
+        const DCClass *dclass = class_param->get_class();
+        if (invoke_extension(dclass).has_class_def()) {
+          // If we know what kind of class object this is and it has a valid
+          // constructor, create the class object instead of just a tuple.
+          object = unpack_class_object(dclass);
+          if (object == nullptr) {
+            std::cerr << "Unable to construct object of class "
+                 << dclass->get_name() << "\n";
+          } else {
+            break;
+          }
+        }
+      }
+    }
+    // Fall through (if no constructor)
+
+    // If we don't know what kind of class object it is, or it doesn't have a
+    // constructor, fall through and make a tuple.
+  default:
+    {
+      // First, build up a list from the nested objects.
+      object = PyList_New(0);
+
+      _this->push();
+      while (_this->more_nested_fields()) {
+        PyObject *element = unpack_object();
+        PyList_Append(object, element);
+        Py_DECREF(element);
+      }
+      _this->pop();
+
+      if (pack_type != PT_array) {
+        // For these other kinds of objects, we'll convert the list into a
+        // tuple.
+        PyObject *tuple = PyList_AsTuple(object);
+        Py_DECREF(object);
+        object = tuple;
+      }
+    }
+    break;
+  }
+
+  nassertr(object != nullptr, nullptr);
+  return object;
+}
+
+/**
+ * Given that the current element is a ClassParameter for a Python class
+ * object, try to extract the appropriate values from the class object and
+ * pack in.
+ */
+void Extension<DCPacker>::
+pack_class_object(const DCClass *dclass, PyObject *object) {
+  _this->push();
+  while (_this->more_nested_fields() && !_this->_pack_error) {
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertv(field != nullptr);
+    get_class_element(dclass, object, field);
+  }
+  _this->pop();
+}
+
+/**
+ * Given that the current element is a ClassParameter for a Python class for
+ * which we have a valid constructor, unpack it and fill in its values.
+ */
+PyObject *Extension<DCPacker>::
+unpack_class_object(const DCClass *dclass) {
+  PyObject *class_def = invoke_extension(dclass).get_class_def();
+  nassertr(class_def != nullptr, nullptr);
+
+  PyObject *object = nullptr;
+
+  if (!dclass->has_constructor()) {
+    // If the class uses a default constructor, go ahead and create the Python
+    // object for it now.
+    object = PyObject_CallObject(class_def, nullptr);
+    if (object == nullptr) {
+      return nullptr;
+    }
+  }
+
+  _this->push();
+  if (object == nullptr && _this->more_nested_fields()) {
+    // The first nested field will be the constructor.
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertr(field != nullptr, object);
+    nassertr(field == dclass->get_constructor(), object);
+
+    set_class_element(class_def, object, field);
+
+    // By now, the object should have been constructed.
+    if (object == nullptr) {
+      return nullptr;
+    }
+  }
+  while (_this->more_nested_fields()) {
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertr(field != nullptr, object);
+
+    set_class_element(class_def, object, field);
+  }
+  _this->pop();
+
+  return object;
+}
+
+/**
+ * Unpacks the current element and stuffs it on the Python class object in
+ * whatever way is appropriate.
+ */
+void Extension<DCPacker>::
+set_class_element(PyObject *class_def, PyObject *&object,
+                  const DCField *field) {
+  std::string field_name = field->get_name();
+  DCPackType pack_type = _this->get_pack_type();
+
+  if (field_name.empty()) {
+    switch (pack_type) {
+    case PT_class:
+    case PT_switch:
+      // If the field has no name, but it is one of these container objects,
+      // we want to unpack its nested objects directly into the class.
+      _this->push();
+      while (_this->more_nested_fields()) {
+        const DCField *field = _this->get_current_field()->as_field();
+        nassertv(field != nullptr);
+        nassertv(object != nullptr);
+        set_class_element(class_def, object, field);
+      }
+      _this->pop();
+      break;
+
+    default:
+      // Otherwise, we just skip over the field.
+      _this->unpack_skip();
+    }
+
+  } else {
+    // If the field does have a name, we will want to store it on the class,
+    // either by calling a method (for a PT_field pack_type) or by setting a
+    // value (for any other kind of pack_type).
+
+    PyObject *element = unpack_object();
+
+    if (pack_type == PT_field) {
+      if (object == nullptr) {
+        // If the object hasn't been constructed yet, assume this is the
+        // constructor.
+        object = PyObject_CallObject(class_def, element);
+
+      } else {
+        if (PyObject_HasAttrString(object, (char *)field_name.c_str())) {
+          PyObject *func = PyObject_GetAttrString(object, (char *)field_name.c_str());
+          if (func != nullptr) {
+            PyObject *result = PyObject_CallObject(func, element);
+            Py_XDECREF(result);
+            Py_DECREF(func);
+          }
+        }
+      }
+
+    } else {
+      nassertv(object != nullptr);
+      PyObject_SetAttrString(object, (char *)field_name.c_str(), element);
+    }
+
+    Py_DECREF(element);
+  }
+}
+
+/**
+ * Gets the current element from the Python object and packs it.
+ */
+void Extension<DCPacker>::
+get_class_element(const DCClass *dclass, PyObject *object,
+                  const DCField *field) {
+  std::string field_name = field->get_name();
+  DCPackType pack_type = _this->get_pack_type();
+
+  if (field_name.empty()) {
+    switch (pack_type) {
+    case PT_class:
+    case PT_switch:
+      // If the field has no name, but it is one of these container objects,
+      // we want to get its nested objects directly from the class.
+      _this->push();
+      while (_this->more_nested_fields() && !_this->_pack_error) {
+        const DCField *field = _this->get_current_field()->as_field();
+        nassertv(field != nullptr);
+        get_class_element(dclass, object, field);
+      }
+      _this->pop();
+      break;
+
+    default:
+      // Otherwise, we just pack the default value.
+      _this->pack_default_value();
+    }
+
+  } else {
+    // If the field does have a name, we will want to get it from the class
+    // and pack it.  It just so happens that there's already a method that
+    // does this on DCClass.
+
+    if (!invoke_extension(dclass).pack_required_field(*_this, object, field)) {
+      _this->_pack_error = true;
+    }
+  }
+}
+
+#endif  // HAVE_PYTHON

+ 45 - 0
direct/src/dcparser/dcPacker_ext.h

@@ -0,0 +1,45 @@
+/**
+ * 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 dcPacker_ext.h
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#ifndef DCPACKER_EXT_H
+#define DCPACKER_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "dcPacker.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for DCPacker, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<DCPacker> : public ExtensionBase<DCPacker> {
+public:
+  void pack_object(PyObject *object);
+  PyObject *unpack_object();
+
+  void pack_class_object(const DCClass *dclass, PyObject *object);
+  PyObject *unpack_class_object(const DCClass *dclass);
+  void set_class_element(PyObject *class_def, PyObject *&object,
+                         const DCField *field);
+  void get_class_element(const DCClass *dclass, PyObject *object,
+                         const DCField *field);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // DCPACKER_EXT_H

+ 0 - 1
direct/src/dcparser/dcParserDefs.h

@@ -16,7 +16,6 @@
 
 #include "dcbase.h"
 #include "dcSubatomicType.h"
-#include "vector_uchar.h"
 
 class DCFile;
 class DCClass;

+ 0 - 42
direct/src/dcparser/dcPython.h

@@ -1,42 +0,0 @@
-/**
- * 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 dcPython.h
- * @author drose
- * @date 2004-06-22
- */
-
-#ifndef DCPYTHON_H
-#define DCPYTHON_H
-
-// The only purpose of this file is to serve as a common place to put the
-// nonsense associated with #including <Python.h>.
-
-#ifdef HAVE_PYTHON
-
-#define PY_SSIZE_T_CLEAN 1
-
-#undef _POSIX_C_SOURCE
-#undef _XOPEN_SOURCE
-#include <Python.h>
-
-// Python 2.5 adds Py_ssize_t; earlier versions don't have it.
-#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
-typedef int Py_ssize_t;
-#define PY_SSIZE_T_MAX INT_MAX
-#define PY_SSIZE_T_MIN INT_MIN
-#endif
-
-// Several interfaces in this module that use Python also require these header
-// files, so we might as well pick them up too.
-#include "datagram.h"
-#include "datagramIterator.h"
-
-#endif  // HAVE_PYTHON
-
-#endif

+ 4 - 0
direct/src/dcparser/dcbase.h

@@ -32,6 +32,7 @@
 #include "pvector.h"
 #include "pmap.h"
 #include "pset.h"
+#include "vector_uchar.h"
 
 #else  // WITHIN_PANDA
 
@@ -81,6 +82,7 @@
 #define nassertr_always(condition, return_value) assert(condition)
 #define nassertv(condition) assert(condition)
 #define nassertv_always(condition) assert(condition)
+#define nassert_raise(message) {std::cerr << message << std::endl; abort();}
 
 // Panda defines a special Filename class.  We'll use an ordinary string
 // instead.
@@ -97,8 +99,10 @@ typedef std::string Filename;
 #define pvector std::vector
 #define pmap std::map
 #define pset std::set
+#define vector_uchar std::vector<unsigned char>
 
 #include <stdint.h>
+#include <string.h>
 
 typedef std::ifstream pifstream;
 typedef std::ofstream pofstream;

+ 0 - 1
direct/src/dcparser/hashGenerator.h

@@ -16,7 +16,6 @@
 
 #include "dcbase.h"
 #include "primeNumberGenerator.h"
-#include "vector_uchar.h"
 
 /**
  * This class generates an arbitrary hash number from a sequence of ints.

+ 3 - 0
direct/src/dcparser/p3dcparser_ext_composite.cxx

@@ -0,0 +1,3 @@
+#include "dcClass_ext.cxx"
+#include "dcField_ext.cxx"
+#include "dcPacker_ext.cxx"

+ 1 - 1
direct/src/directdevices/DirectDeviceManager.py

@@ -1,4 +1,4 @@
-""" Class used to create and control vrpn devices """
+"""Class used to create and control VRPN devices."""
 
 from direct.showbase.DirectObject import DirectObject
 from panda3d.core import *

+ 2 - 2
direct/src/directdevices/DirectFastrak.py

@@ -20,9 +20,9 @@ class DirectFastrak(DirectObject):
     fastrakCount = 0
     notify = DirectNotifyGlobal.directNotify.newCategory('DirectFastrak')
 
-    def __init__(self, device = 'Tracker0', nodePath = base.direct.camera):
+    def __init__(self, device = 'Tracker0', nodePath = None):
         # See if device manager has been initialized
-        if base.direct.deviceManager == None:
+        if base.direct.deviceManager is None:
             base.direct.deviceManager = DirectDeviceManager()
 
         # Set name

+ 5 - 1
direct/src/directnotify/DirectNotifyGlobal.py

@@ -1,8 +1,12 @@
-"""instantiate global DirectNotify used in Direct"""
+"""Instantiates global DirectNotify used in Direct."""
 
 __all__ = ['directNotify', 'giveNotify']
 
 from . import DirectNotify
 
+#: The global :class:`~.DirectNotify.DirectNotify` object.
 directNotify = DirectNotify.DirectNotify()
+
+#: Shorthand function for adding a DirectNotify category to a given class
+#: object.  Alias of `.DirectNotify.DirectNotify.giveNotify`.
 giveNotify = directNotify.giveNotify

+ 2 - 1
direct/src/directnotify/LoggerGlobal.py

@@ -1,5 +1,6 @@
-"""instantiate global Logger object"""
+"""Instantiates a global :class:`~.Logger.Logger` object."""
 
 from . import Logger
 
+#: Contains a global :class:`~.Logger.Logger` object.
 defaultLogger = Logger.Logger()

+ 6 - 6
direct/src/directnotify/Notifier.py

@@ -8,6 +8,7 @@ from panda3d.core import ConfigVariableBool, NotifyCategory, StreamWriter, Notif
 import time
 import sys
 
+
 class Notifier:
     serverDelta = 0
 
@@ -23,12 +24,11 @@ class Notifier:
 
     def __init__(self, name, logger=None):
         """
-        name is a string
-        logger is a Logger
-
-        Create a new instance of the Notifier class with a given name
-        and an optional Logger class for piping output to. If no logger
-        specified, use the global default
+        Parameters:
+            name (str): a string name given to this Notifier instance.
+            logger (Logger, optional): an optional Logger object for
+                piping output to.  If none is specified, the global
+                :data:`~.LoggerGlobal.defaultLogger` is used.
         """
         self.__name = name
 

+ 15 - 15
direct/src/directnotify/RotatingLog.py

@@ -1,8 +1,7 @@
-
-
 import os
 import time
 
+
 class RotatingLog:
     """
     A file() (or open()) replacement that will automatically open and write
@@ -11,22 +10,23 @@ class RotatingLog:
 
     def __init__(self, path="./log_file", hourInterval=24, megabyteLimit=1024):
         """
-        path is a full or partial path with file name.
-        hourInterval is the number of hours at which to rotate the file.
-        megabyteLimit is the number of megabytes of file size the log
-            may grow to, after which the log is rotated.  Note: The log
-            file may get a bit larger than limit do to writing out whole
-            lines (last line may exceed megabyteLimit or "megabyteGuidline").
+        Args:
+            path: a full or partial path with file name.
+            hourInterval: the number of hours at which to rotate the file.
+            megabyteLimit: the number of megabytes of file size the log may
+                grow to, after which the log is rotated.  Note: The log file
+                may get a bit larger than limit do to writing out whole lines
+                (last line may exceed megabyteLimit or "megabyteGuidline").
         """
-        self.path=path
-        self.timeInterval=None
-        self.timeLimit=None
-        self.sizeLimit=None
+        self.path = path
+        self.timeInterval = None
+        self.timeLimit = None
+        self.sizeLimit = None
         if hourInterval is not None:
-            self.timeInterval=hourInterval*60*60
-            self.timeLimit=time.time()+self.timeInterval
+            self.timeInterval = hourInterval*60*60
+            self.timeLimit = time.time()+self.timeInterval
         if megabyteLimit is not None:
-            self.sizeLimit=megabyteLimit*1024*1024
+            self.sizeLimit = megabyteLimit*1024*1024
 
     def __del__(self):
         self.close()

+ 0 - 99
direct/src/directscripts/DetectPanda3D.js

@@ -1,99 +0,0 @@
-// Based on Apple sample code at
-// http://developer.apple.com/internet/webcontent/examples/detectplugins_source.html
-
-
-// initialize global variables
-var detectableWithVB = false;
-var pluginFound = false;
-
-function goURL(daURL) {
-    // Assume we have Javascript 1.1 functionality.
-    window.location.replace(daURL);
-    return;
-}
-
-function redirectCheck(pluginFound, redirectURL, redirectIfFound) {
-    // check for redirection
-    if( redirectURL && ((pluginFound && redirectIfFound) || 
-	(!pluginFound && !redirectIfFound)) ) {
-	// go away
-	goURL(redirectURL);
-	return pluginFound;
-    } else {
-	// stay here and return result of plugin detection
-	return pluginFound;
-    }	
-}
-
-function canDetectPlugins() {
-    if( detectableWithVB || (navigator.plugins && navigator.plugins.length > 0) ) {
-	return true;
-    } else {
-	return false;
-    }
-}
-
-function detectPanda3D(redirectURL, redirectIfFound) {
-    pluginFound = detectPlugin('Panda3D'); 
-    // if not found, try to detect with VisualBasic
-    if(!pluginFound && detectableWithVB) {
-	pluginFound = detectActiveXControl('P3DACTIVEX.P3DActiveXCtrl.1');
-    }
-    // check for redirection
-    return redirectCheck(pluginFound, redirectURL, redirectIfFound);
-}
-
-function detectPlugin() {
-    // allow for multiple checks in a single pass
-    var daPlugins = detectPlugin.arguments;
-    // consider pluginFound to be false until proven true
-    var pluginFound = false;
-    // if plugins array is there and not fake
-    if (navigator.plugins && navigator.plugins.length > 0) {
-	var pluginsArrayLength = navigator.plugins.length;
-	// for each plugin...
-	for (pluginsArrayCounter=0; pluginsArrayCounter < pluginsArrayLength; pluginsArrayCounter++ ) {
-	    // loop through all desired names and check each against the current plugin name
-	    var numFound = 0;
-	    for(namesCounter=0; namesCounter < daPlugins.length; namesCounter++) {
-		// if desired plugin name is found in either plugin name or description
-		if( (navigator.plugins[pluginsArrayCounter].name.indexOf(daPlugins[namesCounter]) >= 0) || 
-		    (navigator.plugins[pluginsArrayCounter].description.indexOf(daPlugins[namesCounter]) >= 0) ) {
-		    // this name was found
-		    numFound++;
-		}   
-	    }
-	    // now that we have checked all the required names against this one plugin,
-	    // if the number we found matches the total number provided then we were successful
-	    if(numFound == daPlugins.length) {
-		pluginFound = true;
-		// if we've found the plugin, we can stop looking through at the rest of the plugins
-		break;
-	    }
-	}
-    }
-    return pluginFound;
-} // detectPlugin
-
-
-// Here we write out the VBScript block for MSIE Windows
-if ((navigator.userAgent.indexOf('MSIE') != -1) && (navigator.userAgent.indexOf('Win') != -1)) {
-    document.writeln('<script language="VBscript">');
-
-    document.writeln('\'do a one-time test for a version of VBScript that can handle this code');
-    document.writeln('detectableWithVB = False');
-    document.writeln('If ScriptEngineMajorVersion >= 2 then');
-    document.writeln('  detectableWithVB = True');
-    document.writeln('End If');
-
-    document.writeln('\'this next function will detect most plugins');
-    document.writeln('Function detectActiveXControl(activeXControlName)');
-    document.writeln('  on error resume next');
-    document.writeln('  detectActiveXControl = False');
-    document.writeln('  If detectableWithVB Then');
-    document.writeln('     detectActiveXControl = IsObject(CreateObject(activeXControlName))');
-    document.writeln('  End If');
-    document.writeln('End Function');
-
-    document.writeln('</script>');
-}

+ 0 - 132
direct/src/directscripts/RunPanda3D.js

@@ -1,132 +0,0 @@
-// This script injects the appropriate syntax into the document to
-// embed Panda3D, either for the ActiveX or Mozilla-based plugin.
-
-// It is also possible to write browser-independent code by nesting
-// <object> tags, but this causes problems when you need to reference
-// the particular object that is actually running (which object is
-// it?) for scripting purposes.
-
-// This script writes only a single <object> tag, and it can be
-// assigned the id you specify, avoiding this ambiguity.
-
-var isIE  = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false;
-var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false;
-var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false;
-
-
-function P3D_Generateobj(objAttrs, params, embedAttrs, imageAttrs) 
-{ 
-  var str = '';
-
-  if (isIE && isWin && !isOpera)
-  {
-    str += '<object ';
-    for (var i in objAttrs)
-    {
-      str += i + '="' + objAttrs[i] + '" ';
-    }
-    str += '>';
-    for (var i in params)
-    {
-      str += '<param name="' + i + '" value="' + params[i] + '" /> ';
-    }
-  }
-  else
-  {
-    str += '<object ';
-    for (var i in embedAttrs)
-    {
-      str += i + '="' + embedAttrs[i] + '" ';
-    }
-    str += '> ';
-  }
-  if (imageAttrs["src"]) {
-    if (imageAttrs["href"]) {
-      str += '<a href="' + imageAttrs["href"] + '">';
-    }
-    str += '<img ';
-    for (var i in imageAttrs) {
-      if (i != "href") {
-        str += i + '="' + imageAttrs[i] + '" ';
-      }
-    }
-    str += '>';
-    if (imageAttrs["href"]) {
-      str += '</a>';
-    }
-  }
-  str += '</object>';
-
-  document.write(str);
-}
-
-function P3D_RunContent() {
-  var ret = 
-    P3D_GetArgs
-      (arguments, "clsid:924b4927-d3ba-41ea-9f7e-8a89194ab3ac",
-       "application/x-panda3d");
-  P3D_Generateobj(ret.objAttrs, ret.params, ret.embedAttrs, ret.imageAttrs);
-}
-
-function P3D_GetArgs(args, classid, mimeType){
-  var ret = new Object();
-  ret.embedAttrs = new Object();
-  ret.params = new Object();
-  ret.objAttrs = new Object();
-  ret.imageAttrs = new Object();
-
-  for (var i = 0; i < args.length; i = i + 2){
-    var currArg = args[i].toLowerCase();    
-
-    switch (currArg){	
-    case "src":
-    case "data":
-        ret.embedAttrs['data'] = args[i+1];
-        ret.params['data'] = args[i+1];
-        break;
-
-    case "codebase":
-        ret.objAttrs['codebase'] = args[i+1];
-        break;
-
-    case "noplugin_img":
-        ret.imageAttrs["src"] = args[i+1];
-        ret.imageAttrs["border"] = '0';
-        break;
-
-    case "noplugin_href":
-        ret.imageAttrs["href"] = args[i+1];
-        break;
-
-    case "splash_img":
-        ret.embedAttrs[args[i]] = ret.params[args[i]] = args[i+1];
-        if (!ret.imageAttrs["src"]) {
-          ret.imageAttrs["src"] = args[i+1];
-        }
-        break;
-
-    case "width":
-    case "height":
-        ret.imageAttrs[args[i]] = ret.embedAttrs[args[i]] = ret.objAttrs[args[i]] = args[i+1];
-        break;
-
-    case "id":
-    case "align":
-    case "vspace": 
-    case "hspace":
-    case "class":
-    case "title":
-    case "accesskey":
-    case "name":
-    case "tabindex":
-        ret.embedAttrs[args[i]] = ret.objAttrs[args[i]] = args[i+1];
-        break;
-
-    default:
-        ret.embedAttrs[args[i]] = ret.params[args[i]] = args[i+1];
-    }
-  }
-  ret.objAttrs["classid"] = classid;
-  if (mimeType) ret.embedAttrs["type"] = mimeType;
-  return ret;
-}

+ 3 - 1
direct/src/directscripts/extract_docs.py

@@ -273,6 +273,7 @@ def processModule(handle, package):
             if "panda3d." + package == module_name:
                 processType(handle, type)
         else:
+            typename = interrogate_type_name(type)
             print("Type %s has no module name" % typename)
 
     for i_func in range(interrogate_number_of_global_functions()):
@@ -283,7 +284,8 @@ def processModule(handle, package):
             if "panda3d." + package == module_name:
                 processFunction(handle, func)
         else:
-            print("Type %s has no module name" % typename)
+            funcname = interrogate_function_name(func)
+            print("Function %s has no module name" % funcname)
 
     print("}", file=handle)
 

+ 2 - 0
direct/src/directtools/DirectUtil.py

@@ -1,5 +1,7 @@
 
 from .DirectGlobals import *
+from panda3d.core import VBase4
+from direct.task.Task import Task
 
 # Routines to adjust values
 def ROUND_TO(value, divisor):

+ 38 - 32
direct/src/directutil/Verify.py

@@ -1,27 +1,31 @@
 """
-You can use verify() just like assert, with these small differences:
-    - you may need to "import Verify", if someone hasn't done it
-      for you.
-    - unlike assert where using parenthises are optional, verify()
-      requires them.
-      e.g.:
-        assert foo  # OK
-        verify foo  # Error
-        assert foo  # Not Recomended (may be interpreted as a tuple)
-        verify(foo) # OK
-    - verify() will print something like the following before raising
-      an exception:
-        verify failed:
-            File "direct/src/showbase/ShowBase.py", line 60
-    - verify() will optionally start pdb for you (this is currently
-      false by default).  You can either edit Verify.py to set
-      wantVerifyPdb = 1 or if you are using ShowBase you can set
-      want-verify-pdb 1 in your Configrc to start pdb automatically.
-    - verify() will still function in the release build.  It will
-      not be removed by -O like assert will.
-
-verify() will also throw an AssertionError, but you can ignore that if you
-like (I don't suggest trying to catch it, it's just doing it so that it can
+You can use :func:`verify()` just like assert, with these small differences:
+
+- you may need to ``import Verify``, if someone hasn't done it for you.
+
+- unlike assert where using parentheses are optional, :func:`verify()`
+  requires them, e.g.::
+
+    assert foo  # OK
+    verify foo  # Error
+    assert foo  # Not Recomended (may be interpreted as a tuple)
+    verify(foo) # OK
+
+- :func:`verify()` will print something like this before raising an exception::
+
+    verify failed:
+        File "direct/src/showbase/ShowBase.py", line 60
+
+- :func:`verify()` will optionally start pdb for you (this is currently false
+  by default).  You can either edit Verify.py to set ``wantVerifyPdb = 1`` or
+  if you are using ShowBase you can set ``want-verify-pdb 1`` in your
+  Config.prc file to start pdb automatically.
+
+- :func:`verify()` will still function in the release build.  It will not be
+  removed by -O like assert will.
+
+:func:`verify()` will also throw an AssertionError, but you can ignore that if
+you like (I don't suggest trying to catch it, it's just doing it so that it can
 replace assert more fully).
 
 Please do not use assert for things that you want run on release builds.
@@ -31,19 +35,20 @@ an exception can get it mistaken for an error handler.  If your code
 needs to handle an error or throw an exception, you should do that
 (and not just assert for it).
 
-If you want to be a super keen software engineer then avoid using verify().
-If you want to be, or already are, a super keen software engineer, but
-you don't always have the time to write proper error handling, go ahead
-and use verify() -- that's what it's for.
+If you want to be a super keen software engineer then avoid using
+:func:`verify()`.  If you want to be, or already are, a super keen software
+engineer, but you don't always have the time to write proper error handling,
+go ahead and use :func:`verify()` -- that's what it's for.
 
-Please use assert (properly) and do proper error handling; and use verify()
-only when debugging (i.e. when it won't be checked-in) or where it helps
-you resist using assert for error handling.
+Please use assert (properly) and do proper error handling; and use
+:func:`verify()` only when debugging (i.e. when it won't be checked-in) or
+where it helps you resist using assert for error handling.
 """
 
 from panda3d.core import ConfigVariableBool
 
-wantVerifyPdb = ConfigVariableBool('want-verify-pdb', False) # Set to true to load pdb on failure.
+# Set to true to load pdb on failure.
+wantVerifyPdb = ConfigVariableBool('want-verify-pdb', False)
 
 
 def verify(assertion):
@@ -54,7 +59,7 @@ def verify(assertion):
     if not assertion:
         print("\n\nverify failed:")
         import sys
-        print("    File \"%s\", line %d"%(
+        print("    File \"%s\", line %d" % (
                 sys._getframe(1).f_code.co_filename,
                 sys._getframe(1).f_lineno))
         if wantVerifyPdb:
@@ -62,5 +67,6 @@ def verify(assertion):
             pdb.set_trace()
         raise AssertionError
 
+
 if not hasattr(__builtins__, "verify"):
     __builtins__["verify"] = verify

+ 94 - 17
direct/src/dist/FreezeTool.py

@@ -785,7 +785,7 @@ class Freezer:
         # already-imported modules.  (Some of them might do their own
         # special path mangling.)
         for moduleName, module in list(sys.modules.items()):
-            if module and hasattr(module, '__path__'):
+            if module and getattr(module, '__path__', None) is not None:
                 path = list(getattr(module, '__path__'))
                 if path:
                     modulefinder.AddPackagePath(moduleName, path[0])
@@ -800,29 +800,36 @@ class Freezer:
                     self.moduleSuffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE)
         else:
             self.moduleSuffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)]
+
+            abi_version = '{0}{1}'.format(*sys.version_info)
+            abi_flags = ''
+            if sys.version_info < (3, 8):
+                abi_flags += 'm'
+
             if 'linux' in self.platform:
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m-x86_64-linux-gnu.so'.format(*sys.version_info), 'rb', 3),
-                    ('.cpython-{0}{1}m-i686-linux-gnu.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}-x86_64-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
+                    ('.cpython-{0}{1}-i686-linux-gnu.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
             elif 'win' in self.platform:
+                # ABI flags are not appended on Windows.
                 self.moduleSuffixes += [
-                    ('.cp{0}{1}-win_amd64.pyd'.format(*sys.version_info), 'rb', 3),
-                    ('.cp{0}{1}-win32.pyd'.format(*sys.version_info), 'rb', 3),
+                    ('.cp{0}-win_amd64.pyd'.format(abi_version), 'rb', 3),
+                    ('.cp{0}-win32.pyd'.format(abi_version), 'rb', 3),
                     ('.pyd', 'rb', 3),
                 ]
             elif 'mac' in self.platform:
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m-darwin.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}-darwin.so'.format(abi_version, abi_flags), 'rb', 3),
                     ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
             else: # FreeBSD et al.
                 self.moduleSuffixes += [
-                    ('.cpython-{0}{1}m.so'.format(*sys.version_info), 'rb', 3),
-                    ('.abi{0}.so'.format(*sys.version_info), 'rb', 3),
+                    ('.cpython-{0}{1}.so'.format(abi_version, abi_flags), 'rb', 3),
+                    ('.abi{0}.so'.format(sys.version_info[0]), 'rb', 3),
                     ('.so', 'rb', 3),
                 ]
 
@@ -845,7 +852,7 @@ class Freezer:
         allowChildren is true, the children of the indicated module
         may still be included."""
 
-        assert self.mf == None
+        assert self.mf is None
 
         self.modules[moduleName] = self.ModuleDef(
             moduleName, exclude = True,
@@ -879,7 +886,7 @@ class Freezer:
             print("couldn't import %s" % (moduleName))
             module = None
 
-        if module != None:
+        if module is not None:
             for symbol in moduleName.split('.')[1:]:
                 module = getattr(module, symbol)
             if hasattr(module, '__path__'):
@@ -894,7 +901,7 @@ class Freezer:
         if '.' in baseName:
             parentName, baseName = moduleName.rsplit('.', 1)
             path = self.getModulePath(parentName)
-            if path == None:
+            if path is None:
                 return None
 
         try:
@@ -918,7 +925,7 @@ class Freezer:
             print("couldn't import %s" % (moduleName))
             module = None
 
-        if module != None:
+        if module is not None:
             for symbol in moduleName.split('.')[1:]:
                 module = getattr(module, symbol)
             if hasattr(module, '__all__'):
@@ -931,7 +938,7 @@ class Freezer:
         if '.' in baseName:
             parentName, baseName = moduleName.rsplit('.', 1)
             path = self.getModulePath(parentName)
-            if path == None:
+            if path is None:
                 return None
 
         try:
@@ -988,9 +995,9 @@ class Freezer:
         for parentName, newParentName in parentNames:
             modules = self.getModuleStar(parentName)
 
-            if modules == None:
+            if modules is None:
                 # It's actually a regular module.
-                mdef[newParentName] = self.ModuleDef(
+                mdefs[newParentName] = self.ModuleDef(
                     parentName, implicit = implicit, guess = guess,
                     fromSource = fromSource, text = text)
 
@@ -1024,7 +1031,7 @@ class Freezer:
         directories within a particular directory.
         """
 
-        assert self.mf == None
+        assert self.mf is None
 
         if not newName:
             newName = moduleName
@@ -1046,7 +1053,7 @@ class Freezer:
         to done(), you may not add any more modules until you call
         reset(). """
 
-        assert self.mf == None
+        assert self.mf is None
 
         # If we are building an exe, we also need to implicitly
         # bring in Python's startup modules.
@@ -2214,6 +2221,10 @@ class Freezer:
 
         return True
 
+
+_PKG_NAMESPACE_DIRECTORY = object()
+
+
 class PandaModuleFinder(modulefinder.ModuleFinder):
 
     def __init__(self, *args, **kw):
@@ -2272,6 +2283,44 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         return None
 
+    def _dir_exists(self, path):
+        """Returns True if the given directory exists, either on disk or inside
+        a wheel."""
+
+        if os.path.isdir(path):
+            return True
+
+        # Is there a zip file along the path?
+        dir, dirname = os.path.split(path.rstrip(os.path.sep + '/'))
+        fn = dirname
+        while dirname:
+            if os.path.isfile(dir):
+                # Okay, this is actually a file.  Is it a zip file?
+                if dir in self._zip_files:
+                    # Yes, and we've previously opened this.
+                    zip = self._zip_files[dir]
+                elif zipfile.is_zipfile(dir):
+                    zip = zipfile.ZipFile(dir)
+                    self._zip_files[dir] = zip
+                else:
+                    # It's a different kind of file.  Stop looking.
+                    return None
+
+                # (Most) zip files do not store directories; check instead for a
+                # file whose path starts with this directory name.
+                prefix = fn.replace(os.path.sep, '/') + '/'
+                for name in zip.namelist():
+                    if name.startswith(prefix):
+                        return True
+
+                return False
+
+            # Look at the parent directory.
+            dir, dirname = os.path.split(dir)
+            fn = os.path.join(dirname, fn)
+
+        return False
+
     def load_module(self, fqname, fp, pathname, file_info):
         """Copied from ModuleFinder.load_module with fixes to handle sending bytes
         to compile() for PY_SOURCE types. Sending bytes to compile allows it to
@@ -2284,6 +2333,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             self.msgout(2, "load_module ->", m)
             return m
 
+        if type is _PKG_NAMESPACE_DIRECTORY:
+            m = self.add_module(fqname)
+            m.__code__ = compile('', '', 'exec')
+            m.__path__ = pathname
+            return m
+
         if type == imp.PY_SOURCE:
             if fqname in overrideModules:
                 # This module has a custom override.
@@ -2365,7 +2420,20 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
             path = self.path
 
+            if fullname == 'distutils' and hasattr(sys, 'real_prefix'):
+                # The PyPI version of virtualenv inserts a special version of
+                # distutils that does some bizarre stuff that won't work in our
+                # deployed application.  Force it to find the regular one.
+                try:
+                    fp, fn, stuff = self.find_module('opcode')
+                    if fn:
+                        path = [os.path.dirname(fn)] + path
+                except ImportError:
+                    pass
+
         # Look for the module on the search path.
+        ns_dirs = []
+
         for dir_path in path:
             basename = os.path.join(dir_path, name.split('.')[-1])
 
@@ -2382,6 +2450,10 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 if self._open_file(init, mode):
                     return (None, basename, ('', '', imp.PKG_DIRECTORY))
 
+            # This may be a namespace package.
+            if self._dir_exists(basename):
+                ns_dirs.append(basename)
+
         # It wasn't found through the normal channels.  Maybe it's one of
         # ours, or maybe it's frozen?
         if not path:
@@ -2390,6 +2462,11 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 # It's a frozen module.
                 return (None, name, ('', '', imp.PY_FROZEN))
 
+        # If we found folders on the path with this module name without an
+        # __init__.py file, we should consider this a namespace package.
+        if ns_dirs and sys.version_info >= (3, 3):
+            return (None, ns_dirs, ('', '', _PKG_NAMESPACE_DIRECTORY))
+
         raise ImportError(name)
 
     def find_all_submodules(self, m):

+ 4 - 0
direct/src/dist/__init__.py

@@ -0,0 +1,4 @@
+"""This package contains tools to help with distributing Panda3D
+applications.  See the :ref:`distribution` section in the programming
+manual for further details.
+"""

+ 69 - 2
direct/src/dist/commands.py

@@ -1,3 +1,9 @@
+"""Extends setuptools with the ``build_apps`` and ``bdist_apps`` commands.
+
+See the :ref:`distribution` section of the programming manual for information
+on how to use these commands.
+"""
+
 from __future__ import print_function
 
 import collections
@@ -14,12 +20,14 @@ import struct
 import imp
 import string
 import time
+import tempfile
 
 import setuptools
 import distutils.log
 
 from . import FreezeTool
 from . import pefile
+from .icon import Icon
 import panda3d.core as p3d
 
 
@@ -64,6 +72,8 @@ def _parse_dict(input):
 
 
 def egg2bam(_build_cmd, srcpath, dstpath):
+    if dstpath.endswith('.gz') or dstpath.endswith('.pz'):
+        dstpath = dstpath[:-3]
     dstpath = dstpath + '.bam'
     try:
         subprocess.check_call([
@@ -224,9 +234,10 @@ class build_apps(setuptools.Command):
         self.exclude_patterns = []
         self.include_modules = {}
         self.exclude_modules = {}
+        self.icons = {}
         self.platforms = [
             'manylinux1_x86_64',
-            'macosx_10_6_x86_64',
+            'macosx_10_9_x86_64',
             'win_amd64',
         ]
         self.plugins = []
@@ -298,6 +309,7 @@ class build_apps(setuptools.Command):
             key: _parse_list(value)
             for key, value in _parse_dict(self.exclude_modules).items()
         }
+        self.icons = _parse_dict(self.icons)
         self.platforms = _parse_list(self.platforms)
         self.plugins = _parse_list(self.plugins)
         self.extra_prc_files = _parse_list(self.extra_prc_files)
@@ -353,10 +365,22 @@ class build_apps(setuptools.Command):
         tmp.update(self.file_handlers)
         self.file_handlers = tmp
 
-        tmp = self.package_data_dirs.copy()
+        tmp = PACKAGE_DATA_DIRS.copy()
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
 
+        self.icon_objects = {}
+        for app, iconpaths in self.icons.items():
+            if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
+                iconpaths = (iconpaths,)
+
+            iconobj = Icon()
+            for iconpath in iconpaths:
+                iconobj.addImage(iconpath)
+
+            iconobj.generateMissingImages()
+            self.icon_objects[app] = iconobj
+
     def run(self):
         self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
 
@@ -433,6 +457,22 @@ class build_apps(setuptools.Command):
 
         return wheelpaths
 
+    def update_pe_resources(self, appname, runtime):
+        """Update resources (e.g., icons) in windows PE file"""
+
+        icon = self.icon_objects.get(
+            appname,
+            self.icon_objects.get('*', None),
+        )
+
+        if icon is not None:
+            pef = pefile.PEFile()
+            pef.open(runtime, 'r+')
+            pef.add_icon(icon)
+            pef.add_resource_section()
+            pef.write_changes()
+            pef.close()
+
     def bundle_macos_app(self, builddir):
         """Bundle built runtime into a .app for macOS"""
 
@@ -474,6 +514,15 @@ class build_apps(setuptools.Command):
             'CFBundleSignature': '', #TODO
             'CFBundleExecutable': self.macos_main_app,
         }
+
+        icon = self.icon_objects.get(
+            self.macos_main_app,
+            self.icon_objects.get('*', None)
+        )
+        if icon is not None:
+            plist['CFBundleIconFile'] = 'iconfile'
+            icon.makeICNS(os.path.join(resdir, 'iconfile.icns'))
+
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             if hasattr(plistlib, 'dump'):
                 plistlib.dump(plist, f)
@@ -618,6 +667,18 @@ class build_apps(setuptools.Command):
                 stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
                 stub_file = open(stub_path, 'rb')
 
+            # Do we need an icon?  On Windows, we need to add this to the stub
+            # before we add the blob.
+            if 'win' in platform:
+                temp_file = tempfile.NamedTemporaryFile(suffix='-icon.exe', delete=False)
+                temp_file.write(stub_file.read())
+                stub_file.close()
+                temp_file.close()
+                self.update_pe_resources(appname, temp_file.name)
+                stub_file = open(temp_file.name, 'rb')
+            else:
+                temp_file = None
+
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'default_prc_dir': self.default_prc_dir,
@@ -633,6 +694,9 @@ class build_apps(setuptools.Command):
             }, self.log_append)
             stub_file.close()
 
+            if temp_file:
+                os.unlink(temp_file.name)
+
             # Copy the dependencies.
             search_path = [builddir]
             if use_wheels:
@@ -839,6 +903,9 @@ class build_apps(setuptools.Command):
                 os.makedirs(dst_dir)
 
             ext = os.path.splitext(src)[1]
+            # If the file ends with .gz/.pz, we strip this off.
+            if ext in ('.gz', '.pz'):
+                ext = os.path.splitext(src[:-3])[1]
             if not ext:
                 ext = os.path.basename(src)
 

+ 269 - 0
direct/src/dist/icon.py

@@ -0,0 +1,269 @@
+from direct.directnotify.DirectNotifyGlobal import *
+from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, StringStream
+import struct
+
+
+class Icon:
+    """ This class is used to create an icon for various platforms. """
+    notify = directNotify.newCategory("Icon")
+
+    def __init__(self):
+        self.images = {}
+
+    def addImage(self, image):
+        """ Adds an image to the icon.  Returns False on failure, True on success.
+        Only one image per size can be loaded, and the image size must be square. """
+
+        if not isinstance(image, PNMImage):
+            fn = image
+            if not isinstance(fn, Filename):
+                fn = Filename.fromOsSpecific(fn)
+
+            image = PNMImage()
+            if not image.read(fn):
+                Icon.notify.warning("Image '%s' could not be read" % fn.getBasename())
+                return False
+
+        if image.getXSize() != image.getYSize():
+            Icon.notify.warning("Ignoring image without square size")
+            return False
+
+        self.images[image.getXSize()] = image
+
+        return True
+
+    def generateMissingImages(self):
+        """ Generates image sizes that should be present but aren't by scaling
+        from the next higher size. """
+
+        for required_size in (256, 128, 48, 32, 16):
+            if required_size in self.images:
+                continue
+
+            sizes = sorted(self.images.keys())
+            if required_size * 2 in sizes:
+                from_size = required_size * 2
+            else:
+                from_size = 0
+                for from_size in sizes:
+                    if from_size > required_size:
+                        break
+
+            if from_size > required_size:
+                Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
+
+                image = PNMImage(required_size, required_size)
+                if self.images[from_size].hasAlpha():
+                    image.addAlpha()
+                image.quickFilterFrom(self.images[from_size])
+                self.images[required_size] = image
+            else:
+                Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
+
+    def _write_bitmap(self, fp, image, size, bpp):
+        """ Writes the bitmap header and data of an .ico file. """
+
+        fp.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
+
+        # XOR mask
+        if bpp == 24:
+            # Align rows to 4-byte boundary
+            rowalign = b'\0' * (-(size * 3) & 3)
+            for y in range(size):
+                for x in range(size):
+                    r, g, b = image.getXel(x, size - y - 1)
+                    fp.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
+                fp.write(rowalign)
+
+        elif bpp == 32:
+            for y in range(size):
+                for x in range(size):
+                    r, g, b, a = image.getXelA(x, size - y - 1)
+                    fp.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
+
+        elif bpp == 8:
+            # We'll have to generate a palette of 256 colors.
+            hist = PNMImage.Histogram()
+            image2 = PNMImage(image)
+            if image2.hasAlpha():
+                image2.premultiplyAlpha()
+                image2.removeAlpha()
+            image2.quantize(256)
+            image2.make_histogram(hist)
+            colors = list(hist.get_pixels())
+            assert len(colors) <= 256
+
+            # Write the palette.
+            i = 0
+            while i < 256 and i < len(colors):
+                r, g, b, a = colors[i]
+                fp.write(struct.pack('<BBBB', b, g, r, 0))
+                i += 1
+            if i < 256:
+                # Fill the rest with zeroes.
+                fp.write(b'\x00' * (4 * (256 - i)))
+
+            # Write indices.  Align rows to 4-byte boundary.
+            rowalign = b'\0' * (-size & 3)
+            for y in range(size):
+                for x in range(size):
+                    pixel = image2.get_pixel(x, size - y - 1)
+                    index = colors.index(pixel)
+                    if index >= 256:
+                        # Find closest pixel instead.
+                        index = closest_indices[index - 256]
+                    fp.write(struct.pack('<B', index))
+                fp.write(rowalign)
+        else:
+            raise ValueError("Invalid bpp %d" % (bpp))
+
+        # Create an AND mask, aligned to 4-byte boundary
+        if image.hasAlpha() and bpp <= 8:
+            rowalign = b'\0' * (-((size + 7) >> 3) & 3)
+            for y in range(size):
+                mask = 0
+                num_bits = 7
+                for x in range(size):
+                    a = image.get_alpha_val(x, size - y - 1)
+                    if a <= 1:
+                        mask |= (1 << num_bits)
+                    num_bits -= 1
+                    if num_bits < 0:
+                        fp.write(struct.pack('<B', mask))
+                        mask = 0
+                        num_bits = 7
+                if num_bits < 7:
+                    fp.write(struct.pack('<B', mask))
+                fp.write(rowalign)
+        else:
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            fp.write(b'\x00' * (andsize * size))
+
+    def makeICO(self, fn):
+        """ Writes the images to a Windows ICO file.  Returns True on success. """
+
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+
+        # ICO files only support resolutions up to 256x256.
+        count = 0
+        for size in self.images.keys():
+            if size < 256:
+                count += 1
+            if size <= 256:
+                count += 1
+        dataoffs = 6 + count * 16
+
+        ico = open(fn, 'wb')
+        ico.write(struct.pack('<HHH', 0, 1, count))
+
+        # Write 8-bpp image headers for sizes under 256x256.
+        for size, image in self.images.items():
+            if size >= 256:
+                continue
+            ico.write(struct.pack('<BB', size, size))
+
+            # Calculate row sizes
+            xorsize = size
+            if xorsize % 4 != 0:
+                xorsize += 4 - (xorsize % 4)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + 256 * 4 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, 8, datasize, dataoffs))
+            dataoffs += datasize
+
+        # Write 24/32-bpp image headers.
+        for size, image in self.images.items():
+            if size > 256:
+                continue
+            elif size == 256:
+                ico.write(b'\0\0')
+            else:
+                ico.write(struct.pack('<BB', size, size))
+
+            # Calculate the size so we can write the offset within the file.
+            if image.hasAlpha():
+                bpp = 32
+                xorsize = size * 4
+            else:
+                bpp = 24
+                xorsize = size * 3 + (-(size * 3) & 3)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, datasize, dataoffs))
+            dataoffs += datasize
+
+        # Now write the actual icon bitmap data.
+        for size, image in self.images.items():
+            if size < 256:
+                self._write_bitmap(ico, image, size, 8)
+
+        for size, image in self.images.items():
+            if size <= 256:
+                bpp = 32 if image.hasAlpha() else 24
+                self._write_bitmap(ico, image, size, bpp)
+
+        assert ico.tell() == dataoffs
+        ico.close()
+
+        return True
+
+    def makeICNS(self, fn):
+        """ Writes the images to an Apple ICNS file.  Returns True on success. """
+
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+
+        icns = open(fn, 'wb')
+        icns.write(b'icns\0\0\0\0')
+
+        icon_types = {16: b'is32', 32: b'il32', 48: b'ih32', 128: b'it32'}
+        mask_types = {16: b's8mk', 32: b'l8mk', 48: b'h8mk', 128: b't8mk'}
+        png_types = {256: b'ic08', 512: b'ic09', 1024: b'ic10'}
+
+        pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
+
+        for size, image in sorted(self.images.items(), key=lambda item:item[0]):
+            if size in png_types and pngtype is not None:
+                stream = StringStream()
+                image.write(stream, "", pngtype)
+                pngdata = stream.data
+
+                icns.write(png_types[size])
+                icns.write(struct.pack('>I', len(pngdata)))
+                icns.write(pngdata)
+
+            elif size in icon_types:
+                # If it has an alpha channel, we write out a mask too.
+                if image.hasAlpha():
+                    icns.write(mask_types[size])
+                    icns.write(struct.pack('>I', size * size + 8))
+
+                    for y in range(size):
+                        for x in range(size):
+                            icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
+
+                icns.write(icon_types[size])
+                icns.write(struct.pack('>I', size * size * 4 + 8))
+
+                for y in range(size):
+                    for x in range(size):
+                        r, g, b = image.getXel(x, y)
+                        icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
+
+        length = icns.tell()
+        icns.seek(4)
+        icns.write(struct.pack('>I', length))
+        icns.close()
+
+        return True

+ 1 - 1
direct/src/distributed/AsyncRequest.py

@@ -15,7 +15,7 @@ if __debug__:
 
 class AsyncRequest(DirectObject):
     """
-    This class is used to make asynchronos reads and creates to a database.
+    This class is used to make asynchronous reads and creates to a database.
 
     You can create a list of self.neededObjects and then ask for each to be
     read or created, or if you only have one object that you need you can

+ 2 - 1
direct/src/distributed/DistributedObject.py

@@ -26,11 +26,12 @@ ESNum2Str = {
     ESGenerated: 'ESGenerated',
     }
 
+
 class DistributedObject(DistributedObjectBase):
     """
     The Distributed Object class is the base class for all network based
     (i.e. distributed) objects.  These will usually (always?) have a
-    dclass entry in a *.dc file.
+    dclass entry in a \\*.dc file.
     """
     notify = directNotify.newCategory("DistributedObject")
 

+ 2 - 1
direct/src/distributed/DistributedObjectBase.py

@@ -1,11 +1,12 @@
 from direct.showbase.DirectObject import DirectObject
 from direct.directnotify.DirectNotifyGlobal import directNotify
 
+
 class DistributedObjectBase(DirectObject):
     """
     The Distributed Object class is the base class for all network based
     (i.e. distributed) objects.  These will usually (always?) have a
-    dclass entry in a *.dc file.
+    dclass entry in a \\*.dc file.
     """
     notify = directNotify.newCategory("DistributedObjectBase")
 

+ 9 - 8
direct/src/distributed/DoCollectionManager.py

@@ -5,6 +5,7 @@ import re
 BAD_DO_ID = BAD_ZONE_ID = 0 # 0xFFFFFFFF
 BAD_CHANNEL_ID = 0 # 0xFFFFFFFFFFFFFFFF
 
+
 class DoCollectionManager:
     def __init__(self):
         # Dict of {DistributedObject ids: DistributedObjects}
@@ -186,7 +187,6 @@ class DoCollectionManager:
             strToReturn = '%s%s' % (strToReturn, self._returnObjects(self.getDoTable(ownerView=False)))
         return strToReturn
 
-
     def printObjectCount(self):
         # print object counts by distributed object type
         print('==== OBJECT COUNT ====')
@@ -199,13 +199,14 @@ class DoCollectionManager:
 
     def getDoList(self, parentId, zoneId=None, classType=None):
         """
-        parentId is any distributed object id.
-        zoneId is a uint32, defaults to None (all zones).  Try zone 2 if
-            you're not sure which zone to use (0 is a bad/null zone and
-            1 has had reserved use in the past as a no messages zone, while
-            2 has traditionally been a global, uber, misc stuff zone).
-        dclassType is a distributed class type filter, defaults
-            to None (no filter).
+        Args:
+            parentId: any distributed object id.
+            zoneId: a uint32, defaults to None (all zones).  Try zone 2 if
+                you're not sure which zone to use (0 is a bad/null zone and
+                1 has had reserved use in the past as a no messages zone, while
+                2 has traditionally been a global, uber, misc stuff zone).
+            dclassType: a distributed class type filter, defaults to None
+                (no filter).
 
         If dclassName is None then all objects in the zone are returned;
         otherwise the list is filtered to only include objects of that type.

+ 8 - 9
direct/src/distributed/DoHierarchy.py

@@ -26,15 +26,14 @@ class DoHierarchy:
 
     def getDoIds(self, getDo, parentId, zoneId=None, classType=None):
         """
-        Moved from DoCollectionManager
-        ==============================
-        parentId is any distributed object id.
-        zoneId is a uint32, defaults to None (all zones).  Try zone 2 if
-            you're not sure which zone to use (0 is a bad/null zone and
-            1 has had reserved use in the past as a no messages zone, while
-            2 has traditionally been a global, uber, misc stuff zone).
-        dclassType is a distributed class type filter, defaults
-            to None (no filter).
+        Args:
+            parentId: any distributed object id.
+            zoneId: a uint32, defaults to None (all zones).  Try zone 2 if
+                you're not sure which zone to use (0 is a bad/null zone and
+                1 has had reserved use in the past as a no messages zone, while
+                2 has traditionally been a global, uber, misc stuff zone).
+            dclassType: a distributed class type filter, defaults to None
+                (no filter).
 
         If dclassName is None then all objects in the zone are returned;
         otherwise the list is filtered to only include objects of that type.

+ 1 - 4
direct/src/distributed/PyDatagram.py

@@ -9,6 +9,7 @@ from panda3d.direct import *
 
 from direct.distributed.MsgTypes import *
 
+
 class PyDatagram(Datagram):
 
     # This is a little helper Dict to replace the huge <if> statement
@@ -29,8 +30,6 @@ class PyDatagram(Datagram):
         STBlob32: (Datagram.addBlob32, None),
         }
 
-    #def addChannel(self, channelId):
-    #    ...
     addChannel = Datagram.addUint64
 
     def addServerHeader(self, channel, sender, code):
@@ -39,14 +38,12 @@ class PyDatagram(Datagram):
         self.addChannel(sender)
         self.addUint16(code)
 
-
     def addOldServerHeader(self, channel, sender, code):
         self.addChannel(channel)
         self.addChannel(sender)
         self.addChannel('A')
         self.addUint16(code)
 
-
     def addServerControlHeader(self, code):
         self.addInt8(1)
         self.addChannel(CONTROL_CHANNEL)

+ 1 - 0
direct/src/distributed/PyDatagramIterator.py

@@ -7,6 +7,7 @@ from panda3d.core import *
 from panda3d.direct import *
 # Import the type numbers
 
+
 class PyDatagramIterator(DatagramIterator):
 
     # This is a little helper Dict to replace the huge <if> statement

+ 1 - 1
direct/src/distributed/ServerRepository.py

@@ -137,7 +137,7 @@ class ServerRepository:
 
         # An allocator object that assigns the next doIdBase to each
         # client.
-        self.idAllocator = UniqueIdAllocator(0, 0xffffffff / self.doIdRange)
+        self.idAllocator = UniqueIdAllocator(0, 0xffffffff // self.doIdRange)
 
         self.dcFile = DCFile()
         self.dcSuffix = ''

+ 4 - 3
direct/src/distributed/cConnectionRepository.cxx

@@ -26,6 +26,7 @@
 
 #ifdef HAVE_PYTHON
 #include "py_panda.h"
+#include "dcClass_ext.h"
 #endif
 
 using std::endl;
@@ -736,7 +737,7 @@ handle_update_field() {
       // get into trouble if it tried to delete the object from the doId2do
       // map.
       Py_INCREF(distobj);
-      dclass->receive_update(distobj, _di);
+      invoke_extension(dclass).receive_update(distobj, _di);
       Py_DECREF(distobj);
 
       if (PyErr_Occurred()) {
@@ -820,7 +821,7 @@ handle_update_field_owner() {
         // make a copy of the datagram iterator so that we can use the main
         // iterator for the non-owner update
         DatagramIterator _odi(_di);
-        dclass->receive_update(distobjOV, _odi);
+        invoke_extension(dclass).receive_update(distobjOV, _odi);
         Py_DECREF(distobjOV);
 
         if (PyErr_Occurred()) {
@@ -861,7 +862,7 @@ handle_update_field_owner() {
         // get into trouble if it tried to delete the object from the doId2do
         // map.
         Py_INCREF(distobj);
-        dclass->receive_update(distobj, _di);
+        invoke_extension(dclass).receive_update(distobj, _di);
         Py_DECREF(distobj);
 
         if (PyErr_Occurred()) {

+ 1 - 1
direct/src/distributed/cConnectionRepository.h

@@ -53,7 +53,7 @@ class SocketStream;
  * the C++ layer, while server messages that are not understood by the C++
  * layer are returned up to the Python layer for processing.
  */
-class EXPCL_DIRECT_DISTRIBUTED CConnectionRepository {
+class CConnectionRepository {
 PUBLISHED:
   explicit CConnectionRepository(bool has_owner_view = false,
                                  bool threaded_net = false);

+ 4 - 0
direct/src/distributed/cDistributedSmoothNodeBase.cxx

@@ -18,6 +18,10 @@
 #include "dcmsgtypes.h"
 #include "config_distributed.h"
 
+#ifdef HAVE_PYTHON
+#include "py_panda.h"
+#endif
+
 static const PN_stdfloat smooth_node_epsilon = 0.01;
 static const double network_time_precision = 100.0;  // Matches ClockDelta.py
 

+ 1 - 2
direct/src/distributed/cDistributedSmoothNodeBase.h

@@ -18,7 +18,6 @@
 #include "nodePath.h"
 #include "dcbase.h"
 #include "dcPacker.h"
-#include "dcPython.h"  // to pick up Python.h
 #include "clockObject.h"
 
 class DCClass;
@@ -28,7 +27,7 @@ class CConnectionRepository;
  * This class defines some basic methods of DistributedSmoothNodeBase which
  * have been moved into C++ as a performance optimization.
  */
-class EXPCL_DIRECT_DISTRIBUTED CDistributedSmoothNodeBase {
+class CDistributedSmoothNodeBase {
 PUBLISHED:
   CDistributedSmoothNodeBase();
   ~CDistributedSmoothNodeBase();

+ 4 - 4
direct/src/distributed/config_distributed.h

@@ -23,10 +23,10 @@
 
 NotifyCategoryDecl(distributed, EXPCL_DIRECT_DISTRIBUTED, EXPTP_DIRECT_DISTRIBUTED);
 
-extern ConfigVariableInt game_server_timeout_ms;
-extern ConfigVariableDouble min_lag;
-extern ConfigVariableDouble max_lag;
-extern ConfigVariableBool handle_datagrams_internally;
+extern EXPCL_DIRECT_DISTRIBUTED ConfigVariableInt game_server_timeout_ms;
+extern EXPCL_DIRECT_DISTRIBUTED ConfigVariableDouble min_lag;
+extern EXPCL_DIRECT_DISTRIBUTED ConfigVariableDouble max_lag;
+extern EXPCL_DIRECT_DISTRIBUTED ConfigVariableBool handle_datagrams_internally;
 
 extern EXPCL_DIRECT_DISTRIBUTED void init_libdistributed();
 

+ 13 - 12
direct/src/fsm/ClassicFSM.py

@@ -1,9 +1,8 @@
 """Finite State Machine module: contains the ClassicFSM class.
 
-.. note::
-
-   This module and class exist only for backward compatibility with
-   existing code.  New code should use the :mod:`.FSM` module instead.
+Note:
+    This module and class exist only for backward compatibility with
+    existing code.  New code should use the :mod:`.FSM` module instead.
 """
 
 __all__ = ['ClassicFSM']
@@ -14,12 +13,14 @@ import weakref
 
 if __debug__:
     _debugFsms = {}
+
     def printDebugFsmList():
         global _debugFsms
         for k in sorted(_debugFsms.keys()):
             print("%s %s" % (k, _debugFsms[k]()))
     __builtins__['debugFsmList'] = printDebugFsmList
 
+
 class ClassicFSM(DirectObject):
     """
     Finite State Machine class.
@@ -45,14 +46,14 @@ class ClassicFSM(DirectObject):
         """__init__(self, string, State[], string, string, int)
 
         ClassicFSM constructor: takes name, list of states, initial state and
-        final state as:
-
-        fsm = ClassicFSM.ClassicFSM('stopLight',
-          [State.State('red', enterRed, exitRed, ['green']),
-            State.State('yellow', enterYellow, exitYellow, ['red']),
-            State.State('green', enterGreen, exitGreen, ['yellow'])],
-          'red',
-          'red')
+        final state as::
+
+            fsm = ClassicFSM.ClassicFSM('stopLight',
+              [State.State('red', enterRed, exitRed, ['green']),
+                State.State('yellow', enterYellow, exitYellow, ['red']),
+                State.State('green', enterGreen, exitGreen, ['yellow'])],
+              'red',
+              'red')
 
         each state's last argument, a list of allowed state transitions,
         is optional; if left out (or explicitly specified to be

+ 37 - 33
direct/src/fsm/FSM.py

@@ -1,5 +1,8 @@
 """The new Finite State Machine module. This replaces the module
 previously called FSM (now called :mod:`.ClassicFSM`).
+
+For more information on FSMs, consult the :ref:`finite-state-machines` section
+of the programming manual.
 """
 
 __all__ = ['FSMException', 'FSM']
@@ -14,12 +17,15 @@ from direct.stdpy.threading import RLock
 class FSMException(Exception):
     pass
 
+
 class AlreadyInTransition(FSMException):
     pass
 
+
 class RequestDenied(FSMException):
     pass
 
+
 class FSM(DirectObject):
     """
     A Finite State Machine.  This is intended to be the base class
@@ -34,25 +40,25 @@ class FSM(DirectObject):
 
     To define specialized behavior when entering or exiting a
     particular state, define a method named enterState() and/or
-    exitState(), where "State" is the name of the state, e.g.:
+    exitState(), where "State" is the name of the state, e.g.::
 
-    def enterRed(self):
-        ... do stuff ...
+        def enterRed(self):
+            ... do stuff ...
 
-    def exitRed(self):
-        ... cleanup stuff ...
+        def exitRed(self):
+            ... cleanup stuff ...
 
-    def enterYellow(self):
-        ... do stuff ...
+        def enterYellow(self):
+            ... do stuff ...
 
-    def exitYellow(self):
-        ... cleanup stuff ...
+        def exitYellow(self):
+            ... cleanup stuff ...
 
-    def enterGreen(self):
-        ... do stuff ...
+        def enterGreen(self):
+            ... do stuff ...
 
-    def exitGreen(self):
-        ... cleanup stuff ...
+        def exitGreen(self):
+            ... cleanup stuff ...
 
     Both functions can access the previous state name as
     self.oldState, and the new state name we are transitioning to as
@@ -70,22 +76,22 @@ class FSM(DirectObject):
     input is always a string and a tuple of optional parameters (which
     is often empty), and the return value should either be None to do
     nothing, or the name of the state to transition into.  For
-    example:
+    example::
 
-    def filterRed(self, request, args):
-        if request in ['Green']:
-            return (request,) + args
-        return None
+        def filterRed(self, request, args):
+            if request in ['Green']:
+                return (request,) + args
+            return None
 
-    def filterYellow(self, request, args):
-        if request in ['Red']:
-            return (request,) + args
-        return None
+        def filterYellow(self, request, args):
+            if request in ['Red']:
+                return (request,) + args
+            return None
 
-    def filterGreen(self, request, args):
-        if request in ['Yellow']:
-            return (request,) + args
-        return None
+        def filterGreen(self, request, args):
+            if request in ['Yellow']:
+                return (request,) + args
+            return None
 
     As above, the filterState() functions are optional.  If any is
     omitted, the defaultFilter() method is called instead.  A standard
@@ -111,7 +117,7 @@ class FSM(DirectObject):
     at construction time; it is simply in Off already by convention.
     If you need to call code in enterOff() to initialize your FSM
     properly, call it explicitly in the constructor.  Similarly, when
-    cleanup() is called or the FSM is destructed, the FSM transitions
+    `cleanup()` is called or the FSM is destructed, the FSM transitions
     back to 'Off' by convention.  (It does call enterOff() at this
     point, but does not call exitOff().)
 
@@ -255,9 +261,9 @@ class FSM(DirectObject):
     def demand(self, request, *args):
         """Requests a state transition, by code that does not expect
         the request to be denied.  If the request is denied, raises a
-        RequestDenied exception.
+        `RequestDenied` exception.
 
-        Unlike request(), this method allows a new request to be made
+        Unlike `request()`, this method allows a new request to be made
         while the FSM is currently in transition.  In this case, the
         request is queued up and will be executed when the current
         transition finishes.  Multiple requests will queue up in
@@ -284,7 +290,7 @@ class FSM(DirectObject):
         """Requests a state transition (or other behavior).  The
         request may be denied by the FSM's filter function.  If it is
         denied, the filter function may either raise an exception
-        (RequestDenied), or it may simply return None, without
+        (`RequestDenied`), or it may simply return None, without
         changing the FSM's state.
 
         The request parameter should be a string.  The request, along
@@ -299,7 +305,7 @@ class FSM(DirectObject):
 
         If the FSM is currently in transition (i.e. in the middle of
         executing an enterState or exitState function), an
-        AlreadyInTransition exception is raised (but see demand(),
+        `AlreadyInTransition` exception is raised (but see `demand()`,
         which will queue these requests up and apply when the
         transition is complete)."""
 
@@ -395,7 +401,6 @@ class FSM(DirectObject):
             return (request,) + args
         return self.defaultFilter(request, args)
 
-
     def setStateArray(self, stateArray):
         """array of unique states to iterate through"""
         self.fsmLock.acquire()
@@ -404,7 +409,6 @@ class FSM(DirectObject):
         finally:
             self.fsmLock.release()
 
-
     def requestNext(self, *args):
         """Request the 'next' state in the predefined state array."""
         self.fsmLock.acquire()

+ 3 - 6
direct/src/fsm/FourState.py

@@ -44,25 +44,22 @@ class FourState:
 
     def __init__(self, names, durations = [0, 1, None, 1, 1]):
         """
-        names is a list of state names
+        Names is a list of state names.  Some examples are::
 
-        E.g.
             ['off', 'opening', 'open', 'closing', 'closed',]
 
-        e.g. 2:
             ['off', 'locking', 'locked', 'unlocking', 'unlocked',]
 
-        e.g. 3:
             ['off', 'deactivating', 'deactive', 'activating', 'activated',]
 
         durations is a list of time values (floats) or None values.
 
         Each list must have five entries.
 
-        More Details
+        .. rubric:: More Details
 
         Here is a diagram showing the where the names from the list
-        are used:
+        are used::
 
             +---------+
             | 0 (off) |----> (any other state and vice versa).

+ 12 - 14
direct/src/fsm/FourStateAI.py

@@ -45,27 +45,25 @@ class FourStateAI:
 
     def __init__(self, names, durations = [0, 1, None, 1, 1]):
         """
-        names is a list of state names
-            E.g.
-                ['off', 'opening', 'open', 'closing', 'closed',]
+        Names is a list of state names.  Some examples are::
 
-            e.g. 2:
-                ['off', 'locking', 'locked', 'unlocking', 'unlocked',]
+            ['off', 'opening', 'open', 'closing', 'closed',]
 
-            e.g. 3:
-                ['off', 'deactivating', 'deactive', 'activating', 'activated',]
+            ['off', 'locking', 'locked', 'unlocking', 'unlocked',]
+
+            ['off', 'deactivating', 'deactive', 'activating', 'activated',]
 
         durations is a list of durations in seconds or None values.
-            The list of duration values should be the same length
-            as the list of state names and the lists correspond.
-            For each state, after n seconds, the ClassicFSM will move to
-            the next state.  That does not happen for any duration
-            values of None.
+        The list of duration values should be the same length
+        as the list of state names and the lists correspond.
+        For each state, after n seconds, the ClassicFSM will move to
+        the next state.  That does not happen for any duration
+        values of None.
 
-        More Details
+        .. rubric:: More Details
 
         Here is a diagram showing the where the names from the list
-        are used:
+        are used::
 
             +---------+
             | 0 (off) |----> (any other state and vice versa).

+ 3 - 0
direct/src/fsm/__init__.py

@@ -3,4 +3,7 @@ This package contains implementations of a Finite State Machine, an
 abstract construct that holds a particular state and can transition
 between several defined states.  These are useful for a range of logic
 programming tasks.
+
+For more information on FSMs, consult the :ref:`finite-state-machines` section
+of the programming manual.
 """

+ 5 - 1
direct/src/gui/DirectButton.py

@@ -1,4 +1,8 @@
-"""This module contains the DirectButton class."""
+"""This module contains the DirectButton class.
+
+See the :ref:`directbutton` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectButton']
 

+ 5 - 1
direct/src/gui/DirectCheckButton.py

@@ -1,6 +1,10 @@
 """A DirectCheckButton is a type of button that toggles between two states
 when clicked.  It also has a separate indicator that can be modified
-separately."""
+separately.
+
+See the :ref:`directcheckbutton` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectCheckButton']
 

+ 50 - 45
direct/src/gui/DirectDialog.py

@@ -1,16 +1,24 @@
-"""This module defines various dialog windows for the DirectGUI system."""
+"""This module defines various dialog windows for the DirectGUI system.
 
-__all__ = ['findDialog', 'cleanupDialog', 'DirectDialog', 'OkDialog', 'OkCancelDialog', 'YesNoDialog', 'YesNoCancelDialog', 'RetryCancelDialog']
+See the :ref:`directdialog` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
+
+__all__ = [
+    'findDialog', 'cleanupDialog', 'DirectDialog', 'OkDialog',
+    'OkCancelDialog', 'YesNoDialog', 'YesNoCancelDialog', 'RetryCancelDialog',
+]
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from .DirectFrame import *
 from .DirectButton import *
 import types
 
-def findDialog(uniqueName):
-    """findPanel(string uniqueName)
 
+def findDialog(uniqueName):
+    """
     Returns the panel whose uniqueName is given.  This is mainly
     useful for debugging, to get a pointer to the current onscreen
     panel of a particular type.
@@ -19,6 +27,7 @@ def findDialog(uniqueName):
         return DirectDialog.AllDialogs[uniqueName]
     return None
 
+
 def cleanupDialog(uniqueName):
     """cleanupPanel(string uniqueName)
 
@@ -34,52 +43,48 @@ def cleanupDialog(uniqueName):
         # self.cleanup() directly
         DirectDialog.AllDialogs[uniqueName].cleanup()
 
+
 class DirectDialog(DirectFrame):
 
     AllDialogs = {}
     PanelIndex = 0
 
-    def __init__(self, parent = None, **kw):
-        """
-        DirectDialog(kw)
-
-        Creates a popup dialog to alert and/or interact with user.
+    def __init__(self, parent=None, **kw):
+        """Creates a popup dialog to alert and/or interact with user.
         Some of the main keywords that can be used to customize the dialog:
-            Keyword              Definition
-            -------              ----------
-            text                 Text message/query displayed to user
-            geom                 Geometry to be displayed in dialog
-            buttonTextList       List of text to show on each button
-            buttonGeomList       List of geometry to show on each button
-            buttonImageList      List of images to show on each button
-            buttonValueList      List of values sent to dialog command for
-                                 each button.  If value is [] then the
-                                 ordinal rank of the button is used as
-                                 its value
-            buttonHotKeyList     List of hotkeys to bind to each button.
-                                 Typing hotkey is equivalent to pressing
-                                 the corresponding button.
-            suppressKeys         Set to true if you wish to suppress keys
-                                 (i.e. Dialog eats key event), false if
-                                 you wish Dialog to pass along key event
-            buttonSize           4-tuple used to specify custom size for
-                                 each button (to make bigger then geom/text
-                                 for example)
-            pad                  Space between border and interior graphics
-            topPad               Extra space added above text/geom/image
-            midPad               Extra space added between text/buttons
-            sidePad              Extra space added to either side of
-                                 text/buttons
-            buttonPadSF          Scale factor used to expand/contract
-                                 button horizontal spacing
-            command              Callback command used when a button is
-                                 pressed.  Value supplied to command
-                                 depends on values in buttonValueList
-
-         Note: Number of buttons on the dialog depends upon the maximum
-               length of any button[Text|Geom|Image|Value]List specified.
-               Values of None are substituted for lists that are shorter
-               than the max length
+
+        Parameters:
+            text (str): Text message/query displayed to user
+            geom: Geometry to be displayed in dialog
+            buttonTextList: List of text to show on each button
+            buttonGeomList: List of geometry to show on each button
+            buttonImageList: List of images to show on each button
+            buttonValueList: List of values sent to dialog command for
+                each button.  If value is [] then the ordinal rank of
+                the button is used as its value.
+            buttonHotKeyList: List of hotkeys to bind to each button.
+                Typing the hotkey is equivalent to pressing the
+                corresponding button.
+            suppressKeys: Set to true if you wish to suppress keys
+                (i.e. Dialog eats key event), false if you wish Dialog
+                to pass along key event.
+            buttonSize: 4-tuple used to specify custom size for each
+                button (to make bigger then geom/text for example)
+            pad: Space between border and interior graphics
+            topPad: Extra space added above text/geom/image
+            midPad: Extra space added between text/buttons
+            sidePad: Extra space added to either side of text/buttons
+            buttonPadSF: Scale factor used to expand/contract button
+                horizontal spacing
+            command: Callback command used when a button is pressed.
+                Value supplied to command depends on values in
+                buttonValueList.
+
+        Note:
+            The number of buttons on the dialog depends on the maximum
+            length of any button[Text|Geom|Image|Value]List specified.
+            Values of None are substituted for lists that are shorter
+            than the max length
          """
 
         # Inherits from DirectFrame
@@ -207,7 +212,7 @@ class DirectDialog(DirectFrame):
             image = None
         # Get size of text/geom without image (for state 0)
         if image:
-            image.reparentTo(hidden)
+            image.reparentTo(ShowBaseGlobal.hidden)
         bounds = self.stateNodePath[0].getTightBounds()
         if image:
             image.reparentTo(self.stateNodePath[0])

+ 14 - 8
direct/src/gui/DirectEntry.py

@@ -1,9 +1,14 @@
 """Contains the DirectEntry class, a type of DirectGUI widget that accepts
-text entered using the keyboard."""
+text entered using the keyboard.
+
+See the :ref:`directentry` page in the programming manual for a more in-depth
+explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectEntry']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from .DirectFrame import *
 from .OnscreenText import OnscreenText
@@ -94,7 +99,7 @@ class DirectEntry(DirectFrame):
         self.onscreenText = self.createcomponent(
             'text', (), None,
             OnscreenText,
-            (), parent = hidden,
+            (), parent = ShowBaseGlobal.hidden,
             # Pass in empty text to avoid extra work, since its really
             # The PGEntry which will use the TextNode to generate geometry
             text = '',
@@ -215,11 +220,11 @@ class DirectEntry(DirectFrame):
         self._autoCapitalize()
 
     def _autoCapitalize(self):
-        name = self.get().decode('utf-8')
+        name = self.guiItem.getWtext()
         # capitalize each word, allowing for things like McMutton
-        capName = ''
+        capName = u''
         # track each individual word to detect prefixes like Mc
-        wordSoFar = ''
+        wordSoFar = u''
         # track whether the previous character was part of a word or not
         wasNonWordChar = True
         for i, character in enumerate(name):
@@ -228,9 +233,9 @@ class DirectEntry(DirectFrame):
             #   This assumes that string.lower and string.upper will return different
             #   values for all unicode letters.
             # - Don't count apostrophes as a break between words
-            if character.lower() == character.upper() and character != "'":
+            if character.lower() == character.upper() and character != u"'":
                 # we are between words
-                wordSoFar = ''
+                wordSoFar = u''
                 wasNonWordChar = True
             else:
                 capitalize = False
@@ -254,7 +259,8 @@ class DirectEntry(DirectFrame):
                 wordSoFar += character
                 wasNonWordChar = False
             capName += character
-        self.enterText(capName.encode('utf-8'))
+        self.guiItem.setWtext(capName)
+        self.guiItem.setCursorPosition(self.guiItem.getNumCharacters())
 
     def focusOutCommandFunc(self):
         if self['focusOutCommand']:

+ 25 - 3
direct/src/gui/DirectEntryScroll.py

@@ -29,19 +29,41 @@ class DirectEntryScroll(DirectFrame):
            # frameSize = (-0.006, 3.2, -0.015, 0.036),
         # if you need to scale the entry scale it's parent instead
 
-        self.entry = entry
         self.canvas = NodePath(self.guiItem.getCanvasNode())
-        self.entry.reparentTo(self.canvas)
         self.canvas.setPos(0,0,0)
 
-        self.entry.bind(DGG.CURSORMOVE,self.cursorMove)
+        self.entry = None
+        if entry is not None:
+            self.entry = entry
+            self.entry.reparentTo(self.canvas)
+            self.entry.bind(DGG.CURSORMOVE, self.cursorMove)
 
         self.canvas.node().setBounds(OmniBoundingVolume())
         self.canvas.node().setFinal(1)
         self.resetCanvas()
 
+    def setEntry(self, entry):
+        """
+        Sets a DirectEntry element for this scroll frame. A DirectEntryScroll
+        can only hold one entry at a time, so make sure to not call this
+        function twice or call clearEntry before to make sure no entry
+        is already set.
+        """
+        assert self.entry is None, "An entry was already set for this DirectEntryScroll element"
+        self.entry = entry
+        self.entry.reparentTo(self.canvas)
 
+        self.entry.bind(DGG.CURSORMOVE, self.cursorMove)
 
+    def clearEntry(self):
+        """
+        detaches and unbinds the entry from the scroll frame and its
+        events. You'll be responsible for destroying it.
+        """
+        if self.entry is None: return
+        self.entry.unbind(DGG.CURSORMOVE)
+        self.entry.detachNode()
+        self.entry = None
 
     def cursorMove(self, cursorX, cursorY):
         cursorX = self.entry.guiItem.getCursorX() * self.entry['text_scale'][0]

+ 77 - 128
direct/src/gui/DirectFrame.py

@@ -11,6 +11,9 @@ A DirectFrame can have:
 Each of these has 1 or more states.  The same object can be used for
 all states or each state can have a different text/geom/image (for
 radio button and check button indicators, for example).
+
+See the :ref:`directframe` page in the programming manual for a more in-depth
+explanation and an example of how to use this class.
 """
 
 __all__ = ['DirectFrame']
@@ -20,6 +23,7 @@ from . import DirectGuiGlobals as DGG
 from .DirectGuiBase import *
 from .OnscreenImage import OnscreenImage
 from .OnscreenGeom import OnscreenGeom
+from .OnscreenText import OnscreenText
 import sys
 
 if sys.version_info >= (3, 0):
@@ -30,7 +34,8 @@ else:
 
 class DirectFrame(DirectGuiWidget):
     DefDynGroups = ('text', 'geom', 'image')
-    def __init__(self, parent = None, **kw):
+
+    def __init__(self, parent=None, **kw):
         # Inherits from DirectGuiWidget
         optiondefs = (
             # Define type of DirectGuiWidget
@@ -47,10 +52,10 @@ class DirectFrame(DirectGuiWidget):
             # Change default value of text mayChange flag from 0
             # (OnscreenText.py) to 1
             ('textMayChange',  1,          None),
-            )
+        )
         # Merge keyword options with default options
         self.defineoptions(kw, optiondefs,
-                           dynamicGroups = DirectFrame.DefDynGroups)
+                           dynamicGroups=DirectFrame.DefDynGroups)
 
         # Initialize superclasses
         DirectGuiWidget.__init__(self, parent)
@@ -58,8 +63,38 @@ class DirectFrame(DirectGuiWidget):
         # Call option initialization functions
         self.initialiseoptions(DirectFrame)
 
-    def destroy(self):
-        DirectGuiWidget.destroy(self)
+    def __reinitComponent(self, name, component_class, states, **kwargs):
+        """Recreates the given component using the given keyword args."""
+        assert name in ("geom", "image", "text")
+
+        # constants should be local to or default arguments of constructors
+        for c in range(self['numStates']):
+            component_name = name + str(c)
+
+            try:
+                state = states[c]
+            except IndexError:
+                state = states[-1]
+
+            if self.hascomponent(component_name):
+                if state is None:
+                    self.destroycomponent(component_name)
+                else:
+                    self[component_name + "_" + name] = state
+            else:
+                if state is None:
+                    return
+
+                kwargs[name] = state
+                self.createcomponent(
+                    component_name,
+                    (),
+                    name,
+                    component_class,
+                    (),
+                    parent=self.stateNodePath[c],
+                    **kwargs
+                )
 
     def clearText(self):
         self['text'] = None
@@ -67,45 +102,18 @@ class DirectFrame(DirectGuiWidget):
 
     def setText(self, text=None):
         if text is not None:
-            self['text'] = text
-
-        # Determine if user passed in single string or a sequence
-        if self['text'] == None:
-            textList = (None,) * self['numStates']
-        elif isinstance(self['text'], stringType):
-            # If just passing in a single string, make a tuple out of it
-            textList = (self['text'],) * self['numStates']
+            self["text"] = text
+
+        text = self["text"]
+        if text is None or isinstance(text, stringType):
+            text_list = (text,) * self['numStates']
         else:
-            # Otherwise, hope that the user has passed in a tuple/list
-            textList = self['text']
-        # Create/destroy components
-        for i in range(self['numStates']):
-            component = 'text' + repr(i)
-            # If fewer items specified than numStates,
-            # just repeat last item
-            try:
-                text = textList[i]
-            except IndexError:
-                text = textList[-1]
+            text_list = text
 
-            if self.hascomponent(component):
-                if text == None:
-                    # Destroy component
-                    self.destroycomponent(component)
-                else:
-                    self[component + '_text'] = text
-            else:
-                if text == None:
-                    return
-                else:
-                    from .OnscreenText import OnscreenText
-                    self.createcomponent(
-                        component, (), 'text',
-                        OnscreenText,
-                        (), parent = self.stateNodePath[i],
-                        text = text, scale = 1, mayChange = self['textMayChange'],
-                        sort = DGG.TEXT_SORT_INDEX,
-                        )
+        self.__reinitComponent("text", OnscreenText, text_list,
+            scale=1,
+            mayChange=self['textMayChange'],
+            sort=DGG.TEXT_SORT_INDEX)
 
     def clearGeom(self):
         self['geom'] = None
@@ -113,48 +121,19 @@ class DirectFrame(DirectGuiWidget):
 
     def setGeom(self, geom=None):
         if geom is not None:
-            self['geom'] = geom
-
-        # Determine argument type
-        geom = self['geom']
-
-        if geom == None:
-            # Passed in None
-            geomList = (None,) * self['numStates']
-        elif isinstance(geom, NodePath) or \
-             isinstance(geom, stringType):
-            # Passed in a single node path, make a tuple out of it
-            geomList = (geom,) * self['numStates']
+            self["geom"] = geom
+
+        geom = self["geom"]
+        if geom is None or \
+           isinstance(geom, NodePath) or \
+           isinstance(geom, stringType):
+            geom_list = (geom,) * self['numStates']
         else:
-            # Otherwise, hope that the user has passed in a tuple/list
-            geomList = geom
-
-        # Create/destroy components
-        for i in range(self['numStates']):
-            component = 'geom' + repr(i)
-            # If fewer items specified than numStates,
-            # just repeat last item
-            try:
-                geom = geomList[i]
-            except IndexError:
-                geom = geomList[-1]
+            geom_list = geom
 
-            if self.hascomponent(component):
-                if geom == None:
-                    # Destroy component
-                    self.destroycomponent(component)
-                else:
-                    self[component + '_geom'] = geom
-            else:
-                if geom == None:
-                    return
-                else:
-                    self.createcomponent(
-                        component, (), 'geom',
-                        OnscreenGeom,
-                        (), parent = self.stateNodePath[i],
-                        geom = geom, scale = 1,
-                        sort = DGG.GEOM_SORT_INDEX)
+        self.__reinitComponent("geom", OnscreenGeom, geom_list,
+            scale=1,
+            sort=DGG.GEOM_SORT_INDEX)
 
     def clearImage(self):
         self['image'] = None
@@ -162,51 +141,21 @@ class DirectFrame(DirectGuiWidget):
 
     def setImage(self, image=None):
         if image is not None:
-            self['image'] = image
-
-        # Determine argument type
-        arg = self['image']
-        if arg == None:
-            # Passed in None
-            imageList = (None,) * self['numStates']
-        elif isinstance(arg, NodePath) or \
-             isinstance(arg, Texture) or \
-             isinstance(arg, stringType):
-            # Passed in a single node path, make a tuple out of it
-            imageList = (arg,) * self['numStates']
+            self["image"] = image
+
+        image = self["image"]
+        if image is None or \
+           isinstance(image, NodePath) or \
+           isinstance(image, Texture) or \
+           isinstance(image, stringType) or \
+           isinstance(image, Filename) or \
+           (len(image) == 2 and \
+            isinstance(image[0], stringType) and \
+            isinstance(image[1], stringType)):
+            image_list = (image,) * self['numStates']
         else:
-            # Otherwise, hope that the user has passed in a tuple/list
-            if ((len(arg) == 2) and
-                isinstance(arg[0], stringType) and
-                isinstance(arg[1], stringType)):
-                # Its a model/node pair of strings
-                imageList = (arg,) * self['numStates']
-            else:
-                # Assume its a list of node paths
-                imageList = arg
-        # Create/destroy components
-        for i in range(self['numStates']):
-            component = 'image' + repr(i)
-            # If fewer items specified than numStates,
-            # just repeat last item
-            try:
-                image = imageList[i]
-            except IndexError:
-                image = imageList[-1]
+            image_list = image
 
-            if self.hascomponent(component):
-                if image == None:
-                    # Destroy component
-                    self.destroycomponent(component)
-                else:
-                    self[component + '_image'] = image
-            else:
-                if image == None:
-                    return
-                else:
-                    self.createcomponent(
-                        component, (), 'image',
-                        OnscreenImage,
-                        (), parent = self.stateNodePath[i],
-                        image = image, scale = 1,
-                        sort = DGG.IMAGE_SORT_INDEX)
+        self.__reinitComponent("image", OnscreenImage, image_list,
+            scale=1,
+            sort=DGG.IMAGE_SORT_INDEX)

+ 1 - 1
direct/src/gui/DirectGui.py

@@ -1,4 +1,4 @@
-""" Imports all of the DirectGUI classes. """
+"""Imports all of the :ref:`directgui` classes."""
 
 from . import DirectGuiGlobals as DGG
 from .OnscreenText import *

+ 47 - 39
direct/src/gui/DirectGuiBase.py

@@ -1,49 +1,57 @@
 """
-Base class for all Direct Gui items.  Handles composite widgets and
+Base class for all DirectGui items.  Handles composite widgets and
 command line argument parsing.
 
-Code Overview:
+Code overview:
 
-1   Each widget defines a set of options (optiondefs) as a list of tuples
-    of the form ('name', defaultValue, handler).
+1)  Each widget defines a set of options (optiondefs) as a list of tuples
+    of the form ``('name', defaultValue, handler)``.
     'name' is the name of the option (used during construction of configure)
-    handler can be: None, method, or INITOPT.  If a method is specified,
-    it will be called during widget construction (via initialiseoptions),
-    if the Handler is specified as an INITOPT, this is an option that can
-    only be set during widget construction.
+    handler can be: None, method, or INITOPT.  If a method is specified, it
+    will be called during widget construction (via initialiseoptions), if the
+    Handler is specified as an INITOPT, this is an option that can only be set
+    during widget construction.
 
-2)  DirectGuiBase.defineoptions is called.  defineoption creates:
+2)  :func:`~DirectGuiBase.defineoptions` is called.  defineoption creates:
 
     self._constructorKeywords = { keyword: [value, useFlag] }
-    a dictionary of the keyword options specified as part of the constructor
-    keywords can be of the form 'component_option', where component is
-    the name of a widget's component, a component group or a component alias
-
-    self._dynamicGroups, a list of group names for which it is permissible
-    to specify options before components of that group are created.
-    If a widget is a derived class the order of execution would be:
-    foo.optiondefs = {}
-    foo.defineoptions()
-      fooParent()
-         fooParent.optiondefs = {}
-         fooParent.defineoptions()
-
-3)  addoptions is called.  This combines options specified as keywords to
-    the widget constructor (stored in self._constuctorKeywords)
-    with the default options (stored in optiondefs).  Results are stored in
-    self._optionInfo = { keyword: [default, current, handler] }
+        A dictionary of the keyword options specified as part of the
+        constructor keywords can be of the form 'component_option', where
+        component is the name of a widget's component, a component group or a
+        component alias.
+
+    self._dynamicGroups
+        A list of group names for which it is permissible to specify options
+        before components of that group are created.
+        If a widget is a derived class the order of execution would be::
+
+          foo.optiondefs = {}
+          foo.defineoptions()
+            fooParent()
+               fooParent.optiondefs = {}
+               fooParent.defineoptions()
+
+3)  :func:`~DirectGuiBase.addoptions` is called.  This combines options
+    specified as keywords to the widget constructor (stored in
+    self._constructorKeywords) with the default options (stored in optiondefs).
+    Results are stored in
+    ``self._optionInfo = { keyword: [default, current, handler] }``.
     If a keyword is of the form 'component_option' it is left in the
     self._constructorKeywords dictionary (for use by component constructors),
     otherwise it is 'used', and deleted from self._constructorKeywords.
-    Notes: - constructor keywords override the defaults.
-           - derived class default values override parent class defaults
-           - derived class handler functions override parent class functions
+
+    Notes:
+
+    - constructor keywords override the defaults.
+    - derived class default values override parent class defaults
+    - derived class handler functions override parent class functions
 
 4)  Superclass initialization methods are called (resulting in nested calls
     to define options (see 2 above)
 
-5)  Widget components are created via calls to self.createcomponent.
-    User can specify aliases and groups for each component created.
+5)  Widget components are created via calls to
+    :func:`~DirectGuiBase.createcomponent`.  User can specify aliases and groups
+    for each component created.
 
     Aliases are alternate names for components, e.g. a widget may have a
     component with a name 'entryField', which itself may have a component
@@ -55,8 +63,8 @@ Code Overview:
     Groups allow option specifications that apply to all members of the group.
     If a widget has components: 'text1', 'text2', and 'text3' which all belong
     to the 'text' group, they can be all configured with keywords of the form:
-    'text_keyword' (e.g. text_font = 'comic.rgb').  A component's group
-    is stored as the fourth element of its entry in self.__componentInfo
+    'text_keyword' (e.g. ``text_font='comic.rgb'``).  A component's group
+    is stored as the fourth element of its entry in self.__componentInfo.
 
     Note: the widget constructors have access to all remaining keywords in
     _constructorKeywords (those not transferred to _optionInfo by
@@ -71,9 +79,9 @@ Code Overview:
     component.  If any constructor keywords remain at the end of component
     construction (and initialisation), an error is raised.
 
-5)  initialiseoptions is called.  This method calls any option handlers to
-    respond to any keyword/default values, then checks to see if any keywords
-    are left unused.  If so, an error is raised.
+5)  :func:`~DirectGuiBase.initialiseoptions` is called.  This method calls any
+    option handlers to respond to any keyword/default values, then checks to
+    see if any keywords are left unused.  If so, an error is raised.
 """
 
 __all__ = ['DirectGuiBase', 'DirectGuiWidget']
@@ -634,7 +642,7 @@ class DirectGuiBase(DirectObject.DirectObject):
         """
         # Need to tack on gui item specific id
         gEvent = event + self.guiId
-        if ShowBase.config.GetBool('debug-directgui-msgs', False):
+        if ShowBaseGlobal.config.GetBool('debug-directgui-msgs', False):
             from direct.showbase.PythonUtil import StackTrace
             print(gEvent)
             print(StackTrace())
@@ -663,7 +671,7 @@ class DirectGuiWidget(DirectGuiBase, NodePath):
     # Determine the default initial state for inactive (or
     # unclickable) components.  If we are in edit mode, these are
     # actually clickable by default.
-    guiEdit = ShowBase.config.GetBool('direct-gui-edit', False)
+    guiEdit = ShowBaseGlobal.config.GetBool('direct-gui-edit', False)
     if guiEdit:
         inactiveInitState = DGG.NORMAL
     else:
@@ -729,7 +737,7 @@ class DirectGuiWidget(DirectGuiBase, NodePath):
             guiObjectCollector.addLevel(1)
             guiObjectCollector.flushLevel()
             # track gui items by guiId for tracking down leaks
-            if ShowBase.config.GetBool('track-gui-items', False):
+            if ShowBaseGlobal.config.GetBool('track-gui-items', False):
                 if not hasattr(ShowBase, 'guiItems'):
                     ShowBase.guiItems = {}
                 if self.guiId in ShowBase.guiItems:

+ 5 - 6
direct/src/gui/DirectGuiGlobals.py

@@ -17,8 +17,9 @@ drawOrder = 100
 panel = None
 
 # USEFUL GUI CONSTANTS
-# Constant used to indicate that an option can only be set by a call
-# to the constructor.
+
+#: Constant used to indicate that an option can only be set by a call
+#: to the constructor.
 INITOPT = ['initopt']
 
 # Mouse buttons
@@ -69,6 +70,8 @@ B3PRESS = PGButton.getPressPrefix() + MouseButton.three().getName() + '-'
 B1RELEASE = PGButton.getReleasePrefix() + MouseButton.one().getName() + '-'
 B2RELEASE = PGButton.getReleasePrefix() + MouseButton.two().getName() + '-'
 B3RELEASE = PGButton.getReleasePrefix() + MouseButton.three().getName() + '-'
+WHEELUP = PGButton.getReleasePrefix() + MouseButton.wheelUp().getName() + '-'
+WHEELDOWN = PGButton.getReleasePrefix() + MouseButton.wheelDown().getName() + '-'
 # For DirectEntry widgets
 OVERFLOW = PGEntry.getOverflowPrefix()
 ACCEPT = PGEntry.getAcceptPrefix() + KeyboardButton.enter().getName() + '-'
@@ -158,7 +161,3 @@ def getDefaultPanel():
 def setDefaultPanel(newPanel):
     global panel
     panel = newPanel
-
-#from OnscreenText import *
-#from OnscreenGeom import *
-#from OnscreenImage import *

+ 5 - 1
direct/src/gui/DirectLabel.py

@@ -1,4 +1,8 @@
-"""Contains the DirectLabel class."""
+"""Contains the DirectLabel class.
+
+See the :ref:`directlabel` page in the programming manual for a more in-depth
+explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectLabel']
 

+ 21 - 9
direct/src/gui/DirectOptionMenu.py

@@ -1,13 +1,19 @@
-"""Implements a pop-up menu containing multiple clickable options."""
+"""Implements a pop-up menu containing multiple clickable options.
+
+See the :ref:`directoptionmenu` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectOptionMenu']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from .DirectButton import *
 from .DirectLabel import *
 from .DirectFrame import *
 
+
 class DirectOptionMenu(DirectButton):
     """
     DirectOptionMenu(parent) - Create a DirectButton which pops up a
@@ -68,6 +74,10 @@ class DirectOptionMenu(DirectButton):
         self.popupMenu = None
         self.selectedIndex = None
         self.highlightedIndex = None
+        if 'item_text_scale' in kw:
+            self._prevItemTextScale = kw['item_text_scale']
+        else:
+            self._prevItemTextScale = (1,1)
         # A big screen encompassing frame to catch the cancel clicks
         self.cancelFrame = self.createcomponent(
             'cancelframe', (), None,
@@ -77,6 +87,7 @@ class DirectOptionMenu(DirectButton):
             state = 'normal')
         # Make sure this is on top of all the other widgets
         self.cancelFrame.setBin('gui-popup', 0)
+        self.cancelFrame.node().setBounds(OmniBoundingVolume())
         self.cancelFrame.bind(DGG.B1PRESS, self.hidePopupMenu)
         # Default action on press is to show popup menu
         self.bind(DGG.B1PRESS, self.showPopupMenu)
@@ -213,27 +224,27 @@ class DirectOptionMenu(DirectButton):
         self.popupMenu.setZ(
             self, self.minZ + (self.selectedIndex + 1)*self.maxHeight)
         # Make sure the whole popup menu is visible
-        pos = self.popupMenu.getPos(render2d)
-        scale = self.popupMenu.getScale(render2d)
+        pos = self.popupMenu.getPos(ShowBaseGlobal.render2d)
+        scale = self.popupMenu.getScale(ShowBaseGlobal.render2d)
         # How are we doing relative to the right side of the screen
         maxX = pos[0] + fb[1] * scale[0]
         if maxX > 1.0:
             # Need to move menu to the left
-            self.popupMenu.setX(render2d, pos[0] + (1.0 - maxX))
+            self.popupMenu.setX(ShowBaseGlobal.render2d, pos[0] + (1.0 - maxX))
         # How about up and down?
         minZ = pos[2] + fb[2] * scale[2]
         maxZ = pos[2] + fb[3] * scale[2]
         if minZ < -1.0:
             # Menu too low, move it up
-            self.popupMenu.setZ(render2d, pos[2] + (-1.0 - minZ))
+            self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (-1.0 - minZ))
         elif maxZ > 1.0:
             # Menu too high, move it down
-            self.popupMenu.setZ(render2d, pos[2] + (1.0 - maxZ))
+            self.popupMenu.setZ(ShowBaseGlobal.render2d, pos[2] + (1.0 - maxZ))
         # Also display cancel frame to catch clicks outside of the popup
         self.cancelFrame.show()
         # Position and scale cancel frame to fill entire window
-        self.cancelFrame.setPos(render2d, 0, 0, 0)
-        self.cancelFrame.setScale(render2d, 1, 1, 1)
+        self.cancelFrame.setPos(ShowBaseGlobal.render2d, 0, 0, 0)
+        self.cancelFrame.setScale(ShowBaseGlobal.render2d, 1, 1, 1)
 
     def hidePopupMenu(self, event = None):
         """ Put away popup and cancel frame """
@@ -242,6 +253,7 @@ class DirectOptionMenu(DirectButton):
 
     def _highlightItem(self, item, index):
         """ Set frame color of highlighted item, record index """
+        self._prevItemTextScale = item['text_scale']
         item['frameColor'] = self['highlightColor']
         item['frameSize'] = (self['highlightScale'][0]*self.minX, self['highlightScale'][0]*self.maxX, self['highlightScale'][1]*self.minZ, self['highlightScale'][1]*self.maxZ)
         item['text_scale'] = self['highlightScale']
@@ -251,7 +263,7 @@ class DirectOptionMenu(DirectButton):
         """ Clear frame color, clear highlightedIndex """
         item['frameColor'] = frameColor
         item['frameSize'] = (self.minX, self.maxX, self.minZ, self.maxZ)
-        item['text_scale'] = (1,1)
+        item['text_scale'] = self._prevItemTextScale
         self.highlightedIndex = None
 
     def selectHighlightedIndex(self, event = None):

+ 5 - 1
direct/src/gui/DirectRadioButton.py

@@ -1,7 +1,11 @@
 """A DirectRadioButton is a type of button that, similar to a
 DirectCheckButton, has a separate indicator and can be toggled between
 two states.  However, only one DirectRadioButton in a group can be enabled
-at a particular time."""
+at a particular time.
+
+See the :ref:`directradiobutton` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectRadioButton']
 

+ 5 - 1
direct/src/gui/DirectScrollBar.py

@@ -1,4 +1,8 @@
-"""Defines the DirectScrollBar class."""
+"""Defines the DirectScrollBar class.
+
+See the :ref:`directscrollbar` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectScrollBar']
 

+ 14 - 2
direct/src/gui/DirectScrolledFrame.py

@@ -1,4 +1,8 @@
-"""Contains the DirectScrolledFrame class."""
+"""Contains the DirectScrolledFrame class.
+
+See the :ref:`directscrolledframe` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectScrolledFrame']
 
@@ -33,7 +37,7 @@ class DirectScrolledFrame(DirectFrame):
             ('canvasSize',     (-1, 1, -1, 1),        self.setCanvasSize),
             ('manageScrollBars', 1,                self.setManageScrollBars),
             ('autoHideScrollBars', 1,              self.setAutoHideScrollBars),
-            ('scrollBarWidth', 0.08,               None),
+            ('scrollBarWidth', 0.08,               self.setScrollBarWidth),
             ('borderWidth',    (0.01, 0.01),       self.setBorderWidth),
             )
 
@@ -72,11 +76,19 @@ class DirectScrolledFrame(DirectFrame):
         # Call option initialization functions
         self.initialiseoptions(DirectScrolledFrame)
 
+    def setScrollBarWidth(self):
+        w = self['scrollBarWidth']
+        self.verticalScroll["frameSize"] = (-w / 2.0, w / 2.0, -1, 1)
+        self.horizontalScroll["frameSize"] = (-1, 1, -w / 2.0, w / 2.0)
+
     def setCanvasSize(self):
         f = self['canvasSize']
         self.guiItem.setVirtualFrame(f[0], f[1], f[2], f[3])
 
     def getCanvas(self):
+        """Returns the NodePath of the virtual canvas.  Nodes parented to this
+        canvas will show inside the scrolled area.
+        """
         return self.canvas
 
     def setManageScrollBars(self):

+ 31 - 20
direct/src/gui/DirectScrolledList.py

@@ -1,13 +1,24 @@
-"""Contains the DirectScrolledList class."""
+"""Contains the DirectScrolledList class.
+
+See the :ref:`directscrolledlist` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectScrolledListItem', 'DirectScrolledList']
 
 from panda3d.core import *
+from direct.showbase import ShowBaseGlobal
 from . import DirectGuiGlobals as DGG
 from direct.directnotify import DirectNotifyGlobal
 from direct.task.Task import Task
 from .DirectFrame import *
 from .DirectButton import *
+import sys
+
+if sys.version_info >= (3,0):
+    stringType = str
+else:
+    stringType = basestring
 
 
 class DirectScrolledListItem(DirectButton):
@@ -60,7 +71,7 @@ class DirectScrolledList(DirectFrame):
         # so we can modify it without mangling the user's list
         if 'items' in kw:
             for item in kw['items']:
-                if type(item) != type(''):
+                if not isinstance(item, stringType):
                     break
             else:
                 # we get here if every item in 'items' is a string
@@ -105,7 +116,7 @@ class DirectScrolledList(DirectFrame):
                                               DirectFrame, (self,),
                                               )
         for item in self["items"]:
-            if item.__class__.__name__ != 'str':
+            if not isinstance(item, stringType):
                 item.reparentTo(self.itemFrame)
 
         self.initialiseoptions(DirectScrolledList)
@@ -123,7 +134,7 @@ class DirectScrolledList(DirectFrame):
         else:
             self.maxHeight = 0.0
             for item in self["items"]:
-                if item.__class__.__name__ != 'str':
+                if not isinstance(item, stringType):
                     self.maxHeight = max(self.maxHeight, item.getHeight())
 
     def setScrollSpeed(self):
@@ -171,7 +182,7 @@ class DirectScrolledList(DirectFrame):
         if len(self["items"]) == 0:
             return 0
 
-        if type(self["items"][0]) == type(''):
+        if isinstance(self["items"][0], stringType):
             self.notify.warning("getItemIndexForItemID: cant find itemID for non-class list items!")
             return 0
 
@@ -237,7 +248,7 @@ class DirectScrolledList(DirectFrame):
 
         # Hide them all
         for item in self["items"]:
-            if item.__class__.__name__ != 'str':
+            if not isinstance(item, stringType):
                 item.hide()
 
         # Then show the ones in range, and stack their positions
@@ -247,7 +258,7 @@ class DirectScrolledList(DirectFrame):
             #print "stacking buttontext[", i,"]", self["items"][i]["text"]
             # If the item is a 'str', then it has not been created (scrolled list is 'as needed')
             #  Therefore, use the the function given to make it or just make it a frame
-            if item.__class__.__name__ == 'str':
+            if isinstance(item, stringType):
                 if self['itemMakeFunction']:
                     # If there is a function to create the item
                     item = self['itemMakeFunction'](item, i, self['itemMakeExtraArgs'])
@@ -279,7 +290,7 @@ class DirectScrolledList(DirectFrame):
             # Therefore, use the the function given to make it or
             # just make it a frame
             #print "Making " + str(item)
-            if item.__class__.__name__ == 'str':
+            if isinstance(item, stringType):
                 if self['itemMakeFunction']:
                     # If there is a function to create the item
                     item = self['itemMakeFunction'](item, i, self['itemMakeExtraArgs'])
@@ -344,16 +355,16 @@ class DirectScrolledList(DirectFrame):
         Add this string and extraArg to the list
         """
         assert self.notify.debugStateCall(self)
-        if type(item) != type(''):
+        if not isinstance(item, stringType):
             # cant add attribs to non-classes (like strings & ints)
             item.itemID = self.nextItemID
             self.nextItemID += 1
         self['items'].append(item)
-        if type(item) != type(''):
+        if not isinstance(item, stringType):
             item.reparentTo(self.itemFrame)
         if refresh:
             self.refresh()
-        if type(item) != type(''):
+        if not isinstance(item, stringType):
             return item.itemID  # to pass to scrollToItemID
 
     def removeItem(self, item, refresh=1):
@@ -368,8 +379,8 @@ class DirectScrolledList(DirectFrame):
             if hasattr(self, "currentSelected") and self.currentSelected is item:
                 del self.currentSelected
             self["items"].remove(item)
-            if type(item) != type(''):
-                item.reparentTo(hidden)
+            if not isinstance(item, stringType):
+                item.reparentTo(ShowBaseGlobal.hidden)
             self.refresh()
             return 1
         else:
@@ -386,8 +397,8 @@ class DirectScrolledList(DirectFrame):
             if (hasattr(item, 'destroy') and hasattr(item.destroy, '__call__')):
                 item.destroy()
             self["items"].remove(item)
-            if type(item) != type(''):
-                item.reparentTo(hidden)
+            if not isinstance(item, stringType):
+                item.reparentTo(ShowBaseGlobal.hidden)
             self.refresh()
             return 1
         else:
@@ -408,9 +419,9 @@ class DirectScrolledList(DirectFrame):
             if hasattr(self, "currentSelected") and self.currentSelected is item:
                 del self.currentSelected
             self["items"].remove(item)
-            if type(item) != type(''):
+            if not isinstance(item, stringType):
                 #RAU possible leak here, let's try to do the right thing
-                #item.reparentTo(hidden)
+                #item.reparentTo(ShowBaseGlobal.hidden)
                 item.removeNode()
             retval = 1
 
@@ -433,9 +444,9 @@ class DirectScrolledList(DirectFrame):
             if (hasattr(item, 'destroy') and hasattr(item.destroy, '__call__')):
                 item.destroy()
             self["items"].remove(item)
-            if type(item) != type(''):
+            if not isinstance(item, stringType):
                 #RAU possible leak here, let's try to do the right thing
-                #item.reparentTo(hidden)
+                #item.reparentTo(ShowBaseGlobal.hidden)
                 item.removeNode()
             retval = 1
         if (refresh):
@@ -458,7 +469,7 @@ class DirectScrolledList(DirectFrame):
 
     def getSelectedText(self):
         assert self.notify.debugStateCall(self)
-        if self['items'][self.index].__class__.__name__ == 'str':
+        if isinstance(self['items'][self.index], stringType):
           return self['items'][self.index]
         else:
           return self['items'][self.index]['text']

+ 5 - 1
direct/src/gui/DirectSlider.py

@@ -1,4 +1,8 @@
-"""Defines the DirectSlider class."""
+"""Defines the DirectSlider class.
+
+See the :ref:`directslider` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectSlider']
 

+ 5 - 1
direct/src/gui/DirectWaitBar.py

@@ -1,4 +1,8 @@
-"""Contains the DirectWaitBar class, a progress bar widget."""
+"""Contains the DirectWaitBar class, a progress bar widget.
+
+See the :ref:`directwaitbar` page in the programming manual for a more
+in-depth explanation and an example of how to use this class.
+"""
 
 __all__ = ['DirectWaitBar']
 

+ 11 - 6
direct/src/gui/OnscreenImage.py

@@ -1,4 +1,8 @@
-"""OnscreenImage module: contains the OnscreenImage class"""
+"""OnscreenImage module: contains the OnscreenImage class.
+
+See the :ref:`onscreenimage` page in the programming manual for explanation of
+this class.
+"""
 
 __all__ = ['OnscreenImage']
 
@@ -21,14 +25,15 @@ class OnscreenImage(DirectObject, NodePath):
                  parent = None,
                  sort = 0):
         """
-        Make a image node from string or a node path,
-        put it into the 2d sg and set it up with all the indicated parameters.
+        Make a image node from string or a `~panda3d.core.NodePath`, put
+        it into the 2-D scene graph and set it up with all the indicated
+        parameters.
 
-        The parameters are as follows:
+        Parameters:
 
           image: the actual geometry to display or a file name.
-                This may be omitted and specified later via setImage()
-                if you don't have it available.
+                 This may be omitted and specified later via setImage()
+                 if you don't have it available.
 
           pos: the x, y, z position of the geometry on the screen.
                This maybe a 3-tuple of floats or a vector.

+ 6 - 2
direct/src/gui/OnscreenText.py

@@ -1,4 +1,8 @@
-"""OnscreenText module: contains the OnscreenText class"""
+"""OnscreenText module: contains the OnscreenText class.
+
+See the :ref:`onscreentext` page in the programming manual for explanation of
+this class.
+"""
 
 __all__ = ['OnscreenText', 'Plain', 'ScreenTitle', 'ScreenPrompt', 'NameConfirm', 'BlackOnWhite']
 
@@ -41,7 +45,7 @@ class OnscreenText(NodePath):
         Make a text node from string, put it into the 2d sg and set it
         up with all the indicated parameters.
 
-        The parameters are as follows:
+        Parameters:
 
           text: the actual text to display.  This may be omitted and
               specified later via setText() if you don't have it

+ 1 - 1
direct/src/gui/__init__.py

@@ -1,5 +1,5 @@
 """
-This package contains the DirectGui system, a set of classes
+This package contains the :ref:`directgui` system, a set of classes
 responsible for drawing graphical widgets to the 2-D scene graph.
 
 It is based on the lower-level PGui system, which is implemented in

+ 5 - 1
direct/src/interval/ActorInterval.py

@@ -1,4 +1,8 @@
-"""ActorInterval module: contains the ActorInterval class"""
+"""ActorInterval module: contains the ActorInterval class.
+
+See the :ref:`actor-intervals` page in the programming manual for explanation
+of this class.
+"""
 
 __all__ = ['ActorInterval', 'LerpAnimInterval']
 

+ 16 - 20
direct/src/interval/ParticleInterval.py

@@ -34,26 +34,22 @@ class ParticleInterval(Interval):
                  cleanup = False,
                  name = None):
         """
-        particleEffect is a ParticleEffect
-        parent is a NodePath: this is where the effect will be
-                              parented in the scenegraph
-        worldRelative is a boolean: this will override 'renderParent'
-                                    with render
-        renderParent is a NodePath: this is where the particles will
-                                    be rendered in the scenegraph
-        duration is a float: for the time
-        softStopT is a float: no effect if 0.0,
-                              a positive value will count from the
-                              start of the interval,
-                              a negative value will count from the
-                              end of the interval
-        cleanup is a boolean: if True the effect will be destroyed
-                              and removed from the scenegraph upon
-                              interval completion
-                              set to False if planning on reusing
-                              the interval
-        name is a string: use this for unique intervals so that
-                          they can be easily found in the taskMgr
+        Args:
+            particleEffect (ParticleEffect): a particle effect
+            parent (NodePath): this is where the effect will be parented in the
+                scene graph
+            worldRelative (bool): this will override 'renderParent' with render
+            renderParent (NodePath): this is where the particles will be
+                rendered in the scenegraph
+            duration (float): for the time
+            softStopT (float): no effect if 0.0, a positive value will count
+                from the start of the interval, a negative value will count
+                from the end of the interval
+            cleanup (boolean): if True the effect will be destroyed and removed
+                from the scenegraph upon interval completion.  Set to False if
+                planning on reusing the interval.
+            name (string): use this for unique intervals so that they can be
+                easily found in the taskMgr.
         """
 
         # Generate unique name

+ 2 - 0
direct/src/interval/__init__.py

@@ -9,4 +9,6 @@ All interval types can be conveniently imported from the
 :mod:`.IntervalGlobal` module::
 
    from direct.interval.IntervalGlobal import *
+
+For more information about intervals, see the :ref:`intervals` manual page.
 """

+ 1 - 0
direct/src/leveleditor/ActionMgr.py

@@ -1,4 +1,5 @@
 from panda3d.core import *
+from direct.showbase.PythonUtil import Functor
 from . import ObjectGlobals as OG
 
 class ActionMgr:

+ 0 - 1245
direct/src/p3d/AppRunner.py

@@ -1,1245 +0,0 @@
-"""
-This module is intended to be compiled into the Panda3D runtime
-distributable, to execute a packaged p3d application, but it can also
-be run directly via the Python interpreter (if the current Panda3D and
-Python versions match the version expected by the application).  See
-runp3d.py for a command-line tool to invoke this module.
-
-The global AppRunner instance may be imported as follows::
-
-   from direct.showbase.AppRunnerGlobal import appRunner
-
-This will be None if Panda was not run from the runtime environment.
-"""
-
-__all__ = ["AppRunner", "dummyAppRunner", "ArgumentError"]
-
-import sys
-import os
-
-if sys.version_info >= (3, 0):
-    import builtins
-else:
-    import __builtin__ as builtins
-
-from direct.showbase import VFSImporter
-from direct.showbase.DirectObject import DirectObject
-from panda3d.core import VirtualFileSystem, Filename, Multifile, loadPrcFileData, unloadPrcFile, getModelPath, WindowProperties, ExecutionEnvironment, PandaSystem, Notify, StreamWriter, ConfigVariableString, ConfigPageManager
-from panda3d.direct import init_app_for_gui
-from panda3d import core
-from direct.stdpy import file, glob
-from direct.task.TaskManagerGlobal import taskMgr
-from direct.showbase.MessengerGlobal import messenger
-from direct.showbase import AppRunnerGlobal
-from direct.directnotify.DirectNotifyGlobal import directNotify
-from direct.p3d.HostInfo import HostInfo
-from direct.p3d.ScanDirectoryNode import ScanDirectoryNode
-from direct.p3d.InstalledHostData import InstalledHostData
-from direct.p3d.InstalledPackageData import InstalledPackageData
-
-# These imports are read by the C++ wrapper in p3dPythonRun.cxx.
-from direct.p3d.JavaScript import Undefined, ConcreteStruct
-
-class ArgumentError(AttributeError):
-    pass
-
-class ScriptAttributes:
-    """ This dummy class serves as the root object for the scripting
-    interface.  The Python code can store objects and functions here
-    for direct inspection by the browser's JavaScript code. """
-    pass
-
-class AppRunner(DirectObject):
-
-    """ This class is intended to be compiled into the Panda3D runtime
-    distributable, to execute a packaged p3d application.  It also
-    provides some useful runtime services while running in that
-    packaged environment.
-
-    It does not usually exist while running Python directly, but you
-    can use dummyAppRunner() to create one at startup for testing or
-    development purposes.  """
-
-    notify = directNotify.newCategory("AppRunner")
-
-    ConfigBasename = 'config.xml'
-
-    # Default values for parameters that are absent from the config file:
-    maxDiskUsage = 2048 * 1048576  # 2 GB
-
-    # Values for verifyContents, from p3d_plugin.h
-    P3DVCNone = 0
-    P3DVCNormal = 1
-    P3DVCForce = 2
-    P3DVCNever = 3
-
-    # Also from p3d_plugin.h
-    P3D_CONTENTS_DEFAULT_MAX_AGE = 5
-
-    def __init__(self):
-        DirectObject.__init__(self)
-
-        # We direct both our stdout and stderr objects onto Panda's
-        # Notify stream.  This ensures that unadorned print statements
-        # made within Python will get routed into the log properly.
-        stream = StreamWriter(Notify.out(), False)
-        sys.stdout = stream
-        sys.stderr = stream
-
-        # This is set true by dummyAppRunner(), below.
-        self.dummy = False
-
-        # These will be set from the application flags when
-        # setP3DFilename() is called.
-        self.allowPythonDev = False
-        self.guiApp = False
-        self.interactiveConsole = False
-        self.initialAppImport = False
-        self.trueFileIO = False
-        self.respectPerPlatform = None
-
-        self.verifyContents = self.P3DVCNone
-
-        self.sessionId = 0
-        self.packedAppEnvironmentInitialized = False
-        self.gotWindow = False
-        self.gotP3DFilename = False
-        self.p3dFilename = None
-        self.p3dUrl = None
-        self.started = False
-        self.windowOpened = False
-        self.windowPrc = None
-
-        self.http = None
-        if hasattr(core, 'HTTPClient'):
-            self.http = core.HTTPClient.getGlobalPtr()
-
-        self.Undefined = Undefined
-        self.ConcreteStruct = ConcreteStruct
-
-        # This is per session.
-        self.nextScriptId = 0
-
-        # TODO: we need one of these per instance, not per session.
-        self.instanceId = None
-
-        # The root Panda3D install directory.  This is filled in when
-        # the instance starts up.
-        self.rootDir = None
-
-        # The log directory.  Also filled in when the instance starts.
-        self.logDirectory = None
-
-        # self.superMirrorUrl, if nonempty, is the "super mirror" URL
-        # that should be contacted first before trying the actual
-        # host.  This is primarily used for "downloading" from a
-        # locally-stored Panda3D installation.  This is also filled in
-        # when the instance starts up.
-        self.superMirrorUrl = None
-
-        # A list of the Panda3D packages that have been loaded.
-        self.installedPackages = []
-
-        # A list of the Panda3D packages that in the queue to be
-        # downloaded.
-        self.downloadingPackages = []
-
-        # A dictionary of HostInfo objects for the various download
-        # hosts we have imported packages from.
-        self.hosts = {}
-
-        # The altHost string that is in effect from the HTML tokens,
-        # if any, and the dictionary of URL remapping: orig host url
-        # -> alt host url.
-        self.altHost = None
-        self.altHostMap = {}
-
-        # The URL from which Panda itself should be downloaded.
-        self.pandaHostUrl = PandaSystem.getPackageHostUrl()
-
-        # Application code can assign a callable object here; if so,
-        # it will be invoked when an uncaught exception propagates to
-        # the top of the TaskMgr.run() loop.
-        self.exceptionHandler = None
-
-        # Managing packages for runtime download.
-        self.downloadingPackages = []
-        self.downloadTask = None
-
-        # The mount point for the multifile.  For now, this is always
-        # the current working directory, for convenience; but when we
-        # move to multiple-instance sessions, it may have to be
-        # different for each instance.
-        self.multifileRoot = str(ExecutionEnvironment.getCwd())
-
-        # The "main" object will be exposed to the DOM as a property
-        # of the plugin object; that is, document.pluginobject.main in
-        # JavaScript will be appRunner.main here.  This may be
-        # replaced with a direct reference to the JavaScript object
-        # later, in setInstanceInfo().
-        self.main = ScriptAttributes()
-
-        # By default, we publish a stop() method so the browser can
-        # easy stop the plugin.  A particular application can remove
-        # this if it chooses.
-        self.main.stop = self.stop
-
-        # This will be the browser's toplevel window DOM object;
-        # e.g. self.dom.document will be the document.
-        self.dom = None
-
-        # This is the list of expressions we will evaluate when
-        # self.dom gets assigned.
-        self.deferredEvals = []
-
-        # This is the default requestFunc that is installed if we
-        # never call setRequestFunc().
-        def defaultRequestFunc(*args):
-            if args[1] == 'notify':
-                # Quietly ignore notifies.
-                return
-            self.notify.info("Ignoring request: %s" % (args,))
-        self.requestFunc = defaultRequestFunc
-
-        # This will be filled in with the default WindowProperties for
-        # this instance, e.g. the WindowProperties necessary to
-        # re-embed a window in the browser frame.
-        self.windowProperties = None
-
-        # Store our pointer so DirectStart-based apps can find us.
-        if AppRunnerGlobal.appRunner is None:
-            AppRunnerGlobal.appRunner = self
-
-        # We use this messenger hook to dispatch this __startIfReady()
-        # call back to the main thread.
-        self.accept('AppRunner_startIfReady', self.__startIfReady)
-
-    def getToken(self, tokenName):
-        """ Returns the value of the indicated web token as a string,
-        if it was set, or None if it was not. """
-
-        return self.tokenDict.get(tokenName.lower(), None)
-
-    def getTokenInt(self, tokenName):
-        """ Returns the value of the indicated web token as an integer
-        value, if it was set, or None if it was not, or not an
-        integer. """
-
-        value = self.getToken(tokenName)
-        if value is not None:
-            try:
-                value = int(value)
-            except ValueError:
-                value = None
-        return value
-
-    def getTokenFloat(self, tokenName):
-        """ Returns the value of the indicated web token as a
-        floating-point value value, if it was set, or None if it was
-        not, or not a number. """
-
-        value = self.getToken(tokenName)
-        if value is not None:
-            try:
-                value = float(value)
-            except ValueError:
-                value = None
-        return value
-
-    def getTokenBool(self, tokenName):
-        """ Returns the value of the indicated web token as a boolean
-        value, if it was set, or None if it was not. """
-
-        value = self.getTokenInt(tokenName)
-        if value is not None:
-            value = bool(value)
-        return value
-
-
-
-    def installPackage(self, packageName, version = None, hostUrl = None):
-
-        """ Installs the named package, downloading it first if
-        necessary.  Returns true on success, false on failure.  This
-        method runs synchronously, and will block until it is
-        finished; see the PackageInstaller class if you want this to
-        happen asynchronously instead. """
-
-        host = self.getHostWithAlt(hostUrl)
-        if not host.downloadContentsFile(self.http):
-            return False
-
-        # All right, get the package info now.
-        package = host.getPackage(packageName, version)
-        if not package:
-            self.notify.warning("Package %s %s not known on %s" % (
-                packageName, version, hostUrl))
-            return False
-
-        return self.__rInstallPackage(package, [])
-
-    def __rInstallPackage(self, package, nested):
-        """ The recursive implementation of installPackage().  The new
-        parameter, nested, is a list of packages that we are
-        recursively calling this from, to avoid recursive loops. """
-
-        package.checkStatus()
-        if not package.downloadDescFile(self.http):
-            return False
-
-        # Now that we've downloaded and read the desc file, we can
-        # install all of the required packages first.
-        nested = nested[:] + [self]
-        for packageName, version, host in package.requires:
-            if host.downloadContentsFile(self.http):
-                p2 = host.getPackage(packageName, version)
-                if not p2:
-                    self.notify.warning("Couldn't find %s %s on %s" % (packageName, version, host.hostUrl))
-                else:
-                    if p2 not in nested:
-                        self.__rInstallPackage(p2, nested)
-
-        # Now that all of the required packages are installed, carry
-        # on to download and install this package.
-        if not package.downloadPackage(self.http):
-            return False
-
-        if not package.installPackage(self):
-            return False
-
-        self.notify.info("Package %s %s installed." % (
-            package.packageName, package.packageVersion))
-        return True
-
-    def getHostWithAlt(self, hostUrl):
-        """ Returns a suitable HostInfo object for downloading
-        contents from the indicated URL.  This is almost always the
-        same thing as getHost(), except in the rare case when we have
-        an alt_host specified in the HTML tokens; in this case, we may
-        actually want to download the contents from a different URL
-        than the one given, for instance to download a version in
-        testing. """
-
-        if hostUrl is None:
-            hostUrl = self.pandaHostUrl
-
-        altUrl = self.altHostMap.get(hostUrl, None)
-        if altUrl:
-            # We got an alternate host.  Use it.
-            return self.getHost(altUrl)
-
-        # We didn't get an aternate host, use the original.
-        host = self.getHost(hostUrl)
-
-        # But we might need to consult the host itself to see if *it*
-        # recommends an altHost.
-        if self.altHost:
-            # This means forcing the host to download its contents
-            # file on the spot, a blocking operation.  This is a
-            # little unfortunate, but since alt_host is so rarely
-            # used, probably not really a problem.
-            host.downloadContentsFile(self.http)
-            altUrl = host.altHosts.get(self.altHost, None)
-            if altUrl:
-                return self.getHost(altUrl)
-
-        # No shenanigans, just return the requested host.
-        return host
-
-    def getHost(self, hostUrl, hostDir = None):
-        """ Returns a new HostInfo object corresponding to the
-        indicated host URL.  If we have already seen this URL
-        previously, returns the same object.
-
-        This returns the literal referenced host.  To return the
-        mapped host, which is the one we should actually download
-        from, see getHostWithAlt().  """
-
-        if not hostUrl:
-            hostUrl = self.pandaHostUrl
-
-        host = self.hosts.get(hostUrl, None)
-        if not host:
-            host = HostInfo(hostUrl, appRunner = self, hostDir = hostDir)
-            self.hosts[hostUrl] = host
-        return host
-
-    def getHostWithDir(self, hostDir):
-        """ Returns the HostInfo object that corresponds to the
-        indicated on-disk host directory.  This would be used when
-        reading a host directory from disk, instead of downloading it
-        from a server.  Supply the full path to the host directory, as
-        a Filename.  Returns None if the contents.xml in the indicated
-        host directory cannot be read or doesn't seem consistent. """
-
-        host = HostInfo(None, hostDir = hostDir, appRunner = self)
-        if not host.hasContentsFile:
-            if not host.readContentsFile():
-                # Couldn't read the contents.xml file
-                return None
-
-        if not host.hostUrl:
-            # The contents.xml file there didn't seem to indicate the
-            # same host directory.
-            return None
-
-        host2 = self.hosts.get(host.hostUrl)
-        if host2 is None:
-            # No such host already; store this one.
-            self.hosts[host.hostUrl] = host
-            return host
-
-        if host2.hostDir != host.hostDir:
-            # Hmm, we already have that host somewhere else.
-            return None
-
-        # We already have that host, and it's consistent.
-        return host2
-
-    def deletePackages(self, packages):
-        """ Removes all of the indicated packages from the disk,
-        uninstalling them and deleting all of their files.  The
-        packages parameter must be a list of one or more PackageInfo
-        objects, for instance as returned by getHost().getPackage().
-        Returns the list of packages that were NOT found. """
-
-        for hostUrl, host in self.hosts.items():
-            packages = host.deletePackages(packages)
-
-            if not host.packages:
-                # If that's all of the packages for this host, delete
-                # the host directory too.
-                del self.hosts[hostUrl]
-                self.__deleteHostFiles(host)
-
-        return packages
-
-    def __deleteHostFiles(self, host):
-        """ Called by deletePackages(), this removes all the files for
-        the indicated host (for which we have presumably already
-        removed all of the packages). """
-
-        self.notify.info("Deleting host %s: %s" % (host.hostUrl, host.hostDir))
-        self.rmtree(host.hostDir)
-
-        self.sendRequest('forget_package', host.hostUrl, '', '')
-
-
-    def freshenFile(self, host, fileSpec, localPathname):
-        """ Ensures that the localPathname is the most current version
-        of the file defined by fileSpec, as offered by host.  If not,
-        it downloads a new version on-the-spot.  Returns true on
-        success, false on failure. """
-
-        assert self.http
-        return host.freshenFile(self.http, fileSpec, localPathname)
-
-    def scanInstalledPackages(self):
-        """ Scans the hosts and packages already installed locally on
-        the system.  Returns a list of InstalledHostData objects, each
-        of which contains a list of InstalledPackageData objects. """
-
-        result = []
-        hostsFilename = Filename(self.rootDir, 'hosts')
-        hostsDir = ScanDirectoryNode(hostsFilename)
-        for dirnode in hostsDir.nested:
-            host = self.getHostWithDir(dirnode.pathname)
-            hostData = InstalledHostData(host, dirnode)
-
-            if host:
-                for package in host.getAllPackages(includeAllPlatforms = True):
-                    packageDir = package.getPackageDir()
-                    if not packageDir.exists():
-                        continue
-
-                    subdir = dirnode.extractSubdir(packageDir)
-                    if not subdir:
-                        # This package, while defined by the host, isn't installed
-                        # locally; ignore it.
-                        continue
-
-                    packageData = InstalledPackageData(package, subdir)
-                    hostData.packages.append(packageData)
-
-            # Now that we've examined all of the packages for the host,
-            # anything left over is junk.
-            for subdir in dirnode.nested:
-                packageData = InstalledPackageData(None, subdir)
-                hostData.packages.append(packageData)
-
-            result.append(hostData)
-
-        return result
-
-    def readConfigXml(self):
-        """ Reads the config.xml file that may be present in the root
-        directory. """
-
-        if not hasattr(core, 'TiXmlDocument'):
-            return
-
-        filename = Filename(self.rootDir, self.ConfigBasename)
-        doc = core.TiXmlDocument(filename.toOsSpecific())
-        if not doc.LoadFile():
-            return
-
-        xconfig = doc.FirstChildElement('config')
-        if xconfig:
-            maxDiskUsage = xconfig.Attribute('max_disk_usage')
-            try:
-                self.maxDiskUsage = int(maxDiskUsage or '')
-            except ValueError:
-                pass
-
-    def writeConfigXml(self):
-        """ Rewrites the config.xml to the root directory.  This isn't
-        called automatically; an application may call this after
-        adjusting some parameters (such as self.maxDiskUsage). """
-
-        from panda3d.core import TiXmlDocument, TiXmlDeclaration, TiXmlElement
-
-        filename = Filename(self.rootDir, self.ConfigBasename)
-        doc = TiXmlDocument(filename.toOsSpecific())
-        decl = TiXmlDeclaration("1.0", "utf-8", "")
-        doc.InsertEndChild(decl)
-
-        xconfig = TiXmlElement('config')
-        xconfig.SetAttribute('max_disk_usage', str(self.maxDiskUsage))
-        doc.InsertEndChild(xconfig)
-
-        # Write the file to a temporary filename, then atomically move
-        # it to its actual filename, to avoid race conditions when
-        # updating this file.
-        tfile = Filename.temporary(str(self.rootDir), '.xml')
-        if doc.SaveFile(tfile.toOsSpecific()):
-            tfile.renameTo(filename)
-
-
-    def checkDiskUsage(self):
-        """ Checks the total disk space used by all packages, and
-        removes old packages if necessary. """
-
-        totalSize = 0
-        hosts = self.scanInstalledPackages()
-        for hostData in hosts:
-            for packageData in hostData.packages:
-                totalSize += packageData.totalSize
-        self.notify.info("Total Panda3D disk space used: %s MB" % (
-            (totalSize + 524288) // 1048576))
-
-        if self.verifyContents == self.P3DVCNever:
-            # We're not allowed to delete anything anyway.
-            return
-
-        self.notify.info("Configured max usage is: %s MB" % (
-            (self.maxDiskUsage + 524288) // 1048576))
-        if totalSize <= self.maxDiskUsage:
-            # Still within budget; no need to clean up anything.
-            return
-
-        # OK, we're over budget.  Now we have to remove old packages.
-        usedPackages = []
-        for hostData in hosts:
-            for packageData in hostData.packages:
-                if packageData.package and packageData.package.installed:
-                    # Don't uninstall any packages we're currently using.
-                    continue
-
-                usedPackages.append((packageData.lastUse, packageData))
-
-        # Sort the packages into oldest-first order.
-        usedPackages.sort()
-
-        # Delete packages until we free up enough space.
-        packages = []
-        for lastUse, packageData in usedPackages:
-            if totalSize <= self.maxDiskUsage:
-                break
-            totalSize -= packageData.totalSize
-
-            if packageData.package:
-                packages.append(packageData.package)
-            else:
-                # If it's an unknown package, just delete it directly.
-                print("Deleting unknown package %s" % (packageData.pathname))
-                self.rmtree(packageData.pathname)
-
-        packages = self.deletePackages(packages)
-        if packages:
-            print("Unable to delete %s packages" % (len(packages)))
-
-        return
-
-    def stop(self):
-        """ This method can be called by JavaScript to stop the
-        application. """
-
-        # We defer the actual exit for a few frames, so we don't raise
-        # an exception and invalidate the JavaScript call; and also to
-        # help protect against race conditions as the application
-        # shuts down.
-        taskMgr.doMethodLater(0.5, sys.exit, 'exit')
-
-    def run(self):
-        """ This method calls taskMgr.run(), with an optional
-        exception handler.  This is generally the program's main loop
-        when running in a p3d environment (except on unusual platforms
-        like the iPhone, which have to hand the main loop off to the
-        OS, and don't use this interface). """
-
-        try:
-            taskMgr.run()
-
-        except SystemExit as err:
-            # Presumably the window has already been shut down here, but shut
-            # it down again for good measure.
-            if hasattr(builtins, "base"):
-                base.destroy()
-
-            self.notify.info("Normal exit with status %s." % repr(err.code))
-            raise
-
-        except:
-            # Some unexpected Python exception; pass it to the
-            # optional handler, if it is defined.
-            if self.exceptionHandler and not self.interactiveConsole:
-                self.exceptionHandler()
-            else:
-                raise
-
-    def rmtree(self, filename):
-        """ This is like shutil.rmtree(), but it can remove read-only
-        files on Windows.  It receives a Filename, the root directory
-        to delete. """
-        if filename.isDirectory():
-            for child in filename.scanDirectory():
-                self.rmtree(Filename(filename, child))
-            if not filename.rmdir():
-                print("could not remove directory %s" % (filename))
-        else:
-            if not filename.unlink():
-                print("could not delete %s" % (filename))
-
-    def setSessionId(self, sessionId):
-        """ This message should come in at startup. """
-        self.sessionId = sessionId
-        self.nextScriptId = self.sessionId * 1000 + 10000
-
-    def initPackedAppEnvironment(self):
-        """ This function sets up the Python environment suitably for
-        running a packed app.  It should only run once in any given
-        session (and it includes logic to ensure this). """
-
-        if self.packedAppEnvironmentInitialized:
-            return
-
-        self.packedAppEnvironmentInitialized = True
-
-        vfs = VirtualFileSystem.getGlobalPtr()
-
-        # Now set up Python to import this stuff.
-        VFSImporter.register()
-        sys.path.append(self.multifileRoot)
-
-        # Make sure that $MAIN_DIR is set to the p3d root before we
-        # start executing the code in this file.
-        ExecutionEnvironment.setEnvironmentVariable("MAIN_DIR", Filename(self.multifileRoot).toOsSpecific())
-
-        # Put our root directory on the model-path, too.
-        getModelPath().appendDirectory(self.multifileRoot)
-
-        if not self.trueFileIO:
-            # Replace the builtin open and file symbols so user code will get
-            # our versions by default, which can open and read files out of
-            # the multifile.
-            builtins.open = file.open
-            if sys.version_info < (3, 0):
-                builtins.file = file.open
-                builtins.execfile = file.execfile
-            os.listdir = file.listdir
-            os.walk = file.walk
-            os.path.join = file.join
-            os.path.isfile = file.isfile
-            os.path.isdir = file.isdir
-            os.path.exists = file.exists
-            os.path.lexists = file.lexists
-            os.path.getmtime = file.getmtime
-            os.path.getsize = file.getsize
-            sys.modules['glob'] = glob
-
-        self.checkDiskUsage()
-
-    def __startIfReady(self):
-        """ Called internally to start the application. """
-        if self.started:
-            return
-
-        if self.gotWindow and self.gotP3DFilename:
-            self.started = True
-
-            # Now we can ignore future calls to startIfReady().
-            self.ignore('AppRunner_startIfReady')
-
-            # Hang a hook so we know when the window is actually opened.
-            self.acceptOnce('window-event', self.__windowEvent)
-
-            # Look for the startup Python file.  This might be a magic
-            # filename (like "__main__", or any filename that contains
-            # invalid module characters), so we can't just import it
-            # directly; instead, we go through the low-level importer.
-
-            # If there's no p3d_info.xml file, we look for "main".
-            moduleName = 'main'
-            if self.p3dPackage:
-                mainName = self.p3dPackage.Attribute('main_module')
-                if mainName:
-                    moduleName = mainName
-
-            # Temporarily set this flag while we import the app, so
-            # that if the app calls run() within its own main.py, it
-            # will properly get ignored by ShowBase.
-            self.initialAppImport = True
-
-            # Python won't let us import a module named __main__.  So,
-            # we have to do that manually, via the VFSImporter.
-            if moduleName == '__main__':
-                dirName = Filename(self.multifileRoot).toOsSpecific()
-                importer = VFSImporter.VFSImporter(dirName)
-                loader = importer.find_module('__main__')
-                if loader is None:
-                    raise ImportError('No module named __main__')
-
-                mainModule = loader.load_module('__main__')
-            else:
-                __import__(moduleName)
-                mainModule = sys.modules[moduleName]
-
-            # Check if it has a main() function.  If so, call it.
-            if hasattr(mainModule, 'main') and hasattr(mainModule.main, '__call__'):
-                mainModule.main(self)
-
-            # Now clear this flag.
-            self.initialAppImport = False
-
-            if self.interactiveConsole:
-                # At this point, we have successfully loaded the app.
-                # If the interactive_console flag is enabled, stop the
-                # main loop now and give the user a Python prompt.
-                taskMgr.stop()
-
-    def getPandaScriptObject(self):
-        """ Called by the browser to query the Panda instance's
-        toplevel scripting object, for querying properties in the
-        Panda instance.  The attributes on this object are mapped to
-        document.pluginobject.main within the DOM. """
-
-        return self.main
-
-    def setBrowserScriptObject(self, dom):
-        """ Called by the browser to supply the browser's toplevel DOM
-        object, for controlling the JavaScript and the document in the
-        same page with the Panda3D plugin. """
-
-        self.dom = dom
-
-        # Now evaluate any deferred expressions.
-        for expression in self.deferredEvals:
-            self.scriptRequest('eval', self.dom, value = expression,
-                               needsResponse = False)
-        self.deferredEvals = []
-
-    def setInstanceInfo(self, rootDir, logDirectory, superMirrorUrl,
-                        verifyContents, main, respectPerPlatform):
-        """ Called by the browser to set some global information about
-        the instance. """
-
-        # rootDir is the root Panda3D install directory on the local
-        # machine.
-        self.rootDir = Filename.fromOsSpecific(rootDir)
-
-        # logDirectory is the directory name where all log files end
-        # up.
-        if logDirectory:
-            self.logDirectory = Filename.fromOsSpecific(logDirectory)
-        else:
-            self.logDirectory = Filename(rootDir, 'log')
-
-        # The "super mirror" URL, generally used only by panda3d.exe.
-        self.superMirrorUrl = superMirrorUrl
-
-        # How anxious should we be about contacting the server for
-        # the latest code?
-        self.verifyContents = verifyContents
-
-        # The initial "main" object, if specified.
-        if main is not None:
-            self.main = main
-
-        self.respectPerPlatform = respectPerPlatform
-        #self.notify.info("respectPerPlatform = %s" % (self.respectPerPlatform))
-
-        # Now that we have rootDir, we can read the config file.
-        self.readConfigXml()
-
-
-    def addPackageInfo(self, name, platform, version, hostUrl, hostDir = None,
-                       recurse = False):
-        """ Called by the browser for each one of the "required"
-        packages that were preloaded before starting the application.
-        If for some reason the package isn't already downloaded, this
-        will download it on the spot.  Raises OSError on failure. """
-
-        host = self.getHost(hostUrl, hostDir = hostDir)
-
-        if not host.hasContentsFile:
-            # Always pre-read these hosts' contents.xml files, even if
-            # we have P3DVCForce in effect, since presumably we've
-            # already forced them on the plugin side.
-            host.readContentsFile()
-
-        if not host.downloadContentsFile(self.http):
-            # Couldn't download?  Must have failed to download in the
-            # plugin as well.  But since we launched, we probably have
-            # a copy already local; let's use it.
-            message = "Host %s cannot be downloaded, cannot preload %s." % (hostUrl, name)
-            if not host.hasContentsFile:
-                # This is weird.  How did we launch without having
-                # this file at all?
-                raise OSError(message)
-
-            # Just make it a warning and continue.
-            self.notify.warning(message)
-
-        if name == 'panda3d' and not self.pandaHostUrl:
-            # A special case: in case we don't have the PackageHostUrl
-            # compiled in, infer it from the first package we
-            # installed named "panda3d".
-            self.pandaHostUrl = hostUrl
-
-        if not platform:
-            platform = None
-        package = host.getPackage(name, version, platform = platform)
-        if not package:
-            if not recurse:
-                # Maybe the contents.xml file isn't current.  Re-fetch it.
-                if host.redownloadContentsFile(self.http):
-                    return self.addPackageInfo(name, platform, version, hostUrl, hostDir = hostDir, recurse = True)
-
-            message = "Couldn't find %s %s on %s" % (name, version, hostUrl)
-            raise OSError(message)
-
-        package.checkStatus()
-        if not package.downloadDescFile(self.http):
-            message = "Couldn't get desc file for %s" % (name)
-            raise OSError(message)
-
-        if not package.downloadPackage(self.http):
-            message = "Couldn't download %s" % (name)
-            raise OSError(message)
-
-        if not package.installPackage(self):
-            message = "Couldn't install %s" % (name)
-            raise OSError(message)
-
-        if package.guiApp:
-            self.guiApp = True
-            init_app_for_gui()
-
-    def setP3DFilename(self, p3dFilename, tokens, argv, instanceId,
-                       interactiveConsole, p3dOffset = 0, p3dUrl = None):
-        """ Called by the browser to specify the p3d file that
-        contains the application itself, along with the web tokens
-        and/or command-line arguments.  Once this method has been
-        called, the application is effectively started. """
-
-        # One day we will have support for multiple instances within a
-        # Python session.  Against that day, we save the instance ID
-        # for this instance.
-        self.instanceId = instanceId
-
-        self.tokens = tokens
-        self.argv = argv
-
-        # We build up a token dictionary with care, so that if a given
-        # token appears twice in the token list, we record only the
-        # first value, not the second or later.  This is consistent
-        # with the internal behavior of the core API.
-        self.tokenDict = {}
-        for token, keyword in tokens:
-            self.tokenDict.setdefault(token, keyword)
-
-        # Also store the arguments on sys, for applications that
-        # aren't instance-ready.
-        sys.argv = argv
-
-        # That means we now know the altHost in effect.
-        self.altHost = self.tokenDict.get('alt_host', None)
-
-        # Tell the browser that Python is up and running, and ready to
-        # respond to queries.
-        self.notifyRequest('onpythonload')
-
-        # Now go load the applet.
-        fname = Filename.fromOsSpecific(p3dFilename)
-        vfs = VirtualFileSystem.getGlobalPtr()
-
-        if not vfs.exists(fname):
-            raise ArgumentError("No such file: %s" % (p3dFilename))
-
-        fname.makeAbsolute()
-        fname.setBinary()
-        mf = Multifile()
-        if p3dOffset == 0:
-            if not mf.openRead(fname):
-                raise ArgumentError("Not a Panda3D application: %s" % (p3dFilename))
-        else:
-            if not mf.openRead(fname, p3dOffset):
-                raise ArgumentError("Not a Panda3D application: %s at offset: %s" % (p3dFilename, p3dOffset))
-
-        # Now load the p3dInfo file.
-        self.p3dInfo = None
-        self.p3dPackage = None
-        self.p3dConfig = None
-        self.allowPythonDev = False
-
-        i = mf.findSubfile('p3d_info.xml')
-        if i >= 0 and hasattr(core, 'readXmlStream'):
-            stream = mf.openReadSubfile(i)
-            self.p3dInfo = core.readXmlStream(stream)
-            mf.closeReadSubfile(stream)
-        if self.p3dInfo:
-            self.p3dPackage = self.p3dInfo.FirstChildElement('package')
-        if self.p3dPackage:
-            self.p3dConfig = self.p3dPackage.FirstChildElement('config')
-
-            xhost = self.p3dPackage.FirstChildElement('host')
-            while xhost:
-                self.__readHostXml(xhost)
-                xhost = xhost.NextSiblingElement('host')
-
-        if self.p3dConfig:
-            allowPythonDev = self.p3dConfig.Attribute('allow_python_dev')
-            if allowPythonDev:
-                self.allowPythonDev = int(allowPythonDev)
-            guiApp = self.p3dConfig.Attribute('gui_app')
-            if guiApp:
-                self.guiApp = int(guiApp)
-
-            trueFileIO = self.p3dConfig.Attribute('true_file_io')
-            if trueFileIO:
-                self.trueFileIO = int(trueFileIO)
-
-        # The interactiveConsole flag can only be set true if the
-        # application has allow_python_dev set.
-        if not self.allowPythonDev and interactiveConsole:
-            raise Exception("Impossible, interactive_console set without allow_python_dev.")
-        self.interactiveConsole = interactiveConsole
-
-        if self.allowPythonDev:
-            # Set the fps text to remind the user that
-            # allow_python_dev is enabled.
-            ConfigVariableString('frame-rate-meter-text-pattern').setValue('allow_python_dev %0.1f fps')
-
-        if self.guiApp:
-            init_app_for_gui()
-
-        self.initPackedAppEnvironment()
-
-        # Mount the Multifile under self.multifileRoot.
-        vfs.mount(mf, self.multifileRoot, vfs.MFReadOnly)
-        self.p3dMultifile = mf
-        VFSImporter.reloadSharedPackages()
-
-        self.loadMultifilePrcFiles(mf, self.multifileRoot)
-        self.gotP3DFilename = True
-        self.p3dFilename = fname
-        if p3dUrl:
-            # The url from which the p3d file was downloaded is
-            # provided if available.  It is only for documentation
-            # purposes; the actual p3d file has already been
-            # downloaded to p3dFilename.
-            self.p3dUrl = core.URLSpec(p3dUrl)
-
-        # Send this call to the main thread; don't call it directly.
-        messenger.send('AppRunner_startIfReady', taskChain = 'default')
-
-    def __readHostXml(self, xhost):
-        """ Reads the data in the indicated <host> entry. """
-
-        url = xhost.Attribute('url')
-        host = self.getHost(url)
-        host.readHostXml(xhost)
-
-        # Scan for a matching <alt_host>.  If found, it means we
-        # should use the alternate URL instead of the original URL.
-        if self.altHost:
-            xalthost = xhost.FirstChildElement('alt_host')
-            while xalthost:
-                keyword = xalthost.Attribute('keyword')
-                if keyword == self.altHost:
-                    origUrl = xhost.Attribute('url')
-                    newUrl = xalthost.Attribute('url')
-                    self.altHostMap[origUrl] = newUrl
-                    break
-
-                xalthost = xalthost.NextSiblingElement('alt_host')
-
-    def loadMultifilePrcFiles(self, mf, root):
-        """ Loads any prc files in the root of the indicated
-        Multifile, which is presumed to have been mounted already
-        under root. """
-
-        # We have to load these prc files explicitly, since the
-        # ConfigPageManager can't directly look inside the vfs.  Use
-        # the Multifile interface to find the prc files, rather than
-        # vfs.scanDirectory(), so we only pick up the files in this
-        # particular multifile.
-        cpMgr = ConfigPageManager.getGlobalPtr()
-        for f in mf.getSubfileNames():
-            fn = Filename(f)
-            if fn.getDirname() == '' and fn.getExtension() == 'prc':
-                pathname = '%s/%s' % (root, f)
-
-                alreadyLoaded = False
-                for cpi in range(cpMgr.getNumImplicitPages()):
-                    if cpMgr.getImplicitPage(cpi).getName() == pathname:
-                        # No need to load this file twice.
-                        alreadyLoaded = True
-                        break
-
-                if not alreadyLoaded:
-                    data = file.open(Filename(pathname), 'r').read()
-                    cp = loadPrcFileData(pathname, data)
-                    # Set it to sort value 20, behind the implicit pages.
-                    cp.setSort(20)
-
-
-    def __clearWindowProperties(self):
-        """ Clears the windowPrc file that was created in a previous
-        call to setupWindow(), if any. """
-
-        if self.windowPrc:
-            unloadPrcFile(self.windowPrc)
-            self.windowPrc = None
-        WindowProperties.clearDefault()
-
-        # However, we keep the self.windowProperties object around, in
-        # case an application wants to return the window to the
-        # browser frame.
-
-    def setupWindow(self, windowType, x, y, width, height,
-                    parent):
-        """ Applies the indicated window parameters to the prc
-        settings, for future windows; or applies them directly to the
-        main window if the window has already been opened.  This is
-        called by the browser. """
-
-        if self.started and base.win:
-            # If we've already got a window, this must be a
-            # resize/reposition request.
-            wp = WindowProperties()
-            if x or y or windowType == 'embedded':
-                wp.setOrigin(x, y)
-            if width or height:
-                wp.setSize(width, height)
-            if windowType == 'embedded':
-                wp.setParentWindow(parent)
-            wp.setFullscreen(False)
-            base.win.requestProperties(wp)
-            self.windowProperties = wp
-            return
-
-        # If we haven't got a window already, start 'er up.  Apply the
-        # requested setting to the prc file, and to the default
-        # WindowProperties structure.
-
-        self.__clearWindowProperties()
-
-        if windowType == 'hidden':
-            data = 'window-type none\n'
-        else:
-            data = 'window-type onscreen\n'
-
-        wp = WindowProperties.getDefault()
-
-        wp.clearParentWindow()
-        wp.clearOrigin()
-        wp.clearSize()
-
-        wp.setFullscreen(False)
-        if windowType == 'fullscreen':
-            wp.setFullscreen(True)
-
-        if windowType == 'embedded':
-            wp.setParentWindow(parent)
-
-        if x or y or windowType == 'embedded':
-            wp.setOrigin(x, y)
-
-        if width or height:
-            wp.setSize(width, height)
-
-        self.windowProperties = wp
-        self.windowPrc = loadPrcFileData("setupWindow", data)
-        WindowProperties.setDefault(wp)
-
-        self.gotWindow = True
-
-        # Send this call to the main thread; don't call it directly.
-        messenger.send('AppRunner_startIfReady', taskChain = 'default')
-
-    def setRequestFunc(self, func):
-        """ This method is called by the browser at startup to supply a
-        function that can be used to deliver requests upstream, to the
-        core API, and thereby to the browser. """
-        self.requestFunc = func
-
-    def sendRequest(self, request, *args):
-        """ Delivers a request to the browser via self.requestFunc.
-        This low-level function is not intended to be called directly
-        by user code. """
-
-        assert self.requestFunc
-        return self.requestFunc(self.instanceId, request, args)
-
-    def __windowEvent(self, win):
-        """ This method is called when we get a window event.  We
-        listen for this to detect when the window has been
-        successfully opened. """
-
-        if not self.windowOpened:
-            self.windowOpened = True
-
-            # Now that the window is open, we don't need to keep those
-            # prc settings around any more.
-            self.__clearWindowProperties()
-
-            # Inform the plugin and browser.
-            self.notifyRequest('onwindowopen')
-
-    def notifyRequest(self, message):
-        """ Delivers a notify request to the browser.  This is a "this
-        happened" type notification; it also triggers some JavaScript
-        code execution, if indicated in the HTML tags, and may also
-        trigger some internal automatic actions.  (For instance, the
-        plugin takes down the splash window when it sees the
-        onwindowopen notification. """
-
-        self.sendRequest('notify', message.lower())
-
-    def evalScript(self, expression, needsResponse = False):
-        """ Evaluates an arbitrary JavaScript expression in the global
-        DOM space.  This may be deferred if necessary if needsResponse
-        is False and self.dom has not yet been assigned.  If
-        needsResponse is true, this waits for the value and returns
-        it, which means it cannot be deferred. """
-
-        if not self.dom:
-            # Defer the expression.
-            assert not needsResponse
-            self.deferredEvals.append(expression)
-        else:
-            # Evaluate it now.
-            return self.scriptRequest('eval', self.dom, value = expression,
-                                      needsResponse = needsResponse)
-
-    def scriptRequest(self, operation, object, propertyName = '',
-                      value = None, needsResponse = True):
-        """ Issues a new script request to the browser.  This queries
-        or modifies one of the browser's DOM properties.  This is a
-        low-level method that user code should not call directly;
-        instead, just operate on the Python wrapper objects that
-        shadow the DOM objects, beginning with appRunner.dom.
-
-        operation may be one of [ 'get_property', 'set_property',
-        'call', 'evaluate' ].
-
-        object is the browser object to manipulate, or the scope in
-        which to evaluate the expression.
-
-        propertyName is the name of the property to manipulate, if
-        relevant (set to None for the default method name).
-
-        value is the new value to assign to the property for
-        set_property, or the parameter list for call, or the string
-        expression for evaluate.
-
-        If needsResponse is true, this method will block until the
-        return value is received from the browser, and then it returns
-        that value.  Otherwise, it returns None immediately, without
-        waiting for the browser to process the request.
-        """
-        uniqueId = self.nextScriptId
-        self.nextScriptId = (self.nextScriptId + 1) % 0xffffffff
-        self.sendRequest('script', operation, object,
-                         propertyName, value, needsResponse, uniqueId)
-
-        if needsResponse:
-            # Now wait for the response to come in.
-            result = self.sendRequest('wait_script_response', uniqueId)
-            return result
-
-    def dropObject(self, objectId):
-        """ Inform the parent process that we no longer have an
-        interest in the P3D_object corresponding to the indicated
-        objectId.  Not intended to be called by user code. """
-
-        self.sendRequest('drop_p3dobj', objectId)
-
-def dummyAppRunner(tokens = [], argv = None):
-    """ This function creates a dummy global AppRunner object, which
-    is useful for testing running in a packaged environment without
-    actually bothering to package up the application.  Call this at
-    the start of your application to enable it.
-
-    It places the current working directory under /mf, as if it were
-    mounted from a packed multifile.  It doesn't convert egg files to
-    bam files, of course; and there are other minor differences from
-    running in an actual packaged environment.  But it can be a useful
-    first-look sanity check. """
-
-    if AppRunnerGlobal.appRunner:
-        print("Already have AppRunner, not creating a new one.")
-        return AppRunnerGlobal.appRunner
-
-    appRunner = AppRunner()
-    appRunner.dummy = True
-    AppRunnerGlobal.appRunner = appRunner
-
-    platform = PandaSystem.getPlatform()
-    version = PandaSystem.getPackageVersionString()
-    hostUrl = PandaSystem.getPackageHostUrl()
-
-    if platform.startswith('win'):
-        rootDir = Filename(Filename.getUserAppdataDirectory(), 'Panda3D')
-    elif platform.startswith('osx'):
-        rootDir = Filename(Filename.getHomeDirectory(), 'Library/Caches/Panda3D')
-    else:
-        rootDir = Filename(Filename.getHomeDirectory(), '.panda3d')
-
-    appRunner.rootDir = rootDir
-    appRunner.logDirectory = Filename(rootDir, 'log')
-
-    # Of course we will have the panda3d application loaded.
-    appRunner.addPackageInfo('panda3d', platform, version, hostUrl)
-
-    appRunner.tokens = tokens
-    appRunner.tokenDict = dict(tokens)
-    if argv is None:
-        argv = sys.argv
-    appRunner.argv = argv
-    appRunner.altHost = appRunner.tokenDict.get('alt_host', None)
-
-    appRunner.p3dInfo = None
-    appRunner.p3dPackage = None
-
-    # Mount the current directory under the multifileRoot, as if it
-    # were coming from a multifile.
-    cwd = ExecutionEnvironment.getCwd()
-    vfs = VirtualFileSystem.getGlobalPtr()
-    vfs.mount(cwd, appRunner.multifileRoot, vfs.MFReadOnly)
-
-    appRunner.initPackedAppEnvironment()
-
-    return appRunner
-

+ 0 - 96
direct/src/p3d/DWBPackageInstaller.py

@@ -1,96 +0,0 @@
-__all__ = ["DWBPackageInstaller"]
-
-from direct.p3d.PackageInstaller import PackageInstaller
-from direct.gui.DirectWaitBar import DirectWaitBar
-from direct.gui import DirectGuiGlobals as DGG
-
-class DWBPackageInstaller(DirectWaitBar, PackageInstaller):
-    """ This class presents a PackageInstaller that also inherits from
-    DirectWaitBar, so it updates its own GUI as it downloads.
-
-    Specify perPackage = True to make the progress bar reset for each
-    package, or False (the default) to show one continuous progress
-    bar for all packages.
-
-    Specify updateText = True (the default) to update the text label
-    with the name of the package or False to leave it up to you to set
-    it.
-
-    You can specify a callback function with finished = func; this
-    function will be called, with one boolean parameter, when the
-    download has completed.  The parameter will be true on success, or
-    false on failure.
-    """
-
-    def __init__(self, appRunner, parent = None, **kw):
-        PackageInstaller.__init__(self, appRunner)
-
-        optiondefs = (
-            ('borderWidth',    (0.01, 0.01),       None),
-            ('relief',         DGG.SUNKEN,         self.setRelief),
-            ('range',          1,                  self.setRange),
-            ('barBorderWidth', (0.01, 0.01),       self.setBarBorderWidth),
-            ('barColor',       (0.424, 0.647, 0.878, 1),  self.setBarColor),
-            ('barRelief',      DGG.RAISED,         self.setBarRelief),
-            ('text',           'Starting',         self.setText),
-            ('text_pos',       (0, -0.025),        None),
-            ('text_scale',     0.1,                None),
-            ('perPackage',     False,              None),
-            ('updateText',     True,               None),
-            ('finished',       None,               None),
-            )
-        self.defineoptions(kw, optiondefs)
-        DirectWaitBar.__init__(self, parent, **kw)
-        self.initialiseoptions(DWBPackageInstaller)
-        self.updateBarStyle()
-
-        # Hidden by default until the download begins.
-        self.hide()
-
-    def cleanup(self):
-        PackageInstaller.cleanup(self)
-        DirectWaitBar.destroy(self)
-
-    def destroy(self):
-        PackageInstaller.cleanup(self)
-        DirectWaitBar.destroy(self)
-
-    def packageStarted(self, package):
-        """ This callback is made for each package between
-        downloadStarted() and downloadFinished() to indicate the start
-        of a new package. """
-        if self['updateText']:
-            self['text'] = 'Installing %s' % (package.getFormattedName())
-        self.show()
-
-    def packageProgress(self, package, progress):
-        """ This callback is made repeatedly between packageStarted()
-        and packageFinished() to update the current progress on the
-        indicated package only.  The progress value ranges from 0
-        (beginning) to 1 (complete). """
-
-        if self['perPackage']:
-            self['value'] = progress * self['range']
-
-    def downloadProgress(self, overallProgress):
-        """ This callback is made repeatedly between downloadStarted()
-        and downloadFinished() to update the current progress through
-        all packages.  The progress value ranges from 0 (beginning) to
-        1 (complete). """
-
-        if not self['perPackage']:
-            self['value'] = overallProgress * self['range']
-
-    def downloadFinished(self, success):
-        """ This callback is made when all of the packages have been
-        downloaded and installed (or there has been some failure).  If
-        all packages where successfully installed, success is True.
-
-        If there were no packages that required downloading, this
-        callback will be made immediately, *without* a corresponding
-        call to downloadStarted(). """
-
-        self.hide()
-
-        if self['finished']:
-            self['finished'](success)

+ 0 - 1379
direct/src/p3d/DeploymentTools.py

@@ -1,1379 +0,0 @@
-""" This module is used to build a graphical installer
-or a standalone executable from a p3d file. It will try
-to build for as many platforms as possible. """
-
-__all__ = ["Standalone", "Installer"]
-
-import os, sys, subprocess, tarfile, shutil, time, zipfile, socket, getpass, struct
-import gzip, plistlib
-from direct.directnotify.DirectNotifyGlobal import *
-from direct.showbase.AppRunnerGlobal import appRunner
-from panda3d.core import PandaSystem, HTTPClient, Filename, VirtualFileSystem, Multifile
-from panda3d.core import TiXmlDocument, TiXmlDeclaration, TiXmlElement, readXmlStream
-from panda3d.core import PNMImage, PNMFileTypeRegistry
-from direct.stdpy.file import *
-from direct.p3d.HostInfo import HostInfo
-# This is important for some reason
-import encodings
-
-try:
-    import pwd
-except ImportError:
-    pwd = None
-
-if sys.version_info >= (3, 0):
-    xrange = range
-    from io import BytesIO, TextIOWrapper
-else:
-    from io import BytesIO
-    from StringIO import StringIO
-
-# Make sure this matches with the magic in p3dEmbedMain.cxx.
-P3DEMBED_MAGIC = 0xFF3D3D00
-
-# This filter function is used when creating
-# an archive that should be owned by root.
-def archiveFilter(info):
-    basename = os.path.basename(info.name)
-    if basename in [".DS_Store", "Thumbs.db"]:
-        return None
-
-    info.uid = info.gid = 0
-    info.uname = info.gname = "root"
-
-    # All files without an extension get mode 0755,
-    # all other files are chmodded to 0644.
-    # Somewhat hacky, but it's the only way
-    # permissions can work on a Windows box.
-    if info.type != tarfile.DIRTYPE and '.' in info.name.rsplit('/', 1)[-1]:
-        info.mode = 0o644
-    else:
-        info.mode = 0o755
-
-    return info
-
-# Hack to make all files in a tar file owned by root.
-# The tarfile module doesn't have filter functionality until Python 2.7.
-# Yay duck typing!
-class TarInfoRoot(tarfile.TarInfo):
-    uid = property(lambda self: 0, lambda self, x: None)
-    gid = property(lambda self: 0, lambda self, x: None)
-    uname = property(lambda self: "root", lambda self, x: None)
-    gname = property(lambda self: "root", lambda self, x: None)
-    mode = property(lambda self: 0o644 if self.type != tarfile.DIRTYPE and \
-                    '.' in self.name.rsplit('/', 1)[-1] else 0o755,
-                    lambda self, x: None)
-
-# On OSX, the root group is named "wheel".
-class TarInfoRootOSX(TarInfoRoot):
-    gname = property(lambda self: "wheel", lambda self, x: None)
-
-
-class Standalone:
-    """ This class creates a standalone executable from a given .p3d file. """
-    notify = directNotify.newCategory("Standalone")
-
-    def __init__(self, p3dfile, tokens = {}):
-        self.p3dfile = Filename(p3dfile)
-        self.basename = self.p3dfile.getBasenameWoExtension()
-        self.tokens = tokens
-
-        self.tempDir = Filename.temporary("", self.basename, "") + "/"
-        self.tempDir.makeDir()
-        self.host = HostInfo(PandaSystem.getPackageHostUrl(), appRunner = appRunner, hostDir = self.tempDir, asMirror = False, perPlatform = True)
-
-        self.http = HTTPClient.getGlobalPtr()
-        if not self.host.hasContentsFile:
-            if not self.host.readContentsFile():
-                if not self.host.downloadContentsFile(self.http):
-                    Standalone.notify.error("couldn't read host")
-                    return
-
-    def __del__(self):
-        try:
-            appRunner.rmtree(self.tempDir)
-        except:
-            try: shutil.rmtree(self.tempDir.toOsSpecific())
-            except: pass
-
-    def buildAll(self, outputDir = "."):
-        """ Builds standalone executables for every known platform,
-        into the specified output directory. """
-
-        platforms = set()
-        for package in self.host.getPackages(name = "p3dembed"):
-            platforms.add(package.platform)
-        if len(platforms) == 0:
-            Standalone.notify.warning("No platforms found to build for!")
-
-        if 'win32' in platforms and 'win_i386' in platforms:
-            platforms.remove('win32')
-
-        outputDir = Filename(outputDir + "/")
-        outputDir.makeDir()
-        for platform in platforms:
-            if platform.startswith("win"):
-                self.build(Filename(outputDir, platform + "/" + self.basename + ".exe"), platform)
-            else:
-                self.build(Filename(outputDir, platform + "/" + self.basename), platform)
-
-    def build(self, output, platform = None, extraTokens = {}):
-        """ Builds a standalone executable and stores it into the path
-        indicated by the 'output' argument. You can specify to build for
-        a different platform by altering the 'platform' argument. """
-
-        if platform == None:
-            platform = PandaSystem.getPlatform()
-
-        vfs = VirtualFileSystem.getGlobalPtr()
-
-        for package in self.host.getPackages(name = "p3dembed", platform = platform):
-            if not package.downloadDescFile(self.http):
-                Standalone.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
-                continue
-            if not package.downloadPackage(self.http):
-                Standalone.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
-                continue
-
-            # Figure out where p3dembed might be now.
-            if package.platform.startswith("win"):
-                # Use p3dembedw unless console_environment was set.
-                cEnv = extraTokens.get("console_environment", self.tokens.get("console_environment", 0))
-                if cEnv != "" and int(cEnv) != 0:
-                    p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembed.exe" % package.platform)
-                else:
-                    p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembedw.exe" % package.platform)
-                    # Fallback for older p3dembed versions
-                    if not vfs.exists(p3dembed):
-                        Filename(self.host.hostDir, "p3dembed/%s/p3dembed.exe" % package.platform)
-            else:
-                p3dembed = Filename(self.host.hostDir, "p3dembed/%s/p3dembed" % package.platform)
-
-            if not vfs.exists(p3dembed):
-                Standalone.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
-                continue
-
-            return self.embed(output, p3dembed, extraTokens)
-
-        Standalone.notify.error("Failed to build standalone for platform %s" % platform)
-
-    def embed(self, output, p3dembed, extraTokens = {}):
-        """ Embeds the p3d file into the provided p3dembed executable.
-        This function is not really useful - use build() or buildAll() instead. """
-
-        # Load the p3dembed data into memory
-        size = p3dembed.getFileSize()
-        p3dembed_data = VirtualFileSystem.getGlobalPtr().readFile(p3dembed, True)
-        assert len(p3dembed_data) == size
-
-        # Find the magic size string and replace it with the real size,
-        # regardless of the endianness of the p3dembed executable.
-        p3dembed_data = p3dembed_data.replace(struct.pack('>I', P3DEMBED_MAGIC),
-                                              struct.pack('>I', size))
-        p3dembed_data = p3dembed_data.replace(struct.pack('<I', P3DEMBED_MAGIC),
-                                              struct.pack('<I', size))
-
-        # Write the output file
-        Standalone.notify.info("Creating %s..." % output)
-        output.makeDir()
-        ohandle = open(output.toOsSpecific(), "wb")
-        ohandle.write(p3dembed_data)
-
-        # Write out the tokens. Set log_basename to the basename by default
-        tokens = {"log_basename": self.basename}
-        tokens.update(self.tokens)
-        tokens.update(extraTokens)
-        for key, value in tokens.items():
-            ohandle.write(b"\0")
-            ohandle.write(key.encode('ascii'))
-            ohandle.write(b"=")
-            ohandle.write(value.encode())
-        ohandle.write(b"\0\0")
-
-        # Buffer the p3d file to the output file. 1 MB buffer size.
-        phandle = open(self.p3dfile.toOsSpecific(), "rb")
-        buf = phandle.read(1024 * 1024)
-        while len(buf) != 0:
-            ohandle.write(buf)
-            buf = phandle.read(1024 * 1024)
-        ohandle.close()
-        phandle.close()
-
-        os.chmod(output.toOsSpecific(), 0o755)
-
-    def getExtraFiles(self, platform):
-        """ Returns a list of extra files that will need to be included
-        with the standalone executable in order for it to run, such as
-        dependent libraries. The returned paths are full absolute paths. """
-
-        package = self.host.getPackages(name = "p3dembed", platform = platform)[0]
-
-        if not package.downloadDescFile(self.http):
-            Standalone.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
-            return []
-        if not package.downloadPackage(self.http):
-            Standalone.notify.warning("  -> %s failed for platform %s" % (package.packageName, package.platform))
-            return []
-
-        filenames = []
-        vfs = VirtualFileSystem.getGlobalPtr()
-        for e in package.extracts:
-            if e.basename not in ["p3dembed", "p3dembed.exe", "p3dembed.exe.manifest", "p3dembedw.exe", "p3dembedw.exe.manifest"]:
-                filename = Filename(package.getPackageDir(), e.filename)
-                filename.makeAbsolute()
-                if vfs.exists(filename):
-                    filenames.append(filename)
-                else:
-                    Standalone.notify.error("%s mentioned in xml, but does not exist" % e.filename)
-
-        return filenames
-
-
-class PackageTree:
-    """ A class used internally to build a temporary package
-    tree for inclusion into an installer. """
-
-    def __init__(self, platform, hostDir, hostUrl):
-        self.platform = platform
-        self.hosts = {}
-        self.packages = {}
-        self.hostUrl = hostUrl
-        self.hostDir = Filename(hostDir)
-        self.hostDir.makeDir()
-        self.http = HTTPClient.getGlobalPtr()
-
-    def getHost(self, hostUrl):
-        if hostUrl in self.hosts:
-            return self.hosts[hostUrl]
-
-        host = HostInfo(hostUrl, appRunner = appRunner, hostDir = self.hostDir, asMirror = False, perPlatform = True)
-        if not host.hasContentsFile:
-            if not host.readContentsFile():
-                if not host.downloadContentsFile(self.http):
-                    Installer.notify.error("couldn't read host %s" % host.hostUrl)
-                    return None
-        self.hosts[hostUrl] = host
-        return host
-
-    def installPackage(self, name, version, hostUrl = None):
-        """ Installs the named package into the tree. """
-
-        if hostUrl is None:
-            hostUrl = self.hostUrl
-
-        pkgIdent = (name, version)
-        if pkgIdent in self.packages:
-            return self.packages[pkgIdent]
-
-        package = None
-        # Always try the super host first, if any.
-        if appRunner and appRunner.superMirrorUrl:
-            superHost = self.getHost(appRunner.superMirrorUrl)
-            if self.platform:
-                package = superHost.getPackage(name, version, self.platform)
-            if not package:
-                package = superHost.getPackage(name, version)
-
-        if not package:
-            host = self.getHost(hostUrl)
-            if self.platform:
-                package = host.getPackage(name, version, self.platform)
-            if not package:
-                package = host.getPackage(name, version)
-
-        if not package:
-            Installer.notify.error("Package %s %s for %s not known on %s" % (
-                name, version, self.platform, hostUrl))
-            return
-
-        package.installed = True # Hack not to let it unnecessarily install itself
-        if not package.downloadDescFile(self.http):
-            Installer.notify.error("  -> %s failed for platform %s" % (package.packageName, package.platform))
-            return
-        if not package.downloadPackage(self.http):
-            Installer.notify.error("  -> %s failed for platform %s" % (package.packageName, package.platform))
-            return
-
-        self.packages[pkgIdent] = package
-
-        # Check for any dependencies.
-        for rname, rversion, rhost in package.requires:
-            self.installPackage(rname, rversion, rhost.hostUrl)
-
-        return package
-
-
-class Icon:
-    """ This class is used to create an icon for various platforms. """
-    notify = directNotify.newCategory("Icon")
-
-    def __init__(self):
-        self.images = {}
-
-    def addImage(self, image):
-        """ Adds an image to the icon.  Returns False on failure, True on success.
-        Only one image per size can be loaded, and the image size must be square. """
-
-        if not isinstance(image, PNMImage):
-            fn = image
-            if not isinstance(fn, Filename):
-                fn = Filename.fromOsSpecific(fn)
-
-            image = PNMImage()
-            if not image.read(fn):
-                Icon.notify.warning("Image '%s' could not be read" % fn.getBasename())
-                return False
-
-        if image.getXSize() != image.getYSize():
-            Icon.notify.warning("Ignoring image without square size")
-            return False
-
-        self.images[image.getXSize()] = image
-
-        return True
-
-    def makeICO(self, fn):
-        """ Writes the images to a Windows ICO file.  Returns True on success. """
-
-        if not isinstance(fn, Filename):
-            fn = Filename.fromOsSpecific(fn)
-        fn.setBinary()
-
-        count = 0
-        for size in self.images.keys():
-            if size <= 256:
-                count += 1
-
-        ico = open(fn, 'wb')
-        ico.write(struct.pack('<HHH', 0, 1, count))
-
-        # Write the directory
-        for size, image in self.images.items():
-            if size == 256:
-                ico.write('\0\0')
-            else:
-                ico.write(struct.pack('<BB', size, size))
-            bpp = 32 if image.hasAlpha() else 24
-            ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, 0, 0))
-
-        # Now write the actual icons
-        ptr = 14
-        for size, image in self.images.items():
-            loc = ico.tell()
-            bpp = 32 if image.hasAlpha() else 24
-            ico.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
-
-            # XOR mask
-            if bpp == 24:
-                # Align rows to 4-byte boundary
-                rowalign = '\0' * (-(size * 3) & 3)
-                for y in xrange(size):
-                    for x in xrange(size):
-                        r, g, b = image.getXel(x, size - y - 1)
-                        ico.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
-                    ico.write(rowalign)
-            else:
-                for y in xrange(size):
-                    for x in xrange(size):
-                        r, g, b, a = image.getXelA(x, size - y - 1)
-                        ico.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
-
-            # Empty AND mask, aligned to 4-byte boundary
-            #TODO: perhaps we should convert alpha into an AND mask
-            # to support older versions of Windows that don't support alpha.
-            ico.write('\0' * (size * (size / 8 + (-((size / 8) * 3) & 3))))
-
-            # Go back to write the location
-            dataend = ico.tell()
-            ico.seek(ptr)
-            ico.write(struct.pack('<II', dataend - loc, loc))
-            ico.seek(dataend)
-            ptr += 16
-
-        ico.close()
-
-        return True
-
-    def makeICNS(self, fn):
-        """ Writes the images to an Apple ICNS file.  Returns True on success. """
-
-        if not isinstance(fn, Filename):
-            fn = Filename.fromOsSpecific(fn)
-        fn.setBinary()
-
-        vfs = VirtualFileSystem.getGlobalPtr()
-        stream = vfs.openWriteFile(fn, False, True)
-        icns = open(stream, 'wb')
-        icns.write(b'icns\0\0\0\0')
-
-        icon_types = {16: 'is32', 32: 'il32', 48: 'ih32', 128: 'it32'}
-        mask_types = {16: 's8mk', 32: 'l8mk', 48: 'h8mk', 128: 't8mk'}
-        png_types = {256: 'ic08', 512: 'ic09'}
-
-        pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
-
-        for size, image in self.images.items():
-            if size in png_types:
-                if pngtype is None:
-                    continue
-                icns.write(png_types[size])
-                icns.write(b'\0\0\0\0')
-                start = icns.tell()
-
-                image.write(stream, "", pngtype)
-                pngsize = icns.tell() - start
-                icns.seek(start - 4)
-                icns.write(struct.pack('>I', pngsize + 8))
-                icns.seek(start + pngsize)
-
-            elif size in icon_types:
-                icns.write(icon_types[size])
-                icns.write(struct.pack('>I', size * size * 4 + 8))
-
-                for y in xrange(size):
-                    for x in xrange(size):
-                        r, g, b = image.getXel(x, y)
-                        icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
-
-                if not image.hasAlpha():
-                    continue
-                icns.write(mask_types[size])
-                icns.write(struct.pack('>I', size * size + 8))
-
-                for y in xrange(size):
-                    for x in xrange(size):
-                        icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
-
-        length = icns.tell()
-        icns.seek(4)
-        icns.write(struct.pack('>I', length))
-        icns.close()
-
-        return True
-
-
-class Installer:
-    """ This class creates a (graphical) installer from a given .p3d file. """
-    notify = directNotify.newCategory("Installer")
-
-    def __init__(self, p3dfile, shortname, fullname, version, tokens = {}):
-        self.p3dFilename = p3dfile
-        if not shortname:
-            shortname = p3dfile.getBasenameWoExtension()
-        self.shortname = shortname
-        self.fullname = fullname
-        self.version = str(version)
-        self.includeRequires = False
-        self.offerRun = True
-        self.offerDesktopShortcut = True
-        self.licensename = ""
-        self.licensefile = Filename()
-        self.authorid = "org.panda3d"
-        self.authorname = os.environ.get("DEBFULLNAME", "")
-        self.authoremail = os.environ.get("DEBEMAIL", "")
-        self.icon = None
-
-        # Try to determine a default author name ourselves.
-        uname = None
-        if pwd is not None and hasattr(os, 'getuid'):
-            uinfo = pwd.getpwuid(os.getuid())
-            if uinfo:
-                uname = uinfo.pw_name
-                if not self.authorname:
-                    self.authorname = \
-                        uinfo.pw_gecos.split(',', 1)[0]
-
-        # Fallbacks in case that didn't work or wasn't supported.
-        if not uname:
-            uname = getpass.getuser()
-        if not self.authorname:
-            self.authorname = uname
-        if not self.authoremail and ' ' not in uname:
-            self.authoremail = "%s@%s" % (uname, socket.gethostname())
-
-        # Load the p3d file to read out the required packages
-        mf = Multifile()
-        if not mf.openRead(self.p3dFilename):
-            Installer.notify.error("Not a Panda3D application: %s" % (p3dfile))
-            return
-
-        # Now load the p3dInfo file.
-        self.hostUrl = None
-        self.requires = []
-        self.extracts = []
-        i = mf.findSubfile('p3d_info.xml')
-        if i >= 0:
-            stream = mf.openReadSubfile(i)
-            p3dInfo = readXmlStream(stream)
-            mf.closeReadSubfile(stream)
-            if p3dInfo:
-                p3dPackage = p3dInfo.FirstChildElement('package')
-                p3dHost = p3dPackage.FirstChildElement('host')
-                if p3dHost.Attribute('url'):
-                    self.hostUrl = p3dHost.Attribute('url')
-                p3dRequires = p3dPackage.FirstChildElement('requires')
-                while p3dRequires:
-                    self.requires.append((
-                        p3dRequires.Attribute('name'),
-                        p3dRequires.Attribute('version'),
-                        p3dRequires.Attribute('host')))
-                    p3dRequires = p3dRequires.NextSiblingElement('requires')
-
-                p3dExtract = p3dPackage.FirstChildElement('extract')
-                while p3dExtract:
-                    filename = p3dExtract.Attribute('filename')
-                    self.extracts.append(filename)
-                    p3dExtract = p3dExtract.NextSiblingElement('extract')
-
-                if not self.fullname:
-                    p3dConfig = p3dPackage.FirstChildElement('config')
-                    if p3dConfig:
-                        self.fullname = p3dConfig.Attribute('display_name')
-        else:
-            Installer.notify.warning("No p3d_info.xml was found in .p3d archive.")
-
-        mf.close()
-
-        if not self.hostUrl:
-            self.hostUrl = PandaSystem.getPackageHostUrl()
-            if not self.hostUrl:
-                self.hostUrl = self.standalone.host.hostUrl
-            Installer.notify.warning("No host URL was specified by .p3d archive.  Falling back to %s" % (self.hostUrl))
-
-        if not self.fullname:
-            self.fullname = self.shortname
-
-        self.tempDir = Filename.temporary("", self.shortname, "") + "/"
-        self.tempDir.makeDir()
-        self.__tempRoots = {}
-
-        if self.extracts:
-            # Copy .p3d to a temporary file so we can remove the extracts.
-            p3dfile = Filename(self.tempDir, self.p3dFilename.getBasename())
-            shutil.copyfile(self.p3dFilename.toOsSpecific(), p3dfile.toOsSpecific())
-            mf = Multifile()
-            if not mf.openReadWrite(p3dfile):
-                Installer.notify.error("Failure to open %s for writing." % (p3dfile))
-
-            # We don't really need this silly thing when embedding, anyway.
-            mf.setHeaderPrefix("")
-
-            for fn in self.extracts:
-                if not mf.removeSubfile(fn):
-                    Installer.notify.error("Failure to remove %s from multifile." % (p3dfile))
-
-            mf.repack()
-            mf.close()
-
-        self.standalone = Standalone(p3dfile, tokens)
-
-    def __del__(self):
-        try:
-            appRunner.rmtree(self.tempDir)
-        except:
-            try: shutil.rmtree(self.tempDir.toOsSpecific())
-            except: pass
-
-    def installPackagesInto(self, hostDir, platform):
-        """ Installs the packages required by the .p3d file into
-        the specified directory, for the given platform. """
-
-        if not self.includeRequires:
-            return
-
-        # Write out the extracts from the original .p3d.
-        if self.extracts:
-            mf = Multifile()
-            if not mf.openRead(self.p3dFilename):
-                Installer.notify.error("Failed to open .p3d archive: %s" % (filename))
-
-            for filename in self.extracts:
-                i = mf.findSubfile(filename)
-                if i < 0:
-                    Installer.notify.error("Cannot find extract in .p3d archive: %s" % (filename))
-                    continue
-
-                if not mf.extractSubfile(i, Filename(hostDir, filename)):
-                    Installer.notify.error("Failed to extract file from .p3d archive: %s" % (filename))
-            mf.close()
-
-        pkgTree = PackageTree(platform, hostDir, self.hostUrl)
-        pkgTree.installPackage("images", None, self.standalone.host.hostUrl)
-
-        for name, version, hostUrl in self.requires:
-            pkgTree.installPackage(name, version, hostUrl)
-
-        # Remove the extracted files from the compressed archive, to save space.
-        vfs = VirtualFileSystem.getGlobalPtr()
-        for package in pkgTree.packages.values():
-            if package.uncompressedArchive:
-                archive = Filename(package.getPackageDir(), package.uncompressedArchive.filename)
-                if not archive.exists():
-                    continue
-
-                mf = Multifile()
-                # Make sure that it isn't mounted before altering it, just to be safe
-                vfs.unmount(archive)
-                os.chmod(archive.toOsSpecific(), 0o644)
-                if not mf.openReadWrite(archive):
-                    Installer.notify.warning("Failed to open archive %s" % (archive))
-                    continue
-
-                # We don't iterate over getNumSubfiles because we're
-                # removing subfiles while we're iterating over them.
-                subfiles = mf.getSubfileNames()
-                for subfile in subfiles:
-                    # We do *NOT* call vfs.exists here in case the package is mounted.
-                    if Filename(package.getPackageDir(), subfile).exists():
-                        Installer.notify.debug("Removing already-extracted %s from multifile" % (subfile))
-                        mf.removeSubfile(subfile)
-
-                # This seems essential for mf.close() not to crash later.
-                mf.repack()
-
-                # If we have no subfiles left, we can just remove the multifile.
-                #XXX rdb: it seems that removing it causes trouble, so let's not.
-                #if mf.getNumSubfiles() == 0:
-                #    Installer.notify.info("Removing empty archive %s" % (package.uncompressedArchive.filename))
-                #    mf.close()
-                #    archive.unlink()
-                #else:
-                mf.close()
-                try: os.chmod(archive.toOsSpecific(), 0o444)
-                except: pass
-
-        # Write out our own contents.xml file.
-        doc = TiXmlDocument()
-        decl = TiXmlDeclaration("1.0", "utf-8", "")
-        doc.InsertEndChild(decl)
-
-        xcontents = TiXmlElement("contents")
-        for package in pkgTree.packages.values():
-            xpackage = TiXmlElement('package')
-            xpackage.SetAttribute('name', package.packageName)
-
-            filename = package.packageName + "/"
-
-            if package.packageVersion:
-                xpackage.SetAttribute('version', package.packageVersion)
-                filename += package.packageVersion + "/"
-
-            if package.platform:
-                xpackage.SetAttribute('platform', package.platform)
-                filename += package.platform + "/"
-                assert package.platform == platform
-
-            xpackage.SetAttribute('per_platform', '1')
-
-            filename += package.descFileBasename
-            xpackage.SetAttribute('filename', filename)
-            xcontents.InsertEndChild(xpackage)
-
-        doc.InsertEndChild(xcontents)
-        doc.SaveFile(Filename(hostDir, "contents.xml").toOsSpecific())
-
-    def buildAll(self, outputDir = "."):
-        """ Creates a (graphical) installer for every known platform.
-        Call this after you have set the desired parameters. """
-
-        platforms = set()
-        for package in self.standalone.host.getPackages(name = "p3dembed"):
-            platforms.add(package.platform)
-        if len(platforms) == 0:
-            Installer.notify.warning("No platforms found to build for!")
-
-        if 'win32' in platforms and 'win_i386' in platforms:
-            platforms.remove('win32')
-
-        outputDir = Filename(outputDir + "/")
-        outputDir.makeDir()
-        for platform in platforms:
-            output = Filename(outputDir, platform + "/")
-            output.makeDir()
-            self.build(output, platform)
-
-    def build(self, output, platform = None):
-        """ Builds (graphical) installers and stores it into the path
-        indicated by the 'output' argument. You can specify to build for
-        a different platform by altering the 'platform' argument.
-        If 'output' is a directory, the installer will be stored in it. """
-
-        if platform == None:
-            platform = PandaSystem.getPlatform()
-
-        if platform.startswith("win"):
-            self.buildNSIS(output, platform)
-            return
-        elif "_" in platform:
-            osname, arch = platform.split("_", 1)
-            if osname == "linux":
-                self.buildDEB(output, platform)
-                self.buildArch(output, platform)
-                return
-            elif osname == "osx":
-                self.buildPKG(output, platform)
-                return
-        Installer.notify.info("Ignoring unknown platform " + platform)
-
-    def __buildTempLinux(self, platform):
-        """ Builds a filesystem for Linux.  Used so that buildDEB,
-        buildRPM and buildArch can share the same temp directory. """
-
-        if platform in self.__tempRoots:
-            return self.__tempRoots[platform]
-
-        tempdir = Filename(self.tempDir, platform)
-        tempdir.makeDir()
-
-        Filename(tempdir, "usr/bin/").makeDir()
-        if self.includeRequires:
-            extraTokens = {"host_dir" : "/usr/lib/" + self.shortname.lower(),
-                           "start_dir" : "/usr/lib/" + self.shortname.lower()}
-        else:
-            extraTokens = {}
-        self.standalone.build(Filename(tempdir, "usr/bin/" + self.shortname.lower()), platform, extraTokens)
-        if not self.licensefile.empty():
-            Filename(tempdir, "usr/share/doc/%s/" % self.shortname.lower()).makeDir()
-            shutil.copyfile(self.licensefile.toOsSpecific(), Filename(tempdir, "usr/share/doc/%s/copyright" % self.shortname.lower()).toOsSpecific())
-            shutil.copyfile(self.licensefile.toOsSpecific(), Filename(tempdir, "usr/share/doc/%s/LICENSE" % self.shortname.lower()).toOsSpecific())
-
-        # Add an image file to /usr/share/pixmaps/
-        iconFile = None
-        if self.icon is not None:
-            iconImage = None
-            if 48 in self.icon.images:
-                iconImage = self.icon.images[48]
-            elif 64 in self.icon.images:
-                iconImage = self.icon.images[64]
-            elif 32 in self.icon.images:
-                iconImage = self.icon.images[32]
-            else:
-                Installer.notify.warning("No suitable icon image for Linux provided, should preferably be 48x48 in size")
-
-            if iconImage is not None:
-                iconFile = Filename(tempdir, "usr/share/pixmaps/%s.png" % self.shortname)
-                iconFile.setBinary()
-                iconFile.makeDir()
-                if not iconImage.write(iconFile):
-                    Installer.notify.warning("Failed to write icon file for Linux")
-                    iconFile.unlink()
-                    iconFile = None
-
-        # Write a .desktop file to /usr/share/applications/
-        desktopFile = Filename(tempdir, "usr/share/applications/%s.desktop" % self.shortname.lower())
-        desktopFile.setText()
-        desktopFile.makeDir()
-        desktop = open(desktopFile.toOsSpecific(), 'w')
-        desktop.write("[Desktop Entry]\n")
-        desktop.write("Name=%s\n" % self.fullname)
-        desktop.write("Exec=%s\n" % self.shortname.lower())
-        if iconFile is not None:
-            desktop.write("Icon=%s\n" % iconFile.getBasename())
-
-        # Set the "Terminal" option based on whether or not a console env is requested
-        cEnv = self.standalone.tokens.get("console_environment", "")
-        if cEnv == "" or int(cEnv) == 0:
-            desktop.write("Terminal=false\n")
-        else:
-            desktop.write("Terminal=true\n")
-
-        desktop.write("Type=Application\n")
-        desktop.close()
-
-        if self.includeRequires or self.extracts:
-            hostDir = Filename(tempdir, "usr/lib/%s/" % self.shortname.lower())
-            hostDir.makeDir()
-            self.installPackagesInto(hostDir, platform)
-
-        totsize = 0
-        for root, dirs, files in self.os_walk(tempdir.toOsSpecific()):
-            for name in files:
-                totsize += os.path.getsize(os.path.join(root, name))
-
-        self.__tempRoots[platform] = (tempdir, totsize)
-        return self.__tempRoots[platform]
-
-    def buildDEB(self, output, platform):
-        """ Builds a .deb archive and stores it in the path indicated
-        by the 'output' argument. It will be built for the architecture
-        specified by the 'arch' argument.
-        If 'output' is a directory, the deb file will be stored in it. """
-
-        arch = platform.rsplit("_", 1)[-1]
-        output = Filename(output)
-        if output.isDirectory():
-            output = Filename(output, "%s_%s_%s.deb" % (self.shortname.lower(), self.version, arch))
-        Installer.notify.info("Creating %s..." % output)
-        modtime = int(time.time())
-
-        # Create a temporary directory and write the launcher and dependencies to it.
-        tempdir, totsize = self.__buildTempLinux(platform)
-
-        # Create a control file in memory.
-        controlfile = BytesIO()
-        if sys.version_info >= (3, 0):
-            cout = TextIOWrapper(controlfile, encoding='utf-8', newline='')
-        else:
-            cout = StringIO()
-
-        cout.write("Package: %s\n" % self.shortname.lower())
-        cout.write("Version: %s\n" % self.version)
-        cout.write("Maintainer: %s <%s>\n" % (self.authorname, self.authoremail))
-        cout.write("Section: games\n")
-        cout.write("Priority: optional\n")
-        cout.write("Architecture: %s\n" % arch)
-        cout.write("Installed-Size: %d\n" % -(-totsize // 1024))
-        cout.write("Description: %s\n" % self.fullname)
-        cout.write("Depends: libc6, libgcc1, libstdc++6, libx11-6\n")
-        cout.flush()
-        if sys.version_info < (3, 0):
-            controlfile.write(cout.getvalue().encode('utf-8'))
-
-        controlinfo = TarInfoRoot("control")
-        controlinfo.mtime = modtime
-        controlinfo.size = controlfile.tell()
-        controlfile.seek(0)
-
-        # Open the deb file and write to it. It's actually
-        # just an AR file, which is very easy to make.
-        if output.exists():
-            output.unlink()
-        debfile = open(output.toOsSpecific(), "wb")
-        debfile.write(b"!<arch>\x0A")
-        pad_mtime = str(modtime).encode().ljust(12, b' ')
-
-        # The first entry is a special file that marks it a .deb.
-        debfile.write(b"debian-binary   ")
-        debfile.write(pad_mtime)
-        debfile.write(b"0     0     100644  4         \x60\x0A")
-        debfile.write(b"2.0\x0A")
-
-        # Write the control.tar.gz to the archive.  We'll leave the
-        # size 0 for now, and go back and fill it in later.
-        debfile.write(b"control.tar.gz  ")
-        debfile.write(pad_mtime)
-        debfile.write(b"0     0     100644  0         \x60\x0A")
-        ctaroffs = debfile.tell()
-        ctarfile = tarfile.open("control.tar.gz", "w:gz", debfile, tarinfo = TarInfoRoot)
-        ctarfile.addfile(controlinfo, controlfile)
-        ctarfile.close()
-        ctarsize = debfile.tell() - ctaroffs
-        if (ctarsize & 1): debfile.write(b"\x0A")
-
-        # Write the data.tar.gz to the archive.  Again, leave size 0.
-        debfile.write(b"data.tar.gz     ")
-        debfile.write(pad_mtime)
-        debfile.write(b"0     0     100644  0         \x60\x0A")
-        dtaroffs = debfile.tell()
-        dtarfile = tarfile.open("data.tar.gz", "w:gz", debfile, tarinfo = TarInfoRoot)
-        dtarfile.add(Filename(tempdir, "usr").toOsSpecific(), "/usr")
-        dtarfile.close()
-        dtarsize = debfile.tell() - dtaroffs
-        if (dtarsize & 1): debfile.write(b"\x0A")
-
-        # Write the correct sizes of the archives.
-        debfile.seek(ctaroffs - 12)
-        debfile.write(str(ctarsize).encode().ljust(10, b' '))
-        debfile.seek(dtaroffs - 12)
-        debfile.write(str(dtarsize).encode().ljust(10, b' '))
-
-        debfile.close()
-
-        return output
-
-    def buildArch(self, output, platform):
-        """ Builds an ArchLinux package and stores it in the path
-        indicated by the 'output' argument. It will be built for the
-        architecture specified by the 'arch' argument.
-        If 'output' is a directory, the deb file will be stored in it. """
-
-        arch = platform.rsplit("_", 1)[-1]
-        assert arch in ("i386", "amd64")
-        arch = {"i386" : "i686", "amd64" : "x86_64"}[arch]
-        pkgver = self.version + "-1"
-
-        output = Filename(output)
-        if output.isDirectory():
-            output = Filename(output, "%s-%s-%s.pkg.tar.gz" % (self.shortname.lower(), pkgver, arch))
-        Installer.notify.info("Creating %s..." % output)
-        modtime = int(time.time())
-
-        # Create a temporary directory and write the launcher and dependencies to it.
-        tempdir, totsize = self.__buildTempLinux(platform)
-
-        # Create a pkginfo file in memory.
-        pkginfo = BytesIO()
-        if sys.version_info >= (3, 0):
-            pout = TextIOWrapper(pkginfo, encoding='utf-8', newline='')
-        else:
-            pout = StringIO()
-
-        pout.write("# Generated using pdeploy\n")
-        pout.write("# %s\n" % time.ctime(modtime))
-        pout.write("pkgname = %s\n" % self.shortname.lower())
-        pout.write("pkgver = %s\n" % pkgver)
-        pout.write("pkgdesc = %s\n" % self.fullname)
-        pout.write("builddate = %s\n" % modtime)
-        pout.write("packager = %s <%s>\n" % (self.authorname, self.authoremail))
-        pout.write("size = %d\n" % totsize)
-        pout.write("arch = %s\n" % arch)
-        if self.licensename != "":
-            pout.write("license = %s\n" % self.licensename)
-        pout.flush()
-        if sys.version_info < (3, 0):
-            pkginfo.write(pout.getvalue().encode('utf-8'))
-
-        pkginfoinfo = TarInfoRoot(".PKGINFO")
-        pkginfoinfo.mtime = modtime
-        pkginfoinfo.size = pkginfo.tell()
-        pkginfo.seek(0)
-
-        # Create the actual package now.
-        pkgfile = tarfile.open(output.toOsSpecific(), "w:gz", tarinfo = TarInfoRoot)
-        pkgfile.addfile(pkginfoinfo, pkginfo)
-        pkgfile.add(tempdir.toOsSpecific(), "/")
-        if not self.licensefile.empty():
-            pkgfile.add(self.licensefile.toOsSpecific(), "/usr/share/licenses/%s/LICENSE" % self.shortname.lower())
-        pkgfile.close()
-
-        return output
-
-    def buildAPP(self, output, platform):
-
-        output = Filename(output)
-        if output.isDirectory() and output.getExtension() != 'app':
-            output = Filename(output, "%s.app" % self.fullname)
-        Installer.notify.info("Creating %s..." % output)
-
-        # Create the executable for the application bundle
-        exefile = Filename(output, "Contents/MacOS/" + self.shortname)
-        exefile.makeDir()
-        if self.includeRequires:
-            extraTokens = {"host_dir": "../Resources", "start_dir": "../Resources"}
-        else:
-            extraTokens = {}
-        self.standalone.build(exefile, platform, extraTokens)
-        hostDir = Filename(output, "Contents/Resources/")
-        hostDir.makeDir()
-        self.installPackagesInto(hostDir, platform)
-
-        hasIcon = False
-        if self.icon is not None:
-            Installer.notify.info("Generating %s.icns..." % self.shortname)
-            hasIcon = self.icon.makeICNS(Filename(hostDir, "%s.icns" % self.shortname))
-
-        # Create the application plist file using Python's plistlib module.
-        plist = {
-            'CFBundleDevelopmentRegion': 'English',
-            'CFBundleDisplayName': self.fullname,
-            'CFBundleExecutable': exefile.getBasename(),
-            'CFBundleIdentifier': '%s.%s' % (self.author, self.shortname),
-            'CFBundleInfoDictionaryVersion': '6.0',
-            'CFBundleName': self.shortname,
-            'CFBundlePackageType': 'APPL',
-            'CFBundleShortVersionString': self.version,
-            'CFBundleVersion': self.version,
-            'LSHasLocalizedDisplayName': False,
-            'NSAppleScriptEnabled': False,
-            'NSPrincipalClass': 'NSApplication',
-        }
-        if hasIcon:
-            plist['CFBundleIconFile'] = self.shortname + '.icns'
-
-        plistlib.writePlist(plist, Filename(output, "Contents/Info.plist").toOsSpecific())
-        return output
-
-    def buildPKG(self, output, platform):
-        appfn = self.buildAPP(output, platform)
-        appname = "/Applications/" + appfn.getBasename()
-        output = Filename(output)
-        if output.isDirectory():
-            output = Filename(output, "%s %s.pkg" % (self.fullname, self.version))
-        Installer.notify.info("Creating %s..." % output)
-
-        Filename(output, "Contents/Resources/en.lproj/").makeDir()
-        if self.licensefile:
-            shutil.copyfile(self.licensefile.toOsSpecific(), Filename(output, "Contents/Resources/License.txt").toOsSpecific())
-        pkginfo = open(Filename(output, "Contents/PkgInfo").toOsSpecific(), "w")
-        pkginfo.write("pmkrpkg1")
-        pkginfo.close()
-        pkginfo = open(Filename(output, "Contents/Resources/package_version").toOsSpecific(), "w")
-        pkginfo.write("major: 1\nminor: 9")
-        pkginfo.close()
-
-        # Although it might make more sense to use Python's plistlib here,
-        # it is not available on non-OSX systems before Python 2.6.
-        plist = open(Filename(output, "Contents/Info.plist").toOsSpecific(), "w")
-        plist.write('<?xml version="1.0" encoding="UTF-8"?>\n')
-        plist.write('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n')
-        plist.write('<plist version="1.0">\n')
-        plist.write('<dict>\n')
-        plist.write('\t<key>CFBundleIdentifier</key>\n')
-        plist.write('\t<string>%s.pkg.%s</string>\n' % (self.authorid, self.shortname))
-        plist.write('\t<key>CFBundleShortVersionString</key>\n')
-        plist.write('\t<string>%s</string>\n' % self.version)
-        plist.write('\t<key>IFMajorVersion</key>\n')
-        plist.write('\t<integer>1</integer>\n')
-        plist.write('\t<key>IFMinorVersion</key>\n')
-        plist.write('\t<integer>9</integer>\n')
-        plist.write('\t<key>IFPkgFlagAllowBackRev</key>\n')
-        plist.write('\t<false/>\n')
-        plist.write('\t<key>IFPkgFlagAuthorizationAction</key>\n')
-        plist.write('\t<string>RootAuthorization</string>\n')
-        plist.write('\t<key>IFPkgFlagDefaultLocation</key>\n')
-        plist.write('\t<string>/</string>\n')
-        plist.write('\t<key>IFPkgFlagFollowLinks</key>\n')
-        plist.write('\t<true/>\n')
-        plist.write('\t<key>IFPkgFlagIsRequired</key>\n')
-        plist.write('\t<false/>\n')
-        plist.write('\t<key>IFPkgFlagOverwritePermissions</key>\n')
-        plist.write('\t<false/>\n')
-        plist.write('\t<key>IFPkgFlagRelocatable</key>\n')
-        plist.write('\t<false/>\n')
-        plist.write('\t<key>IFPkgFlagRestartAction</key>\n')
-        plist.write('\t<string>None</string>\n')
-        plist.write('\t<key>IFPkgFlagRootVolumeOnly</key>\n')
-        plist.write('\t<true/>\n')
-        plist.write('\t<key>IFPkgFlagUpdateInstalledLanguages</key>\n')
-        plist.write('\t<false/>\n')
-        plist.write('\t<key>IFPkgFormatVersion</key>\n')
-        plist.write('\t<real>0.10000000149011612</real>\n')
-        plist.write('\t<key>IFPkgPathMappings</key>\n')
-        plist.write('\t<dict>\n')
-        plist.write('\t\t<key>%s</key>\n' % appname)
-        plist.write('\t\t<string>{pkmk-token-2}</string>\n')
-        plist.write('\t</dict>\n')
-        plist.write('</dict>\n')
-        plist.write('</plist>\n')
-        plist.close()
-
-        plist = open(Filename(output, "Contents/Resources/TokenDefinitions.plist").toOsSpecific(), "w")
-        plist.write('<?xml version="1.0" encoding="UTF-8"?>\n')
-        plist.write('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n')
-        plist.write('<plist version="1.0">\n')
-        plist.write('<dict>\n')
-        plist.write('\t<key>pkmk-token-2</key>\n')
-        plist.write('\t<array>\n')
-        plist.write('\t\t<dict>\n')
-        plist.write('\t\t\t<key>identifier</key>\n')
-        plist.write('\t\t\t<string>%s.%s</string>\n' % (self.authorid, self.shortname))
-        plist.write('\t\t\t<key>path</key>\n')
-        plist.write('\t\t\t<string>%s</string>\n' % appname)
-        plist.write('\t\t\t<key>searchPlugin</key>\n')
-        plist.write('\t\t\t<string>CommonAppSearch</string>\n')
-        plist.write('\t\t</dict>\n')
-        plist.write('\t</array>\n')
-        plist.write('</dict>\n')
-        plist.write('</plist>\n')
-        plist.close()
-
-        plist = open(Filename(output, "Contents/Resources/en.lproj/Description.plist").toOsSpecific(), "w")
-        plist.write('<?xml version="1.0" encoding="UTF-8"?>\n')
-        plist.write('<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n')
-        plist.write('<plist version="1.0">\n')
-        plist.write('<dict>\n')
-        plist.write('\t<key>IFPkgDescriptionDescription</key>\n')
-        plist.write('\t<string></string>\n')
-        plist.write('\t<key>IFPkgDescriptionTitle</key>\n')
-        plist.write('\t<string>%s</string>\n' % self.fullname)
-        plist.write('</dict>\n')
-        plist.write('</plist>\n')
-        plist.close()
-
-        # OS X El Capitan no longer accepts .pax archives - it must be a CPIO archive named .pax.
-        archive = gzip.open(Filename(output, "Contents/Archive.pax.gz").toOsSpecific(), 'wb')
-        self.__ino = 0
-        self.__writeCPIO(archive, appfn, appname)
-        archive.write(b"0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!!\0")
-        archive.close()
-
-        # Put the .pkg into a zipfile
-        zip_fn = Filename(output.getDirname(), "%s %s.pkg.zip" % (self.fullname, self.version))
-        dir = Filename(output.getDirname())
-        dir.makeAbsolute()
-        zip = zipfile.ZipFile(zip_fn.toOsSpecific(), 'w')
-        for root, dirs, files in self.os_walk(output.toOsSpecific()):
-            for name in files:
-                file = Filename.fromOsSpecific(os.path.join(root, name))
-                file.makeAbsolute()
-                file.makeRelativeTo(dir)
-                zip.write(os.path.join(root, name), str(file))
-        zip.close()
-
-        return output
-
-    def __writeCPIO(self, archive, fn, name):
-        """ Adds the given fn under the given name to the CPIO archive. """
-
-        st = os.lstat(fn.toOsSpecific())
-
-        archive.write(b"070707") # magic
-        archive.write(b"000000") # dev
-
-        # Synthesize an inode number, different for each entry.
-        self.__ino += 1
-        archive.write("%06o" % (self.__ino))
-
-        # Determine based on the type which mode to write.
-        if os.path.islink(fn.toOsSpecific()):
-            archive.write("%06o" % (st.st_mode))
-            target = os.path.readlink(fn.toOsSpecific()).encode('utf-8')
-            size = len(target)
-        elif os.path.isdir(fn.toOsSpecific()):
-            archive.write(b"040755")
-            size = 0
-        elif not fn.getExtension():  # Binary file?
-            archive.write(b"100755")
-            size = st.st_size
-        else:
-            archive.write(b"100644")
-            size = st.st_size
-
-        archive.write("000000") # uid (root)
-        archive.write("000000") # gid (wheel)
-        archive.write("%06o" % (st.st_nlink))
-        archive.write("000000") # rdev
-        archive.write("%011o" % (st.st_mtime))
-        archive.write("%06o" % (len(name) + 1))
-        archive.write("%011o" % (size))
-
-        # Write the filename, plus terminating NUL byte.
-        archive.write(name.encode('utf-8'))
-        archive.write(b"\0")
-
-        # Copy the file data to the archive.
-        if os.path.islink(fn.toOsSpecific()):
-            archive.write(target)
-        elif size:
-            handle = open(fn.toOsSpecific(), 'rb')
-            data = handle.read(1024 * 1024)
-            while data:
-                archive.write(data)
-                data = handle.read(1024 * 1024)
-            handle.close()
-
-        # If this is a directory, recurse.
-        if os.path.isdir(fn.toOsSpecific()):
-            for child in os.listdir(fn.toOsSpecific()):
-                self.__writeCPIO(archive, Filename(fn, child), name + "/" + child)
-
-    def buildNSIS(self, output, platform):
-        # Check if we have makensis first
-        makensis = None
-        if (sys.platform.startswith("win")):
-            syspath = os.defpath.split(";") + os.environ["PATH"].split(";")
-            for p in set(syspath):
-                p1 = os.path.join(p, "makensis.exe")
-                p2 = os.path.join(os.path.dirname(p), "nsis", "makensis.exe")
-                if os.path.isfile(p1):
-                    makensis = p1
-                    break
-                elif os.path.isfile(p2):
-                    makensis = p2
-                    break
-            if not makensis:
-                import pandac
-                makensis = os.path.dirname(os.path.dirname(pandac.__file__))
-                makensis = os.path.join(makensis, "nsis", "makensis.exe")
-                if not os.path.isfile(makensis): makensis = None
-        else:
-            for p in os.defpath.split(":") + os.environ["PATH"].split(":"):
-                if os.path.isfile(os.path.join(p, "makensis")):
-                    makensis = os.path.join(p, "makensis")
-
-        if makensis == None:
-            Installer.notify.warning("Makensis utility not found, no Windows installer will be built!")
-            return None
-
-        output = Filename(output)
-        if output.isDirectory():
-            output = Filename(output, "%s %s.exe" % (self.fullname, self.version))
-        Installer.notify.info("Creating %s..." % output)
-        output.makeAbsolute()
-        extrafiles = self.standalone.getExtraFiles(platform)
-
-        exefile = Filename(Filename.getTempDirectory(), self.shortname + ".exe")
-        exefile.unlink()
-        if self.includeRequires:
-            extraTokens = {"host_dir": ".", "start_dir": "."}
-        else:
-            extraTokens = {}
-        self.standalone.build(exefile, platform, extraTokens)
-
-        # Temporary directory to store the hostdir in
-        hostDir = Filename(self.tempDir, platform + "/")
-        if not hostDir.exists():
-            hostDir.makeDir()
-            self.installPackagesInto(hostDir, platform)
-
-        # See if we can generate an icon
-        icofile = None
-        if self.icon is not None:
-            icofile = Filename(Filename.getTempDirectory(), self.shortname + ".ico")
-            icofile.unlink()
-            Installer.notify.info("Generating %s.ico..." % self.shortname)
-            if not self.icon.makeICO(icofile):
-                icofile = None
-
-        # Create the .nsi installer script
-        nsifile = Filename(Filename.getTempDirectory(), self.shortname + ".nsi")
-        nsifile.unlink()
-        nsi = open(nsifile.toOsSpecific(), "w")
-
-        # Some global info
-        nsi.write('Name "%s"\n' % self.fullname)
-        nsi.write('OutFile "%s"\n' % output.toOsSpecific())
-        if platform == 'win_amd64':
-            nsi.write('InstallDir "$PROGRAMFILES64\\%s"\n' % self.fullname)
-        else:
-            nsi.write('InstallDir "$PROGRAMFILES\\%s"\n' % self.fullname)
-        nsi.write('SetCompress auto\n')
-        nsi.write('SetCompressor lzma\n')
-        nsi.write('ShowInstDetails nevershow\n')
-        nsi.write('ShowUninstDetails nevershow\n')
-        nsi.write('InstType "Typical"\n')
-
-        # Tell Vista that we require admin rights
-        nsi.write('RequestExecutionLevel admin\n')
-        nsi.write('\n')
-        if self.offerRun:
-            nsi.write('Function launch\n')
-            nsi.write('  ExecShell "open" "$INSTDIR\\%s.exe"\n' % self.shortname)
-            nsi.write('FunctionEnd\n')
-            nsi.write('\n')
-
-        if self.offerDesktopShortcut:
-            nsi.write('Function desktopshortcut\n')
-            if icofile is None:
-                nsi.write('  CreateShortcut "$DESKTOP\\%s.lnk" "$INSTDIR\\%s.exe"\n' % (self.fullname, self.shortname))
-            else:
-                nsi.write('  CreateShortcut "$DESKTOP\\%s.lnk" "$INSTDIR\\%s.exe" "" "$INSTDIR\\%s.ico"\n' % (self.fullname, self.shortname, self.shortname))
-            nsi.write('FunctionEnd\n')
-            nsi.write('\n')
-
-        nsi.write('!include "MUI2.nsh"\n')
-        nsi.write('!define MUI_ABORTWARNING\n')
-        if self.offerRun:
-            nsi.write('!define MUI_FINISHPAGE_RUN\n')
-            nsi.write('!define MUI_FINISHPAGE_RUN_NOTCHECKED\n')
-            nsi.write('!define MUI_FINISHPAGE_RUN_FUNCTION launch\n')
-            nsi.write('!define MUI_FINISHPAGE_RUN_TEXT "Run %s"\n' % self.fullname)
-        if self.offerDesktopShortcut:
-            nsi.write('!define MUI_FINISHPAGE_SHOWREADME ""\n')
-            nsi.write('!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED\n')
-            nsi.write('!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut"\n')
-            nsi.write('!define MUI_FINISHPAGE_SHOWREADME_FUNCTION desktopshortcut\n')
-        nsi.write('\n')
-        nsi.write('Var StartMenuFolder\n')
-        nsi.write('!insertmacro MUI_PAGE_WELCOME\n')
-        if not self.licensefile.empty():
-            abs = Filename(self.licensefile)
-            abs.makeAbsolute()
-            nsi.write('!insertmacro MUI_PAGE_LICENSE "%s"\n' % abs.toOsSpecific())
-        nsi.write('!insertmacro MUI_PAGE_DIRECTORY\n')
-        nsi.write('!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n')
-        nsi.write('!insertmacro MUI_PAGE_INSTFILES\n')
-        nsi.write('!insertmacro MUI_PAGE_FINISH\n')
-        nsi.write('!insertmacro MUI_UNPAGE_WELCOME\n')
-        nsi.write('!insertmacro MUI_UNPAGE_CONFIRM\n')
-        nsi.write('!insertmacro MUI_UNPAGE_INSTFILES\n')
-        nsi.write('!insertmacro MUI_UNPAGE_FINISH\n')
-        nsi.write('!insertmacro MUI_LANGUAGE "English"\n')
-
-        # This section defines the installer.
-        nsi.write('Section "" SecCore\n')
-        nsi.write('  SetOutPath "$INSTDIR"\n')
-        nsi.write('  File "%s"\n' % exefile.toOsSpecific())
-        if icofile is not None:
-            nsi.write('  File "%s"\n' % icofile.toOsSpecific())
-        for f in extrafiles:
-            nsi.write('  File "%s"\n' % f.toOsSpecific())
-        curdir = ""
-        for root, dirs, files in self.os_walk(hostDir.toOsSpecific()):
-            for name in files:
-                basefile = Filename.fromOsSpecific(os.path.join(root, name))
-                file = Filename(basefile)
-                file.makeAbsolute()
-                file.makeRelativeTo(hostDir)
-                outdir = file.getDirname().replace('/', '\\')
-                if curdir != outdir:
-                    nsi.write('  SetOutPath "$INSTDIR\\%s"\n' % outdir)
-                    curdir = outdir
-                nsi.write('  File "%s"\n' % (basefile.toOsSpecific()))
-        nsi.write('  SetOutPath "$INSTDIR"\n')
-        nsi.write('  WriteUninstaller "$INSTDIR\\Uninstall.exe"\n')
-        nsi.write('  ; Start menu items\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n')
-        nsi.write('    CreateDirectory "$SMPROGRAMS\\$StartMenuFolder"\n')
-        if icofile is None:
-            nsi.write('    CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s.exe"\n' % (self.fullname, self.shortname))
-        else:
-            nsi.write('    CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk" "$INSTDIR\\%s.exe" "" "$INSTDIR\\%s.ico"\n' % (self.fullname, self.shortname, self.shortname))
-        nsi.write('    CreateShortCut "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk" "$INSTDIR\\Uninstall.exe"\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_WRITE_END\n')
-        nsi.write('SectionEnd\n')
-
-        # This section defines the uninstaller.
-        nsi.write('Section Uninstall\n')
-        nsi.write('  Delete "$INSTDIR\\%s.exe"\n' % self.shortname)
-        if icofile is not None:
-            nsi.write('  Delete "$INSTDIR\\%s.ico"\n' % self.shortname)
-        for f in extrafiles:
-            nsi.write('  Delete "%s"\n' % f.getBasename())
-        nsi.write('  Delete "$INSTDIR\\Uninstall.exe"\n')
-        nsi.write('  RMDir /r "$INSTDIR"\n')
-        nsi.write('  ; Desktop icon\n')
-        nsi.write('  Delete "$DESKTOP\\%s.lnk"\n' % self.fullname)
-        nsi.write('  ; Start menu items\n')
-        nsi.write('  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n')
-        nsi.write('  Delete "$SMPROGRAMS\\$StartMenuFolder\\%s.lnk"\n' % self.fullname)
-        nsi.write('  Delete "$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk"\n')
-        nsi.write('  RMDir "$SMPROGRAMS\\$StartMenuFolder"\n')
-        nsi.write('SectionEnd\n')
-        nsi.close()
-
-        cmd = [makensis]
-        for o in ["V2"]:
-            if sys.platform.startswith("win"):
-                cmd.append("/" + o)
-            else:
-                cmd.append("-" + o)
-        cmd.append(nsifile.toOsSpecific())
-        print(cmd)
-        try:
-            retcode = subprocess.call(cmd, shell = False)
-            if retcode != 0:
-                self.notify.warning("Failure invoking NSIS command.")
-            else:
-                nsifile.unlink()
-        except OSError:
-            self.notify.warning("Unable to invoke NSIS command.")
-
-        if icofile is not None:
-            icofile.unlink()
-
-        return output
-
-    def os_walk(self, top):
-        """ Re-implements os.walk().  For some reason the built-in
-        definition is failing on Windows when this is run within a p3d
-        environment!? """
-
-        dirnames = []
-        filenames = []
-
-        dirlist = os.listdir(top)
-        if dirlist:
-            for file in dirlist:
-                path = os.path.join(top, file)
-                if os.path.isdir(path):
-                    dirnames.append(file)
-                else:
-                    filenames.append(file)
-
-        yield (top, dirnames, filenames)
-
-        for dir in dirnames:
-            next = os.path.join(top, dir)
-            for tuple in self.os_walk(next):
-                yield tuple

+ 0 - 246
direct/src/p3d/FileSpec.py

@@ -1,246 +0,0 @@
-__all__ = ["FileSpec"]
-
-import os
-import time
-from panda3d.core import Filename, HashVal, VirtualFileSystem
-
-class FileSpec:
-    """ This class represents a disk file whose hash and size
-    etc. were read from an xml file.  This class provides methods to
-    verify whether the file on disk matches the version demanded by
-    the xml. """
-
-    def __init__(self):
-        self.actualFile = None
-        self.filename = None
-        self.size = 0
-        self.timestamp = 0
-        self.hash = None
-
-    def fromFile(self, packageDir, filename, pathname = None, st = None):
-        """ Reads the file information from the indicated file.  If st
-        is supplied, it is the result of os.stat on the filename. """
-
-        vfs = VirtualFileSystem.getGlobalPtr()
-
-        filename = Filename(filename)
-        if pathname is None:
-            pathname = Filename(packageDir, filename)
-
-        self.filename = str(filename)
-        self.basename = filename.getBasename()
-
-        if st is None:
-            st = os.stat(pathname.toOsSpecific())
-        self.size = st.st_size
-        self.timestamp = int(st.st_mtime)
-
-        self.readHash(pathname)
-
-    def readHash(self, pathname):
-        """ Reads the hash only from the indicated pathname. """
-
-        hv = HashVal()
-        hv.hashFile(pathname)
-        self.hash = hv.asHex()
-
-
-    def loadXml(self, xelement):
-        """ Reads the file information from the indicated XML
-        element. """
-
-        self.filename = xelement.Attribute('filename')
-        self.basename = None
-        if self.filename:
-            self.basename = Filename(self.filename).getBasename()
-
-        size = xelement.Attribute('size')
-        try:
-            self.size = int(size)
-        except:
-            self.size = 0
-
-        timestamp = xelement.Attribute('timestamp')
-        try:
-            self.timestamp = int(timestamp)
-        except:
-            self.timestamp = 0
-
-        self.hash = xelement.Attribute('hash')
-
-    def storeXml(self, xelement):
-        """ Adds the file information to the indicated XML
-        element. """
-
-        if self.filename:
-            xelement.SetAttribute('filename', self.filename)
-        if self.size:
-            xelement.SetAttribute('size', str(self.size))
-        if self.timestamp:
-            xelement.SetAttribute('timestamp', str(int(self.timestamp)))
-        if self.hash:
-            xelement.SetAttribute('hash', self.hash)
-
-    def storeMiniXml(self, xelement):
-        """ Adds the just the "mini" file information--size and
-        hash--to the indicated XML element. """
-
-        if self.size:
-            xelement.SetAttribute('size', str(self.size))
-        if self.hash:
-            xelement.SetAttribute('hash', self.hash)
-
-    def quickVerify(self, packageDir = None, pathname = None,
-                    notify = None, correctSelf = False):
-        """ Performs a quick test to ensure the file has not been
-        modified.  This test is vulnerable to people maliciously
-        attempting to fool the program (by setting datestamps etc.).
-
-        if correctSelf is True, then any discrepency is corrected by
-        updating the appropriate fields internally, making the
-        assumption that the file on disk is the authoritative version.
-
-        Returns true if it is intact, false if it is incorrect.  If
-        correctSelf is true, raises OSError if the self-update is
-        impossible (for instance, because the file does not exist)."""
-
-        if not pathname:
-            pathname = Filename(packageDir, self.filename)
-        try:
-            st = os.stat(pathname.toOsSpecific())
-        except OSError:
-            # If the file is missing, the file fails.
-            if notify:
-                notify.debug("file not found: %s" % (pathname))
-                if correctSelf:
-                    raise
-            return False
-
-        if st.st_size != self.size:
-            # If the size is wrong, the file fails.
-            if notify:
-                notify.debug("size wrong: %s" % (pathname))
-            if correctSelf:
-                self.__correctHash(packageDir, pathname, st, notify)
-            return False
-
-        if int(st.st_mtime) == self.timestamp:
-            # If the size is right and the timestamp is right, the
-            # file passes.
-            if notify:
-                notify.debug("file ok: %s" % (pathname))
-            return True
-
-        if notify:
-            notify.debug("modification time wrong: %s" % (pathname))
-
-        # If the size is right but the timestamp is wrong, the file
-        # soft-fails.  We follow this up with a hash check.
-        if not self.checkHash(packageDir, pathname, st):
-            # Hard fail, the hash is wrong.
-            if notify:
-                notify.debug("hash check wrong: %s" % (pathname))
-                notify.debug("  found %s, expected %s" % (self.actualFile.hash, self.hash))
-            if correctSelf:
-                self.__correctHash(packageDir, pathname, st, notify)
-            return False
-
-        if notify:
-            notify.debug("hash check ok: %s" % (pathname))
-
-        # The hash is OK after all.  Change the file's timestamp back
-        # to what we expect it to be, so we can quick-verify it
-        # successfully next time.
-        if correctSelf:
-            # Or update our own timestamp.
-            self.__correctTimestamp(pathname, st, notify)
-            return False
-        else:
-            self.__updateTimestamp(pathname, st)
-
-        return True
-
-
-    def fullVerify(self, packageDir = None, pathname = None, notify = None):
-        """ Performs a more thorough test to ensure the file has not
-        been modified.  This test is less vulnerable to malicious
-        attacks, since it reads and verifies the entire file.
-
-        Returns true if it is intact, false if it needs to be
-        redownloaded. """
-
-        if not pathname:
-            pathname = Filename(packageDir, self.filename)
-        try:
-            st = os.stat(pathname.toOsSpecific())
-        except OSError:
-            # If the file is missing, the file fails.
-            if notify:
-                notify.debug("file not found: %s" % (pathname))
-            return False
-
-        if st.st_size != self.size:
-            # If the size is wrong, the file fails;
-            if notify:
-                notify.debug("size wrong: %s" % (pathname))
-            return False
-
-        if not self.checkHash(packageDir, pathname, st):
-            # Hard fail, the hash is wrong.
-            if notify:
-                notify.debug("hash check wrong: %s" % (pathname))
-                notify.debug("  found %s, expected %s" % (self.actualFile.hash, self.hash))
-            return False
-
-        if notify:
-            notify.debug("hash check ok: %s" % (pathname))
-
-        # The hash is OK.  If the timestamp is wrong, change it back
-        # to what we expect it to be, so we can quick-verify it
-        # successfully next time.
-        if int(st.st_mtime) != self.timestamp:
-            self.__updateTimestamp(pathname, st)
-
-        return True
-
-    def __updateTimestamp(self, pathname, st):
-        # On Windows, we have to change the file to read-write before
-        # we can successfully update its timestamp.
-        try:
-            os.chmod(pathname.toOsSpecific(), 0o755)
-            os.utime(pathname.toOsSpecific(), (st.st_atime, self.timestamp))
-            os.chmod(pathname.toOsSpecific(), 0o555)
-        except OSError:
-            pass
-
-    def __correctTimestamp(self, pathname, st, notify):
-        """ Corrects the internal timestamp to match the one on
-        disk. """
-        if notify:
-            notify.info("Correcting timestamp of %s to %d (%s)" % (
-                self.filename, st.st_mtime, time.asctime(time.localtime(st.st_mtime))))
-        self.timestamp = int(st.st_mtime)
-
-    def checkHash(self, packageDir, pathname, st):
-        """ Returns true if the file has the expected md5 hash, false
-        otherwise.  As a side effect, stores a FileSpec corresponding
-        to the on-disk file in self.actualFile. """
-
-        fileSpec = FileSpec()
-        fileSpec.fromFile(packageDir, self.filename,
-                          pathname = pathname, st = st)
-        self.actualFile = fileSpec
-
-        return (fileSpec.hash == self.hash)
-
-    def __correctHash(self, packageDir, pathname, st, notify):
-        """ Corrects the internal hash to match the one on disk. """
-        if not self.actualFile:
-            self.checkHash(packageDir, pathname, st)
-
-        if notify:
-            notify.info("Correcting hash %s to %s" % (
-                self.filename, self.actualFile.hash))
-        self.hash = self.actualFile.hash
-        self.size = self.actualFile.size
-        self.timestamp = self.actualFile.timestamp

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff