Ver Fonte

Merge branch 'master' into vulkan

rdb há 6 anos atrás
pai
commit
d564ef5dbc
100 ficheiros alterados com 2804 adições e 8272 exclusões
  1. 2 0
      .github/FUNDING.yml
  2. 57 0
      .github/workflows/ci.yml
  3. 0 1
      .travis.yml
  4. 6 0
      BACKERS.md
  5. 4 4
      README.md
  6. 3 0
      contrib/src/ai/aiBehaviors.h
  7. 29 30
      direct/src/actor/Actor.py
  8. 2 1
      direct/src/cluster/ClusterClient.py
  9. 20 17
      direct/src/cluster/ClusterConfig.py
  10. 16 15
      direct/src/controls/ControlManager.py
  11. 8 6
      direct/src/controls/DevWalker.py
  12. 8 6
      direct/src/controls/GhostWalker.py
  13. 8 6
      direct/src/controls/GravityWalker.py
  14. 20 18
      direct/src/controls/InputState.py
  15. 8 6
      direct/src/controls/NonPhysicsWalker.py
  16. 8 6
      direct/src/controls/ObserverWalker.py
  17. 0 2
      direct/src/controls/PhysicsRoller.py
  18. 8 6
      direct/src/controls/PhysicsWalker.py
  19. 1 1
      direct/src/controls/TwoDWalker.py
  20. 5 689
      direct/src/dcparser/dcClass.cxx
  21. 58 44
      direct/src/dcparser/dcClass.h
  22. 665 0
      direct/src/dcparser/dcClass_ext.cxx
  23. 93 0
      direct/src/dcparser/dcClass_ext.h
  24. 6 313
      direct/src/dcparser/dcField.cxx
  25. 13 15
      direct/src/dcparser/dcField.h
  26. 305 0
      direct/src/dcparser/dcField_ext.cxx
  27. 48 0
      direct/src/dcparser/dcField_ext.h
  28. 0 508
      direct/src/dcparser/dcPacker.cxx
  29. 17 14
      direct/src/dcparser/dcPacker.h
  30. 508 0
      direct/src/dcparser/dcPacker_ext.cxx
  31. 45 0
      direct/src/dcparser/dcPacker_ext.h
  32. 0 42
      direct/src/dcparser/dcPython.h
  33. 3 0
      direct/src/dcparser/p3dcparser_ext_composite.cxx
  34. 1 1
      direct/src/directdevices/DirectDeviceManager.py
  35. 2 2
      direct/src/directdevices/DirectFastrak.py
  36. 5 1
      direct/src/directnotify/DirectNotifyGlobal.py
  37. 2 1
      direct/src/directnotify/LoggerGlobal.py
  38. 6 6
      direct/src/directnotify/Notifier.py
  39. 15 15
      direct/src/directnotify/RotatingLog.py
  40. 0 99
      direct/src/directscripts/DetectPanda3D.js
  41. 0 132
      direct/src/directscripts/RunPanda3D.js
  42. 3 1
      direct/src/directscripts/extract_docs.py
  43. 2 0
      direct/src/directtools/DirectUtil.py
  44. 38 32
      direct/src/directutil/Verify.py
  45. 23 16
      direct/src/dist/FreezeTool.py
  46. 4 0
      direct/src/dist/__init__.py
  47. 67 0
      direct/src/dist/commands.py
  48. 269 0
      direct/src/dist/icon.py
  49. 1 1
      direct/src/distributed/AsyncRequest.py
  50. 2 1
      direct/src/distributed/DistributedObject.py
  51. 2 1
      direct/src/distributed/DistributedObjectBase.py
  52. 9 8
      direct/src/distributed/DoCollectionManager.py
  53. 8 9
      direct/src/distributed/DoHierarchy.py
  54. 1 4
      direct/src/distributed/PyDatagram.py
  55. 1 0
      direct/src/distributed/PyDatagramIterator.py
  56. 1 1
      direct/src/distributed/ServerRepository.py
  57. 4 3
      direct/src/distributed/cConnectionRepository.cxx
  58. 1 1
      direct/src/distributed/cConnectionRepository.h
  59. 4 0
      direct/src/distributed/cDistributedSmoothNodeBase.cxx
  60. 1 2
      direct/src/distributed/cDistributedSmoothNodeBase.h
  61. 4 4
      direct/src/distributed/config_distributed.h
  62. 13 12
      direct/src/fsm/ClassicFSM.py
  63. 37 33
      direct/src/fsm/FSM.py
  64. 3 6
      direct/src/fsm/FourState.py
  65. 12 14
      direct/src/fsm/FourStateAI.py
  66. 3 0
      direct/src/fsm/__init__.py
  67. 5 1
      direct/src/gui/DirectButton.py
  68. 5 1
      direct/src/gui/DirectCheckButton.py
  69. 50 45
      direct/src/gui/DirectDialog.py
  70. 14 8
      direct/src/gui/DirectEntry.py
  71. 25 3
      direct/src/gui/DirectEntryScroll.py
  72. 3 0
      direct/src/gui/DirectFrame.py
  73. 1 1
      direct/src/gui/DirectGui.py
  74. 47 39
      direct/src/gui/DirectGuiBase.py
  75. 3 6
      direct/src/gui/DirectGuiGlobals.py
  76. 5 1
      direct/src/gui/DirectLabel.py
  77. 21 9
      direct/src/gui/DirectOptionMenu.py
  78. 5 1
      direct/src/gui/DirectRadioButton.py
  79. 5 1
      direct/src/gui/DirectScrollBar.py
  80. 14 2
      direct/src/gui/DirectScrolledFrame.py
  81. 31 20
      direct/src/gui/DirectScrolledList.py
  82. 5 1
      direct/src/gui/DirectSlider.py
  83. 5 1
      direct/src/gui/DirectWaitBar.py
  84. 11 6
      direct/src/gui/OnscreenImage.py
  85. 6 2
      direct/src/gui/OnscreenText.py
  86. 1 1
      direct/src/gui/__init__.py
  87. 5 1
      direct/src/interval/ActorInterval.py
  88. 16 20
      direct/src/interval/ParticleInterval.py
  89. 2 0
      direct/src/interval/__init__.py
  90. 1 0
      direct/src/leveleditor/ActionMgr.py
  91. 0 1245
      direct/src/p3d/AppRunner.py
  92. 0 96
      direct/src/p3d/DWBPackageInstaller.py
  93. 0 1379
      direct/src/p3d/DeploymentTools.py
  94. 0 246
      direct/src/p3d/FileSpec.py
  95. 0 751
      direct/src/p3d/HostInfo.py
  96. 0 24
      direct/src/p3d/InstalledHostData.py
  97. 0 30
      direct/src/p3d/InstalledPackageData.py
  98. 0 298
      direct/src/p3d/JavaScript.py
  99. 0 1237
      direct/src/p3d/PackageInfo.py
  100. 0 640
      direct/src/p3d/PackageInstaller.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-10.14]
+    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.4.1/panda3d-1.10.4.1-tools-mac.tar.gz
+        tar -xf panda3d-1.10.4.1-tools-mac.tar.gz
+        mv panda3d-1.10.4.1/thirdparty thirdparty
+        rmdir panda3d-1.10.4.1
+        (cd thirdparty/darwin-libs-a && rm -rf ode openal vrpn openexr assimp 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

+ 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:

+ 6 - 0
BACKERS.md

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

+ 4 - 4
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.4.1/panda3d-1.10.4.1-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:

+ 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;
 

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

@@ -67,7 +67,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 +104,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 +149,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 +1133,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)
@@ -1947,6 +1945,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 +2437,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"

+ 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 - 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

+ 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

+ 23 - 16
direct/src/dist/FreezeTool.py

@@ -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.

+ 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.
+"""

+ 67 - 0
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,6 +234,7 @@ 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',
@@ -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)
@@ -357,6 +369,18 @@ class build_apps(setuptools.Command):
         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]

+ 3 - 0
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']

+ 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:

+ 3 - 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
@@ -158,7 +159,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

+ 0 - 751
direct/src/p3d/HostInfo.py

@@ -1,751 +0,0 @@
-__all__ = ["HostInfo"]
-
-from panda3d.core import HashVal, Filename, PandaSystem, DocumentSpec, Ramfile
-from panda3d.core import HTTPChannel, ConfigVariableInt
-from panda3d import core
-from direct.p3d.PackageInfo import PackageInfo
-from direct.p3d.FileSpec import FileSpec
-from direct.directnotify.DirectNotifyGlobal import directNotify
-import time
-
-class HostInfo:
-    """ This class represents a particular download host serving up
-    Panda3D packages.  It is the Python equivalent of the P3DHost
-    class in the core API. """
-
-    notify = directNotify.newCategory("HostInfo")
-
-    def __init__(self, hostUrl, appRunner = None, hostDir = None,
-                 rootDir = None, asMirror = False, perPlatform = None):
-
-        """ You must specify either an appRunner or a hostDir to the
-        HostInfo constructor.
-
-        If you pass asMirror = True, it means that this HostInfo
-        object is to be used to populate a "mirror" folder, a
-        duplicate (or subset) of the contents hosted by a server.
-        This means when you use this HostInfo to download packages, it
-        will only download the compressed archive file and leave it
-        there.  At the moment, mirror folders do not download old
-        patch files from the server.
-
-        If you pass perPlatform = True, then files are unpacked into a
-        platform-specific directory, which is appropriate when you
-        might be downloading multiple platforms.  The default is
-        perPlatform = False, which means all files are unpacked into
-        the host directory directly, without an intervening
-        platform-specific directory name.  If asMirror is True, then
-        the default is perPlatform = True.
-
-        Note that perPlatform is also restricted by the individual
-        package's specification.  """
-
-        self.__setHostUrl(hostUrl)
-        self.appRunner = appRunner
-        self.rootDir = rootDir
-        if rootDir is None and appRunner:
-            self.rootDir = appRunner.rootDir
-
-        if hostDir and not isinstance(hostDir, Filename):
-            hostDir = Filename.fromOsSpecific(hostDir)
-
-        self.hostDir = hostDir
-        self.asMirror = asMirror
-        self.perPlatform = perPlatform
-        if perPlatform is None:
-            self.perPlatform = asMirror
-
-        # Initially false, this is set true when the contents file is
-        # successfully read.
-        self.hasContentsFile = False
-
-        # This is the time value at which the current contents file is
-        # no longer valid.
-        self.contentsExpiration = 0
-
-        # Contains the md5 hash of the original contents.xml file.
-        self.contentsSpec = FileSpec()
-
-        # descriptiveName will be filled in later, when the
-        # contents file is read.
-        self.descriptiveName = None
-
-        # A list of known mirrors for this host, all URL's guaranteed
-        # to end with a slash.
-        self.mirrors = []
-
-        # A map of keyword -> altHost URL's.  An altHost is different
-        # than a mirror; an altHost is an alternate URL to download a
-        # different (e.g. testing) version of this host's contents.
-        # It is rarely used.
-        self.altHosts = {}
-
-        # This is a dictionary of packages by (name, version).  It
-        # will be filled in when the contents file is read.
-        self.packages = {}
-
-        if self.appRunner and self.appRunner.verifyContents != self.appRunner.P3DVCForce:
-            # Attempt to pre-read the existing contents.xml; maybe it
-            # will be current enough for our purposes.
-            self.readContentsFile()
-
-    def __setHostUrl(self, hostUrl):
-        """ Assigns self.hostUrl, and related values. """
-        self.hostUrl = hostUrl
-
-        if not self.hostUrl:
-            # A special case: the URL will be set later.
-            self.hostUrlPrefix = None
-            self.downloadUrlPrefix = None
-        else:
-            # hostUrlPrefix is the host URL, but it is guaranteed to end
-            # with a slash.
-            self.hostUrlPrefix = hostUrl
-            if self.hostUrlPrefix[-1] != '/':
-                self.hostUrlPrefix += '/'
-
-            # downloadUrlPrefix is the URL prefix that should be used for
-            # everything other than the contents.xml file.  It might be
-            # the same as hostUrlPrefix, but in the case of an
-            # https-protected hostUrl, it will be the cleartext channel.
-            self.downloadUrlPrefix = self.hostUrlPrefix
-
-    def freshenFile(self, http, 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. """
-
-        if fileSpec.quickVerify(pathname = localPathname):
-            # It's good, keep it.
-            return True
-
-        # It's stale, get a new one.
-        doc = None
-        if self.appRunner and self.appRunner.superMirrorUrl:
-            # Use the "super mirror" first.
-            url = core.URLSpec(self.appRunner.superMirrorUrl + fileSpec.filename)
-            self.notify.info("Freshening %s" % (url))
-            doc = http.getDocument(url)
-
-        if not doc or not doc.isValid():
-            # Failing the super mirror, contact the actual host.
-            url = core.URLSpec(self.hostUrlPrefix + fileSpec.filename)
-            self.notify.info("Freshening %s" % (url))
-            doc = http.getDocument(url)
-            if not doc.isValid():
-                return False
-
-        file = Filename.temporary('', 'p3d_')
-        if not doc.downloadToFile(file):
-            # Failed to download.
-            file.unlink()
-            return False
-
-        # Successfully downloaded!
-        localPathname.makeDir()
-        if not file.renameTo(localPathname):
-            # Couldn't move it into place.
-            file.unlink()
-            return False
-
-        if not fileSpec.fullVerify(pathname = localPathname, notify = self.notify):
-            # No good after download.
-            self.notify.info("%s is still no good after downloading." % (url))
-            return False
-
-        return True
-
-    def downloadContentsFile(self, http, redownload = False,
-                             hashVal = None):
-        """ Downloads the contents.xml file for this particular host,
-        synchronously, and then reads it.  Returns true on success,
-        false on failure.  If hashVal is not None, it should be a
-        HashVal object, which will be filled with the hash from the
-        new contents.xml file."""
-
-        if self.hasCurrentContentsFile():
-            # We've already got one.
-            return True
-
-        if self.appRunner and self.appRunner.verifyContents == self.appRunner.P3DVCNever:
-            # Not allowed to.
-            return False
-
-        rf = None
-        if http:
-            if not redownload and self.appRunner and self.appRunner.superMirrorUrl:
-                # We start with the "super mirror", if it's defined.
-                url = self.appRunner.superMirrorUrl + 'contents.xml'
-                request = DocumentSpec(url)
-                self.notify.info("Downloading contents file %s" % (request))
-
-                rf = Ramfile()
-                channel = http.makeChannel(False)
-                channel.getDocument(request)
-                if not channel.downloadToRam(rf):
-                    self.notify.warning("Unable to download %s" % (url))
-                    rf = None
-
-            if not rf:
-                # Then go to the main host, if our super mirror let us
-                # down.
-
-                url = self.hostUrlPrefix + 'contents.xml'
-                # Append a uniquifying query string to the URL to force the
-                # download to go all the way through any caches.  We use the
-                # time in seconds; that's unique enough.
-                url += '?' + str(int(time.time()))
-
-                # We might as well explicitly request the cache to be disabled
-                # too, since we have an interface for that via HTTPChannel.
-                request = DocumentSpec(url)
-                request.setCacheControl(DocumentSpec.CCNoCache)
-
-                self.notify.info("Downloading contents file %s" % (request))
-                statusCode = None
-                statusString = ''
-                for attempt in range(int(ConfigVariableInt('contents-xml-dl-attempts', 3))):
-                    if attempt > 0:
-                        self.notify.info("Retrying (%s)..."%(attempt,))
-                    rf = Ramfile()
-                    channel = http.makeChannel(False)
-                    channel.getDocument(request)
-                    if channel.downloadToRam(rf):
-                        self.notify.info("Successfully downloaded %s" % (url,))
-                        break
-                    else:
-                        rf = None
-                        statusCode = channel.getStatusCode()
-                        statusString = channel.getStatusString()
-                        self.notify.warning("Could not contact download server at %s" % (url,))
-                        self.notify.warning("Status code = %s %s" % (statusCode, statusString))
-
-                if not rf:
-                    self.notify.warning("Unable to download %s" % (url,))
-                    try:
-                        # Something screwed up.
-                        if statusCode == HTTPChannel.SCDownloadOpenError or \
-                           statusCode == HTTPChannel.SCDownloadWriteError:
-                            launcher.setPandaErrorCode(2)
-                        elif statusCode == 404:
-                            # 404 not found
-                            launcher.setPandaErrorCode(5)
-                        elif statusCode < 100:
-                            # statusCode < 100 implies the connection attempt itself
-                            # failed.  This is usually due to firewall software
-                            # interfering.  Apparently some firewall software might
-                            # allow the first connection and disallow subsequent
-                            # connections; how strange.
-                            launcher.setPandaErrorCode(4)
-                        else:
-                            # There are other kinds of failures, but these will
-                            # generally have been caught already by the first test; so
-                            # if we get here there may be some bigger problem.  Just
-                            # give the generic "big problem" message.
-                            launcher.setPandaErrorCode(6)
-                    except NameError as e:
-                        # no launcher
-                        pass
-                    except AttributeError as e:
-                        self.notify.warning("%s" % (str(e),))
-                        pass
-                    return False
-
-        tempFilename = Filename.temporary('', 'p3d_', '.xml')
-        if rf:
-            f = open(tempFilename.toOsSpecific(), 'wb')
-            f.write(rf.getData())
-            f.close()
-            if hashVal:
-                hashVal.hashString(rf.getData())
-
-            if not self.readContentsFile(tempFilename, freshDownload = True):
-                self.notify.warning("Failure reading %s" % (url))
-                tempFilename.unlink()
-                return False
-
-            tempFilename.unlink()
-            return True
-
-        # Couldn't download the file.  Maybe we should look for a
-        # previously-downloaded copy already on disk?
-        return False
-
-    def redownloadContentsFile(self, http):
-        """ Downloads a new contents.xml file in case it has changed.
-        Returns true if the file has indeed changed, false if it has
-        not. """
-        assert self.hasContentsFile
-
-        if self.appRunner and self.appRunner.verifyContents == self.appRunner.P3DVCNever:
-            # Not allowed to.
-            return False
-
-        url = self.hostUrlPrefix + 'contents.xml'
-        self.notify.info("Redownloading %s" % (url))
-
-        # Get the hash of the original file.
-        assert self.hostDir
-        hv1 = HashVal()
-        if self.contentsSpec.hash:
-            hv1.setFromHex(self.contentsSpec.hash)
-        else:
-            filename = Filename(self.hostDir, 'contents.xml')
-            hv1.hashFile(filename)
-
-        # Now download it again.
-        self.hasContentsFile = False
-        hv2 = HashVal()
-        if not self.downloadContentsFile(http, redownload = True,
-                                         hashVal = hv2):
-            return False
-
-        if hv2 == HashVal():
-            self.notify.info("%s didn't actually redownload." % (url))
-            return False
-        elif hv1 != hv2:
-            self.notify.info("%s has changed." % (url))
-            return True
-        else:
-            self.notify.info("%s has not changed." % (url))
-            return False
-
-    def hasCurrentContentsFile(self):
-        """ Returns true if a contents.xml file has been successfully
-        read for this host and is still current, false otherwise. """
-        if not self.appRunner \
-            or self.appRunner.verifyContents == self.appRunner.P3DVCNone \
-            or self.appRunner.verifyContents == self.appRunner.P3DVCNever:
-            # If we're not asking to verify contents, then
-            # contents.xml files never expires.
-            return self.hasContentsFile
-
-        now = int(time.time())
-        return now < self.contentsExpiration and self.hasContentsFile
-
-    def readContentsFile(self, tempFilename = None, freshDownload = False):
-        """ Reads the contents.xml file for this particular host, once
-        it has been downloaded into the indicated temporary file.
-        Returns true on success, false if the contents file is not
-        already on disk or is unreadable.
-
-        If tempFilename is specified, it is the filename read, and it
-        is copied the file into the standard location if it's not
-        there already.  If tempFilename is not specified, the standard
-        filename is read if it is known. """
-
-        if not hasattr(core, 'TiXmlDocument'):
-            return False
-
-        if not tempFilename:
-            if self.hostDir:
-                # If the filename is not specified, we can infer it
-                # if we already know our hostDir
-                hostDir = self.hostDir
-            else:
-                # Otherwise, we have to guess the hostDir.
-                hostDir = self.__determineHostDir(None, self.hostUrl)
-
-            tempFilename = Filename(hostDir, 'contents.xml')
-
-        doc = core.TiXmlDocument(tempFilename.toOsSpecific())
-        if not doc.LoadFile():
-            return False
-
-        xcontents = doc.FirstChildElement('contents')
-        if not xcontents:
-            return False
-
-        maxAge = xcontents.Attribute('max_age')
-        if maxAge:
-            try:
-                maxAge = int(maxAge)
-            except:
-                maxAge = None
-        if maxAge is None:
-            # Default max_age if unspecified (see p3d_plugin.h).
-            from direct.p3d.AppRunner import AppRunner
-            maxAge = AppRunner.P3D_CONTENTS_DEFAULT_MAX_AGE
-
-        # Get the latest possible expiration time, based on the max_age
-        # indication.  Any expiration time later than this is in error.
-        now = int(time.time())
-        self.contentsExpiration = now + maxAge
-
-        if freshDownload:
-            self.contentsSpec.readHash(tempFilename)
-
-            # Update the XML with the new download information.
-            xorig = xcontents.FirstChildElement('orig')
-            while xorig:
-                xcontents.RemoveChild(xorig)
-                xorig = xcontents.FirstChildElement('orig')
-
-            xorig = core.TiXmlElement('orig')
-            self.contentsSpec.storeXml(xorig)
-            xorig.SetAttribute('expiration', str(self.contentsExpiration))
-
-            xcontents.InsertEndChild(xorig)
-
-        else:
-            # Read the download hash and expiration time from the XML.
-            expiration = None
-            xorig = xcontents.FirstChildElement('orig')
-            if xorig:
-                self.contentsSpec.loadXml(xorig)
-                expiration = xorig.Attribute('expiration')
-                if expiration:
-                    try:
-                        expiration = int(expiration)
-                    except:
-                        expiration = None
-            if not self.contentsSpec.hash:
-                self.contentsSpec.readHash(tempFilename)
-
-            if expiration is not None:
-                self.contentsExpiration = min(self.contentsExpiration, expiration)
-
-        # Look for our own entry in the hosts table.
-        if self.hostUrl:
-            self.__findHostXml(xcontents)
-        else:
-            assert self.hostDir
-            self.__findHostXmlForHostDir(xcontents)
-
-        if self.rootDir and not self.hostDir:
-            self.hostDir = self.__determineHostDir(None, self.hostUrl)
-
-        # Get the list of packages available for download and/or import.
-        xpackage = xcontents.FirstChildElement('package')
-        while xpackage:
-            name = xpackage.Attribute('name')
-            platform = xpackage.Attribute('platform')
-            version = xpackage.Attribute('version')
-            try:
-                solo = int(xpackage.Attribute('solo') or '')
-            except ValueError:
-                solo = False
-            try:
-                perPlatform = int(xpackage.Attribute('per_platform') or '')
-            except ValueError:
-                perPlatform = False
-
-            package = self.__makePackage(name, platform, version, solo, perPlatform)
-            package.descFile = FileSpec()
-            package.descFile.loadXml(xpackage)
-            package.setupFilenames()
-
-            package.importDescFile = None
-            ximport = xpackage.FirstChildElement('import')
-            if ximport:
-                package.importDescFile = FileSpec()
-                package.importDescFile.loadXml(ximport)
-
-            xpackage = xpackage.NextSiblingElement('package')
-
-        self.hasContentsFile = True
-
-        # Now save the contents.xml file into the standard location.
-        if self.appRunner and self.appRunner.verifyContents != self.appRunner.P3DVCNever:
-            assert self.hostDir
-            filename = Filename(self.hostDir, 'contents.xml')
-            filename.makeDir()
-            if freshDownload:
-                doc.SaveFile(filename.toOsSpecific())
-            else:
-                if filename != tempFilename:
-                    tempFilename.copyTo(filename)
-
-        return True
-
-    def __findHostXml(self, xcontents):
-        """ Looks for the <host> or <alt_host> entry in the
-        contents.xml that corresponds to the URL that we actually
-        downloaded from. """
-
-        xhost = xcontents.FirstChildElement('host')
-        while xhost:
-            url = xhost.Attribute('url')
-            if url == self.hostUrl:
-                self.readHostXml(xhost)
-                return
-
-            xalthost = xhost.FirstChildElement('alt_host')
-            while xalthost:
-                url = xalthost.Attribute('url')
-                if url == self.hostUrl:
-                    self.readHostXml(xalthost)
-                    return
-                xalthost = xalthost.NextSiblingElement('alt_host')
-
-            xhost = xhost.NextSiblingElement('host')
-
-    def __findHostXmlForHostDir(self, xcontents):
-        """ Looks for the <host> or <alt_host> entry in the
-        contents.xml that corresponds to the host dir that we read the
-        contents.xml from.  This is used when reading a contents.xml
-        file found on disk, as opposed to downloading it from a
-        site. """
-
-        xhost = xcontents.FirstChildElement('host')
-        while xhost:
-            url = xhost.Attribute('url')
-            hostDirBasename = xhost.Attribute('host_dir')
-            hostDir = self.__determineHostDir(hostDirBasename, url)
-            if hostDir == self.hostDir:
-                self.__setHostUrl(url)
-                self.readHostXml(xhost)
-                return
-
-            xalthost = xhost.FirstChildElement('alt_host')
-            while xalthost:
-                url = xalthost.Attribute('url')
-                hostDirBasename = xalthost.Attribute('host_dir')
-                hostDir = self.__determineHostDir(hostDirBasename, url)
-                if hostDir == self.hostDir:
-                    self.__setHostUrl(url)
-                    self.readHostXml(xalthost)
-                    return
-                xalthost = xalthost.NextSiblingElement('alt_host')
-
-            xhost = xhost.NextSiblingElement('host')
-
-    def readHostXml(self, xhost):
-        """ Reads a <host> or <alt_host> entry and applies the data to
-        this object. """
-
-        descriptiveName = xhost.Attribute('descriptive_name')
-        if descriptiveName and not self.descriptiveName:
-            self.descriptiveName = descriptiveName
-
-        hostDirBasename = xhost.Attribute('host_dir')
-        if self.rootDir and not self.hostDir:
-            self.hostDir = self.__determineHostDir(hostDirBasename, self.hostUrl)
-
-        # Get the "download" URL, which is the source from which we
-        # download everything other than the contents.xml file.
-        downloadUrl = xhost.Attribute('download_url')
-        if downloadUrl:
-            self.downloadUrlPrefix = downloadUrl
-            if self.downloadUrlPrefix[-1] != '/':
-                self.downloadUrlPrefix += '/'
-        else:
-            self.downloadUrlPrefix = self.hostUrlPrefix
-
-        xmirror = xhost.FirstChildElement('mirror')
-        while xmirror:
-            url = xmirror.Attribute('url')
-            if url:
-                if url[-1] != '/':
-                    url += '/'
-                if url not in self.mirrors:
-                    self.mirrors.append(url)
-            xmirror = xmirror.NextSiblingElement('mirror')
-
-        xalthost = xhost.FirstChildElement('alt_host')
-        while xalthost:
-            keyword = xalthost.Attribute('keyword')
-            url = xalthost.Attribute('url')
-            if url and keyword:
-                self.altHosts[keyword] = url
-            xalthost = xalthost.NextSiblingElement('alt_host')
-
-    def __makePackage(self, name, platform, version, solo, perPlatform):
-        """ Creates a new PackageInfo entry for the given name,
-        version, and platform.  If there is already a matching
-        PackageInfo, returns it. """
-
-        if not platform:
-            platform = None
-
-        platforms = self.packages.setdefault((name, version or ""), {})
-        package = platforms.get("", None)
-        if not package:
-            package = PackageInfo(self, name, version, platform = platform,
-                                  solo = solo, asMirror = self.asMirror,
-                                  perPlatform = perPlatform)
-            platforms[platform or ""] = package
-
-        return package
-
-    def getPackage(self, name, version, platform = None):
-        """ Returns a PackageInfo that matches the indicated name and
-        version and the indicated platform or the current runtime
-        platform, if one is provided by this host, or None if not. """
-
-        assert self.hasContentsFile
-        platforms = self.packages.get((name, version or ""), {})
-
-        if platform:
-            # In this case, we are looking for a specific platform
-            # only.
-            return platforms.get(platform, None)
-
-        # We are looking for one matching the current runtime
-        # platform.  First, look for a package matching the current
-        # platform exactly.
-        package = platforms.get(PandaSystem.getPlatform(), None)
-
-        # If not found, look for one matching no particular platform.
-        if not package:
-            package = platforms.get("", None)
-
-        return package
-
-    def getPackages(self, name = None, platform = None):
-        """ Returns a list of PackageInfo objects that match the
-        indicated name and/or platform, with no particular regards to
-        version.  If name is None, all packages are returned. """
-
-        assert self.hasContentsFile
-
-        packages = []
-        for (pn, version), platforms in self.packages.items():
-            if name and pn != name:
-                continue
-
-            if not platform:
-                for p2 in platforms:
-                    package = self.getPackage(pn, version, platform = p2)
-                    if package:
-                        packages.append(package)
-            else:
-                package = self.getPackage(pn, version, platform = platform)
-                if package:
-                    packages.append(package)
-
-        return packages
-
-    def getAllPackages(self, includeAllPlatforms = False):
-        """ Returns a list of all available packages provided by this
-        host. """
-
-        result = []
-
-        items = sorted(self.packages.items())
-        for key, platforms in items:
-            if self.perPlatform or includeAllPlatforms:
-                # If we maintain a different answer per platform,
-                # return all of them.
-                pitems = sorted(platforms.items())
-                for pkey, package in pitems:
-                    result.append(package)
-            else:
-                # If we maintain a host for the current platform
-                # only (e.g. a client copy), then return only the
-                # current platform, or no particular platform.
-                package = platforms.get(PandaSystem.getPlatform(), None)
-                if not package:
-                    package = platforms.get("", None)
-
-                if package:
-                    result.append(package)
-
-        return result
-
-    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 getPackage().  Returns
-        the list of packages that were NOT found. """
-
-        packages = packages[:]
-
-        for key, platforms in list(self.packages.items()):
-            for platform, package in list(platforms.items()):
-                if package in packages:
-                    self.__deletePackageFiles(package)
-                    del platforms[platform]
-                    packages.remove(package)
-
-            if not platforms:
-                # If we've removed all the platforms for a given
-                # package, remove the key from the toplevel map.
-                del self.packages[key]
-
-        return packages
-
-    def __deletePackageFiles(self, package):
-        """ Called by deletePackage(), this actually removes the files
-        for the indicated package. """
-
-        if self.appRunner:
-            self.notify.info("Deleting package %s: %s" % (package.packageName, package.getPackageDir()))
-            self.appRunner.rmtree(package.getPackageDir())
-
-            self.appRunner.sendRequest('forget_package', self.hostUrl, package.packageName, package.packageVersion or '')
-
-    def __determineHostDir(self, hostDirBasename, hostUrl):
-        """ Hashes the host URL into a (mostly) unique directory
-        string, which will be the root of the host's install tree.
-        Returns the resulting path, as a Filename.
-
-        This code is duplicated in C++, in
-        P3DHost::determine_host_dir(). """
-
-        if hostDirBasename:
-            # If the contents.xml specified a host_dir parameter, use
-            # it.
-            hostDir = str(self.rootDir) + '/hosts'
-            for component in hostDirBasename.split('/'):
-                if component:
-                    if component[0] == '.':
-                        # Forbid ".foo" or "..".
-                        component = 'x' + component
-                    hostDir += '/'
-                    hostDir += component
-            return Filename(hostDir)
-
-        hostDir = 'hosts/'
-
-        # Look for a server name in the URL.  Including this string in the
-        # directory name makes it friendlier for people browsing the
-        # directory.
-
-        # We could use URLSpec, but we do it by hand instead, to make
-        # it more likely that our hash code will exactly match the
-        # similar logic in P3DHost.
-        p = hostUrl.find('://')
-        hostname = ''
-        if p != -1:
-            start = p + 3
-            end = hostUrl.find('/', start)
-            # Now start .. end is something like "username@host:port".
-
-            at = hostUrl.find('@', start)
-            if at != -1 and at < end:
-                start = at + 1
-
-            colon = hostUrl.find(':', start)
-            if colon != -1 and colon < end:
-                end = colon
-
-            # Now start .. end is just the hostname.
-            hostname = hostUrl[start : end]
-
-        # Now build a hash string of the whole URL.  We'll use MD5 to
-        # get a pretty good hash, with a minimum chance of collision.
-        # Even if there is a hash collision, though, it's not the end
-        # of the world; it just means that both hosts will dump their
-        # packages into the same directory, and they'll fight over the
-        # toplevel contents.xml file.  Assuming they use different
-        # version numbers (which should be safe since they have the
-        # same hostname), there will be minimal redownloading.
-
-        hashSize = 16
-        keepHash = hashSize
-        if hostname:
-            hostDir += hostname + '_'
-
-            # If we successfully got a hostname, we don't really need the
-            # full hash.  We'll keep half of it.
-            keepHash = keepHash // 2
-
-        md = HashVal()
-        md.hashString(hostUrl)
-        hostDir += md.asHex()[:keepHash * 2]
-
-        hostDir = Filename(self.rootDir, hostDir)
-        return hostDir

+ 0 - 24
direct/src/p3d/InstalledHostData.py

@@ -1,24 +0,0 @@
-__all__ = ["InstalledHostData"]
-
-from panda3d.core import URLSpec
-
-class InstalledHostData:
-    """ A list of instances of this class is returned by
-    AppRunner.scanInstalledPackages().  Each of these corresponds to a
-    particular host that has provided packages that have been
-    installed on the local client. """
-
-    def __init__(self, host, dirnode):
-        self.host = host
-        self.pathname = dirnode.pathname
-        self.totalSize = dirnode.getTotalSize()
-        self.packages = []
-
-        if self.host:
-            self.hostUrl = self.host.hostUrl
-            self.descriptiveName = self.host.descriptiveName
-            if not self.descriptiveName:
-                self.descriptiveName = URLSpec(self.hostUrl).getServer()
-        else:
-            self.hostUrl = 'unknown'
-            self.descriptiveName = 'unknown'

+ 0 - 30
direct/src/p3d/InstalledPackageData.py

@@ -1,30 +0,0 @@
-__all__ = ["InstalledPackageData"]
-
-class InstalledPackageData:
-    """ A list of instances of this class is maintained by
-    InstalledHostData (which is in turn returned by
-    AppRunner.scanInstalledPackages()).  Each of these corresponds to
-    a particular package that has been installed on the local
-    client. """
-
-    def __init__(self, package, dirnode):
-        self.package = package
-        self.pathname = dirnode.pathname
-        self.totalSize = dirnode.getTotalSize()
-        self.lastUse = None
-
-        if self.package:
-            self.displayName = self.package.getFormattedName()
-            xusage = self.package.getUsage()
-
-            if xusage:
-                lastUse = xusage.Attribute('last_use')
-                try:
-                    lastUse = int(lastUse or '')
-                except ValueError:
-                    lastUse = None
-                self.lastUse = lastUse
-
-        else:
-            self.displayName = dirnode.pathname.getBasename()
-

+ 0 - 298
direct/src/p3d/JavaScript.py

@@ -1,298 +0,0 @@
-""" This module defines some simple classes and instances which are
-useful when writing code that integrates with JavaScript, especially
-code that runs in a browser via the web plugin. """
-
-__all__ = ["UndefinedObject", "Undefined", "ConcreteStruct", "BrowserObject", "MethodWrapper"]
-
-class UndefinedObject:
-    """ This is a special object that is returned by the browser to
-    represent an "undefined" or "void" value, typically the value for
-    an uninitialized variable or undefined property.  It has no
-    attributes, similar to None, but it is a slightly different
-    concept in JavaScript. """
-
-    def __bool__(self):
-        return False
-
-    __nonzero__ = __bool__ # Python 2
-
-    def __str__(self):
-        return "Undefined"
-
-# In fact, we normally always return this precise instance of the
-# UndefinedObject.
-Undefined = UndefinedObject()
-
-class ConcreteStruct:
-    """ Python objects that inherit from this class are passed to
-    JavaScript as a concrete struct: a mapping from string -> value,
-    with no methods, passed by value.  This can be more optimal than
-    traditional Python objects which are passed by reference,
-    especially for small objects which might be repeatedly referenced
-    on the JavaScript side. """
-
-    def __init__(self):
-        pass
-
-    def getConcreteProperties(self):
-        """ Returns a list of 2-tuples of the (key, value) pairs that
-        are to be passed to the concrete instance.  By default, this
-        returns all properties of the object.  You can override this
-        to restrict the set of properties that are uploaded. """
-
-        return list(self.__dict__.items())
-
-class BrowserObject:
-    """ This class provides the Python wrapper around some object that
-    actually exists in the plugin host's namespace, e.g. a JavaScript
-    or DOM object. """
-
-    def __init__(self, runner, objectId):
-        self.__dict__['_BrowserObject__runner'] = runner
-        self.__dict__['_BrowserObject__objectId'] = objectId
-
-        # This element is filled in by __getattr__; it connects
-        # the object to its parent.
-        self.__dict__['_BrowserObject__childObject'] = (None, None)
-
-        # This is a cache of method names to MethodWrapper objects in
-        # the parent object.
-        self.__dict__['_BrowserObject__methods'] = {}
-
-    def __del__(self):
-        # When the BrowserObject destructs, tell the parent process it
-        # doesn't need to keep around its corresponding P3D_object any
-        # more.
-        self.__runner.dropObject(self.__objectId)
-
-    def __cacheMethod(self, methodName):
-        """ Stores a pointer to the named method on this object, so
-        that the next time __getattr__ is called, it can retrieve the
-        method wrapper without having to query the browser.  This
-        cache assumes that callable methods don't generally come and
-        go on and object.
-
-        The return value is the MethodWrapper object. """
-
-        method = self.__methods.get(methodName, None)
-        if method is None:
-            method = MethodWrapper(self.__runner, self, methodName)
-            self.__methods[methodName] = method
-        return method
-
-    def __str__(self):
-        return self.toString()
-
-    def __bool__(self):
-        return True
-
-    __nonzero__ = __bool__ # Python 2
-
-    def __call__(self, *args, **kw):
-        needsResponse = True
-        if 'needsResponse' in kw:
-            needsResponse = kw['needsResponse']
-            del kw['needsResponse']
-        if kw:
-            raise ArgumentError('Keyword arguments not supported')
-
-        try:
-            parentObj, attribName = self.__childObject
-            if parentObj:
-                # Call it as a method.
-                if parentObj is self.__runner.dom and attribName == 'alert':
-                    # As a special hack, we don't wait for the return
-                    # value from the alert() call, since this is a
-                    # blocking call, and waiting for this could cause
-                    # problems.
-                    needsResponse = False
-
-                if parentObj is self.__runner.dom and attribName == 'eval' and len(args) == 1 and isinstance(args[0], str):
-                    # As another special hack, we make dom.eval() a
-                    # special case, and map it directly into an eval()
-                    # call.  If the string begins with 'void ', we further
-                    # assume we're not waiting for a response.
-                    if args[0].startswith('void '):
-                        needsResponse = False
-                    result = self.__runner.scriptRequest('eval', parentObj, value = args[0], needsResponse = needsResponse)
-                else:
-                    # This is a normal method call.
-                    try:
-                        result = self.__runner.scriptRequest('call', parentObj, propertyName = attribName, value = args, needsResponse = needsResponse)
-                    except EnvironmentError:
-                        # Problem on the call.  Maybe no such method?
-                        raise AttributeError
-
-                # Hey, the method call appears to have succeeded.
-                # Cache the method object on the parent so we won't
-                # have to look up the method wrapper again next time.
-                parentObj.__cacheMethod(attribName)
-
-            else:
-                # Call it as a plain function.
-                result = self.__runner.scriptRequest('call', self, value = args, needsResponse = needsResponse)
-        except EnvironmentError:
-            # Some odd problem on the call.
-            raise TypeError
-
-        return result
-
-    def __getattr__(self, name):
-        """ Remaps attempts to query an attribute, as in obj.attr,
-        into the appropriate calls to query the actual browser object
-        under the hood.  """
-
-        # First check to see if there's a cached method wrapper from a
-        # previous call.
-        method = self.__methods.get(name, None)
-        if method:
-            return method
-
-        # No cache.  Go query the browser for the desired value.
-        try:
-            value = self.__runner.scriptRequest('get_property', self,
-                                                propertyName = name)
-        except EnvironmentError:
-            # Failed to retrieve the attribute.  But maybe there's a
-            # method instead?
-            if self.__runner.scriptRequest('has_method', self, propertyName = name):
-                # Yes, so create a method wrapper for it.
-                return self.__cacheMethod(name)
-
-            raise AttributeError(name)
-
-        if isinstance(value, BrowserObject):
-            # Fill in the parent object association, so __call__ can
-            # properly call a method.  (Javascript needs to know the
-            # method container at the time of the call, and doesn't
-            # store it on the function object.)
-            value.__dict__['_BrowserObject__childObject'] = (self, name)
-
-        return value
-
-    def __setattr__(self, name, value):
-        if name in self.__dict__:
-            self.__dict__[name] = value
-            return
-
-        result = self.__runner.scriptRequest('set_property', self,
-                                             propertyName = name,
-                                             value = value)
-        if not result:
-            raise AttributeError(name)
-
-    def __delattr__(self, name):
-        if name in self.__dict__:
-            del self.__dict__[name]
-            return
-
-        result = self.__runner.scriptRequest('del_property', self,
-                                             propertyName = name)
-        if not result:
-            raise AttributeError(name)
-
-    def __getitem__(self, key):
-        """ Remaps attempts to query an attribute, as in obj['attr'],
-        into the appropriate calls to query the actual browser object
-        under the hood.  Following the JavaScript convention, we treat
-        obj['attr'] almost the same as obj.attr. """
-
-        try:
-            value = self.__runner.scriptRequest('get_property', self,
-                                                propertyName = str(key))
-        except EnvironmentError:
-            # Failed to retrieve the property.  We return IndexError
-            # for numeric keys so we can properly support Python's
-            # iterators, but we return KeyError for string keys to
-            # emulate mapping objects.
-            if isinstance(key, str):
-                raise KeyError(key)
-            else:
-                raise IndexError(key)
-
-        return value
-
-    def __setitem__(self, key, value):
-        result = self.__runner.scriptRequest('set_property', self,
-                                             propertyName = str(key),
-                                             value = value)
-        if not result:
-            if isinstance(key, str):
-                raise KeyError(key)
-            else:
-                raise IndexError(key)
-
-    def __delitem__(self, key):
-        result = self.__runner.scriptRequest('del_property', self,
-                                             propertyName = str(key))
-        if not result:
-            if isinstance(key, str):
-                raise KeyError(key)
-            else:
-                raise IndexError(key)
-
-class MethodWrapper:
-    """ This is a Python wrapper around a property of a BrowserObject
-    that doesn't appear to be a first-class object in the Python
-    sense, but is nonetheless a callable method. """
-
-    def __init__(self, runner, parentObj, objectId):
-        self.__dict__['_MethodWrapper__runner'] = runner
-        self.__dict__['_MethodWrapper__childObject'] = (parentObj, objectId)
-
-    def __str__(self):
-        parentObj, attribName = self.__childObject
-        return "%s.%s" % (parentObj, attribName)
-
-    def __bool__(self):
-        return True
-
-    __nonzero__ = __bool__ # Python 2
-
-    def __call__(self, *args, **kw):
-        needsResponse = True
-        if 'needsResponse' in kw:
-            needsResponse = kw['needsResponse']
-            del kw['needsResponse']
-        if kw:
-            raise ArgumentError('Keyword arguments not supported')
-
-        try:
-            parentObj, attribName = self.__childObject
-            # Call it as a method.
-            if parentObj is self.__runner.dom and attribName == 'alert':
-                # As a special hack, we don't wait for the return
-                # value from the alert() call, since this is a
-                # blocking call, and waiting for this could cause
-                # problems.
-                needsResponse = False
-
-            if parentObj is self.__runner.dom and attribName == 'eval' and len(args) == 1 and isinstance(args[0], str):
-                # As another special hack, we make dom.eval() a
-                # special case, and map it directly into an eval()
-                # call.  If the string begins with 'void ', we further
-                # assume we're not waiting for a response.
-                if args[0].startswith('void '):
-                    needsResponse = False
-                result = self.__runner.scriptRequest('eval', parentObj, value = args[0], needsResponse = needsResponse)
-            else:
-                # This is a normal method call.
-                try:
-                    result = self.__runner.scriptRequest('call', parentObj, propertyName = attribName, value = args, needsResponse = needsResponse)
-                except EnvironmentError:
-                    # Problem on the call.  Maybe no such method?
-                    raise AttributeError
-
-        except EnvironmentError:
-            # Some odd problem on the call.
-            raise TypeError
-
-        return result
-
-    def __setattr__(self, name, value):
-        """ setattr will generally fail on method objects. """
-        raise AttributeError(name)
-
-    def __delattr__(self, name):
-        """ delattr will generally fail on method objects. """
-        raise AttributeError(name)

+ 0 - 1237
direct/src/p3d/PackageInfo.py

@@ -1,1237 +0,0 @@
-__all__ = ["PackageInfo"]
-
-from panda3d.core import Filename, DocumentSpec, Multifile, Decompressor, EUOk, EUSuccess, VirtualFileSystem, Thread, getModelPath, ExecutionEnvironment, PStatCollector, TiXmlDocument, TiXmlDeclaration, TiXmlElement
-import panda3d.core as core
-from direct.p3d.FileSpec import FileSpec
-from direct.p3d.ScanDirectoryNode import ScanDirectoryNode
-from direct.showbase import VFSImporter
-from direct.directnotify.DirectNotifyGlobal import directNotify
-from direct.task.TaskManagerGlobal import taskMgr
-import os
-import sys
-import random
-import time
-import copy
-
-class PackageInfo:
-
-    """ This class represents a downloadable Panda3D package file that
-    can be (or has been) installed into the current runtime.  It is
-    the Python equivalent of the P3DPackage class in the core API. """
-
-    notify = directNotify.newCategory("PackageInfo")
-
-    # Weight factors for computing download progress.  This
-    # attempts to reflect the relative time-per-byte of each of
-    # these operations.
-    downloadFactor = 1
-    uncompressFactor = 0.01
-    unpackFactor = 0.01
-    patchFactor = 0.01
-
-    # These tokens are yielded (not returned) by __downloadFile() and
-    # other InstallStep functions.
-    stepComplete = 1
-    stepFailed = 2
-    restartDownload = 3
-    stepContinue = 4
-
-    UsageBasename = 'usage.xml'
-
-    class InstallStep:
-        """ This class is one step of the installPlan list; it
-        represents a single atomic piece of the installation step, and
-        the relative effort of that piece.  When the plan is executed,
-        it will call the saved function pointer here. """
-        def __init__(self, func, bytes, factor, stepType):
-            self.__funcPtr = func
-            self.bytesNeeded = bytes
-            self.bytesDone = 0
-            self.bytesFactor = factor
-            self.stepType = stepType
-            self.pStatCol = PStatCollector(':App:PackageInstaller:%s' % (stepType))
-
-        def func(self):
-            """ self.__funcPtr(self) will return a generator of
-            tokens.  This function defines a new generator that yields
-            each of those tokens, but wraps each call into the nested
-            generator within a pair of start/stop collector calls. """
-
-            self.pStatCol.start()
-            for token in self.__funcPtr(self):
-                self.pStatCol.stop()
-                yield token
-                self.pStatCol.start()
-
-            # Shouldn't ever get here.
-            self.pStatCol.stop()
-            raise StopIteration
-
-        def getEffort(self):
-            """ Returns the relative amount of effort of this step. """
-            return self.bytesNeeded * self.bytesFactor
-
-        def getProgress(self):
-            """ Returns the progress of this step, in the range
-            0..1. """
-            if self.bytesNeeded == 0:
-                return 1
-            return min(float(self.bytesDone) / float(self.bytesNeeded), 1)
-
-    def __init__(self, host, packageName, packageVersion, platform = None,
-                 solo = False, asMirror = False, perPlatform = False):
-        self.host = host
-        self.packageName = packageName
-        self.packageVersion = packageVersion
-        self.platform = platform
-        self.solo = solo
-        self.asMirror = asMirror
-        self.perPlatform = perPlatform
-
-        # This will be active while we are in the middle of a download
-        # cycle.
-        self.http = None
-
-        # This will be filled in when the host's contents.xml file is
-        # read.
-        self.packageDir = None
-
-        # These will be filled in by HostInfo when the package is read
-        # from contents.xml.
-        self.descFile = None
-        self.importDescFile = None
-
-        # These are filled in when the desc file is successfully read.
-        self.hasDescFile = False
-        self.patchVersion = None
-        self.displayName = None
-        self.guiApp = False
-        self.uncompressedArchive = None
-        self.compressedArchive = None
-        self.extracts = []
-        self.requires = []
-        self.installPlans = None
-
-        # This is updated during downloadPackage().  It is in the
-        # range 0..1.
-        self.downloadProgress = 0
-
-        # This is set true when the package file has been fully
-        # downloaded and unpacked.
-        self.hasPackage = False
-
-        # This is set true when the package has been "installed",
-        # meaning it's been added to the paths and all.
-        self.installed = False
-
-        # This is set true when the package has been updated in this
-        # session, but not yet written to usage.xml.
-        self.updated = False
-        self.diskSpace = None
-
-    def getPackageDir(self):
-        """ Returns the directory in which this package is installed.
-        This may not be known until the host's contents.xml file has
-        been downloaded, which informs us of the host's own install
-        directory. """
-
-        if not self.packageDir:
-            if not self.host.hasContentsFile:
-                if not self.host.readContentsFile():
-                    self.host.downloadContentsFile(self.http)
-
-            # Derive the packageDir from the hostDir.
-            self.packageDir = Filename(self.host.hostDir, self.packageName)
-            if self.packageVersion:
-                self.packageDir = Filename(self.packageDir, self.packageVersion)
-
-            if self.host.perPlatform:
-                # If we're running on a special host that wants us to
-                # include the platform, we include it.
-                includePlatform = True
-            elif self.perPlatform and self.host.appRunner.respectPerPlatform:
-                # Otherwise, if our package spec wants us to include
-                # the platform (and our plugin knows about this), then
-                # we also include it.
-                includePlatform = True
-            else:
-                # Otherwise, we must be running legacy code
-                # somewhere--either an old package or an old
-                # plugin--and we therefore shouldn't include the
-                # platform in the directory hierarchy.
-                includePlatform = False
-
-            if includePlatform and self.platform:
-                self.packageDir = Filename(self.packageDir, self.platform)
-
-        return self.packageDir
-
-    def getDownloadEffort(self):
-        """ Returns the relative amount of effort it will take to
-        download this package.  The units are meaningless, except
-        relative to other packges."""
-
-        if not self.installPlans:
-            return 0
-
-        # Return the size of plan A, assuming it will work.
-        plan = self.installPlans[0]
-        size = sum([step.getEffort() for step in plan])
-
-        return size
-
-    def getPrevDownloadedEffort(self):
-        """ Returns a rough estimate of this package's total download
-        effort, even if it is already downloaded. """
-
-        effort = 0
-        if self.compressedArchive:
-            effort += self.compressedArchive.size * self.downloadFactor
-        if self.uncompressedArchive:
-            effort += self.uncompressedArchive.size * self.uncompressFactor
-        # Don't bother counting unpacking.
-
-        return effort
-
-    def getFormattedName(self):
-        """ Returns the name of this package, for output to the user.
-        This will be the "public" name of the package, as formatted
-        for user consumption; it will include capital letters and
-        spaces where appropriate. """
-
-        if self.displayName:
-            name = self.displayName
-        else:
-            name = self.packageName
-            if self.packageVersion:
-                name += ' %s' % (self.packageVersion)
-
-        if self.patchVersion:
-            name += ' rev %s' % (self.patchVersion)
-
-        return name
-
-
-    def setupFilenames(self):
-        """ This is called by the HostInfo when the package is read
-        from contents.xml, to set up the internal filenames and such
-        that rely on some of the information from contents.xml. """
-
-        dirname, basename = self.descFile.filename.rsplit('/', 1)
-        self.descFileDirname = dirname
-        self.descFileBasename = basename
-
-    def checkStatus(self):
-        """ Checks the current status of the desc file and the package
-        contents on disk. """
-
-        if self.hasPackage:
-            return True
-
-        if not self.hasDescFile:
-            filename = Filename(self.getPackageDir(), self.descFileBasename)
-            if self.descFile.quickVerify(self.getPackageDir(), pathname = filename, notify = self.notify):
-                if self.__readDescFile():
-                    # Successfully read.  We don't need to call
-                    # checkArchiveStatus again, since readDescFile()
-                    # has just done it.
-                    return self.hasPackage
-
-        if self.hasDescFile:
-            if self.__checkArchiveStatus():
-                # It's all good.
-                self.hasPackage = True
-
-        return self.hasPackage
-
-    def hasCurrentDescFile(self):
-        """ Returns true if a desc file file has been successfully
-        read for this package and is still current, false
-        otherwise. """
-
-        if not self.host.hasCurrentContentsFile():
-            return False
-
-        return self.hasDescFile
-
-    def downloadDescFile(self, http):
-        """ Downloads the desc file for this particular package,
-        synchronously, and then reads it.  Returns true on success,
-        false on failure. """
-
-        for token in self.downloadDescFileGenerator(http):
-            if token != self.stepContinue:
-                break
-            Thread.considerYield()
-
-        return (token == self.stepComplete)
-
-    def downloadDescFileGenerator(self, http):
-        """ A generator function that implements downloadDescFile()
-        one piece at a time.  It yields one of stepComplete,
-        stepFailed, or stepContinue. """
-
-        assert self.descFile
-
-        if self.hasDescFile:
-            # We've already got one.
-            yield self.stepComplete; return
-
-        if not self.host.appRunner or self.host.appRunner.verifyContents != self.host.appRunner.P3DVCNever:
-            # We're allowed to download it.
-            self.http = http
-
-            func = lambda step, self = self: self.__downloadFile(
-                None, self.descFile,
-                urlbase = self.descFile.filename,
-                filename = self.descFileBasename)
-            step = self.InstallStep(func, self.descFile.size, self.downloadFactor, 'downloadDesc')
-
-            for token in step.func():
-                if token == self.stepContinue:
-                    yield token
-                else:
-                    break
-
-            while token == self.restartDownload:
-                # Try again.
-                func = lambda step, self = self: self.__downloadFile(
-                    None, self.descFile,
-                    urlbase = self.descFile.filename,
-                    filename = self.descFileBasename)
-                step = self.InstallStep(func, self.descFile.size, self.downloadFactor, 'downloadDesc')
-                for token in step.func():
-                    if token == self.stepContinue:
-                        yield token
-                    else:
-                        break
-
-            if token == self.stepFailed:
-                # Couldn't download the desc file.
-                yield self.stepFailed; return
-
-            assert token == self.stepComplete
-
-            filename = Filename(self.getPackageDir(), self.descFileBasename)
-            # Now that we've written the desc file, make it read-only.
-            os.chmod(filename.toOsSpecific(), 0o444)
-
-        if not self.__readDescFile():
-            # Weird, it passed the hash check, but we still can't read
-            # it.
-            filename = Filename(self.getPackageDir(), self.descFileBasename)
-            self.notify.warning("Failure reading %s" % (filename))
-            yield self.stepFailed; return
-
-        yield self.stepComplete; return
-
-    def __readDescFile(self):
-        """ Reads the desc xml file for this particular package,
-        assuming it's been already downloaded and verified.  Returns
-        true on success, false on failure. """
-
-        if self.hasDescFile:
-            # No need to read it again.
-            return True
-
-        if self.solo:
-            # If this is a "solo" package, we don't actually "read"
-            # the desc file; that's the entire contents of the
-            # package.
-            self.hasDescFile = True
-            self.hasPackage = True
-            return True
-
-        filename = Filename(self.getPackageDir(), self.descFileBasename)
-
-        if not hasattr(core, 'TiXmlDocument'):
-            return False
-        doc = core.TiXmlDocument(filename.toOsSpecific())
-        if not doc.LoadFile():
-            return False
-
-        xpackage = doc.FirstChildElement('package')
-        if not xpackage:
-            return False
-
-        try:
-            self.patchVersion = int(xpackage.Attribute('patch_version') or '')
-        except ValueError:
-            self.patchVersion = None
-
-        try:
-            perPlatform = int(xpackage.Attribute('per_platform') or '')
-        except ValueError:
-            perPlatform = False
-        if perPlatform != self.perPlatform:
-            self.notify.warning("per_platform disagreement on package %s" % (self.packageName))
-
-        self.displayName = None
-        xconfig = xpackage.FirstChildElement('config')
-        if xconfig:
-            # The name for display to an English-speaking user.
-            self.displayName = xconfig.Attribute('display_name')
-
-            # True if any apps that use this package must be GUI apps.
-            guiApp = xconfig.Attribute('gui_app')
-            if guiApp:
-                self.guiApp = int(guiApp)
-
-        # The uncompressed archive, which will be mounted directly,
-        # and also used for patching.
-        xuncompressedArchive = xpackage.FirstChildElement('uncompressed_archive')
-        if xuncompressedArchive:
-            self.uncompressedArchive = FileSpec()
-            self.uncompressedArchive.loadXml(xuncompressedArchive)
-
-        # The compressed archive, which is what is downloaded.
-        xcompressedArchive = xpackage.FirstChildElement('compressed_archive')
-        if xcompressedArchive:
-            self.compressedArchive = FileSpec()
-            self.compressedArchive.loadXml(xcompressedArchive)
-
-        # The list of files that should be extracted to disk.
-        self.extracts = []
-        xextract = xpackage.FirstChildElement('extract')
-        while xextract:
-            file = FileSpec()
-            file.loadXml(xextract)
-            self.extracts.append(file)
-            xextract = xextract.NextSiblingElement('extract')
-
-        # The list of additional packages that must be installed for
-        # this package to function properly.
-        self.requires = []
-        xrequires = xpackage.FirstChildElement('requires')
-        while xrequires:
-            packageName = xrequires.Attribute('name')
-            version = xrequires.Attribute('version')
-            hostUrl = xrequires.Attribute('host')
-            if packageName and hostUrl:
-                host = self.host.appRunner.getHostWithAlt(hostUrl)
-                self.requires.append((packageName, version, host))
-            xrequires = xrequires.NextSiblingElement('requires')
-
-        self.hasDescFile = True
-
-        # Now that we've read the desc file, go ahead and use it to
-        # verify the download status.
-        if self.__checkArchiveStatus():
-            # It's all fully downloaded, unpacked, and ready.
-            self.hasPackage = True
-            return True
-
-        # Still have to download it.
-        self.__buildInstallPlans()
-        return True
-
-    def __buildInstallPlans(self):
-        """ Sets up self.installPlans, a list of one or more "plans"
-        to download and install the package. """
-
-        pc = PStatCollector(':App:PackageInstaller:buildInstallPlans')
-        pc.start()
-
-        self.hasPackage = False
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # We're not allowed to download anything.
-            self.installPlans = []
-            pc.stop()
-            return
-
-        if self.asMirror:
-            # If we're just downloading a mirror archive, we only need
-            # to get the compressed archive file.
-
-            # Build a one-item install plan to download the compressed
-            # archive.
-            downloadSize = self.compressedArchive.size
-            func = lambda step, fileSpec = self.compressedArchive: self.__downloadFile(step, fileSpec, allowPartial = True)
-
-            step = self.InstallStep(func, downloadSize, self.downloadFactor, 'download')
-            installPlan = [step]
-            self.installPlans = [installPlan]
-            pc.stop()
-            return
-
-        # The normal download process.  Determine what we will need to
-        # download, and build a plan (or two) to download it all.
-        self.installPlans = None
-
-        # We know we will at least need to unpack the archive contents
-        # at the end.
-        unpackSize = 0
-        for file in self.extracts:
-            unpackSize += file.size
-        step = self.InstallStep(self.__unpackArchive, unpackSize, self.unpackFactor, 'unpack')
-        planA = [step]
-
-        # If the uncompressed archive file is good, that's all we'll
-        # need to do.
-        self.uncompressedArchive.actualFile = None
-        if self.uncompressedArchive.quickVerify(self.getPackageDir(), notify = self.notify):
-            self.installPlans = [planA]
-            pc.stop()
-            return
-
-        # Maybe the compressed archive file is good.
-        if self.compressedArchive.quickVerify(self.getPackageDir(), notify = self.notify):
-            uncompressSize = self.uncompressedArchive.size
-            step = self.InstallStep(self.__uncompressArchive, uncompressSize, self.uncompressFactor, 'uncompress')
-            planA = [step] + planA
-            self.installPlans = [planA]
-            pc.stop()
-            return
-
-        # Maybe we can download one or more patches.  We'll come back
-        # to that in a minute as plan A.  For now, construct plan B,
-        # which will be to download the whole archive.
-        planB = planA[:]
-
-        uncompressSize = self.uncompressedArchive.size
-        step = self.InstallStep(self.__uncompressArchive, uncompressSize, self.uncompressFactor, 'uncompress')
-        planB = [step] + planB
-
-        downloadSize = self.compressedArchive.size
-        func = lambda step, fileSpec = self.compressedArchive: self.__downloadFile(step, fileSpec, allowPartial = True)
-
-        step = self.InstallStep(func, downloadSize, self.downloadFactor, 'download')
-        planB = [step] + planB
-
-        # Now look for patches.  Start with the md5 hash from the
-        # uncompressedArchive file we have on disk, and see if we can
-        # find a patch chain from this file to our target.
-        pathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
-        fileSpec = self.uncompressedArchive.actualFile
-        if fileSpec is None and pathname.exists():
-            fileSpec = FileSpec()
-            fileSpec.fromFile(self.getPackageDir(), self.uncompressedArchive.filename)
-        plan = None
-        if fileSpec:
-            plan = self.__findPatchChain(fileSpec)
-        if plan:
-            # We can download patches.  Great!  That means this is
-            # plan A, and the full download is plan B (in case
-            # something goes wrong with the patching).
-            planA = plan + planA
-            self.installPlans = [planA, planB]
-        else:
-            # There are no patches to download, oh well.  Stick with
-            # plan B as the only plan.
-            self.installPlans = [planB]
-
-        # In case of unexpected failures on the internet, we will retry
-        # the full download instead of just giving up.
-        retries = core.ConfigVariableInt('package-full-dl-retries', 1).getValue()
-        for retry in range(retries):
-            self.installPlans.append(planB[:])
-
-        pc.stop()
-
-    def __scanDirectoryRecursively(self, dirname):
-        """ Generates a list of Filename objects: all of the files
-        (not directories) within and below the indicated dirname. """
-
-        contents = []
-        for dirpath, dirnames, filenames in os.walk(dirname.toOsSpecific()):
-            dirpath = Filename.fromOsSpecific(dirpath)
-            if dirpath == dirname:
-                dirpath = Filename('')
-            else:
-                dirpath.makeRelativeTo(dirname)
-            for filename in filenames:
-                contents.append(Filename(dirpath, filename))
-        return contents
-
-    def __removeFileFromList(self, contents, filename):
-        """ Removes the indicated filename from the given list, if it is
-        present.  """
-        try:
-            contents.remove(Filename(filename))
-        except ValueError:
-            pass
-
-    def __checkArchiveStatus(self):
-        """ Returns true if the archive and all extractable files are
-        already correct on disk, false otherwise. """
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # Assume that everything is just fine.
-            return True
-
-        # Get a list of all of the files in the directory, so we can
-        # remove files that don't belong.
-        contents = self.__scanDirectoryRecursively(self.getPackageDir())
-        self.__removeFileFromList(contents, self.descFileBasename)
-        self.__removeFileFromList(contents, self.compressedArchive.filename)
-        self.__removeFileFromList(contents, self.UsageBasename)
-        if not self.asMirror:
-            self.__removeFileFromList(contents, self.uncompressedArchive.filename)
-            for file in self.extracts:
-                self.__removeFileFromList(contents, file.filename)
-
-        # Now, any files that are still in the contents list don't
-        # belong.  It's important to remove these files before we
-        # start verifying the files that we expect to find here, in
-        # case there is a problem with ambiguous filenames or
-        # something (e.g. case insensitivity).
-        for filename in contents:
-            self.notify.info("Removing %s" % (filename))
-            pathname = Filename(self.getPackageDir(), filename)
-            pathname.unlink()
-            self.updated = True
-
-        if self.asMirror:
-            return self.compressedArchive.quickVerify(self.getPackageDir(), notify = self.notify)
-
-        allExtractsOk = True
-        if not self.uncompressedArchive.quickVerify(self.getPackageDir(), notify = self.notify):
-            self.notify.debug("File is incorrect: %s" % (self.uncompressedArchive.filename))
-            allExtractsOk = False
-
-        if allExtractsOk:
-            # OK, the uncompressed archive is good; that means there
-            # shouldn't be a compressed archive file here.
-            pathname = Filename(self.getPackageDir(), self.compressedArchive.filename)
-            pathname.unlink()
-
-            for file in self.extracts:
-                if not file.quickVerify(self.getPackageDir(), notify = self.notify):
-                    self.notify.debug("File is incorrect: %s" % (file.filename))
-                    allExtractsOk = False
-                    break
-
-        if allExtractsOk:
-            self.notify.debug("All %s extracts of %s seem good." % (
-                len(self.extracts), self.packageName))
-
-        return allExtractsOk
-
-    def __updateStepProgress(self, step):
-        """ This callback is made from within the several step
-        functions as the download step proceeds.  It updates
-        self.downloadProgress with the current progress, so the caller
-        can asynchronously query this value. """
-
-        size = self.totalPlanCompleted + self.currentStepEffort * step.getProgress()
-        self.downloadProgress = min(float(size) / float(self.totalPlanSize), 1)
-
-    def downloadPackage(self, http):
-        """ Downloads the package file, synchronously, then
-        uncompresses and unpacks it.  Returns true on success, false
-        on failure.
-
-        This assumes that self.installPlans has already been filled
-        in, which will have been done by self.__readDescFile().
-        """
-
-        for token in self.downloadPackageGenerator(http):
-            if token != self.stepContinue:
-                break
-            Thread.considerYield()
-
-        return (token == self.stepComplete)
-
-    def downloadPackageGenerator(self, http):
-        """ A generator function that implements downloadPackage() one
-        piece at a time.  It yields one of stepComplete, stepFailed,
-        or stepContinue. """
-
-        assert self.hasDescFile
-
-        if self.hasPackage:
-            # We've already got one.
-            yield self.stepComplete; return
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # We're not allowed to download anything. Assume it's already downloaded.
-            yield self.stepComplete; return
-
-        # We should have an install plan by the time we get here.
-        assert self.installPlans
-
-        self.http = http
-        for token in self.__followInstallPlans():
-            if token == self.stepContinue:
-                yield token
-            else:
-                break
-
-        while token == self.restartDownload:
-            # Try again.
-            for token in self.downloadDescFileGenerator(http):
-                if token == self.stepContinue:
-                    yield token
-                else:
-                    break
-            if token == self.stepComplete:
-                for token in self.__followInstallPlans():
-                    if token == self.stepContinue:
-                        yield token
-                    else:
-                        break
-
-        if token == self.stepFailed:
-            yield self.stepFailed; return
-
-        assert token == self.stepComplete
-        yield self.stepComplete; return
-
-
-    def __followInstallPlans(self):
-        """ Performs all of the steps in self.installPlans.  Yields
-        one of stepComplete, stepFailed, restartDownload, or
-        stepContinue. """
-
-        if not self.installPlans:
-            self.__buildInstallPlans()
-
-        installPlans = self.installPlans
-        self.installPlans = None
-        for plan in installPlans:
-            self.totalPlanSize = sum([step.getEffort() for step in plan])
-            self.totalPlanCompleted = 0
-            self.downloadProgress = 0
-
-            planFailed = False
-            for step in plan:
-                self.currentStepEffort = step.getEffort()
-
-                for token in step.func():
-                    if token == self.stepContinue:
-                        yield token
-                    else:
-                        break
-
-                if token == self.restartDownload:
-                    yield token
-                if token == self.stepFailed:
-                    planFailed = True
-                    break
-                assert token == self.stepComplete
-
-                self.totalPlanCompleted += self.currentStepEffort
-
-            if not planFailed:
-                # Successfully downloaded!
-                yield self.stepComplete; return
-
-            if taskMgr.destroyed:
-                yield self.stepFailed; return
-
-        # All plans failed.
-        yield self.stepFailed; return
-
-    def __findPatchChain(self, fileSpec):
-        """ Finds the chain of patches that leads from the indicated
-        patch version to the current patch version.  If found,
-        constructs an installPlan that represents the steps of the
-        patch installation; otherwise, returns None. """
-
-        from direct.p3d.PatchMaker import PatchMaker
-
-        patchMaker = PatchMaker(self.getPackageDir())
-        patchChain = patchMaker.getPatchChainToCurrent(self.descFileBasename, fileSpec)
-        if patchChain is None:
-            # No path.
-            patchMaker.cleanup()
-            return None
-
-        plan = []
-        for patchfile in patchChain:
-            downloadSize = patchfile.file.size
-            func = lambda step, fileSpec = patchfile.file: self.__downloadFile(step, fileSpec, allowPartial = True)
-            step = self.InstallStep(func, downloadSize, self.downloadFactor, 'download')
-            plan.append(step)
-
-            patchSize = patchfile.targetFile.size
-            func = lambda step, patchfile = patchfile: self.__applyPatch(step, patchfile)
-            step = self.InstallStep(func, patchSize, self.patchFactor, 'patch')
-            plan.append(step)
-
-        patchMaker.cleanup()
-        return plan
-
-    def __downloadFile(self, step, fileSpec, urlbase = None, filename = None,
-                       allowPartial = False):
-        """ Downloads the indicated file from the host into
-        packageDir.  Yields one of stepComplete, stepFailed,
-        restartDownload, or stepContinue. """
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # We're not allowed to download anything.
-            yield self.stepFailed; return
-
-        self.updated = True
-
-        if not urlbase:
-            urlbase = self.descFileDirname + '/' + fileSpec.filename
-
-        # Build up a list of URL's to try downloading from.  Unlike
-        # the C++ implementation in P3DPackage.cxx, here we build the
-        # URL's in forward order.
-        tryUrls = []
-
-        if self.host.appRunner and self.host.appRunner.superMirrorUrl:
-            # We start with the "super mirror", if it's defined.
-            url = self.host.appRunner.superMirrorUrl + urlbase
-            tryUrls.append((url, False))
-
-        if self.host.mirrors:
-            # Choose two mirrors at random.
-            mirrors = self.host.mirrors[:]
-            for i in range(2):
-                mirror = random.choice(mirrors)
-                mirrors.remove(mirror)
-                url = mirror + urlbase
-                tryUrls.append((url, False))
-                if not mirrors:
-                    break
-
-        # After trying two mirrors and failing (or if there are no
-        # mirrors), go get it from the original host.
-        url = self.host.downloadUrlPrefix + urlbase
-        tryUrls.append((url, False))
-
-        # And finally, if the original host also fails, try again with
-        # a cache-buster.
-        tryUrls.append((url, True))
-
-        for url, cacheBust in tryUrls:
-            request = DocumentSpec(url)
-
-            if cacheBust:
-                # On the last attempt to download a particular file,
-                # we bust through the cache: append a query string to
-                # do this.
-                url += '?' + str(int(time.time()))
-                request = DocumentSpec(url)
-                request.setCacheControl(DocumentSpec.CCNoCache)
-
-            self.notify.info("%s downloading %s" % (self.packageName, url))
-
-            if not filename:
-                filename = fileSpec.filename
-            targetPathname = Filename(self.getPackageDir(), filename)
-            targetPathname.setBinary()
-
-            channel = self.http.makeChannel(False)
-
-            # If there's a previous partial download, attempt to resume it.
-            bytesStarted = 0
-            if allowPartial and not cacheBust and targetPathname.exists():
-                bytesStarted = targetPathname.getFileSize()
-
-            if bytesStarted < 1024*1024:
-                # Not enough bytes downloaded to be worth the risk of
-                # a partial download.
-                bytesStarted = 0
-            elif bytesStarted >= fileSpec.size:
-                # Couldn't possibly be our file.
-                bytesStarted = 0
-
-            if bytesStarted:
-                self.notify.info("Resuming %s after %s bytes already downloaded" % (url, bytesStarted))
-                # Make sure the file is writable.
-                os.chmod(targetPathname.toOsSpecific(), 0o644)
-                channel.beginGetSubdocument(request, bytesStarted, 0)
-            else:
-                # No partial download possible; get the whole file.
-                targetPathname.makeDir()
-                targetPathname.unlink()
-                channel.beginGetDocument(request)
-
-            channel.downloadToFile(targetPathname)
-            while channel.run():
-                if step:
-                    step.bytesDone = channel.getBytesDownloaded() + channel.getFirstByteDelivered()
-                    if step.bytesDone > step.bytesNeeded:
-                        # Oops, too much data.  Might as well abort;
-                        # it's the wrong file.
-                        self.notify.warning("Got more data than expected for download %s" % (url))
-                        break
-
-                    self.__updateStepProgress(step)
-
-                if taskMgr.destroyed:
-                    # If the task manager has been destroyed, we must
-                    # be shutting down.  Get out of here.
-                    self.notify.warning("Task Manager destroyed, aborting %s" % (url))
-                    yield self.stepFailed; return
-
-                yield self.stepContinue
-
-            if step:
-                step.bytesDone = channel.getBytesDownloaded() + channel.getFirstByteDelivered()
-                self.__updateStepProgress(step)
-
-            if not channel.isValid():
-                self.notify.warning("Failed to download %s" % (url))
-
-            elif not fileSpec.fullVerify(self.getPackageDir(), pathname = targetPathname, notify = self.notify):
-                self.notify.warning("After downloading, %s incorrect" % (Filename(fileSpec.filename).getBasename()))
-
-                # This attempt failed.  Maybe the original contents.xml
-                # file is stale.  Try re-downloading it now, just to be
-                # sure.
-                if self.host.redownloadContentsFile(self.http):
-                    # Yes!  Go back and start over from the beginning.
-                    yield self.restartDownload; return
-
-            else:
-                # Success!
-                yield self.stepComplete; return
-
-            # Maybe the mirror is bad.  Go back and try the next
-            # mirror.
-
-        # All attempts failed.  Maybe the original contents.xml file
-        # is stale.  Try re-downloading it now, just to be sure.
-        if self.host.redownloadContentsFile(self.http):
-            # Yes!  Go back and start over from the beginning.
-            yield self.restartDownload; return
-
-        # All mirrors failed; the server (or the internet connection)
-        # must be just fubar.
-        yield self.stepFailed; return
-
-    def __applyPatch(self, step, patchfile):
-        """ Applies the indicated patching in-place to the current
-        uncompressed archive.  The patchfile is removed after the
-        operation.  Yields one of stepComplete, stepFailed,
-        restartDownload, or stepContinue. """
-
-        self.updated = True
-
-        origPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
-        patchPathname = Filename(self.getPackageDir(), patchfile.file.filename)
-        result = Filename.temporary('', 'patch_')
-        self.notify.info("Patching %s with %s" % (origPathname, patchPathname))
-
-        p = core.Patchfile()  # The C++ class
-
-        ret = p.initiate(patchPathname, origPathname, result)
-        if ret == EUSuccess:
-            ret = p.run()
-        while ret == EUOk:
-            step.bytesDone = step.bytesNeeded * p.getProgress()
-            self.__updateStepProgress(step)
-            if taskMgr.destroyed:
-                # If the task manager has been destroyed, we must
-                # be shutting down.  Get out of here.
-                self.notify.warning("Task Manager destroyed, aborting patch %s" % (origPathname))
-                yield self.stepFailed; return
-
-            yield self.stepContinue
-            ret = p.run()
-        del p
-        patchPathname.unlink()
-
-        if ret < 0:
-            self.notify.warning("Patching of %s failed." % (origPathname))
-            result.unlink()
-            yield self.stepFailed; return
-
-        if not result.renameTo(origPathname):
-            self.notify.warning("Couldn't rename %s to %s" % (result, origPathname))
-            yield self.stepFailed; return
-
-        yield self.stepComplete; return
-
-    def __uncompressArchive(self, step):
-        """ Turns the compressed archive into the uncompressed
-        archive.  Yields one of stepComplete, stepFailed,
-        restartDownload, or stepContinue. """
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # We're not allowed to!
-            yield self.stepFailed; return
-
-        self.updated = True
-
-        sourcePathname = Filename(self.getPackageDir(), self.compressedArchive.filename)
-        targetPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
-        targetPathname.unlink()
-        self.notify.info("Uncompressing %s to %s" % (sourcePathname, targetPathname))
-        decompressor = Decompressor()
-        decompressor.initiate(sourcePathname, targetPathname)
-        totalBytes = self.uncompressedArchive.size
-        result = decompressor.run()
-        while result == EUOk:
-            step.bytesDone = int(totalBytes * decompressor.getProgress())
-            self.__updateStepProgress(step)
-            result = decompressor.run()
-            if taskMgr.destroyed:
-                # If the task manager has been destroyed, we must
-                # be shutting down.  Get out of here.
-                self.notify.warning("Task Manager destroyed, aborting decompresss %s" % (sourcePathname))
-                yield self.stepFailed; return
-
-            yield self.stepContinue
-
-        if result != EUSuccess:
-            yield self.stepFailed; return
-
-        step.bytesDone = totalBytes
-        self.__updateStepProgress(step)
-
-        if not self.uncompressedArchive.quickVerify(self.getPackageDir(), notify= self.notify):
-            self.notify.warning("after uncompressing, %s still incorrect" % (
-                self.uncompressedArchive.filename))
-            yield self.stepFailed; return
-
-        # Now that we've verified the archive, make it read-only.
-        os.chmod(targetPathname.toOsSpecific(), 0o444)
-
-        # Now we can safely remove the compressed archive.
-        sourcePathname.unlink()
-        yield self.stepComplete; return
-
-    def __unpackArchive(self, step):
-        """ Unpacks any files in the archive that want to be unpacked
-        to disk.  Yields one of stepComplete, stepFailed,
-        restartDownload, or stepContinue. """
-
-        if not self.extracts:
-            # Nothing to extract.
-            self.hasPackage = True
-            yield self.stepComplete; return
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # We're not allowed to!
-            yield self.stepFailed; return
-
-        self.updated = True
-
-        mfPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
-        self.notify.info("Unpacking %s" % (mfPathname))
-        mf = Multifile()
-        if not mf.openRead(mfPathname):
-            self.notify.warning("Couldn't open %s" % (mfPathname))
-            yield self.stepFailed; return
-
-        allExtractsOk = True
-        step.bytesDone = 0
-        for file in self.extracts:
-            i = mf.findSubfile(file.filename)
-            if i == -1:
-                self.notify.warning("Not in Multifile: %s" % (file.filename))
-                allExtractsOk = False
-                continue
-
-            targetPathname = Filename(self.getPackageDir(), file.filename)
-            targetPathname.setBinary()
-            targetPathname.unlink()
-            if not mf.extractSubfile(i, targetPathname):
-                self.notify.warning("Couldn't extract: %s" % (file.filename))
-                allExtractsOk = False
-                continue
-
-            if not file.quickVerify(self.getPackageDir(), notify = self.notify):
-                self.notify.warning("After extracting, still incorrect: %s" % (file.filename))
-                allExtractsOk = False
-                continue
-
-            # Make sure it's executable, and not writable.
-            os.chmod(targetPathname.toOsSpecific(), 0o555)
-
-            step.bytesDone += file.size
-            self.__updateStepProgress(step)
-            if taskMgr.destroyed:
-                # If the task manager has been destroyed, we must
-                # be shutting down.  Get out of here.
-                self.notify.warning("Task Manager destroyed, aborting unpacking %s" % (mfPathname))
-                yield self.stepFailed; return
-
-            yield self.stepContinue
-
-        if not allExtractsOk:
-            yield self.stepFailed; return
-
-        self.hasPackage = True
-        yield self.stepComplete; return
-
-    def installPackage(self, appRunner):
-        """ Mounts the package and sets up system paths so it becomes
-        available for use.  Returns true on success, false on failure. """
-
-        assert self.hasPackage
-        if self.installed:
-            # Already installed.
-            return True
-        assert self not in appRunner.installedPackages
-
-        mfPathname = Filename(self.getPackageDir(), self.uncompressedArchive.filename)
-        mf = Multifile()
-        if not mf.openRead(mfPathname):
-            self.notify.warning("Couldn't open %s" % (mfPathname))
-            return False
-
-        # We mount it under its actual location on disk.
-        root = self.getPackageDir()
-
-        vfs = VirtualFileSystem.getGlobalPtr()
-        vfs.mount(mf, root, vfs.MFReadOnly)
-
-        # Add this to the Python search path, if it's not already
-        # there.  We have to take a bit of care to check if it's
-        # already there, since there can be some ambiguity in
-        # os-specific path strings.
-        osRoot = self.getPackageDir().toOsSpecific()
-        foundOnPath = False
-        for p in sys.path:
-            if osRoot == p:
-                # Already here, exactly.
-                foundOnPath = True
-                break
-            elif osRoot == Filename.fromOsSpecific(p).toOsSpecific():
-                # Already here, with some futzing.
-                foundOnPath = True
-                break
-
-        if not foundOnPath:
-            # Not already here; add it.
-            sys.path.append(osRoot)
-
-        # Put it on the model-path, too.  We do this indiscriminantly,
-        # because the Panda3D runtime won't be adding things to the
-        # model-path, so it shouldn't be already there.
-        getModelPath().appendDirectory(self.getPackageDir())
-
-        # Set the environment variable to reference the package root.
-        envvar = '%s_ROOT' % (self.packageName.upper())
-        ExecutionEnvironment.setEnvironmentVariable(envvar, osRoot)
-
-        # Add the package root to the system paths.
-        if sys.platform.startswith('win'):
-            path = os.environ.get('PATH', '')
-            os.environ['PATH'] = "%s;%s" % (osRoot, path)
-        else:
-            path = os.environ.get('PATH', '')
-            os.environ['PATH'] = "%s:%s" % (osRoot, path)
-            path = os.environ.get('LD_LIBRARY_PATH', '')
-            os.environ['LD_LIBRARY_PATH'] = "%s:%s" % (osRoot, path)
-
-        if sys.platform == "darwin":
-            path = os.environ.get('DYLD_LIBRARY_PATH', '')
-            os.environ['DYLD_LIBRARY_PATH'] = "%s:%s" % (osRoot, path)
-
-        # Now that the environment variable is set, read all of the
-        # prc files in the package.
-        appRunner.loadMultifilePrcFiles(mf, self.getPackageDir())
-
-        # Also, find any toplevel Python packages, and add these as
-        # shared packages.  This will allow different packages
-        # installed in different directories to share Python files as
-        # if they were all in the same directory.
-        for filename in mf.getSubfileNames():
-            if filename.endswith('/__init__.pyc') or \
-               filename.endswith('/__init__.pyo') or \
-               filename.endswith('/__init__.py'):
-                components = filename.split('/')[:-1]
-                moduleName = '.'.join(components)
-                VFSImporter.sharedPackages[moduleName] = True
-
-        # Fix up any shared directories so we can load packages from
-        # disparate locations.
-        VFSImporter.reloadSharedPackages()
-
-        self.installed = True
-        appRunner.installedPackages.append(self)
-
-        self.markUsed()
-
-        return True
-
-    def __measureDiskSpace(self):
-        """ Returns the amount of space used by this package, in
-        bytes, as determined by examining the actual contents of the
-        package directory and its subdirectories. """
-
-        thisDir = ScanDirectoryNode(self.getPackageDir(), ignoreUsageXml = True)
-        diskSpace = thisDir.getTotalSize()
-        self.notify.info("Package %s uses %s MB" % (
-            self.packageName, (diskSpace + 524288) // 1048576))
-        return diskSpace
-
-    def markUsed(self):
-        """ Marks the package as having been used.  This is normally
-        called automatically by installPackage(). """
-
-        if not hasattr(core, 'TiXmlDocument'):
-            return
-
-        if self.host.appRunner and self.host.appRunner.verifyContents == self.host.appRunner.P3DVCNever:
-            # Not allowed to write any files to the package directory.
-            return
-
-        if self.updated:
-            # If we've just installed a new version of the package,
-            # re-measure the actual disk space used.
-            self.diskSpace = self.__measureDiskSpace()
-
-        filename = Filename(self.getPackageDir(), self.UsageBasename)
-        doc = TiXmlDocument(filename.toOsSpecific())
-        if not doc.LoadFile():
-            decl = TiXmlDeclaration("1.0", "utf-8", "")
-            doc.InsertEndChild(decl)
-
-        xusage = doc.FirstChildElement('usage')
-        if not xusage:
-            doc.InsertEndChild(TiXmlElement('usage'))
-            xusage = doc.FirstChildElement('usage')
-
-        now = int(time.time())
-
-        count = xusage.Attribute('count_app')
-        try:
-            count = int(count or '')
-        except ValueError:
-            count = 0
-            xusage.SetAttribute('first_use', str(now))
-        count += 1
-        xusage.SetAttribute('count_app', str(count))
-
-        xusage.SetAttribute('last_use', str(now))
-
-        if self.updated:
-            xusage.SetAttribute('last_update', str(now))
-            self.updated = False
-        else:
-            # Since we haven't changed the disk space, we can just
-            # read it from the previous xml file.
-            diskSpace = xusage.Attribute('disk_space')
-            try:
-                diskSpace = int(diskSpace or '')
-            except ValueError:
-                # Unless it wasn't set already.
-                self.diskSpace = self.__measureDiskSpace()
-
-        xusage.SetAttribute('disk_space', str(self.diskSpace))
-
-        # 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.getPackageDir()), '.xml')
-        if doc.SaveFile(tfile.toOsSpecific()):
-            tfile.renameTo(filename)
-
-    def getUsage(self):
-        """ Returns the xusage element that is read from the usage.xml
-        file, or None if there is no usage.xml file. """
-
-        if not hasattr(core, 'TiXmlDocument'):
-            return None
-
-        filename = Filename(self.getPackageDir(), self.UsageBasename)
-        doc = TiXmlDocument(filename.toOsSpecific())
-        if not doc.LoadFile():
-            return None
-
-        xusage = doc.FirstChildElement('usage')
-        if not xusage:
-            return None
-
-        return copy.copy(xusage)
-

+ 0 - 640
direct/src/p3d/PackageInstaller.py

@@ -1,640 +0,0 @@
-__all__ = ["PackageInstaller"]
-
-from direct.showbase.DirectObject import DirectObject
-from direct.stdpy.threading import Lock, RLock
-from direct.showbase.MessengerGlobal import messenger
-from direct.task.TaskManagerGlobal import taskMgr
-from direct.p3d.PackageInfo import PackageInfo
-from panda3d.core import TPLow, PStatCollector
-from direct.directnotify.DirectNotifyGlobal import directNotify
-
-class PackageInstaller(DirectObject):
-
-    """ This class is used in a p3d runtime environment to manage the
-    asynchronous download and installation of packages.  If you just
-    want to install a package synchronously, see
-    appRunner.installPackage() for a simpler interface.
-
-    To use this class, you should subclass from it and override any of
-    the six callback methods: downloadStarted(), packageStarted(),
-    packageProgress(), downloadProgress(), packageFinished(),
-    downloadFinished().
-
-    Also see DWBPackageInstaller, which does exactly this, to add a
-    DirectWaitBar GUI.
-
-    """
-
-    notify = directNotify.newCategory("PackageInstaller")
-
-    globalLock = Lock()
-    nextUniqueId = 1
-
-    # This is a chain of state values progressing forward in time.
-    S_initial = 0    # addPackage() calls are being made
-    S_ready = 1      # donePackages() has been called
-    S_started = 2    # download has started
-    S_done = 3       # download is over
-
-    class PendingPackage:
-        """ This class describes a package added to the installer for
-        download. """
-
-        notify = directNotify.newCategory("PendingPackage")
-
-        def __init__(self, packageName, version, host):
-            self.packageName = packageName
-            self.version = version
-            self.host = host
-
-            # This will be filled in properly by checkDescFile() or
-            # getDescFile(); in the meantime, set a placeholder.
-            self.package = PackageInfo(host, packageName, version)
-
-            # Set true when the package has finished downloading,
-            # either successfully or unsuccessfully.
-            self.done = False
-
-            # Set true or false when self.done has been set.
-            self.success = False
-
-            # Set true when the packageFinished() callback has been
-            # delivered.
-            self.notified = False
-
-            # These are used to ensure the callbacks only get
-            # delivered once for a particular package.
-            self.calledPackageStarted = False
-            self.calledPackageFinished = False
-
-            # This is the amount of stuff we have to process to
-            # install this package, and the amount of stuff we have
-            # processed so far.  "Stuff" includes bytes downloaded,
-            # bytes uncompressed, and bytes extracted; and each of
-            # which is weighted differently into one grand total.  So,
-            # the total doesn't really represent bytes; it's a
-            # unitless number, which means something only as a ratio
-            # to other packages.  Filled in by checkDescFile() or
-            # getDescFile().
-            self.downloadEffort = 0
-
-            # Similar, but this is the theoretical effort if the
-            # package were already downloaded.
-            self.prevDownloadedEffort = 0
-
-        def __cmp__(self, pp):
-            """ Python comparision function.  This makes all
-            PendingPackages withe same (packageName, version, host)
-            combination be deemed equivalent. """
-            return cmp((self.packageName, self.version, self.host),
-                       (pp.packageName, pp.version, pp.host))
-
-        def getProgress(self):
-            """ Returns the download progress of this package in the
-            range 0..1. """
-
-            return self.package.downloadProgress
-
-        def checkDescFile(self):
-            """ Returns true if the desc file is already downloaded
-            and good, or false if it needs to be downloaded. """
-
-            if not self.host.hasCurrentContentsFile():
-                # If the contents file isn't ready yet, we can't check
-                # the desc file yet.
-                return False
-
-            # All right, get the package info now.
-            package = self.host.getPackage(self.packageName, self.version)
-            if not package:
-                self.notify.warning("Package %s %s not known on %s" % (
-                    self.packageName, self.version, self.host.hostUrl))
-                return False
-
-            self.package = package
-            self.package.checkStatus()
-
-            if not self.package.hasDescFile:
-                return False
-
-            self.downloadEffort = self.package.getDownloadEffort()
-            self.prevDownloadEffort = 0
-            if self.downloadEffort == 0:
-                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort()
-
-            return True
-
-
-        def getDescFile(self, http):
-            """ Synchronously downloads the desc files required for
-            the package. """
-
-            if not self.host.downloadContentsFile(http):
-                return False
-
-            # All right, get the package info now.
-            package = self.host.getPackage(self.packageName, self.version)
-            if not package:
-                self.notify.warning("Package %s %s not known on %s" % (
-                    self.packageName, self.version, self.host.hostUrl))
-                return False
-
-            self.package = package
-            if not self.package.downloadDescFile(http):
-                return False
-
-            self.package.checkStatus()
-            self.downloadEffort = self.package.getDownloadEffort()
-            self.prevDownloadEffort = 0
-            if self.downloadEffort == 0:
-                self.prevDownloadedEffort = self.package.getPrevDownloadedEffort()
-
-            return True
-
-    def __init__(self, appRunner, taskChain = 'default'):
-        self.globalLock.acquire()
-        try:
-            self.uniqueId = PackageInstaller.nextUniqueId
-            PackageInstaller.nextUniqueId += 1
-        finally:
-            self.globalLock.release()
-
-        self.appRunner = appRunner
-        self.taskChain = taskChain
-
-        # If we're to be running on an asynchronous task chain, and
-        # the task chain hasn't yet been set up already, create the
-        # default parameters now.
-        if taskChain != 'default' and not taskMgr.hasTaskChain(self.taskChain):
-            taskMgr.setupTaskChain(self.taskChain, numThreads = 1,
-                                   threadPriority = TPLow)
-
-        self.callbackLock = Lock()
-        self.calledDownloadStarted = False
-        self.calledDownloadFinished = False
-
-        # A list of all packages that have been added to the
-        # installer.
-        self.packageLock = RLock()
-        self.packages = []
-        self.state = self.S_initial
-
-        # A list of packages that are waiting for their desc files.
-        self.needsDescFile = []
-        self.descFileTask = None
-
-        # A list of packages that are waiting to be downloaded and
-        # installed.
-        self.needsDownload = []
-        self.downloadTask = None
-
-        # A list of packages that were already done at the time they
-        # were passed to addPackage().
-        self.earlyDone = []
-
-        # A list of packages that have been successfully installed, or
-        # packages that have failed.
-        self.done = []
-        self.failed = []
-
-        # This task is spawned on the default task chain, to update
-        # the status during the download.
-        self.progressTask = None
-
-        self.accept('PackageInstaller-%s-allHaveDesc' % self.uniqueId,
-                    self.__allHaveDesc)
-        self.accept('PackageInstaller-%s-packageStarted' % self.uniqueId,
-                    self.__packageStarted)
-        self.accept('PackageInstaller-%s-packageDone' % self.uniqueId,
-                    self.__packageDone)
-
-    def destroy(self):
-        """ Interrupts all pending downloads.  No further callbacks
-        will be made. """
-        self.cleanup()
-
-    def cleanup(self):
-        """ Interrupts all pending downloads.  No further callbacks
-        will be made. """
-
-        self.packageLock.acquire()
-        try:
-            if self.descFileTask:
-                taskMgr.remove(self.descFileTask)
-                self.descFileTask = None
-            if self.downloadTask:
-                taskMgr.remove(self.downloadTask)
-                self.downloadTask = None
-        finally:
-            self.packageLock.release()
-
-        if self.progressTask:
-            taskMgr.remove(self.progressTask)
-            self.progressTask = None
-
-        self.ignoreAll()
-
-    def addPackage(self, packageName, version = None, hostUrl = None):
-        """ Adds the named package to the list of packages to be
-        downloaded.  Call donePackages() to finish the list. """
-
-        if self.state != self.S_initial:
-            raise ValueError('addPackage called after donePackages')
-
-        host = self.appRunner.getHostWithAlt(hostUrl)
-        pp = self.PendingPackage(packageName, version, host)
-
-        self.packageLock.acquire()
-        try:
-            self.__internalAddPackage(pp)
-        finally:
-            self.packageLock.release()
-
-    def __internalAddPackage(self, pp):
-        """ Adds the indicated "pending package" to the appropriate
-        list(s) for downloading and installing.  Assumes packageLock
-        is already held."""
-
-        if pp in self.packages:
-            # Already added.
-            return
-
-        self.packages.append(pp)
-
-        # We always add the package to needsDescFile, even if we
-        # already have its desc file; this guarantees that packages
-        # are downloaded in the order they are added.
-        self.needsDescFile.append(pp)
-        if not self.descFileTask:
-            self.descFileTask = taskMgr.add(
-                self.__getDescFileTask, 'getDescFile',
-                taskChain = self.taskChain)
-
-    def donePackages(self):
-        """ After calling addPackage() for each package to be
-        installed, call donePackages() to mark the end of the list.
-        This is necessary to determine what the complete set of
-        packages is (and therefore how large the total download size
-        is).  None of the low-level callbacks will be made before this
-        call. """
-
-        if self.state != self.S_initial:
-            # We've already been here.
-            return
-
-        # Throw the messages for packages that were already done
-        # before we started.
-        for pp in self.earlyDone:
-            self.__donePackage(pp, True)
-        self.earlyDone = []
-
-        self.packageLock.acquire()
-        try:
-            if self.state != self.S_initial:
-                return
-            self.state = self.S_ready
-            if not self.needsDescFile:
-                # All package desc files are already available; so begin.
-                self.__prepareToStart()
-        finally:
-            self.packageLock.release()
-
-        if not self.packages:
-            # Trivial no-op.
-            self.__callDownloadFinished(True)
-
-    def downloadStarted(self):
-        """ This callback is made at some point after donePackages()
-        is called; at the time of this callback, the total download
-        size is known, and we can sensibly report progress through the
-        whole. """
-
-        self.notify.info("downloadStarted")
-
-    def packageStarted(self, package):
-        """ This callback is made for each package between
-        downloadStarted() and downloadFinished() to indicate the start
-        of a new package. """
-
-        self.notify.debug("packageStarted: %s" % (package.packageName))
-
-    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). """
-
-        self.notify.debug("packageProgress: %s %s" % (package.packageName, progress))
-
-    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). """
-
-        self.notify.debug("downloadProgress: %s" % (overallProgress))
-
-    def packageFinished(self, package, success):
-        """ This callback is made for each package between
-        downloadStarted() and downloadFinished() to indicate that a
-        package has finished downloading.  If success is true, there
-        were no problems and the package is now installed.
-
-        If this package did not require downloading (because it was
-        already downloaded), this callback will be made immediately,
-        *without* a corresponding call to packageStarted(), and may
-        even be made before downloadStarted(). """
-
-        self.notify.info("packageFinished: %s %s" % (package.packageName, success))
-
-    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.notify.info("downloadFinished: %s" % (success))
-
-    def __prepareToStart(self):
-        """ This is called internally when transitioning from S_ready
-        to S_started.  It sets up whatever initial values are
-        needed.  Assumes self.packageLock is held.  Returns False if
-        there were no packages to download, and the state was
-        therefore transitioned immediately to S_done. """
-
-        if not self.needsDownload:
-            self.state = self.S_done
-            return False
-
-        self.state = self.S_started
-
-        assert not self.downloadTask
-        self.downloadTask = taskMgr.add(
-            self.__downloadPackageTask, 'downloadPackage',
-            taskChain = self.taskChain)
-
-        assert not self.progressTask
-        self.progressTask = taskMgr.add(
-            self.__progressTask, 'packageProgress')
-
-        return True
-
-    def __allHaveDesc(self):
-        """ This method is called internally when all of the pending
-        packages have their desc info. """
-        working = True
-
-        self.packageLock.acquire()
-        try:
-            if self.state == self.S_ready:
-                # We've already called donePackages(), so move on now.
-                working = self.__prepareToStart()
-        finally:
-            self.packageLock.release()
-
-        if not working:
-            self.__callDownloadFinished(True)
-
-    def __packageStarted(self, pp):
-        """ This method is called when a single package is beginning
-        to download. """
-
-        self.__callDownloadStarted()
-        self.__callPackageStarted(pp)
-
-    def __packageDone(self, pp):
-        """ This method is called when a single package has been
-        downloaded and installed, or has failed. """
-
-        self.__callPackageFinished(pp, pp.success)
-        pp.notified = True
-
-        # See if there are more packages to go.
-        success = True
-        allDone = True
-        self.packageLock.acquire()
-        try:
-            for pp in self.packages:
-                if pp.notified:
-                    success = success and pp.success
-                else:
-                    allDone = False
-        finally:
-            self.packageLock.release()
-
-        if allDone:
-            self.__callDownloadFinished(success)
-
-    def __callPackageStarted(self, pp):
-        """ Calls the packageStarted() callback for a particular
-        package if it has not already been called, being careful to
-        avoid race conditions. """
-
-        self.callbackLock.acquire()
-        try:
-            if not pp.calledPackageStarted:
-                self.packageStarted(pp.package)
-                self.packageProgress(pp.package, 0)
-                pp.calledPackageStarted = True
-        finally:
-            self.callbackLock.release()
-
-    def __callPackageFinished(self, pp, success):
-        """ Calls the packageFinished() callback for a paricular
-        package if it has not already been called, being careful to
-        avoid race conditions. """
-
-        self.callbackLock.acquire()
-        try:
-            if not pp.calledPackageFinished:
-                if success:
-                    self.packageProgress(pp.package, 1)
-                self.packageFinished(pp.package, success)
-                pp.calledPackageFinished = True
-        finally:
-            self.callbackLock.release()
-
-    def __callDownloadStarted(self):
-        """ Calls the downloadStarted() callback if it has not already
-        been called, being careful to avoid race conditions. """
-
-        self.callbackLock.acquire()
-        try:
-            if not self.calledDownloadStarted:
-                self.downloadStarted()
-                self.downloadProgress(0)
-                self.calledDownloadStarted = True
-        finally:
-            self.callbackLock.release()
-
-    def __callDownloadFinished(self, success):
-        """ Calls the downloadFinished() callback if it has not
-        already been called, being careful to avoid race
-        conditions. """
-
-        self.callbackLock.acquire()
-        try:
-            if not self.calledDownloadFinished:
-                if success:
-                    self.downloadProgress(1)
-                self.downloadFinished(success)
-                self.calledDownloadFinished = True
-        finally:
-            self.callbackLock.release()
-
-    def __getDescFileTask(self, task):
-
-        """ This task runs on the aysynchronous task chain; each pass,
-        it extracts one package from self.needsDescFile and downloads
-        its desc file.  On success, it adds the package to
-        self.needsDownload. """
-
-        self.packageLock.acquire()
-        try:
-            # If we've finished all of the packages that need desc
-            # files, stop the task.
-            if not self.needsDescFile:
-                self.descFileTask = None
-
-                eventName = 'PackageInstaller-%s-allHaveDesc' % self.uniqueId
-                messenger.send(eventName, taskChain = 'default')
-
-                return task.done
-            pp = self.needsDescFile[0]
-            del self.needsDescFile[0]
-        finally:
-            self.packageLock.release()
-
-        # Now serve this one package.
-        if not pp.checkDescFile():
-            if not pp.getDescFile(self.appRunner.http):
-                self.__donePackage(pp, False)
-                return task.cont
-
-        # This package is now ready to be downloaded.  We always add
-        # it to needsDownload, even if it's already downloaded, to
-        # guarantee ordering of packages.
-
-        self.packageLock.acquire()
-        try:
-            # Also add any packages required by this one.
-            for packageName, version, host in pp.package.requires:
-                pp2 = self.PendingPackage(packageName, version, host)
-                self.__internalAddPackage(pp2)
-            self.needsDownload.append(pp)
-        finally:
-            self.packageLock.release()
-
-        return task.cont
-
-    def __downloadPackageTask(self, task):
-
-        """ This task runs on the aysynchronous task chain; each pass,
-        it extracts one package from self.needsDownload and downloads
-        it. """
-
-        while True:
-            self.packageLock.acquire()
-            try:
-                # If we're done downloading, stop the task.
-                if self.state == self.S_done or not self.needsDownload:
-                    self.downloadTask = None
-                    self.packageLock.release()
-                    yield task.done; return
-
-                assert self.state == self.S_started
-                pp = self.needsDownload[0]
-                del self.needsDownload[0]
-            except:
-                self.packageLock.release()
-                raise
-            self.packageLock.release()
-
-            # Now serve this one package.
-            eventName = 'PackageInstaller-%s-packageStarted' % self.uniqueId
-            messenger.send(eventName, [pp], taskChain = 'default')
-
-            if not pp.package.hasPackage:
-                for token in pp.package.downloadPackageGenerator(self.appRunner.http):
-                    if token == pp.package.stepContinue:
-                        yield task.cont
-                    else:
-                        break
-
-                if token != pp.package.stepComplete:
-                    pc = PStatCollector(':App:PackageInstaller:donePackage:%s' % (pp.package.packageName))
-                    pc.start()
-                    self.__donePackage(pp, False)
-                    pc.stop()
-                    yield task.cont
-                    continue
-
-            # Successfully downloaded and installed.
-            pc = PStatCollector(':App:PackageInstaller:donePackage:%s' % (pp.package.packageName))
-            pc.start()
-            self.__donePackage(pp, True)
-            pc.stop()
-
-            # Continue the loop without yielding, so we pick up the
-            # next package within this same frame.
-
-    def __donePackage(self, pp, success):
-        """ Marks the indicated package as done, either successfully
-        or otherwise. """
-        assert not pp.done
-
-        if success:
-            pc = PStatCollector(':App:PackageInstaller:install:%s' % (pp.package.packageName))
-            pc.start()
-            pp.package.installPackage(self.appRunner)
-            pc.stop()
-
-        self.packageLock.acquire()
-        try:
-            pp.done = True
-            pp.success = success
-            if success:
-                self.done.append(pp)
-            else:
-                self.failed.append(pp)
-        finally:
-            self.packageLock.release()
-
-        eventName = 'PackageInstaller-%s-packageDone' % self.uniqueId
-        messenger.send(eventName, [pp], taskChain = 'default')
-
-    def __progressTask(self, task):
-        self.callbackLock.acquire()
-        try:
-            if not self.calledDownloadStarted:
-                # We haven't yet officially started the download.
-                return task.cont
-
-            if self.calledDownloadFinished:
-                # We've officially ended the download.
-                self.progressTask = None
-                return task.done
-
-            downloadEffort = 0
-            currentDownloadSize = 0
-            for pp in self.packages:
-                downloadEffort += pp.downloadEffort + pp.prevDownloadedEffort
-                packageProgress = pp.getProgress()
-                currentDownloadSize += pp.downloadEffort * packageProgress + pp.prevDownloadedEffort
-                if pp.calledPackageStarted and not pp.calledPackageFinished:
-                    self.packageProgress(pp.package, packageProgress)
-
-            if not downloadEffort:
-                progress = 1
-            else:
-                progress = float(currentDownloadSize) / float(downloadEffort)
-            self.downloadProgress(progress)
-
-        finally:
-            self.callbackLock.release()
-
-        return task.cont
-

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff