瀏覽代碼

Merge branch 'master' into ios

Donny Lawrence 5 年之前
父節點
當前提交
cbb1e678dd
共有 100 個文件被更改,包括 3542 次插入2866 次删除
  1. 22 0
      .editorconfig
  2. 2 0
      .github/FUNDING.yml
  3. 16 0
      .github/codecov.yml
  4. 382 0
      .github/workflows/ci.yml
  5. 6 0
      .gitignore
  6. 14 74
      .travis.yml
  7. 8 0
      BACKERS.md
  8. 68 23
      CMakeLists.txt
  9. 25 21
      README.md
  10. 50 24
      cmake/README.md
  11. 9 5
      cmake/install/Panda3DConfig.cmake
  12. 2 2
      cmake/macros/BuildMetalib.cmake
  13. 32 8
      cmake/macros/CompositeSources.cmake
  14. 40 8
      cmake/macros/Interrogate.cmake
  15. 42 29
      cmake/macros/PackageConfig.cmake
  16. 50 0
      cmake/macros/PerConfigOption.cmake
  17. 3 3
      cmake/macros/Python.cmake
  18. 18 18
      cmake/macros/RunPzip.cmake
  19. 15 4
      cmake/modules/FindAssimp.cmake
  20. 34 53
      cmake/modules/FindCg.cmake
  21. 25 0
      cmake/modules/FindFFMPEG.cmake
  22. 4 4
      cmake/modules/FindFMODEx.cmake
  23. 0 132
      cmake/modules/FindMiles.cmake
  24. 1 1
      cmake/modules/FindOgg.cmake
  25. 1 1
      cmake/modules/FindSWScale.cmake
  26. 0 39
      cmake/modules/FindTar.cmake
  27. 2 2
      cmake/modules/FindVorbisFile.cmake
  28. 5 4
      cmake/scripts/CopyPython.cmake
  29. 7 0
      cmake/templates/METADATA.in
  30. 7 4
      cmake/templates/win32_python/__init__.py
  31. 4 3
      contrib/src/ai/CMakeLists.txt
  32. 3 0
      contrib/src/ai/aiBehaviors.h
  33. 1 1
      contrib/src/contribbase/CMakeLists.txt
  34. 4 3
      contrib/src/rplight/CMakeLists.txt
  35. 4 6
      direct/CMakeLists.txt
  36. 117 104
      direct/src/actor/Actor.py
  37. 2 1
      direct/src/cluster/ClusterClient.py
  38. 20 17
      direct/src/cluster/ClusterConfig.py
  39. 6 8
      direct/src/cluster/ClusterMsgs.py
  40. 17 16
      direct/src/controls/ControlManager.py
  41. 8 6
      direct/src/controls/DevWalker.py
  42. 8 6
      direct/src/controls/GhostWalker.py
  43. 8 6
      direct/src/controls/GravityWalker.py
  44. 20 18
      direct/src/controls/InputState.py
  45. 8 6
      direct/src/controls/NonPhysicsWalker.py
  46. 8 6
      direct/src/controls/ObserverWalker.py
  47. 0 2
      direct/src/controls/PhysicsRoller.py
  48. 8 6
      direct/src/controls/PhysicsWalker.py
  49. 1 1
      direct/src/controls/TwoDWalker.py
  50. 1 1
      direct/src/dcparse/CMakeLists.txt
  51. 13 6
      direct/src/dcparser/CMakeLists.txt
  52. 6 692
      direct/src/dcparser/dcClass.cxx
  53. 58 44
      direct/src/dcparser/dcClass.h
  54. 657 0
      direct/src/dcparser/dcClass_ext.cxx
  55. 93 0
      direct/src/dcparser/dcClass_ext.h
  56. 6 313
      direct/src/dcparser/dcField.cxx
  57. 13 15
      direct/src/dcparser/dcField.h
  58. 293 0
      direct/src/dcparser/dcField_ext.cxx
  59. 48 0
      direct/src/dcparser/dcField_ext.h
  60. 5 3
      direct/src/dcparser/dcPacker.I
  61. 0 508
      direct/src/dcparser/dcPacker.cxx
  62. 17 14
      direct/src/dcparser/dcPacker.h
  63. 1 3
      direct/src/dcparser/dcPackerCatalog.cxx
  64. 3 3
      direct/src/dcparser/dcPackerInterface.I
  65. 1 3
      direct/src/dcparser/dcPackerInterface.cxx
  66. 0 1
      direct/src/dcparser/dcPackerInterface.h
  67. 447 0
      direct/src/dcparser/dcPacker_ext.cxx
  68. 45 0
      direct/src/dcparser/dcPacker_ext.h
  69. 0 1
      direct/src/dcparser/dcParserDefs.h
  70. 0 42
      direct/src/dcparser/dcPython.h
  71. 3 9
      direct/src/dcparser/dcSwitch.cxx
  72. 7 3
      direct/src/dcparser/dcbase.h
  73. 0 1
      direct/src/dcparser/hashGenerator.h
  74. 3 0
      direct/src/dcparser/p3dcparser_ext_composite.cxx
  75. 4 3
      direct/src/deadrec/CMakeLists.txt
  76. 1 1
      direct/src/directbase/CMakeLists.txt
  77. 1 1
      direct/src/directdevices/DirectDeviceManager.py
  78. 2 2
      direct/src/directdevices/DirectFastrak.py
  79. 5 1
      direct/src/directnotify/DirectNotifyGlobal.py
  80. 2 1
      direct/src/directnotify/LoggerGlobal.py
  81. 6 6
      direct/src/directnotify/Notifier.py
  82. 15 15
      direct/src/directnotify/RotatingLog.py
  83. 0 99
      direct/src/directscripts/DetectPanda3D.js
  84. 0 132
      direct/src/directscripts/RunPanda3D.js
  85. 6 9
      direct/src/directscripts/extract_docs.py
  86. 1 5
      direct/src/directtools/DirectSession.py
  87. 2 0
      direct/src/directtools/DirectUtil.py
  88. 38 32
      direct/src/directutil/Verify.py
  89. 119 51
      direct/src/dist/FreezeTool.py
  90. 4 0
      direct/src/dist/__init__.py
  91. 170 128
      direct/src/dist/commands.py
  92. 269 0
      direct/src/dist/icon.py
  93. 6 10
      direct/src/dist/pefile.py
  94. 1 1
      direct/src/distributed/AsyncRequest.py
  95. 11 7
      direct/src/distributed/CMakeLists.txt
  96. 2 1
      direct/src/distributed/DistributedObject.py
  97. 2 1
      direct/src/distributed/DistributedObjectBase.py
  98. 9 8
      direct/src/distributed/DoCollectionManager.py
  99. 8 9
      direct/src/distributed/DoHierarchy.py
  100. 1 6
      direct/src/distributed/NetMessenger.py

+ 22 - 0
.editorconfig

@@ -0,0 +1,22 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{py,pyw}]
+indent_style = space
+indent_size = 4
+
+[*.{h,c,cxx,cpp,I}]
+indent_style = space
+indent_size = 2
+
+[{CMakeLists.txt,*.cmake}]
+indent_style = space
+indent_size = 2
+
+[*.bat]
+end_of_line = crlf

+ 2 - 0
.github/FUNDING.yml

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

+ 16 - 0
.github/codecov.yml

@@ -0,0 +1,16 @@
+coverage:
+  status:
+    project:
+      default:
+        threshold: 0.1
+    patch:
+      default:
+        threshold: 0.1
+codecov:
+  require_ci_to_pass: true
+  notify:
+    after_n_builds: 2
+    wait_for_ci: true
+comment:
+  require_changes: true
+  after_n_builds: 2

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

@@ -0,0 +1,382 @@
+name: Continuous Integration
+on: [push, pull_request]
+
+jobs:
+  cmake:
+    name: CMake Buildsystem
+
+    strategy:
+      fail-fast: false
+
+      matrix:
+        profile:
+        - ubuntu-xenial-standard-unity-makefile
+        - ubuntu-bionic-coverage-ninja
+        - macos-eigen-coverage-unity-xcode
+        - macos-nometa-standard-makefile
+#       - windows-standard-unity-msvc # FIXME when GH Actions runners upgrade CMake to >=3.16.1
+        - windows-nopython-nometa-standard-msvc
+
+        include:
+        - profile: ubuntu-xenial-standard-unity-makefile
+          os: ubuntu-16.04
+          config: Standard
+          unity: YES
+          generator: Unix Makefiles
+          compiler: Default
+          metalibs: YES
+          python: YES
+          eigen: NO
+
+        - profile: ubuntu-bionic-coverage-ninja
+          os: ubuntu-18.04
+          config: Coverage
+          unity: NO
+          generator: Ninja
+          compiler: Clang
+          metalibs: YES
+          python: YES
+          eigen: NO
+
+        - profile: macos-eigen-coverage-unity-xcode
+          os: macOS-latest
+          config: Coverage
+          unity: YES
+          generator: Xcode
+          compiler: Default
+          metalibs: YES
+          python: YES
+          eigen: YES
+
+        - profile: macos-nometa-standard-makefile
+          os: macOS-latest
+          config: Standard
+          unity: NO
+          generator: Unix Makefiles
+          compiler: Default
+          metalibs: NO
+          python: YES
+          eigen: NO
+
+        - profile: windows-standard-unity-msvc
+          os: windows-2019
+          config: Standard
+          unity: YES
+          generator: Visual Studio 16 2019
+          compiler: Default
+          metalibs: YES
+          python: YES
+          eigen: NO
+
+        - profile: windows-nopython-nometa-standard-msvc
+          os: windows-2019
+          config: Standard
+          unity: NO
+          generator: Visual Studio 16 2019
+          compiler: Default
+          metalibs: NO
+          python: NO
+          eigen: NO
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+    - uses: actions/checkout@v1
+      with:
+        fetch-depth: 10
+
+    - name: Self-destruct makepanda
+      run: python makepanda/selfdestruct.py --yes
+
+    - name: Install dependencies (macOS)
+      if: runner.os == 'macOS'
+      run: |
+        curl -O https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz
+        tar -xf panda3d-1.10.5-tools-mac.tar.gz
+
+        brew install ccache
+
+        echo "##[set-env name=thirdpartyOption;]-D THIRDPARTY_DIRECTORY=../panda3d-1.10.5/thirdparty" -DHAVE_CG=OFF
+
+    - name: Install dependencies (Ubuntu)
+      if: startsWith(matrix.os, 'ubuntu')
+      run: >
+        sudo apt-get update
+
+        sudo apt-get install
+        build-essential ninja-build clang llvm ccache
+        bison flex
+        libeigen3-dev 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
+        python3-setuptools
+
+        # Workaround for CMake 3.12 finding this first:
+
+        sudo rm /usr/bin/x86_64-linux-gnu-python2.7-config
+
+    - name: Cache dependencies (Windows)
+      if: runner.os == 'Windows'
+      uses: actions/cache@v1
+      with:
+        path: thirdparty-tools
+        key: ci-cmake-${{ runner.OS }}-thirdparty-v1.10.5-r1
+    - name: Install dependencies (Windows)
+      if: runner.os == 'Windows'
+      shell: powershell
+      run: |
+        if (!(Test-Path thirdparty-tools/panda3d-1.10.5)) {
+          $wc = New-Object System.Net.WebClient
+          $wc.DownloadFile("https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-win64.zip", "thirdparty-tools.zip")
+          Expand-Archive -Path thirdparty-tools.zip
+        }
+
+        echo "##[set-env name=thirdpartyOption;]-D THIRDPARTY_DIRECTORY=../thirdparty-tools/panda3d-1.10.5/thirdparty"
+
+    - name: ccache (non-Windows)
+      if: runner.os != 'Windows'
+      uses: actions/cache@v1
+      with:
+        path: ccache
+        key: ci-cmake-ccache-${{ matrix.profile }}
+
+    - name: Configure
+      shell: bash
+      env:
+        CMAKE_GENERATOR: "${{ matrix.generator }}"
+      run: >
+        mkdir build
+
+        cd build
+
+        if ${{ matrix.compiler == 'Clang' }}; then
+          if [[ "$CMAKE_GENERATOR" == *Studio*2019* ]]; then
+            export CMAKE_GENERATOR_TOOLSET=ClangCL thirdpartyOption="$thirdpartyOption -DHAVE_HARFBUZZ=NO"
+          elif [[ "$CMAKE_GENERATOR" == *Studio* ]]; then
+            export CMAKE_GENERATOR_TOOLSET=LLVM thirdpartyOption="$thirdpartyOption -DHAVE_HARFBUZZ=NO"
+          else
+            export CC=clang CXX=clang++
+          fi
+        fi
+
+        if ${{ runner.os != 'Windows' }}; then
+          compilerLauncher=$(echo -DCMAKE_C{,XX}_COMPILER_LAUNCHER=ccache)
+          echo "##[set-env name=CCACHE_DIR;]$(dirname $PWD)/ccache"
+        fi
+
+        cmake
+        ${compilerLauncher:-}
+        -D CMAKE_UNITY_BUILD=${{ matrix.unity }}
+        -D CMAKE_BUILD_TYPE="${{ matrix.config }}"
+        -D BUILD_METALIBS=${{ matrix.metalibs }}
+        -D HAVE_PYTHON=${{ matrix.python }}
+        -D HAVE_EIGEN=${{ matrix.eigen }}
+        ${thirdpartyOption:-}
+        ..
+
+    - name: Build (no Python)
+      if: contains(matrix.python, 'NO')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+
+    - name: Setup Python (Python 3.5)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.5
+    - name: Configure (Python 3.5)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.5
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+    - name: Build (Python 3.5)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.5)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest
+        $PYTHON_EXECUTABLE -m pytest ../tests
+      # END B
+
+    - name: Setup Python (Python 3.6)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.6
+    - name: Configure (Python 3.6)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.6
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+    - name: Build (Python 3.6)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.6)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest pytest-cov
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
+    - name: Setup Python (Python 3.7)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.7
+    - name: Configure (Python 3.7)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.7
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+    - name: Build (Python 3.7)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.7)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest pytest-cov
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
+    - name: Setup Python (Python 3.8)
+      if: contains(matrix.python, 'YES')
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.8
+    - name: Configure (Python 3.8)
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      run: >
+        cmake -DWANT_PYTHON_VERSION=3.8
+        -DPython_FIND_REGISTRY=NEVER -DPython_ROOT=$pythonLocation .
+    - name: Build (Python 3.8)
+      if: contains(matrix.python, 'YES')
+      # BEGIN A
+      working-directory: build
+      run: cmake --build . --config ${{ matrix.config }} --parallel 4
+      # END A
+    - name: Test (Python 3.8)
+      # BEGIN B
+      if: contains(matrix.python, 'YES')
+      working-directory: build
+      shell: bash
+      env:
+        PYTHONPATH: ${{ matrix.config }}
+      run: |
+        PYTHON_EXECUTABLE=$(grep 'Python_EXECUTABLE:' CMakeCache.txt | sed 's/.*=//')
+        $PYTHON_EXECUTABLE -m pip install pytest pytest-cov
+        export COVERAGE_FILE=.coverage.$RANDOM LLVM_PROFILE_FILE=$PWD/pid-%p.profraw
+        $PYTHON_EXECUTABLE -m pytest ../tests --cov=.
+      # END B
+
+    - name: Upload coverage reports
+      if: always() && matrix.config == 'Coverage'
+      working-directory: build
+      shell: bash
+      env:
+        CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+      run: |
+        shopt -s expand_aliases
+        if ${{ runner.os == 'macOS' }}; then alias llvm-profdata='xcrun llvm-profdata' llvm-cov='xcrun llvm-cov'; fi
+
+        python -m pip install coverage
+        python -m coverage combine $(find . -name '.coverage.*')
+
+        llvm-profdata merge pid-*.profraw -o coverage.profdata
+        llvm-cov show $(grep -Rl LLVM_PROFILE_FILE . | sed 's/^/-object /') -instr-profile=coverage.profdata > coverage.txt
+        bash <(curl -s https://codecov.io/bash) -y ../.github/codecov.yml
+
+  makepanda:
+    strategy:
+      matrix:
+        os: [ubuntu-16.04, windows-2016, macOS-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+    - uses: actions/checkout@v1
+    - name: Install dependencies (Ubuntu)
+      if: matrix.os == 'ubuntu-16.04'
+      run: |
+        sudo apt-get update
+        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.5/panda3d-1.10.5-tools-win64.zip", "thirdparty-tools.zip")
+        Expand-Archive -Path thirdparty-tools.zip
+        Move-Item -Path thirdparty-tools/panda3d-1.10.5/thirdparty -Destination .
+    - name: Get thirdparty packages (macOS)
+      if: runner.os == 'macOS'
+      run: |
+        curl -O https://www.panda3d.org/download/panda3d-1.10.5/panda3d-1.10.5-tools-mac.tar.gz
+        tar -xf panda3d-1.10.5-tools-mac.tar.gz
+        mv panda3d-1.10.5/thirdparty thirdparty
+        rmdir panda3d-1.10.5
+        (cd thirdparty/darwin-libs-a && rm -rf rocket)
+    - name: Set up Python 3.8
+      uses: actions/setup-python@v1
+      with:
+        python-version: 3.8
+    - name: Build Python 3.8
+      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.8
+      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 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: Make installer
+      run: |
+        python makepanda/makepackage.py --verbose --lzma

+ 6 - 0
.gitignore

@@ -54,6 +54,9 @@ CTestTestfile.cmake
 Thumbs.db
 ehthumbs.db
 
+# macOS
+.DS_Store
+
 # Python
 __pycache__/
 *.pyc
@@ -63,3 +66,6 @@ __pycache__/
 .tox/
 .cache/
 .pytest_cache/
+/.settings/
+/.cproject
+/.project

+ 14 - 74
.travis.yml

@@ -1,26 +1,18 @@
 language: cpp
 sudo: false
-
-# Build matrix:
-os:
-- linux
-- osx
-compiler:
-- clang
-env:
-- BUILD_METALIBS=ON COMPOSITE_SOURCE_LIMIT=30
-- BUILD_METALIBS=ON COMPOSITE_SOURCE_LIMIT=30 PYTHON_INTERP=python2.7
-- BUILD_METALIBS=OFF COMPOSITE_SOURCE_LIMIT=30
-- BUILD_METALIBS=ON COMPOSITE_SOURCE_LIMIT=0
 matrix:
-  exclude:
-    - os: osx
-      env: BUILD_METALIBS=ON COMPOSITE_SOURCE_LIMIT=0
   include:
-    - os: osx
-      env: BUILD_METALIBS=ON COMPOSITE_SOURCE_LIMIT=30 COMPOSITE_SOURCE_EXCLUSIONS='p3showbase;p3dtoolutil;p3cocoadisplay;p3osxdisplay;p3tinydisplay;p3display'
+    - compiler: clang
+      env: PYTHONV=python3 FLAGS=--installer
+    - compiler: clang
+      env: PYTHONV=python3 FLAGS=--override=STDFLOAT_DOUBLE=1
     - compiler: gcc
-
+      env: PYTHONV=python3 FLAGS=--optimize=4
+      before_install:
+        - export CC=gcc-4.7
+        - export CXX=g++-4.7
+    - compiler: clang
+      env: PYTHONV=python3 FLAGS=--no-python SKIP_TESTS=1
 addons:
   apt:
     sources:
@@ -30,10 +22,6 @@ addons:
     - g++-4.7
     - bison
     - flex
-    - libavcodec-dev
-    - libavformat-dev
-    - libavresample-dev
-    - libavutil-dev
     - libfreetype6-dev
     - libgl1-mesa-dev
     - libjpeg-dev
@@ -41,7 +29,6 @@ addons:
     - libopenal-dev
     - libpng-dev
     - libssl-dev
-    - libswscale-dev
     - libvorbis-dev
     - libx11-dev
     - libxcursor-dev
@@ -49,62 +36,15 @@ addons:
     - nvidia-cg-toolkit
     - python-dev
     - python3-dev
-    - python3-pip
     - python-virtualenv
     - zlib1g-dev
     - fakeroot
-
-before_install:
-# clean up remnants of makepanda
-- mv makepanda/test_imports.py .
-- makepanda/selfdestruct.py --yes
-
 install:
-- >
-  if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
-    brew install python@3 eigen libtar libsquish zlib ffmpeg freetype \
-    bullet opencv assimp libvorbis openssl || true
-    brew install ode --with-shared
-
-    # We can't trust brew to make the right symlinks, so execute commands as modules
-    ${PYTHON_INTERP:-python3} -m pip install virtualenv
-    ${PYTHON_INTERP:-python3} -m virtualenv venv
-  else
-    virtualenv --python=${PYTHON_INTERP:-python3} venv
-  fi
-- source venv/bin/activate
-- pip install pytest
-
-before_script:
-- mkdir built
-- cd built
-
+    - virtualenv --python=$PYTHONV venv && source venv/bin/activate
+    - $PYTHONV -m pip install pytest
 script:
-# This is kind of a hack, but needed to test non-metalib builds (until
-# interrogate has a way of explicitly calling init_libpanda)
-- '[ "x$BUILD_METALIBS" == "xON" -o "x$TRAVIS_OS_NAME" == "xosx" ] || export LDFLAGS=-Wl,--no-as-needed'
-
-- >
-  if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
-    if [[ "$PYTHON_INTERP" == "python2.7" ]]; then
-      export PY2_CMAKE_ARGS=-DPYTHON_EXECUTABLE=/usr/local/Frameworks/Python.framework/Versions/2.7/bin/python
-    fi
-
-    cmake -DBUILD_METALIBS=$BUILD_METALIBS \
-    -DCOMPOSITE_SOURCE_EXCLUSIONS="$COMPOSITE_SOURCE_EXCLUSIONS" \
-    -DCOMPOSITE_SOURCE_LIMIT=$COMPOSITE_SOURCE_LIMIT \
-    -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl $PY2_CMAKE_ARGS ..
-  else
-    cmake -DHAVE_GTK2=NO -DBUILD_METALIBS=$BUILD_METALIBS \
-    -DCOMPOSITE_SOURCE_EXCLUSIONS="$COMPOSITE_SOURCE_EXCLUSIONS" \
-    -DCOMPOSITE_SOURCE_LIMIT=$COMPOSITE_SOURCE_LIMIT ..
-  fi
-- make -j4
-
-- export PYTHONPATH=$PWD
-- python ../test_imports.py
-- pytest -v ../tests
-
+    - $PYTHONV makepanda/makepanda.py --everything --git-commit $TRAVIS_COMMIT $FLAGS --threads 4
+    - test -n "$SKIP_TESTS" || LD_LIBRARY_PATH=built/lib PYTHONPATH=built $PYTHONV -m pytest -v tests
 notifications:
   irc:
     channels:

+ 8 - 0
BACKERS.md

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

+ 68 - 23
CMakeLists.txt

@@ -7,19 +7,63 @@ endif()
 set(CMAKE_DISABLE_SOURCE_CHANGES ON) # Must go before project() below
 set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) # Must go before project() below
 
-get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(CMAKE_VERSION VERSION_GREATER "3.11" OR POLICY CMP0072)
+  # Prefer GLVND over libGL when available; this will be enabled by default
+  # once the minimum CMake version is at least 3.11.
+  cmake_policy(SET CMP0072 NEW)
+endif()
+
+if(CMAKE_VERSION VERSION_GREATER "3.12" OR POLICY CMP0074)
+  # Needed for THIRDPARTY_DIRECTORY support; this will be enabled by default
+  # once the minimum CMake version is at least 3.12.
+  cmake_policy(SET CMP0074 NEW)
+endif()
+
+if(POLICY CMP0091)
+  # Needed for CMake to pass /MD flag properly with non-VC generators.
+  cmake_policy(SET CMP0091 NEW)
+endif()
+
+# Determine whether we are using a multi-config generator.
+if(CMAKE_VERSION VERSION_GREATER "3.8")
+  get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+  # set_property(GLOBAL PROPERTY XCODE_EMIT_EFFECTIVE_PLATFORM_NAME OFF)
+else()
+  message(WARNING "Multi-configuration builds may not work properly when using
+a CMake < 3.9. Making a guess if this is a multi-config generator.")
+  if(DEFINED CMAKE_CONFIGURATION_TYPES)
+    set(IS_MULTICONFIG ON)
+  else()
+    set(IS_MULTICONFIG OFF)
+  endif()
+endif()
 
-if(${CMAKE_VERSION} VERSION_GREATER 3.8.0)
-  # When using the Xcode generator, don't append the platform name to the
-  # intermediate configuration directory.
-  set_property(GLOBAL PROPERTY XCODE_EMIT_EFFECTIVE_PLATFORM_NAME OFF)
+# Define the type of build we are setting up.
+set(_configs Standard Release RelWithDebInfo Debug MinSizeRel)
+if(CMAKE_CXX_COMPILER_ID MATCHES "(AppleClang|Clang|GCC)")
+  list(APPEND _configs Coverage)
+endif()
+
+if(IS_MULTICONFIG)
+  message(STATUS "Using multi-configuration generator")
+else()
+  # Set the default CMAKE_BUILD_TYPE before calling project().
+  if(NOT CMAKE_BUILD_TYPE)
+    set(CMAKE_BUILD_TYPE Standard CACHE STRING "Choose the type of build." FORCE)
+    message(STATUS "Using default build type ${CMAKE_BUILD_TYPE}")
+  else()
+    message(STATUS "Using build type ${CMAKE_BUILD_TYPE}")
+  endif()
+  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS ${_configs})
 endif()
 
 # Figure out the version
-file(STRINGS "setup.cfg" _version REGEX "^version = ")
-string(REGEX REPLACE "^.*= " "" _version "${_version}")
+set(_s "[\\t ]*") # CMake doesn't support \s*
+file(STRINGS "setup.cfg" _version REGEX "^version${_s}=${_s}")
+string(REGEX REPLACE "^.*=${_s}" "" _version "${_version}")
 project(Panda3D VERSION ${_version})
 unset(_version)
+unset(_s)
 
 enable_testing()
 
@@ -34,12 +78,16 @@ endif()
 
 # Add generic modules to cmake module path,
 # and add Panda3D specific modules to cmake module path
-set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/modules/")
-set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/macros/")
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/")
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros/")
+
 
+# Include modules builtin to CMake
+include(GNUInstallDirs)     # Defines CMAKE_INSTALL_<dir> variables
 
 # Include global modules needed for configure scripts
 include(PackageConfig)      # Defines package_option
+include(PerConfigOption)    # Defines per_config_option
 
 # Configure Panda3D
 include(dtool/CompilerFlags.cmake)
@@ -58,8 +106,8 @@ include(RunPzip)            # Defines run_pzip function
 include(Versioning)         # Hooks 'add_library' to apply VERSION/SOVERSION
 
 # Determine which trees to build.
-option(BUILD_DTOOL  "Build the dtool source tree." ON)
-option(BUILD_PANDA  "Build the panda source tree." ON)
+option(BUILD_DTOOL "Build the dtool source tree." ON)
+option(BUILD_PANDA "Build the panda source tree." ON)
 option(BUILD_DIRECT "Build the direct source tree." ON)
 option(BUILD_PANDATOOL "Build the pandatool source tree." ON)
 option(BUILD_CONTRIB "Build the contrib source tree." ON)
@@ -104,20 +152,20 @@ if(BUILD_MODELS)
                      POST_BUILD
                      COMMAND ${CMAKE_COMMAND}
                              -DSOURCE="${CMAKE_CURRENT_SOURCE_DIR}/models/maps/"
-                             -DDESTINATION="${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR}/models/maps"
+                             -DDESTINATION="${PANDA_OUTPUT_DIR}/models/maps"
                              -P ${PROJECT_SOURCE_DIR}/cmake/scripts/CopyPattern.cmake
                      COMMENT "Copying models/maps")
   add_custom_command(TARGET dmodels
                      POST_BUILD
                      COMMAND ${CMAKE_COMMAND}
                              -DSOURCE="${CMAKE_CURRENT_SOURCE_DIR}/dmodels/src/"
-                             -DDESTINATION="${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR}/models"
+                             -DDESTINATION="${PANDA_OUTPUT_DIR}/models"
                              -DFILES_MATCHING="PATTERN;*.rgb;PATTERN;*.png;PATTERN;*.jpg;PATTERN;*.wav"
                              -P ${PROJECT_SOURCE_DIR}/cmake/scripts/CopyPattern.cmake
                      COMMENT "Copying dmodels' assets")
 
-  install(DIRECTORY "${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR_GEN}/models"
-    COMPONENT Models DESTINATION share/panda3d)
+  install(DIRECTORY "${PANDA_OUTPUT_DIR}/models"
+    COMPONENT Models DESTINATION ${CMAKE_INSTALL_DATADIR}/panda3d)
 endif()
 
 if(INTERROGATE_PYTHON_INTERFACE)
@@ -125,15 +173,12 @@ if(INTERROGATE_PYTHON_INTERFACE)
   # for pytest before adding this test.  If the user doesn't have pytest, we'd
   # like for the tests to fail.
 
-  if(PANDA_CFG_INTDIR STREQUAL ".")
-    set(_workdir "${PROJECT_BINARY_DIR}")
-  else()
-    set(_workdir "${PROJECT_BINARY_DIR}/$<CONFIG>")
-  endif()
+  # In the Coverage configuration, we also require pytest-cov
 
   add_test(NAME pytest
     COMMAND "${PYTHON_EXECUTABLE}" -m pytest "${PROJECT_SOURCE_DIR}/tests"
-    WORKING_DIRECTORY "${_workdir}")
+    $<$<CONFIG:Coverage>:--cov=.>
+    WORKING_DIRECTORY "${PANDA_OUTPUT_DIR}")
 endif()
 
 # Generate the Panda3DConfig.cmake file so find_package(Panda3D) works, and
@@ -142,7 +187,7 @@ endif()
 file(COPY "${PROJECT_SOURCE_DIR}/cmake/install/Panda3DConfig.cmake"
   DESTINATION "${PROJECT_BINARY_DIR}")
 install(FILES "${PROJECT_SOURCE_DIR}/cmake/install/Panda3DConfig.cmake"
-  DESTINATION "lib/cmake/Panda3D")
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D")
 
 include(CMakePackageConfigHelpers)
 write_basic_package_version_file(
@@ -150,7 +195,7 @@ write_basic_package_version_file(
   VERSION "${PROJECT_VERSION}"
   COMPATIBILITY AnyNewerVersion)
 install(FILES "${PROJECT_BINARY_DIR}/Panda3DConfigVersion.cmake"
-  DESTINATION "lib/cmake/Panda3D")
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D")
 
 if(NOT CMAKE_CROSSCOMPILING)
   export(PACKAGE Panda3D)

+ 25 - 21
README.md

@@ -24,9 +24,9 @@ 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-6/).
 If you are familiar with installing Python packages, you can use
-the following comand:
+the following command:
 
 ```bash
 pip install panda3d
@@ -52,11 +52,11 @@ Building Panda3D
 Windows
 -------
 
-You can build Panda3D with the Microsoft Visual C++ 2015 or 2017 compiler,
+You can build Panda3D with the Microsoft Visual C++ 2015, 2017 or 2019 compiler,
 which can be downloaded for free from the [Visual Studio site](https://visualstudio.microsoft.com/downloads/).
 You will also need to install the [Windows 10 SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk),
-and if you intend to target Windows XP, you will also need the
-[Windows 7.1 SDK](https://www.microsoft.com/en-us/download/details.aspx?id=8279).
+and if you intend to target Windows Vista, you will also need the
+[Windows 8.1 SDK](https://go.microsoft.com/fwlink/p/?LinkId=323507).
 
 You will also need to have the third-party dependency libraries available for
 the build scripts to use.  These are available from one of these two URLs,
@@ -64,16 +64,17 @@ 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.6/panda3d-1.10.6-tools-win64.zip
+- https://www.panda3d.org/download/panda3d-1.10.6/panda3d-1.10.6-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
-using Visual C++ 2015 instead of 2017.  Add the `--windows-sdk=10` option if
-you don't need to support Windows XP and did not install the Windows 7.1 SDK.)
+After acquiring these dependencies, you can build Panda3D from the command
+prompt using the following command.  Change the `--msvc-version` option based
+on your version of Visual C++; 2019 is 14.2, 2017 is 14.1, and 2015 is 14.
+Remove the `--windows-sdk=10` option if you need to support Windows Vista,
+which requires the Windows 8.1 SDK.
 
 ```bash
-makepanda\makepanda.bat --everything --installer --msvc-version=14.1 --no-eigen --threads=2
+makepanda\makepanda.bat --everything --installer --msvc-version=14.2 --windows-sdk=10 --no-eigen --threads=2
 ```
 
 When the build succeeds, it will produce an .exe file that you can use to
@@ -116,7 +117,7 @@ sudo apt-get install build-essential pkg-config fakeroot python-dev libpng-dev l
 ```
 
 Once Panda3D has built, you can either install the .deb or .rpm package that
-it produced, depending on which Linux distribution you are using.  For example,
+is produced, depending on which Linux distribution you are using.  For example,
 to install the package on Debian or Ubuntu, use this:
 
 ```bash
@@ -135,7 +136,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.6/panda3d-1.10.6-tools-mac.tar.gz).
 
 After placing the thirdparty directory inside the panda3d source directory,
 you may build Panda3D using a command like the following:
@@ -145,7 +146,7 @@ python makepanda/makepanda.py --everything --installer
 ```
 
 You may target a specific minimum macOS version using the --osxtarget flag
-followed by the release number, eg. 10.7 or 10.9.
+followed by the release number, eg. 10.9 or 10.14.
 
 If the build was successful, makepanda will have generated a .dmg file in
 the source directory containing the installer.  Simply open it and run the
@@ -163,11 +164,11 @@ pkg install pkgconf bison png jpeg-turbo tiff freetype2 harfbuzz eigen squish op
 ```
 
 You will also need to choose which version of Python you want to use.
-Install the appropriate package for it (such as `python2` or `python36`) and
+Install the appropriate package for it (such as `python37` or `python38`) and
 run the makepanda script with your chosen Python version:
 
 ```bash
-python3.6 makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2
+python3.7 makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2
 ```
 
 If successful, this will produce a .pkg file in the root of the source
@@ -185,16 +186,19 @@ from the Play Store.  Many of the dependencies can be installed by running the
 following command in the Termux shell:
 
 ```bash
-pkg install python-dev termux-tools ndk-stl ndk-sysroot clang libvorbis-dev libopus-dev opusfile-dev openal-soft-dev freetype-dev harfbuzz-dev libpng-dev ecj4.6 dx patchelf aapt apksigner libcrypt-dev
+pkg install python ndk-sysroot clang bison freetype harfbuzz libpng eigen openal-soft opusfile libvorbis assimp libopus ecj dx patchelf aapt apksigner libcrypt openssl pkg-config
 ```
 
-Then, you can build and install the .apk right away using these commands:
+Then, you can build the .apk using this command:
 
 ```bash
 python makepanda/makepanda.py --everything --target android-21 --no-tiff --installer
-xdg-open panda3d.apk
 ```
 
+You can install the generated panda3d.apk by browsing to the panda3d folder
+using a file manager.  You may need to copy it to `/sdcard` to be able to
+access it from other apps.
+
 To launch a Python program from Termux, you can use the `run_python.sh` script
 inside the `panda/src/android` directory.  It will launch Python in a separate
 activity, load it with the Python script you passed as argument, and use a
@@ -208,7 +212,7 @@ Running Tests
 
 Install [PyTest](https://docs.pytest.org/en/latest/getting-started.html#installation)
 and run the `pytest` command.  If you have not installed Panda3D, you will
-need to configure your enviroment by pointing the `PYTHONPATH` variable at
+need to configure your environment by pointing the `PYTHONPATH` variable at
 the `built` directory.  On Linux, you will also need to point the
 `LD_LIBRARY_PATH` variable at the `built/lib` directory.
 

+ 50 - 24
cmake/README.md

@@ -1,31 +1,67 @@
 Building with CMake
 -------------------
 
-The quickest way to build and install panda with CMake is to run:
+On Windows and macOS, please ensure that you have the very latest version of
+CMake installed; older versions may work, but if not, please upgrade to the
+latest available version of CMake before requesting help.
+
+On systems that package CMake themselves (e.g. Linux distributions), we most
+likely support the provided version of CMake as long as the system itself is
+supported.
+
+CMake will also require that you already have your system's developer tools
+installed.
+
+The quickest way to build and install Panda with CMake is to install any
+third-party dependencies and then run:
 ```sh
 mkdir build && cd build
 cmake ..
-make
-[sudo] make install
+cmake --build . --config Standard --parallel 4
+[sudo] cmake --build . --config Standard --target install
 ```
 
+Note that, if you are targeting 64-bit on Windows, it is necessary to supply
+the `-A x64` option when first invoking `cmake` (as `cmake -A x64 ..`).
+
+CMake itself does not build Panda; rather, it generates project files for an
+existing IDE or build tool. To select a build tool, pass the `-G` option when
+first invoking CMake, (`cmake -G Ninja ..` is highly recommended on Linux).
+Some of these (Xcode, Visual Studio) support targeting multiple configurations
+(the `--config Standard`, above, selects the `Standard` configuration in those
+cases). Other build tools (Ninja, Makefiles, ...) do not support multiple
+configurations, and the `--config` option is ignored. To change the
+configuration in these cases (from `Standard`, the default), it is necessary to
+change the `CMAKE_BUILD_TYPE` variable as explained below.
+
+The configurations are:
+
+| Configuration  | Explanation                                            |
+| -------------- | ------------------------------------------------------ |
+| Standard       | Default; build provided to users of SDK                |
+| Release        | Distribution for end-users                             |
+| MinSizeRel     | Like Release, but optimized for size                   |
+| RelWithDebInfo | Like Release, but include debug symbols                |
+| Debug          | Do not optimize, enable optional debugging features    |
+| Coverage       | Like Debug, but profile code coverage; developers only |
+
 To configure CMake, it is recommended to use cmake-gui (`cmake-gui .`),
-however it is also possible to configure it entirely through CMake's
-command-line interface; see `man cmake` for more details.
+or ccmake (`ccmake .`), however it is also possible to configure it entirely
+through CMake's command-line interface; see `man cmake` for more details.
 
 In general, the config variable for a particular third party library is:
 ```
-	HAVE_<LIBRARY>=True/False   # Example: USE_JPEG
+	HAVE_<LIBRARY>=YES/NO   # Example: USE_JPEG
 ```
 Panda subpackage building is handled by:
 ```
-	BUILD_<SUBPACKAGE>=True/False   # Example: BUILD_DTOOL, BUILD_PANDA
+	BUILD_<SUBPACKAGE>=YES/NO   # Example: BUILD_DTOOL, BUILD_PANDA
 ```
 Other configuration settings use their historical names (same names as in-source):
 ```
 	# Examples
 	PANDA_DISTRIBUTOR="MyDistributor"
-	LINMATH_ALIGN=On
+	LINMATH_ALIGN=YES
 
 	# ... etc ...
 
@@ -33,20 +69,10 @@ Other configuration settings use their historical names (same names as in-source
 
 For example, `makepanda.py --distributor X` becomes `cmake -DPANDA_DISTRIBUTOR=X`
 
-All found third-party libraries are enabled by default.
-Most config settings are set to a sensible default for typical
-a PC/desktop Panda3D distribution.
-Third-party libraries and other settings can be enabled or disabled
-through configuration with the cmake gui or cli.
-
-Running Panda3D with `-DCMAKE_BUILD_TYPE=` and one of `Release`, `Debug`,
-`MinSizeRel`, or `RelWithDebInfo` will cause some configuration settings
-to change their defaults to more appropriate values.
-
-If cmake has already been generated, changing the build type will not cause
-some of these values to change to their expected values, because the values
-are cached so that they don't overwrite custom settings.
+All found third-party libraries are enabled by default, and makepanda-style
+tools packages are searched in the same path as makepanda (however this may be
+overridden with the `THIRDPARTY_DIRECTORY` option).
 
-To reset CMake's config to defaults, delete the CMakeCache.txt file, and rerun
-CMake with the preferred build mode specified
-(example: `cmake .. -DCMAKE_BUILD_TYPE=Debug`).
+Most config settings are set to a sensible default for a typical PC/desktop
+Panda3D distribution. Third-party libraries and other settings can be enabled
+or disabled through configuration with the cmake GUI or CLI.

+ 9 - 5
cmake/install/Panda3DConfig.cmake

@@ -80,7 +80,7 @@
 #
 #   FMOD      - Support for FMOD audio output.
 #
-#               Panda3D::OpenAL::p3fmod_audio
+#               Panda3D::FMOD::p3fmod_audio
 #
 #
 #   OpenGL    - Support for OpenGL rendering.
@@ -93,10 +93,14 @@
 #               Panda3D::DX9::pandadx9
 #
 #
-#   GLES      - Support for OpenGL ES rendering.
+#   OpenGLES1 - Support for OpenGL ES 1.x rendering.
 #
-#               Panda3D::GLES::pandagles
-#               Panda3D::GLES::pandagles2
+#               Panda3D::OpenGLES1::pandagles
+#
+#
+#   OpenGLES2 - Support for OpenGL ES 2.x+ rendering.
+#
+#               Panda3D::OpenGLES2::pandagles2
 #
 #
 #   Vision    - Support for vision processing.
@@ -122,7 +126,7 @@ set(_panda_components
   Bullet ODE
   FFmpeg
   OpenAL FMOD
-  OpenGL DX9 GLES
+  OpenGL DX9 OpenGLES1 OpenGLES2
   Vision VRPN
 )
 

+ 2 - 2
cmake/macros/BuildMetalib.cmake

@@ -411,7 +411,7 @@ function(add_metalib target_name)
 
     configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/metalib_init.h.in"
       "${init_header_path}")
-    install(FILES "${init_header_path}" DESTINATION include/panda3d)
+    install(FILES "${init_header_path}" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)
   endif()
 
   add_library("${target_name}" ${sources})
@@ -421,6 +421,6 @@ function(add_metalib target_name)
   target_link_libraries("${target_name}" ${libs})
   target_include_directories("${target_name}"
     PUBLIC ${includes}
-    INTERFACE "$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include/panda3d>")
+    INTERFACE "$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}/panda3d>")
 
 endfunction(add_metalib)

+ 32 - 8
cmake/macros/CompositeSources.cmake

@@ -15,10 +15,13 @@
 
 
 # Settings for composite builds.  Should be moved to Config.cmake?
-set(COMPOSITE_SOURCE_LIMIT "30" CACHE STRING
-  "Setting this to a value higher than 1 will enable unity builds, also
-known as SCU (single compilation unit).  A high value will speed up the
-build dramatically but will be more memory intensive than a low value.")
+set(CMAKE_UNITY_BUILD "ON" CACHE BOOL
+  "Enable unity builds; Panda defaults this to on.")
+
+set(CMAKE_UNITY_BUILD_BATCH_SIZE "30" CACHE STRING
+  "How many source files to build at a time through the unity build mechanism.
+  A high value will speed up the build dramatically but will be more memory
+  intensive than a low value.")
 
 set(COMPOSITE_SOURCE_EXTENSIONS ".cxx;.mm;.c" CACHE STRING
   "Only files of these extensions will be composited.")
@@ -27,11 +30,32 @@ set(COMPOSITE_SOURCE_EXCLUSIONS "" CACHE STRING
   "A list of targets to skip when compositing sources. This is mainly
 desirable for CI builds.")
 
-set(COMPOSITE_GENERATOR "${CMAKE_SOURCE_DIR}/cmake/scripts/MakeComposite.cmake")
+set(COMPOSITE_GENERATOR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/scripts/MakeComposite.cmake")
 
 
 # Define composite_sources()
 function(composite_sources target sources_var)
+  if(NOT CMAKE_VERSION VERSION_LESS "3.16")
+    # CMake 3.16+ implements CMAKE_UNITY_BUILD* natively; no need to continue!
+
+    # Actually - <=3.16.2 has difficulty with multi-language support, so only
+    # allow .cxx in. Hopefully this can be removed soon.
+    foreach(_source ${${sources_var}})
+      get_filename_component(_source_ext "${_source}" EXT)
+      if(NOT _source_ext STREQUAL ".cxx")
+        set_source_files_properties(${_source} PROPERTIES
+          SKIP_UNITY_BUILD_INCLUSION YES)
+      endif()
+    endforeach(_source)
+
+    return()
+  endif()
+
+  if(NOT CMAKE_UNITY_BUILD)
+    # We've been turned off
+    return()
+  endif()
+
   # How many sources were specified?
   set(orig_sources ${${sources_var}})
   set(sources ${orig_sources})
@@ -40,7 +64,7 @@ function(composite_sources target sources_var)
   # Don't composite if in the list of exclusions, and don't bother compositing
   # with too few sources
   list (FIND COMPOSITE_SOURCE_EXCLUSIONS ${target} _index)
-  if(num_sources LESS 2 OR ${COMPOSITE_SOURCE_LIMIT} LESS 2 OR ${_index} GREATER -1)
+  if(num_sources LESS 2 OR ${CMAKE_UNITY_BUILD_BATCH_SIZE} LESS 2 OR ${_index} GREATER -1)
     return()
   endif()
 
@@ -49,7 +73,7 @@ function(composite_sources target sources_var)
     get_filename_component(extension "${source}" EXT)
     get_source_file_property(generated "${source}" GENERATED)
     get_source_file_property(is_header "${source}" HEADER_FILE_ONLY)
-    get_source_file_property(skip_compositing "${source}" SKIP_COMPOSITING)
+    get_source_file_property(skip_compositing "${source}" SKIP_UNITY_BUILD_INCLUSION)
 
     # Check if we can safely add this to a composite file.
     if(NOT generated AND NOT is_header AND NOT skip_compositing AND
@@ -100,7 +124,7 @@ function(composite_sources target sources_var)
     endif()
 
     # Check if this is the point where we should cut the file.
-    if(num_sources EQUAL 0 OR NOT num_composite_sources LESS ${COMPOSITE_SOURCE_LIMIT}
+    if(num_sources EQUAL 0 OR NOT num_composite_sources LESS ${CMAKE_UNITY_BUILD_BATCH_SIZE}
        OR NOT composite_ext STREQUAL next_extension)
       # It's pointless to make a composite source from just one file.
       if(num_composite_sources GREATER 1)

+ 40 - 8
cmake/macros/Interrogate.cmake

@@ -31,7 +31,10 @@ set(INTERROGATE_EXCLUDE_REGEXES
 )
 
 if(WIN32)
-  list(APPEND IGATE_FLAGS -D_X86_ -D__STDC__=1 -DWIN32_VC -D "_declspec(param)=" -D "__declspec(param)=" -D_near -D_far -D__near -D__far -D_WIN32 -D__stdcall -DWIN32)
+  list(APPEND IGATE_FLAGS -D_X86_ -D__STDC__=1 -D "_declspec(param)=" -D "__declspec(param)=" -D_near -D_far -D__near -D__far -D_WIN32 -D__stdcall)
+endif()
+if(MSVC_VERSION)
+  list(APPEND IGATE_FLAGS "-D_MSC_VER=${MSVC_VERSION}")
 endif()
 if(INTERROGATE_VERBOSE)
   list(APPEND IGATE_FLAGS "-v")
@@ -215,15 +218,35 @@ function(interrogate_sources target output database language_flags)
     # that's fine, it also ignores '"'
     set(_q "'")
   endif()
-  set(define_flags "$<$<BOOL:${_compile_defs}>:-D${_q}$<JOIN:${_compile_defs},${_q}\t-D${_q}>${_q}>")
+  set(_compile_defs_flags "-D${_q}$<JOIN:${_compile_defs},${_q}\t-D${_q}>${_q}")
+  # We may have just ended up with -D'' if there are no flags; filter that
+  set(define_flags
+    "$<$<NOT:$<STREQUAL:${_compile_defs_flags},-D${_q}${_q}>>:${_compile_defs_flags}>")
+
+  # Some of the definitions may be specified using -D flags in the global
+  # CXX_FLAGS variables; parse those out (this also picks up NDEBUG)
+  set(_configs ${CMAKE_CONFIGURATION_TYPES} ${CMAKE_BUILD_TYPE} "<ALL>")
+  list(REMOVE_DUPLICATES _configs)
+  foreach(_config ${_configs})
+    if(_config STREQUAL "<ALL>")
+      set(flags "${CMAKE_CXX_FLAGS}")
+    else()
+      string(TOUPPER "${_config}" _CONFIG)
+      set(flags "${CMAKE_CXX_FLAGS_${_CONFIG}}")
+    endif()
+
+    # Convert "/D define1" and "-Ddefine2" flags, interspersed with other
+    # compiler nonsense, into a basic "-Ddefine1 -Ddefine2" string
+    string(REGEX MATCHALL "[/-]D[ \t]*[A-Za-z0-9_]+" igate_flags "${flags}")
+    string(REPLACE ";" " " igate_flags "${igate_flags}")
+    string(REPLACE "/D" "-D" igate_flags "${igate_flags}")
 
-  # If this is a release build that has NDEBUG defined, we need that too:
-  foreach(build_type ${CMAKE_CONFIGURATION_TYPES} ${CMAKE_BUILD_TYPE})
-    string(TOUPPER "${build_type}" build_type)
-    if(CMAKE_CXX_FLAGS_${build_type} MATCHES ".*NDEBUG.*")
-      list(APPEND define_flags "$<$<CONFIG:${build_type}>:-DNDEBUG>")
+    if(_config STREQUAL "<ALL>")
+      list(APPEND define_flags "${igate_flags}")
+    else()
+      list(APPEND define_flags "$<$<CONFIG:${_config}>:${igate_flags}>")
     endif()
-  endforeach(build_type)
+  endforeach(_config)
 
   get_filename_component(output_directory "${output}" DIRECTORY)
   get_filename_component(database_directory "${database}" DIRECTORY)
@@ -343,6 +366,15 @@ function(add_python_module module)
     DEPENDS host_interrogate_module ${infiles_abs}
     COMMENT "Generating module ${module}")
 
+  # CMake chokes on ${CMAKE_CFG_INTDIR} in source paths when unity builds are
+  # enabled. The easiest way out of this is to skip unity for those paths.
+  # Since generated Interrogate .cxx files are pretty big already, this doesn't
+  # really inconvenience us at all.
+  set_source_files_properties(
+    "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}/${module}_module.cxx"
+    ${sources_abs} PROPERTIES
+    SKIP_UNITY_BUILD_INCLUSION YES)
+
   add_python_target(${module} COMPONENT "${component}" EXPORT "${component}"
     "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}/${module}_module.cxx"
     ${sources_abs} ${extensions})

+ 42 - 29
cmake/macros/PackageConfig.cmake

@@ -76,6 +76,8 @@ function(package_option name)
   set(license "")
   set(cache_string)
 
+  string(TOUPPER "${name}" name)
+
   foreach(arg ${ARGN})
     if(command STREQUAL "DEFAULT")
       set(default "${arg}")
@@ -137,9 +139,6 @@ function(package_option name)
 
       endif()
 
-    elseif(IS_MINSIZE_BUILD)
-      set(default OFF)
-
     else()
       set(default "${${found_as}_FOUND}")
 
@@ -147,7 +146,8 @@ function(package_option name)
   endif()
 
   # If it was set by the user but not found, display an error.
-  if(HAVE_${name} AND NOT ${found_as}_FOUND)
+  string(TOUPPER "${found_as}" FOUND_AS)
+  if(HAVE_${name} AND NOT ${found_as}_FOUND AND NOT ${FOUND_AS}_FOUND)
     message(SEND_ERROR "NOT FOUND: ${name}.  Disable HAVE_${name} to continue.")
   endif()
 
@@ -207,16 +207,24 @@ function(package_option name)
     endforeach(implib)
 
     if(use_variables)
-      if(${found_as}_INCLUDE_DIRS)
+      if(DEFINED ${found_as}_INCLUDE_DIRS)
         set(includes ${${found_as}_INCLUDE_DIRS})
-      else()
+      elseif(DEFINED ${found_as}_INCLUDE_DIR)
         set(includes "${${found_as}_INCLUDE_DIR}")
+      elseif(DEFINED ${FOUND_AS}_INCLUDE_DIRS)
+        set(includes ${${FOUND_AS}_INCLUDE_DIRS})
+      else()
+        set(includes "${${FOUND_AS}_INCLUDE_DIR}")
       endif()
 
-      if(${found_as}_LIBRARIES)
+      if(DEFINED ${found_as}_LIBRARIES)
         set(libs ${${found_as}_LIBRARIES})
-      else()
+      elseif(DEFINED ${found_as}_LIBRARY)
         set(libs "${${found_as}_LIBRARY}")
+      elseif(DEFINED ${FOUND_AS}_LIBRARIES)
+        set(libs ${${FOUND_AS}_LIBRARIES})
+      else()
+        set(libs "${${FOUND_AS}_LIBRARY}")
       endif()
 
       target_link_libraries(PKG::${name} INTERFACE ${libs})
@@ -239,6 +247,8 @@ function(package_status name desc)
     set(note "${arg}")
   endforeach()
 
+  string(TOUPPER "${name}" name)
+
   if(NOT ";${_ALL_PACKAGE_OPTIONS};" MATCHES ";${name};")
     message(SEND_ERROR "package_status(${name}) was called before package_option(${name}).
                         This is a bug in the cmake build scripts.")
@@ -417,6 +427,11 @@ function(export_packages filename)
     set(exports "${exports})\n")
   endforeach(pkg)
 
+  # file(GENERATE) does not like $<LINK_ONLY:...> (and it's meant to be
+  # consumed by our importer) so we escape it
+  set(_bling "$<1:$>") # genex-escaped $
+  string(REPLACE "$<LINK_ONLY:" "${_bling}<LINK_ONLY:" exports "${exports}")
+
   file(GENERATE OUTPUT "${filename}" CONTENT "${exports}")
 endfunction(export_packages)
 
@@ -457,37 +472,35 @@ function(export_targets set)
     FILE "${PROJECT_BINARY_DIR}/Panda3D${set}Targets.cmake")
   install(EXPORT "${set}" NAMESPACE "${namespace}"
     FILE "Panda3D${set}Targets.cmake"
-    COMPONENT "${component}" DESTINATION lib/cmake/Panda3D)
+    COMPONENT "${component}" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D)
 
 endfunction(export_targets)
 
 #
 # find_package
 #
-# This override is necessary because CMake's default behavior is to run
-# find_package in MODULE mode, *then* in CONFIG mode.  This is silly!  CONFIG
-# mode makes more sense to be done first, since any system config file will
-# know vastly more about the package's configuration than a module can hope to
-# guess.
+# This override implements CMAKE_FIND_PACKAGE_PREFER_CONFIG on versions of
+# CMake too old to include it.
 #
-macro(find_package name)
-  if(";${ARGN};" MATCHES ";(CONFIG|MODULE|NO_MODULE);")
-    # Caller explicitly asking for a certain mode; so be it.
-    _find_package(${ARGV})
+if(CMAKE_VERSION VERSION_LESS "3.15")
+  macro(find_package name)
+    if(";${ARGN};" MATCHES ";(CONFIG|MODULE|NO_MODULE);")
+      # Caller explicitly asking for a certain mode; so be it.
+      _find_package(${ARGV})
 
-  else()
-    string(TOUPPER "${name}" __pkgname_upper)
+    elseif(CMAKE_FIND_PACKAGE_PREFER_CONFIG)
+      # Try CONFIG
+      _find_package("${name}" CONFIG ${ARGN})
 
-    # Try CONFIG
-    _find_package("${name}" CONFIG ${ARGN})
-    if(NOT ${name}_FOUND)
-      # CONFIG didn't work, fall back to MODULE
-      _find_package("${name}" MODULE ${ARGN})
+      if(NOT ${name}_FOUND)
+        # CONFIG didn't work, fall back to MODULE
+        _find_package("${name}" MODULE ${ARGN})
+      endif()
 
     else()
-      # Case-sensitivity
-      set(${__pkgname_upper}_FOUND 1)
+      # Default behavior
+      _find_package(${ARGV})
 
     endif()
-  endif()
-endmacro(find_package)
+  endmacro(find_package)
+endif()

+ 50 - 0
cmake/macros/PerConfigOption.cmake

@@ -0,0 +1,50 @@
+# Filename: PerConfigOption.cmake
+#
+# This contains a convenience function for defining per-config options.
+# In single-config generators, it will set the option based on the defined
+# CMAKE_BUILD_TYPE.  In multi-config generators, it will create separate
+# options, one per config.
+#
+# Function: per_config_option
+# Usage:
+#   option(name "help string" [Config1] [Config2] [...ConfigN])
+#
+# Example:
+#   per_config_option(DO_DEBUGGING "Enables debugging." Debug Standard)
+
+set(_PER_CONFIG_OPTIONS CACHE INTERNAL "Internal variable")
+
+function(per_config_option name help)
+  set(_configs ${ARGN})
+
+  # In single-config generatotrs, we simply create one config.
+  if(NOT IS_MULTICONFIG)
+    list(FIND _configs "${CMAKE_BUILD_TYPE}" _index)
+    if(${_index} GREATER -1)
+      option("${name}" "${help}" ON)
+    else()
+      option("${name}" "${help}" OFF)
+    endif()
+
+  elseif(DEFINED "${name}")
+    # It's been explicitly defined, so that makes it not a multi-configuration
+    # variable anymore.
+    option("${name}" "${help}")
+    return()
+
+  else()
+    foreach(_config ${CMAKE_CONFIGURATION_TYPES})
+      string(TOUPPER "${_config}" _config_upper)
+      list(FIND _configs "${_config}" _index)
+      if(${_index} GREATER -1)
+        option("${name}_${_config_upper}" "${help}" ON)
+      else()
+        option("${name}_${_config_upper}" "${help}" OFF)
+      endif()
+    endforeach()
+
+  endif()
+
+  list(APPEND _PER_CONFIG_OPTIONS "${name}")
+  set(_PER_CONFIG_OPTIONS "${_PER_CONFIG_OPTIONS}" CACHE INTERNAL "Internal variable")
+endfunction(per_config_option)

+ 3 - 3
cmake/macros/Python.cmake

@@ -51,7 +51,7 @@ function(add_python_target target)
   target_link_libraries(${target} PKG::PYTHON)
 
   if(BUILD_SHARED_LIBS)
-    set(_outdir "${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR_GEN}/${slash_namespace}")
+    set(_outdir "${PANDA_OUTPUT_DIR}/${slash_namespace}")
 
     set_target_properties(${target} PROPERTIES
       LIBRARY_OUTPUT_DIRECTORY "${_outdir}"
@@ -75,7 +75,7 @@ function(add_python_target target)
       OUTPUT_NAME "${basename}"
       PREFIX "libpy.${namespace}.")
 
-    install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION lib)
+    install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
 
   endif()
 
@@ -139,7 +139,7 @@ function(install_python_package package_name)
     set(src_path "${CMAKE_SOURCE_DIR}/cmake/templates/win32_python")
   endif()
 
-  set(path "${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR}/${package_name}")
+  set(path "${PANDA_OUTPUT_DIR}/${package_name}")
 
   set(args -D "OUTPUT_DIR=${path}")
   if(src_path)

+ 18 - 18
cmake/macros/RunPzip.cmake

@@ -1,15 +1,4 @@
 function(run_pzip target_name source destination glob)
-  if(NOT TARGET host_pzip)
-    if(CMAKE_CROSSCOMPILING AND NOT EXISTS "${HOST_BIN_DIR}/pzip")
-      # If pzip isn't built, we just copy instead.
-      file(COPY "${source}"
-        DESTINATION "${destination}"
-        FILES_MATCHING PATTERN "${glob}")
-
-      return()
-    endif()
-  endif()
-
   file(GLOB_RECURSE files RELATIVE "${source}" "${source}/${glob}")
 
   if(CMAKE_CROSSCOMPILING)
@@ -24,14 +13,25 @@ function(run_pzip target_name source destination glob)
 
     get_filename_component(dstdir "${destination}/${filename}" DIRECTORY)
 
-    set(dstfile "${filename}.pz")
-    list(APPEND dstfiles "${destination}/${dstfile}")
+    if(TARGET host_pzip)
+      set(dstfile "${filename}.pz")
+      list(APPEND dstfiles "${destination}/${dstfile}")
 
-    add_custom_command(OUTPUT "${destination}/${dstfile}"
-      COMMAND ${CMAKE_COMMAND} -E make_directory "${dstdir}"
-      COMMAND ${pzip_executable} -c > "${destination}/${dstfile}" < "${source}/${filename}"
-      DEPENDS ${pzip_executable}
-      COMMENT "")
+      add_custom_command(OUTPUT "${destination}/${dstfile}"
+        COMMAND ${CMAKE_COMMAND} -E make_directory "${dstdir}"
+        COMMAND host_pzip -o "${destination}/${dstfile}" "${source}/${filename}"
+        DEPENDS host_pzip
+        COMMENT "")
+
+    else()
+      # If pzip isn't built, we just copy instead.
+      list(APPEND dstfiles "${destination}/${filename}")
+
+      add_custom_command(OUTPUT "${destination}/${filename}"
+        COMMAND ${CMAKE_COMMAND} -E
+          copy_if_different "${source}/${filename}" "${destination}/${filename}"
+        COMMENT "")
+    endif()
 
   endforeach(filename)
 

+ 15 - 4
cmake/modules/FindAssimp.cmake

@@ -7,16 +7,27 @@
 # Once done this will define:
 #   ASSIMP_FOUND        - system has Assimp
 #   ASSIMP_INCLUDE_DIR  - the path to the location of the assimp/ directory
-#   ASSIMP_LIBRARY      - the library to link against for Assimp
+#   ASSIMP_LIBRARIES    - the libraries to link against for Assimp
 #
 
 find_path(ASSIMP_INCLUDE_DIR
   NAMES "assimp/Importer.hpp")
 
-find_library(ASSIMP_LIBRARY
+find_library(ASSIMP_ASSIMP_LIBRARY
   NAMES "assimp")
 
-mark_as_advanced(ASSIMP_INCLUDE_DIR ASSIMP_LIBRARY)
+find_library(ASSIMP_IRRXML_LIBRARY
+  NAMES "IrrXML")
+
+if(ASSIMP_ASSIMP_LIBRARY)
+  set(ASSIMP_LIBRARIES "${ASSIMP_ASSIMP_LIBRARY}")
+
+  if(ASSIMP_IRRXML_LIBRARY)
+    list(APPEND ASSIMP_LIBRARIES "${ASSIMP_IRRXML_LIBRARY}")
+  endif()
+endif()
+
+mark_as_advanced(ASSIMP_INCLUDE_DIR ASSIMP_LIBRARIES)
 
 include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Assimp DEFAULT_MSG ASSIMP_INCLUDE_DIR ASSIMP_LIBRARY)
+find_package_handle_standard_args(Assimp DEFAULT_MSG ASSIMP_INCLUDE_DIR ASSIMP_LIBRARIES)

+ 34 - 53
cmake/modules/FindCg.cmake

@@ -23,6 +23,13 @@
 
 # Find Cg for OpenGL
 macro(find_cggl)
+  if(APPLE)
+    # GL support is built-in on Apple
+    set(CGGL_LIBRARY "${CG_LIBRARY}")
+    set(CGGL_LIBRARY_DIR "${CG_LIBRARY_DIR}")
+    set(CGGL_INCLUDE_DIR "${CG_INCLUDE_DIR}")
+  endif()
+
   if(Cg_FIND_QUIETLY)
     set(CgGL_FIND_QUIETLY TRUE)
   endif()
@@ -114,59 +121,33 @@ endmacro()
 
 # Find base Nvidia Cg
 if(NOT CG_LIBRARY_DIR OR NOT CG_INCLUDE_DIRS)
-  # On OSX default to using the framework version of Cg.
-  if(APPLE)
-    include(${CMAKE_ROOT}/Modules/CMakeFindFrameworks.cmake)
-    set(CG_INCLUDES)
-
-    cmake_find_frameworks(Cg)
-    if(Cg_FRAMEWORKS)
-      foreach(dir ${Cg_FRAMEWORKS})
-        list(APPEND CG_INCLUDES ${dir}/Headers ${dir}/PrivateHeaders)
-      endforeach(dir)
-      unset(Cg_FRAMEWORKS)
-
-      # Find the include dir
-      find_path(CG_INCLUDE_DIR
-        NAMES "cg.h"
-        PATHS ${CG_INCLUDES}
-        DOC "The path to NvidiaCg's include directory."
-      )
-      unset(CG_INCLUDES)
-
-      # Set the library dir (TODO: Check the correctness on Mac OS X)
-      set(CG_LIBRARY_DIR "/Library/Frameworks/Cg.framework" CACHE PATH "The path to NvidiaCg's library directory.")
-    endif()
-
-  else()
-    # Find the include directory
-    find_path(CG_INCLUDE_DIR
-      NAMES "cg.h"
-      PATHS "C:/Program Files/Cg"
-            "C:/Program Files/NVIDIA Corporation/Cg/include"
-            "/usr/include"
-            "/usr/local/include"
-            "/opt/Cg"
-            "/opt/nvidia-cg-toolkit/include" # Gentoo
-      PATH_SUFFIXES "" "Cg" "cg"
-      DOC "The path to NvidiaCg's include directory."
-    )
-
-    # Find the library directory
-    find_library(CG_LIBRARY
-      NAMES "Cg" "libCg"
-      PATHS "C:/Program Files/Cg"
-            "C:/Program Files/NVIDIA Corporation/Cg"
-            "/usr"
-            "/usr/lib/x86_64-linux-gnu"
-            "/usr/local"
-            "/opt/Cg"
-            "/opt/nvidia-cg-toolkit" # Gentoo
-      PATH_SUFFIXES "" "lib" "lib32" "lib64"
-    )
-    get_filename_component(CG_LIBRARY_DIR "${CG_LIBRARY}" PATH)
-    set(CG_LIBRARY_DIR "${CG_LIBRARY_DIR}" CACHE PATH "The path to NvidiaCG's library directory.") # Library path
-  endif()
+  # Find the include directory
+  find_path(CG_INCLUDE_DIR
+    NAMES "Cg/cg.h"
+    PATHS "C:/Program Files/Cg"
+          "C:/Program Files/NVIDIA Corporation/Cg/include"
+          "/usr/include"
+          "/usr/local/include"
+          "/opt/Cg"
+          "/opt/nvidia-cg-toolkit/include" # Gentoo
+    PATH_SUFFIXES "" "Cg" "cg"
+    DOC "The path to NvidiaCg's include directory."
+  )
+
+  # Find the library directory
+  find_library(CG_LIBRARY
+    NAMES "Cg" "libCg"
+    PATHS "C:/Program Files/Cg"
+          "C:/Program Files/NVIDIA Corporation/Cg"
+          "/usr"
+          "/usr/lib/x86_64-linux-gnu"
+          "/usr/local"
+          "/opt/Cg"
+          "/opt/nvidia-cg-toolkit" # Gentoo
+    PATH_SUFFIXES "" "lib" "lib32" "lib64"
+  )
+  get_filename_component(CG_LIBRARY_DIR "${CG_LIBRARY}" PATH)
+  set(CG_LIBRARY_DIR "${CG_LIBRARY_DIR}" CACHE PATH "The path to NvidiaCG's library directory.") # Library path
 
   string(REGEX REPLACE "/Cg$" "" CG_BASE_INCLUDE_DIR "${CG_INCLUDE_DIR}")
   set(CG_INCLUDE_DIRS ${CG_BASE_INCLUDE_DIR} ${CG_INCLUDE_DIR})

+ 25 - 0
cmake/modules/FindFFMPEG.cmake

@@ -85,6 +85,31 @@ if(FFMPEG_LIBAVUTIL)
   list(APPEND FFMPEG_LIBRARIES "${FFMPEG_LIBAVUTIL}")
 endif()
 
+if(APPLE)
+  # When statically built for Apple, FFMPEG may have dependencies on these
+  # additional frameworks and libraries.
+
+  find_library(APPLE_COREVIDEO_LIBRARY CoreVideo)
+  if(APPLE_COREVIDEO_LIBRARY)
+    list(APPEND FFMPEG_LIBRARIES "${APPLE_COREVIDEO_LIBRARY}")
+  endif()
+
+  find_library(APPLE_VDA_LIBRARY VideoDecodeAcceleration)
+  if(APPLE_VDA_LIBRARY)
+    list(APPEND FFMPEG_LIBRARIES "${APPLE_VDA_LIBRARY}")
+  endif()
+
+  find_library(APPLE_ICONV_LIBRARY iconv)
+  if(APPLE_ICONV_LIBRARY)
+    list(APPEND FFMPEG_LIBRARIES "${APPLE_ICONV_LIBRARY}")
+  endif()
+
+  find_library(APPLE_BZ2_LIBRARY bz2)
+  if(APPLE_BZ2_LIBRARY)
+    list(APPEND FFMPEG_LIBRARIES "${APPLE_BZ2_LIBRARY}")
+  endif()
+endif()
+
 mark_as_advanced(FFMPEG_LIBRARY_DIR)
 
 include(FindPackageHandleStandardArgs)

+ 4 - 4
cmake/modules/FindFMODEx.cmake

@@ -10,8 +10,8 @@
 #   FMODEX_LIBRARY_DIR - the FMOD Ex library directory
 #   FMODEX_LIBRARY     - the path to the library binary
 #
-#   FMODEX_32_LIBRARY - the filepath of the Miles SDK 32-bit library
-#   FMOXEX_64_LIBRARY - the filepath of the Miles SDK 64-bit library
+#   FMODEX_32_LIBRARY - the filepath of the FMOD Ex SDK 32-bit library
+#   FMOXEX_64_LIBRARY - the filepath of the FMOD Ex SDK 64-bit library
 #
 
 # Find the include directory
@@ -32,7 +32,7 @@ find_path(FMODEX_INCLUDE_DIR
 
 # Find the 32-bit library
 find_library(FMODEX_32_LIBRARY
-  NAMES "fmodex_vc" "fmodex_bc" "fmodex" "fmodexL" "libfmodex" "libfmodexL"
+  NAMES "fmodex_vc" "fmodex_bc" "fmodex" "fmodexL" "libfmodex" "libfmodexL" "fmodex_vc" "fmodexL_vc"
   PATHS "/usr"
         "/usr/local"
         "/usr/X11R6"
@@ -49,7 +49,7 @@ find_library(FMODEX_32_LIBRARY
 
 # Find the 64-bit library
 find_library(FMODEX_64_LIBRARY
-  NAMES "fmodex64" "libfmodex64" "fmodexL64" "libfmodexL64"
+  NAMES "fmodex64" "libfmodex64" "fmodexL64" "libfmodexL64" "fmodex64_vc" "fmodexL64_vc"
   PATHS "/usr"
         "/usr/local"
         "/usr/X11R6"

+ 0 - 132
cmake/modules/FindMiles.cmake

@@ -1,132 +0,0 @@
-# Filename: FindMiles.cmake
-# Author: kestred (9 Dec, 2013)
-#
-# Usage:
-#   find_package(Miles [REQUIRED] [QUIET])
-#
-# Once done this will define:
-#   MILES_FOUND       - system has Radgame's Miles SDK
-#   RAD_MSS_FOUND     - system has Radgame's Miles SDK
-#   MILES_INCLUDE_DIR - the Miles SDK include directory
-#   MILES_LIBRARY_DIR - the Miles SDK library directory
-#   MILES_LIBRARY     - the path to the library binary
-#
-#   MILES_RELEASE_LIBRARY - the filepath of the Miles SDK release library
-#   MILES_RELDBG_LIBRARY  - the filepath of the Miles SDK optimize debug library
-#   MILES_MINSIZE_LIBRARY - the filepath of the Miles SDK minimum size library
-#   MILES_DEBUG_LIBRARY   - the filepath of the Miles SDK debug library
-#
-
-# Find the Miles SDK include files
-find_path(MILES_INCLUDE_DIR
-  NAMES "miles.h"
-  PATHS "/usr/include"
-        "/usr/local/include"
-        "/opt/"
-        "C:/Program Files"
-        "C:/Program Files (x86)"
-  PATH_SUFFIXES "" "miles" "Miles6" "miles/include" "Miles6/include"
-  DOC "The path to the Miles SDK include directory."
-)
-
-# Find the Miles SDK libraries (.a, .so)
-find_library(MILES_RELEASE_LIBRARY
-  NAMES "miles"
-  PATHS "/usr"
-        "/usr/local"
-        "/opt/miles"
-        "/opt/Miles6"
-        "C:/Program Files/miles"
-        "C:/Program Files (x86)/miles"
-        "C:/Program Files/Miles6"
-        "C:/Program Files (x86)/Miles6"
-  PATH_SUFFIXES "lib" "lib32"
-)
-find_library(MILES_MINSIZE_LIBRARY
-  NAMES "miles_s"
-  PATHS "/usr"
-        "/usr/local"
-        "/opt/miles"
-        "C:/Program Files/miles"
-        "C:/Program Files (x86)/miles"
-        "C:/Program Files/Miles6"
-        "C:/Program Files (x86)/Miles6"
-  PATH_SUFFIXES "lib" "lib32"
-)
-find_library(MILES_RELWITHDEBINFO_LIBRARY
-  NAMES "miles_rd"
-  PATHS "/usr"
-        "/usr/local"
-        "/opt/miles"
-        "C:/Program Files/miles"
-        "C:/Program Files (x86)/miles"
-        "C:/Program Files/Miles6"
-        "C:/Program Files (x86)/Miles6"
-  PATH_SUFFIXES "lib" "lib32"
-)
-find_library(MILES_DEBUG_LIBRARY
-  NAMES "miles_d"
-  PATHS "/usr"
-        "/usr/local"
-        "/opt/miles"
-        "C:/Program Files/miles"
-        "C:/Program Files (x86)/miles"
-        "C:/Program Files/Miles6"
-        "C:/Program Files (x86)/Miles6"
-  PATH_SUFFIXES "lib" "lib32"
-)
-
-# Choose library
-if(CMAKE_BUILD_TYPE MATCHES "Release" AND MILES_RELEASE_LIBRARY)
-  set(MILES_LIBRARY ${MILES_RELEASE_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo" AND MILES_RELDBG_LIBRARY)
-  set(MILES_LIBRARY ${MILES_RELWITHDEBINFO_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(CMAKE_BUILD_TYPE MATCHES "MinSizeRel" AND MILES_MINSIZE_LIBRARY)
-  set(MILES_LIBRARY ${MILES_MINSIZE_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(CMAKE_BUILD_TYPE MATCHES "Debug" AND MILES_DEBUG_LIBRARY)
-  set(MILES_LIBRARY ${MILES_DEBUG_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-endif()
-
-# Set library path
-get_filename_component(MILES_LIBRARY_DIR "${MILES_LIBRARY}" PATH)
-set(MILES_LIBRARY_DIR "${MILES_LIBRARY_DIR}" CACHE PATH "The path to the Miles SDK library directory.")
-
-# Check if we have everything we need
-if(MILES_INCLUDE_DIR AND MILES_LIBRARY_DIR)
-  set(FOUND_MILES TRUE)
-  set(MILES_LIBS Mss32)
-endif()
-
-mark_as_advanced(MILES_INCLUDE_DIR)
-mark_as_advanced(MILES_DEBUG_LIBRARY)
-mark_as_advanced(MILES_RELEASE_LIBRARY)
-mark_as_advanced(MILES_RELWITHDEBINFO_LIBRARY)
-mark_as_advanced(MILES_MINSIZE_LIBRARY)
-
-# Choose library
-if(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo" AND MILES_RELDBG_LIBRARY)
-  unset(MILES_LIBRARY CACHE)
-  set(MILES_LIBRARY ${MILES_RELDBG_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(CMAKE_BUILD_TYPE MATCHES "MinSizeRel" AND MILES_MINSIZE_LIBRARY)
-  unset(MILES_LIBRARY CACHE)
-  set(MILES_LIBRARY ${MILES_MINSIZE_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(CMAKE_BUILD_TYPE MATCHES "Debug" AND MILES_DEBUG_LIBRARY)
-  unset(MILES_LIBRARY CACHE)
-  set(MILES_LIBRARY ${MILES_DEBUG_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-elseif(MILES_RELEASE_LIBRARY)
-  unset(MILES_LIBRARY CACHE)
-  set(MILES_LIBRARY ${MILES_RELEASE_LIBRARY} CACHE FILEPATH "The Miles SDK library file.")
-endif()
-
-# Set library path
-if(DEFINED MILES_LIBRARY)
-  unset(MILES_LIBRARY_DIR CACHE)
-  get_filename_component(MILES_LIBRARY_DIR "${MILES_LIBRARY}" PATH)
-  set(MILES_LIBRARY_DIR "${MILES_LIBRARY_DIR}" CACHE PATH "The path to the Miles SDK library directory.")
-endif()
-
-mark_as_advanced(MILES_LIBRARY)
-mark_as_advanced(MILES_LIBRARY_DIR)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Miles DEFAULT_MSG MILES_LIBRARY MILES_INCLUDE_DIR MILES_LIBRARY_DIR)

+ 1 - 1
cmake/modules/FindOgg.cmake

@@ -12,7 +12,7 @@
 
 find_path(OGG_INCLUDE_DIR NAMES "ogg/ogg.h")
 
-find_library(OGG_LIBRARY NAMES "ogg")
+find_library(OGG_LIBRARY NAMES "ogg" "libogg_static")
 
 mark_as_advanced(OGG_INCLUDE_DIR OGG_LIBRARY)
 

+ 1 - 1
cmake/modules/FindSWScale.cmake

@@ -47,4 +47,4 @@ endif()
 mark_as_advanced(SWSCALE_LIBRARY_DIR)
 
 include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(SWSCALE DEFAULT_MSG SWSCALE_LIBRARY SWSCALE_INCLUDE_DIR SWSCALE_LIBRARY_DIR)
+find_package_handle_standard_args(SWScale DEFAULT_MSG SWSCALE_LIBRARY SWSCALE_INCLUDE_DIR SWSCALE_LIBRARY_DIR)

+ 0 - 39
cmake/modules/FindTar.cmake

@@ -1,39 +0,0 @@
-# Filename: FindTar.cmake
-# Author: kestred (29 Nov, 2013)
-#
-# Usage:
-#   find_package(Tar [REQUIRED] [QUIET])
-#
-# It sets the following variables:
-#   TAR_FOUND   - system has libtar
-#   TAR_INCLUDE_DIR   - the tar include directory
-#   TAR_LIBRARY_DIR   - the tar library directory
-#   TAR_LIBRARY - the path to the library binary
-#
-
-# Find the libtar include files
-find_path(TAR_INCLUDE_DIR
-  NAMES "libtar.h"
-  PATHS "/usr/include"
-        "/usr/local/include"
-  PATH_SUFFIXES "" "tar" "libtar"
-  DOC "The path to libtar's include directory."
-)
-
-# Find the libtar library (.a, .so)
-find_library(TAR_LIBRARY
-  NAMES "tar"
-        "libtar"
-  PATHS "/usr"
-        "/usr/local"
-  PATH_SUFFIXES "lib" "lib32" "lib64"
-)
-get_filename_component(TAR_LIBRARY_DIR "${TAR_LIBRARY}" PATH)
-set(TAR_LIBRARY_DIR "${TAR_LIBRARY_DIR}" CACHE PATH "The path to libtar's library directory.") # Library path
-
-mark_as_advanced(TAR_INCLUDE_DIR)
-mark_as_advanced(TAR_LIBRARY_DIR)
-mark_as_advanced(TAR_LIBRARY)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(Tar DEFAULT_MSG TAR_LIBRARY TAR_INCLUDE_DIR TAR_LIBRARY_DIR)

+ 2 - 2
cmake/modules/FindVorbisFile.cmake

@@ -16,8 +16,8 @@ find_package(Ogg QUIET)
 # Find Vorbis
 find_path(VORBIS_INCLUDE_DIR NAMES "vorbis/vorbisfile.h")
 
-find_library(VORBIS_vorbis_LIBRARY NAMES "vorbis")
-find_library(VORBIS_vorbisfile_LIBRARY NAMES "vorbisfile")
+find_library(VORBIS_vorbis_LIBRARY NAMES "vorbis" "libvorbis_static")
+find_library(VORBIS_vorbisfile_LIBRARY NAMES "vorbisfile" "libvorbisfile_static")
 
 mark_as_advanced(VORBIS_INCLUDE_DIR VORBIS_vorbis_LIBRARY VORBIS_vorbisfile_LIBRARY)
 

+ 5 - 4
cmake/scripts/CopyPython.cmake

@@ -36,10 +36,11 @@ if(DEFINED SOURCE_DIR)
     file(TIMESTAMP "${OUTPUT_DIR}/${py_file}" dst_stamp)
 
     # The file is only copied if:
-    # - there's an __init__.py in its dir (i.e. file belongs to a package), and
+    # - there's an __init__.py in its dir (or file is in the root) (i.e. file belongs to a package), and
     # - the modification timestamp differs (i.e. file changed or never copied)
-    if(EXISTS "${SOURCE_DIR}/${py_file_parent}/__init__.py"
-        AND NOT src_stamp STREQUAL dst_stamp)
+    if((py_file_parent STREQUAL "." OR NOT py_file_parent
+        OR EXISTS "${SOURCE_DIR}/${py_file_parent}/__init__.py")
+       AND NOT src_stamp STREQUAL dst_stamp)
 
       file(COPY "${SOURCE_DIR}/${py_file}" DESTINATION "${OUTPUT_DIR}/${py_file_parent}")
       set(changed YES)
@@ -69,7 +70,7 @@ if(changed AND DEFINED PYTHON_EXECUTABLES)
       WORKING_DIRECTORY "${OUTPUT_DIR}")
 
     execute_process(
-      COMMAND "${interp}" -OO -m compileall .
+      COMMAND "${interp}" -O -m compileall .
       OUTPUT_QUIET
       WORKING_DIRECTORY "${OUTPUT_DIR}")
 

+ 7 - 0
cmake/templates/METADATA.in

@@ -0,0 +1,7 @@
+Metadata-Version: 2.0
+Name: Panda3D
+Version: ${PROJECT_VERSION}
+License: BSD
+Home-page: https://www.panda3d.org/
+Author: Panda3D Team
+Author-email: [email protected]

+ 7 - 4
cmake/templates/win32_python/__init__.py

@@ -11,10 +11,13 @@ def _fixup_dlls():
     if not os.path.isdir(dll_path):
         return
 
-    os_path = os.environ.get('PATH', '')
-    os_path = os_path.split(os.pathsep) if os_path else []
-    os_path.insert(0, dll_path)
-    os.environ['PATH'] = os.pathsep.join(os_path)
+    if hasattr(os, 'add_dll_directory'):
+        os.add_dll_directory(dll_path)
+    else:
+        os_path = os.environ.get('PATH', '')
+        os_path = os_path.split(os.pathsep) if os_path else []
+        os_path.insert(0, dll_path)
+        os.environ['PATH'] = os.pathsep.join(os_path)
 
 _fixup_dlls()
 del _fixup_dlls

+ 4 - 3
contrib/src/ai/CMakeLists.txt

@@ -47,7 +47,8 @@ target_interrogate(p3ai ALL)
 
 install(TARGETS p3ai
   EXPORT Contrib COMPONENT Contrib
-  DESTINATION lib
-  RUNTIME DESTINATION bin
+  DESTINATION ${CMAKE_INSTALL_LIBDIR}
+  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
   ARCHIVE COMPONENT ContribDevel)
-install(FILES ${P3AI_HEADERS} COMPONENT ContribDevel DESTINATION include/panda3d)
+install(FILES ${P3AI_HEADERS} COMPONENT ContribDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

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

+ 1 - 1
contrib/src/contribbase/CMakeLists.txt

@@ -11,4 +11,4 @@ add_library(p3contribbase INTERFACE)
 target_link_libraries(p3contribbase INTERFACE panda)
 
 install(TARGETS p3contribbase EXPORT Contrib COMPONENT Contrib)
-install(FILES ${P3CONTRIBBASE_HEADERS} COMPONENT ContribDevel DESTINATION include/panda3d)
+install(FILES ${P3CONTRIBBASE_HEADERS} COMPONENT ContribDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 4 - 3
contrib/src/rplight/CMakeLists.txt

@@ -45,7 +45,8 @@ endif()
 
 install(TARGETS p3rplight
   EXPORT Contrib COMPONENT Contrib
-  DESTINATION lib
-  RUNTIME DESTINATION bin
+  DESTINATION ${CMAKE_INSTALL_LIBDIR}
+  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
   ARCHIVE COMPONENT ContribDevel)
-install(FILES ${P3RPLIGHT_HEADERS} COMPONENT ContribDevel DESTINATION include/panda3d)
+install(FILES ${P3RPLIGHT_HEADERS} COMPONENT ContribDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 4 - 6
direct/CMakeLists.txt

@@ -20,9 +20,6 @@ set(P3DIRECT_COMPONENTS
 )
 if(HAVE_PYTHON)
   list(APPEND P3DIRECT_COMPONENTS p3distributed)
-  get_target_property(_p3distributed_exts p3distributed IGATE_EXTENSIONS)
-  set_source_files_properties(${_p3distributed_exts}
-    PROPERTIES COMPILE_DEFINITIONS BUILDING_DIRECT_DISTRIBUTED)
 endif()
 
 set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "DirectDevel")
@@ -33,8 +30,8 @@ set_property(TARGET p3direct PROPERTY LINKER_LANGUAGE "CXX")
 # Installation:
 install(TARGETS p3direct
   EXPORT Direct COMPONENT Direct
-  DESTINATION lib
-  RUNTIME DESTINATION bin
+  DESTINATION ${CMAKE_INSTALL_LIBDIR}
+  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
   ARCHIVE COMPONENT DirectDevel)
 
 if(HAVE_PYTHON)
@@ -59,11 +56,12 @@ if(HAVE_PYTHON)
 print(\"Warning: pandac.PandaModules is deprecated, import from panda3d.core instead\")\n")
 
   foreach(module ${ALL_INTERROGATE_MODULES})
+    string(REGEX REPLACE "^.*\\." "" module_name "${module}")
     file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/pandac/PandaModules.py" "
 try:
     from ${module} import *
 except ImportError as err:
-    if not (\"No module named\" in str(err) and \"${module}\" in str(err)):
+    if not (\"No module named\" in str(err) and \"${module_name}\" in str(err)):
         raise
 ")
   endforeach()

+ 117 - 104
direct/src/actor/Actor.py

@@ -5,6 +5,7 @@ __all__ = ['Actor']
 from panda3d.core import *
 from panda3d.core import Loader as PandaLoader
 from direct.showbase.DirectObject import DirectObject
+from direct.showbase.Loader import Loader
 from direct.directnotify import DirectNotifyGlobal
 
 
@@ -67,7 +68,7 @@ class Actor(DirectObject, NodePath):
 
         def __init__(self, filename = None, animBundle = None):
             self.filename = filename
-            self.animBundle = None
+            self.animBundle = animBundle
             self.animControl = None
 
         def makeCopy(self):
@@ -104,45 +105,43 @@ class Actor(DirectObject, NodePath):
                  lodNode = None, flattenable = True, setFinal = False,
                  mergeLODBundles = None, allowAsyncBind = None,
                  okMissing = None):
-        """__init__(self, string | string:string{}, string:string{} |
-        string:(string:string{}){}, Actor=None)
-        Actor constructor: can be used to create single or multipart
+        """Actor constructor: can be used to create single or multipart
         actors. If another Actor is supplied as an argument this
         method acts like a copy constructor. Single part actors are
         created by calling with a model and animation dictionary
-        (animName:animPath{}) as follows:
+        ``(animName:animPath{})`` as follows::
 
-           a = Actor("panda-3k.egg", {"walk":"panda-walk.egg" \
+           a = Actor("panda-3k.egg", {"walk":"panda-walk.egg",
                                       "run":"panda-run.egg"})
 
-        This could be displayed and animated as such:
+        This could be displayed and animated as such::
 
            a.reparentTo(render)
            a.loop("walk")
            a.stop()
 
         Multipart actors expect a dictionary of parts and a dictionary
-        of animation dictionaries (partName:(animName:animPath{}){}) as
-        below:
+        of animation dictionaries ``(partName:(animName:animPath{}){})``
+        as below::
 
             a = Actor(
 
                 # part dictionary
-                {"head":"char/dogMM/dogMM_Shorts-head-mod", \
-                 "torso":"char/dogMM/dogMM_Shorts-torso-mod", \
-                 "legs":"char/dogMM/dogMM_Shorts-legs-mod"}, \
+                {"head": "char/dogMM/dogMM_Shorts-head-mod",
+                 "torso": "char/dogMM/dogMM_Shorts-torso-mod",
+                 "legs": "char/dogMM/dogMM_Shorts-legs-mod"},
 
                 # dictionary of anim dictionaries
-                {"head":{"walk":"char/dogMM/dogMM_Shorts-head-walk", \
-                         "run":"char/dogMM/dogMM_Shorts-head-run"}, \
-                 "torso":{"walk":"char/dogMM/dogMM_Shorts-torso-walk", \
-                          "run":"char/dogMM/dogMM_Shorts-torso-run"}, \
-                 "legs":{"walk":"char/dogMM/dogMM_Shorts-legs-walk", \
-                         "run":"char/dogMM/dogMM_Shorts-legs-run"} \
+                {"head":{"walk": "char/dogMM/dogMM_Shorts-head-walk",
+                         "run": "char/dogMM/dogMM_Shorts-head-run"},
+                 "torso":{"walk": "char/dogMM/dogMM_Shorts-torso-walk",
+                          "run": "char/dogMM/dogMM_Shorts-torso-run"},
+                 "legs":{"walk": "char/dogMM/dogMM_Shorts-legs-walk",
+                         "run": "char/dogMM/dogMM_Shorts-legs-run"}
                  })
 
         In addition multipart actor parts need to be connected together
-        in a meaningful fashion:
+        in a meaningful fashion::
 
             a.attach("head", "torso", "joint-head")
             a.attach("torso", "legs", "joint-hips")
@@ -151,7 +150,7 @@ class Actor(DirectObject, NodePath):
         # ADD LOD COMMENT HERE!
         #
 
-        Other useful Actor class functions:
+        Other useful Actor class functions::
 
             #fix actor eye rendering
             a.drawInFront("joint-pupil?", "eyes*")
@@ -213,7 +212,7 @@ class Actor(DirectObject, NodePath):
         self.__LODCenter = Point3(0, 0, 0)
         self.switches = None
 
-        if (other == None):
+        if other is None:
             # act like a normal constructor
 
             # create base hierarchy
@@ -353,7 +352,7 @@ class Actor(DirectObject, NodePath):
             self.gotName = other.gotName
 
             # copy the scene graph elements of other
-            if (overwrite):
+            if overwrite:
                 otherCopy = other.copyTo(NodePath())
                 otherCopy.detachNode()
                 # assign these elements to ourselve (overwrite)
@@ -373,7 +372,7 @@ class Actor(DirectObject, NodePath):
             self.switches = other.switches
             self.__LODNode = self.find('**/+LODNode')
             self.__hasLOD = 0
-            if (not self.__LODNode.isEmpty()):
+            if not self.__LODNode.isEmpty():
                 self.__hasLOD = 1
 
 
@@ -419,7 +418,7 @@ class Actor(DirectObject, NodePath):
         subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
 
         partDef = partBundleDict.get(subpartDef.truePartName)
-        if partDef == None:
+        if partDef is None:
             Actor.notify.error("no part named: %s" % (partName))
 
         self.__doListJoints(0, partDef.getBundle(),
@@ -492,7 +491,7 @@ class Actor(DirectObject, NodePath):
                 for animName, file, animControl in animInfo:
                     print('    Anim: %s' % animName)
                     print('      File: %s' % file)
-                    if animControl == None:
+                    if animControl is None:
                         print(' (not loaded)')
                     else:
                         print('      NumFrames: %d PlayRate: %0.2f' %
@@ -501,7 +500,12 @@ class Actor(DirectObject, NodePath):
 
     def cleanup(self):
         """
-        Actor cleanup function
+        This method should be called when intending to destroy the Actor, and
+        cleans up any additional resources stored on the Actor class before
+        removing the underlying node using `removeNode()`.
+
+        Note that `removeNode()` itself is not sufficient to destroy actors,
+        which is why this method exists.
         """
         self.stop(None)
         self.clearPythonData()
@@ -513,6 +517,11 @@ class Actor(DirectObject, NodePath):
             self.removeNode()
 
     def removeNode(self):
+        """
+        You should call `cleanup()` for Actor objects instead, since
+        :meth:`~panda3d.core.NodePath.removeNode()` is not sufficient for
+        completely destroying Actor objects.
+        """
         if self.__geomNode and (self.__geomNode.getNumChildren() > 0):
             assert self.notify.warning("called actor.removeNode() on %s without calling cleanup()" % self.getName())
         NodePath.removeNode(self)
@@ -526,7 +535,7 @@ class Actor(DirectObject, NodePath):
 
     def flush(self):
         """
-        Actor flush function
+        Actor flush function.  Used by `cleanup()`.
         """
         self.clearPythonData()
 
@@ -558,14 +567,14 @@ class Actor(DirectObject, NodePath):
         bundles = []
 
         for lodName, partBundleDict in self.__partBundleDict.items():
-            if partName == None:
+            if partName is None:
                 for partDef in partBundleDict.values():
                     bundles.append(partDef.getBundle())
 
             else:
                 subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
                 partDef = partBundleDict.get(subpartDef.truePartName)
-                if partDef != None:
+                if partDef is not None:
                     bundles.append(partDef.getBundle())
                 else:
                     Actor.notify.warning("Couldn't find part: %s" % (partName))
@@ -636,7 +645,7 @@ class Actor(DirectObject, NodePath):
         Set the node that switches actor geometry in and out.
         If one is not supplied as an argument, make one
         """
-        if (node == None):
+        if node is None:
             node = LODNode.makeDefaultLod("lod")
 
         if self.__LODNode:
@@ -686,7 +695,7 @@ class Actor(DirectObject, NodePath):
         self.switches[lodName] = [inDist, outDist]
         # add the switch distance info
         self.__LODNode.node().addSwitch(inDist, outDist)
-        if center != None:
+        if center is not None:
             self.setCenter(center)
 
     def setLOD(self, lodName, inDist=0, outDist=0):
@@ -725,7 +734,7 @@ class Actor(DirectObject, NodePath):
         return self.__hasLOD
 
     def setCenter(self, center):
-        if center == None:
+        if center is None:
             center = Point3(0, 0, 0)
         self.__LODCenter = center
         if self.__LODNode:
@@ -783,7 +792,7 @@ class Actor(DirectObject, NodePath):
         Returns True if any joint has changed as a result of this,
         False otherwise. """
 
-        if lodName == None:
+        if lodName is None:
             lodNames = self.getLODNames()
         else:
             lodNames = [lodName]
@@ -791,7 +800,7 @@ class Actor(DirectObject, NodePath):
         anyChanged = False
         if lod < len(lodNames):
             lodName = lodNames[lod]
-            if partName == None:
+            if partName is None:
                 partBundleDict = self.__partBundleDict[lodName]
                 partNames = list(partBundleDict.keys())
             else:
@@ -907,11 +916,11 @@ class Actor(DirectObject, NodePath):
             return
 
         lodName, animControlDict = next(iter(self.__animControlDict.items()))
-        if partName == None:
+        if partName is None:
             partName, animDict = next(iter(animControlDict.items()))
         else:
             animDict = animControlDict.get(partName)
-            if animDict == None:
+            if animDict is None:
                 # part was not present
                 Actor.notify.warning("couldn't find part: %s" % (partName))
                 return None
@@ -932,11 +941,11 @@ class Actor(DirectObject, NodePath):
         in dictionary.  NOTE: only returns info for an arbitrary LOD
         """
         lodName, animControlDict = next(iter(self.__animControlDict.items()))
-        if partName == None:
+        if partName is None:
             partName, animDict = next(iter(animControlDict.items()))
         else:
             animDict = animControlDict.get(partName)
-            if animDict == None:
+            if animDict is None:
                 # part was not present
                 Actor.notify.warning("couldn't find part: %s" % (partName))
                 return None
@@ -970,7 +979,7 @@ class Actor(DirectObject, NodePath):
             return None
         subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
         partDef = partBundleDict.get(subpartDef.truePartName)
-        if partDef != None:
+        if partDef is not None:
             return partDef.partBundleNP
         return None
 
@@ -985,7 +994,7 @@ class Actor(DirectObject, NodePath):
             return None
         subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
         partDef = partBundleDict.get(subpartDef.truePartName)
-        if partDef != None:
+        if partDef is not None:
             return partDef.getBundle()
         return None
 
@@ -1002,9 +1011,9 @@ class Actor(DirectObject, NodePath):
             return
 
         # remove the part
-        if (partName in partBundleDict):
+        if partName in partBundleDict:
             partBundleDict[partName].partBundleNP.removeNode()
-            del(partBundleDict[partName])
+            del partBundleDict[partName]
 
         # find the corresponding anim control dict
         if self.mergeLODBundles:
@@ -1015,8 +1024,8 @@ class Actor(DirectObject, NodePath):
             return
 
         # remove the animations
-        if (partName in partDict):
-            del(partDict[partName])
+        if partName in partDict:
+            del partDict[partName]
 
     def hidePart(self, partName, lodName="lodRoot"):
         """
@@ -1094,7 +1103,7 @@ class Actor(DirectObject, NodePath):
         if node is None:
             node = partDef.partBundleNP.attachNewNode(jointName)
 
-        if (joint):
+        if joint:
             if localTransform:
                 joint.addLocalTransform(node.node())
             else:
@@ -1126,7 +1135,7 @@ class Actor(DirectObject, NodePath):
         # Get a handle to the joint.
         joint = bundle.findChild(jointName)
 
-        if (joint):
+        if joint:
             joint.clearNetTransforms()
             joint.clearLocalTransforms()
         else:
@@ -1135,16 +1144,16 @@ 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)
 
-        if lodName == None and self.mergeLODBundles:
+        if lodName is None and self.mergeLODBundles:
             # Get the common bundle.
             partBundleDicts = [self.__commonBundleHandles]
 
-        elif lodName == None:
+        elif lodName is None:
             # Get all LOD's.
             partBundleDicts = self.__partBundleDict.values()
         else:
@@ -1233,7 +1242,7 @@ class Actor(DirectObject, NodePath):
             return None
 
         joint = bundle.findChild(jointName)
-        if joint == None:
+        if joint is None:
             Actor.notify.warning("no joint named %s!" % (jointName))
             return None
         return joint.getDefaultValue()
@@ -1253,7 +1262,7 @@ class Actor(DirectObject, NodePath):
             return None
 
         joint = bundle.findChild(jointName)
-        if joint == None:
+        if joint is None:
             Actor.notify.warning("no joint named %s!" % (jointName))
             return None
         return joint.getTransformState()
@@ -1278,7 +1287,7 @@ class Actor(DirectObject, NodePath):
         anyGood = False
         for bundleDict in self.__partBundleDict.values():
             bundle = bundleDict[trueName].getBundle()
-            if node == None:
+            if node is None:
                 node = self.attachNewNode(ModelNode(jointName))
                 joint = bundle.findChild(jointName)
                 if joint and isinstance(joint, MovingPartMatrix):
@@ -1300,7 +1309,7 @@ class Actor(DirectObject, NodePath):
         optimal than controlJoint() for cases in which the transform
         is not intended to be animated during the lifetime of the
         Actor. """
-        if transform == None:
+        if transform is None:
             transform = TransformState.makePosHprScale(pos, hpr, scale)
 
         subpartDef = self.__subpartDict.get(partName, Actor.SubpartDef(partName))
@@ -1331,7 +1340,7 @@ class Actor(DirectObject, NodePath):
             partDef = partBundleDict.get(subpartDef.truePartName)
             if partDef:
                 joint = partDef.partBundleNP.find("**/" + jointName)
-                if (joint.isEmpty()):
+                if joint.isEmpty():
                     Actor.notify.warning("%s not found!" % (jointName))
                 else:
                     return path.instanceTo(joint)
@@ -1351,7 +1360,7 @@ class Actor(DirectObject, NodePath):
                 anotherPartDef = partBundleDict.get(anotherPartName)
                 if anotherPartDef:
                     joint = anotherPartDef.partBundleNP.find("**/" + jointName)
-                    if (joint.isEmpty()):
+                    if joint.isEmpty():
                         Actor.notify.warning("%s not found!" % (jointName))
                     else:
                         partDef.partBundleNP.reparentTo(joint)
@@ -1396,10 +1405,10 @@ class Actor(DirectObject, NodePath):
         root under the given lod.
         """
         # check to see if we are working within an lod
-        if lodName != None:
+        if lodName is not None:
             # find the named lod node
             lodRoot = self.__LODNode.find(str(lodName))
-            if root == None:
+            if root is None:
                 # no need to look further
                 root = lodRoot
             else:
@@ -1407,7 +1416,7 @@ class Actor(DirectObject, NodePath):
                 root = lodRoot.find("**/" + root)
         else:
             # start search from self if no root and no lod given
-            if root == None:
+            if root is None:
                 root = self
 
         frontParts = root.findAllMatches("**/" + frontPartName)
@@ -1427,7 +1436,7 @@ class Actor(DirectObject, NodePath):
 
         # Find the back part.
         backPart = root.find("**/" + backPartName)
-        if (backPart.isEmpty()):
+        if backPart.isEmpty():
             Actor.notify.warning("no part named %s!" % (backPartName))
             return
 
@@ -1443,7 +1452,7 @@ class Actor(DirectObject, NodePath):
 
 
     def fixBounds(self, partName = None):
-        if(partName == None):
+        if partName is None:
             #iterate through everything
             for lodData in self.__partBundleDict.values():
                 for partData in lodData.values():
@@ -1474,7 +1483,7 @@ class Actor(DirectObject, NodePath):
         in this actor
         """
         # if no part name specified fix all parts
-        if (part==None):
+        if part is None:
             part = self
 
         # update all characters first
@@ -1532,12 +1541,12 @@ class Actor(DirectObject, NodePath):
         Play the given animation on the given part of the actor.
         If no part is specified, try to play on all parts. NOTE:
         plays over ALL LODs"""
-        if fromFrame == None:
+        if fromFrame is None:
             for control in self.getAnimControls(animName, partName):
                 control.play()
         else:
             for control in self.getAnimControls(animName, partName):
-                if toFrame == None:
+                if toFrame is None:
                     control.play(fromFrame, control.getNumFrames() - 1)
                 else:
                     control.play(fromFrame, toFrame)
@@ -1551,12 +1560,12 @@ class Actor(DirectObject, NodePath):
         all LOD's
         """
 
-        if fromFrame == None:
+        if fromFrame is None:
             for control in self.getAnimControls(animName, partName):
                 control.loop(restart)
         else:
             for control in self.getAnimControls(animName, partName):
-                if toFrame == None:
+                if toFrame is None:
                     control.loop(restart, fromFrame, control.getNumFrames() - 1)
                 else:
                     control.loop(restart, fromFrame, toFrame)
@@ -1568,11 +1577,11 @@ class Actor(DirectObject, NodePath):
         restarting at zero frame if requested. If no part name
         is given then try to loop on all parts. NOTE: loops on
         all LOD's"""
-        if fromFrame == None:
+        if fromFrame is None:
             fromFrame = 0
 
         for control in self.getAnimControls(animName, partName):
-            if toFrame == None:
+            if toFrame is None:
                 control.pingpong(restart, fromFrame, control.getNumFrames() - 1)
             else:
                 control.pingpong(restart, fromFrame, toFrame)
@@ -1624,11 +1633,11 @@ class Actor(DirectObject, NodePath):
         Config.prc variable.
         """
         for bundle in self.getPartBundles(partName = partName):
-            if blendType != None:
+            if blendType is not None:
                 bundle.setBlendType(blendType)
-            if animBlend != None:
+            if animBlend is not None:
                 bundle.setAnimBlendFlag(animBlend)
-            if frameBlend != None:
+            if frameBlend is not None:
                 bundle.setFrameBlendFlag(frameBlend)
 
     def enableBlend(self, blendType = PartBundle.BTNormalizedLinear, partName = None):
@@ -1707,15 +1716,15 @@ class Actor(DirectObject, NodePath):
 
         partDict = self.__animControlDict.get(lodName)
         # if this assertion fails, named lod was not present
-        assert partDict != None
+        assert partDict is not None
 
         animDict = partDict.get(partName)
-        if animDict == None:
+        if animDict is None:
             # part was not present
             Actor.notify.warning("couldn't find part: %s" % (partName))
         else:
             anim = animDict.get(animName)
-            if anim == None:
+            if anim is None:
                 # anim was not present
                 assert Actor.notify.debug("couldn't find anim: %s" % (animName))
                 pass
@@ -1751,7 +1760,7 @@ class Actor(DirectObject, NodePath):
         If lodName is None or omitted, all LOD's are returned.
         """
 
-        if partName == None and self.__subpartsComplete:
+        if partName is None and self.__subpartsComplete:
             # If we have the __subpartsComplete flag, and no partName
             # is specified, it really means to play the animation on
             # all subparts, not on the overall Actor.
@@ -1760,12 +1769,12 @@ class Actor(DirectObject, NodePath):
         controls = []
         # build list of lodNames and corresponding animControlDicts
         # requested.
-        if lodName == None or self.mergeLODBundles:
+        if lodName is None or self.mergeLODBundles:
             # Get all LOD's
             animControlDictItems = self.__animControlDict.items()
         else:
             partDict = self.__animControlDict.get(lodName)
-            if partDict == None:
+            if partDict is None:
                 Actor.notify.warning("couldn't find lod: %s" % (lodName))
                 animControlDictItems = []
             else:
@@ -1774,7 +1783,7 @@ class Actor(DirectObject, NodePath):
         for lodName, partDict in animControlDictItems:
             # Now, build the list of partNames and the corresponding
             # animDicts.
-            if partName == None:
+            if partName is None:
                 # Get all main parts, but not sub-parts.
                 animDictItems = []
                 for thisPart, animDict in partDict.items():
@@ -1792,14 +1801,14 @@ class Actor(DirectObject, NodePath):
 
                 for pName in partNameList:
                     animDict = partDict.get(pName)
-                    if animDict == None:
+                    if animDict is None:
                         # Maybe it's a subpart that hasn't been bound yet.
                         subpartDef = self.__subpartDict.get(pName)
                         if subpartDef:
                             animDict = {}
                             partDict[pName] = animDict
 
-                    if animDict == None:
+                    if animDict is None:
                         # part was not present
                         Actor.notify.warning("couldn't find part: %s" % (pName))
                     else:
@@ -1825,7 +1834,7 @@ class Actor(DirectObject, NodePath):
                         names = animDict.keys()
                     for animName in names:
                         anim = animDict.get(animName)
-                        if anim == None and partName != None:
+                        if anim is None and partName is not None:
                             for pName in partNameList:
                                 # Maybe it's a subpart that hasn't been bound yet.
                                 subpartDef = self.__subpartDict.get(pName)
@@ -1836,14 +1845,14 @@ class Actor(DirectObject, NodePath):
                                         anim = anim.makeCopy()
                                         animDict[animName] = anim
 
-                        if anim == None:
+                        if anim is None:
                             # anim was not present
                             assert Actor.notify.debug("couldn't find anim: %s" % (animName))
                             pass
                         else:
                             # bind the animation first if we need to
                             animControl = anim.animControl
-                            if animControl == None:
+                            if animControl is None:
                                 animControl = self.__bindAnimToPart(
                                     animName, thisPart, lodName,
                                     allowAsyncBind = allowAsyncBind)
@@ -1870,7 +1879,7 @@ class Actor(DirectObject, NodePath):
         if isinstance(modelPath, NodePath):
             # If we got a NodePath instead of a string, use *that* as
             # the model directly.
-            if (copy):
+            if copy:
                 model = modelPath.copyTo(NodePath())
             else:
                 model = modelPath
@@ -1888,6 +1897,9 @@ class Actor(DirectObject, NodePath):
                 else:
                     loaderOptions.setFlags(loaderOptions.getFlags() | LoaderOptions.LFReportErrors)
 
+            # Ensure that custom Python loader hooks are initialized.
+            Loader._loadPythonFileTypes()
+
             # Pass loaderOptions to specify that we want to
             # get the skeleton model.  This only matters to model
             # files (like .mb) for which we can choose to extract
@@ -1896,15 +1908,15 @@ class Actor(DirectObject, NodePath):
             if model is not None:
                 model = NodePath(model)
 
-        if (model == None):
+        if model is None:
             raise IOError("Could not load Actor model %s" % (modelPath))
 
-        if (model.node().isOfType(Character.getClassType())):
+        if model.node().isOfType(Character.getClassType()):
             bundleNP = model
         else:
             bundleNP = model.find("**/+Character")
 
-        if (bundleNP.isEmpty()):
+        if bundleNP.isEmpty():
             Actor.notify.warning("%s is not a character!" % (modelPath))
             model.reparentTo(self.__geomNode)
         else:
@@ -1921,7 +1933,7 @@ class Actor(DirectObject, NodePath):
             # Now extract out the Character and integrate it with
             # the Actor.
 
-            if (lodName!="lodRoot"):
+            if lodName != "lodRoot":
                 # parent to appropriate node under LOD switch
                 bundleNP.reparentTo(self.__LODNode.find(str(lodName)))
             else:
@@ -1947,6 +1959,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
 
@@ -1962,7 +1975,7 @@ class Actor(DirectObject, NodePath):
             self.gotName = 1
 
         bundleDict = self.__partBundleDict.get(lodName, None)
-        if bundleDict == None:
+        if bundleDict is None:
             # make a dictionary to store these parts in
             bundleDict = {}
             self.__partBundleDict[lodName] = bundleDict
@@ -2251,7 +2264,7 @@ class Actor(DirectObject, NodePath):
             for lodName in lodNames:
                 for partName in partNames:
                     for animDef in self.__animControlDict[lodName][partName].values():
-                        if animDef.animControl != None:
+                        if animDef.animControl is not None:
                             # Try to clear any control effects before we let
                             # our handle on them go. This is especially
                             # important if the anim control was blending
@@ -2263,12 +2276,12 @@ class Actor(DirectObject, NodePath):
                 for partName in partNames:
                     for anim in anims:
                         animDef = self.__animControlDict[lodName][partName].get(anim)
-                        if animDef and animDef.animControl != None:
+                        if animDef and animDef.animControl is not None:
                             # Try to clear any control effects before we let
                             # our handle on them go. This is especially
                             # important if the anim control was blending
                             # animations.
-                            animDef.animControl.getPart().clearControlEffects()
+                            animDef.animControl.getPart().setControlEffect(animDef.animControl, 0.0)
                             animDef.animControl = None
 
 
@@ -2326,19 +2339,19 @@ class Actor(DirectObject, NodePath):
 
         partDict = self.__animControlDict[lodName]
         animDict = partDict.get(partName)
-        if animDict == None:
+        if animDict is None:
             # It must be a subpart that hasn't been bound yet.
             animDict = {}
             partDict[partName] = animDict
 
         anim = animDict.get(animName)
-        if anim == None:
+        if anim is None:
             # It must be a subpart that hasn't been bound yet.
             anim = partDict[subpartDef.truePartName].get(animName)
             anim = anim.makeCopy()
             animDict[animName] = anim
 
-        if anim == None:
+        if anim is None:
             Actor.notify.error("actor has no animation %s", animName)
 
         # only bind if not already bound!
@@ -2393,7 +2406,7 @@ class Actor(DirectObject, NodePath):
 
                 # find the part in our tree
                 bundleNP = partLod.find("**/%s%s"%(Actor.partPrefix,partName))
-                if (bundleNP != None):
+                if bundleNP is not None:
                     # store the part bundle
                     self.__prepareBundle(bundleNP, partDef.partModel,
                                          partName, lodName)
@@ -2438,15 +2451,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:
+        """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), ...]),
-                     ...]),
-           ... ]
+           [ (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. """
@@ -2465,7 +2478,7 @@ class Actor(DirectObject, NodePath):
         else:
             lodNames = [lodName]
 
-        if partName == None and self.__subpartsComplete:
+        if partName is None and self.__subpartsComplete:
             partNames = self.__subpartDict.keys()
         else:
             partNames = [partName]

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

+ 6 - 8
direct/src/cluster/ClusterMsgs.py

@@ -31,18 +31,16 @@ CLUSTER_DAEMON_PORT = 8001
 CLUSTER_SERVER_PORT = 1970
 
 # Precede command string with ! to tell server to execute command string
-# NOTE: Had to stick with the import __builtin__ scheme, at startup,
-# __builtins__ is a module, not a dictionary, like it is inside of a module
 # Note, this startup string obviates the need to set any cluster related
 # config variables in the client Configrc files
 SERVER_STARTUP_STRING = (
     '!bash ppython -c ' +
-    '"import __builtin__; ' +
-    '__builtin__.clusterMode = \'server\';' +
-    '__builtin__.clusterServerPort = %s;' +
-    '__builtin__.clusterSyncFlag = %d;' +
-    '__builtin__.clusterDaemonClient = \'%s\';' +
-    '__builtin__.clusterDaemonPort = %d;'
+    '"import builtins; ' +
+    'builtins.clusterMode = \'server\';' +
+    'builtins.clusterServerPort = %s;' +
+    'builtins.clusterSyncFlag = %d;' +
+    'builtins.clusterDaemonClient = \'%s\';' +
+    'builtins.clusterDaemonPort = %d;'
     'from direct.directbase.DirectStart import *; run()"')
 
 class ClusterMsgHandler:

+ 17 - 16
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"):
@@ -143,7 +144,7 @@ class ControlManager:
     def delete(self):
         assert self.notify.debugCall(id(self))
         self.disable()
-        for controls in self.controls.keys():
+        for controls in list(self.controls.keys()):
             self.remove(controls)
         del self.controls
         del self.currentControls

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

+ 1 - 1
direct/src/dcparse/CMakeLists.txt

@@ -1,3 +1,3 @@
 add_executable(p3dcparse dcparse.cxx)
 target_link_libraries(p3dcparse p3direct)
-install(TARGETS p3dcparse EXPORT Direct COMPONENT Direct DESTINATION bin)
+install(TARGETS p3dcparse EXPORT Direct COMPONENT Direct DESTINATION ${CMAKE_INSTALL_BINDIR})

+ 13 - 6
direct/src/dcparser/CMakeLists.txt

@@ -21,7 +21,6 @@ set(P3DCPARSER_HEADERS
   dcNumericRange.h dcNumericRange.I
   dcSwitch.h
   dcTypedef.h
-  dcPython.h
   dcbase.h
   dcindent.h
   dcmsgtypes.h
@@ -55,6 +54,12 @@ set(P3DCPARSER_SOURCES
   primeNumberGenerator.cxx
 )
 
+set(P3DCPARSER_IGATEEXT
+  dcClass_ext.cxx dcClass_ext.h
+  dcField_ext.cxx dcField_ext.h
+  dcPacker_ext.cxx dcPacker_ext.h
+)
+
 add_bison_target(dcParser.cxx dcParser.yxx DEFINES dcParser.h PREFIX dcyy)
 add_flex_target(dcLexer.cxx dcLexer.lxx CASE_INSENSITIVE PREFIX dcyy)
 
@@ -67,14 +72,16 @@ composite_sources(p3dcparser P3DCPARSER_SOURCES)
 add_component_library(p3dcparser NOINIT SYMBOL BUILDING_DIRECT_DCPARSER
   ${P3DCPARSER_HEADERS} ${P3DCPARSER_SOURCES} ${P3DCPARSER_PARSER_SOURCES})
 target_compile_definitions(p3dcparser PUBLIC WITHIN_PANDA)
-target_link_libraries(p3dcparser p3directbase panda PKG::PYTHON)
-target_interrogate(p3dcparser ${P3DCPARSER_HEADERS} ${P3DCPARSER_SOURCES})
+target_link_libraries(p3dcparser p3directbase panda)
+target_interrogate(p3dcparser ${P3DCPARSER_HEADERS} ${P3DCPARSER_SOURCES}
+  EXTENSIONS ${P3DCPARSER_IGATEEXT})
 
 if(NOT BUILD_METALIBS)
   install(TARGETS p3dcparser
     EXPORT Direct COMPONENT Direct
-    DESTINATION lib
-    RUNTIME DESTINATION bin
+    DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
     ARCHIVE COMPONENT DirectDevel)
 endif()
-install(FILES ${P3DCPARSER_HEADERS} COMPONENT DirectDevel DESTINATION include/panda3d)
+install(FILES ${P3DCPARSER_HEADERS} COMPONENT DirectDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 6 - 692
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;
 }
 
 /**
@@ -97,19 +85,12 @@ DCClass(DCFile *dc_file, const string &name, bool is_struct, bool bogus_class) :
  */
 DCClass::
 ~DCClass() {
-  if (_constructor != nullptr) {
-    delete _constructor;
-  }
+  delete _constructor;
 
   Fields::iterator fi;
   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 +316,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 +327,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 +340,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 +400,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"

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

@@ -0,0 +1,657 @@
+/**
+ * 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);
+    std::string field_name = PyUnicode_AsUTF8(py_field_name);
+    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);
+      std::string field_name = PyUnicode_AsUTF8(py_field_name);
+      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
 };
 

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

@@ -0,0 +1,293 @@
+/**
+ * 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) {
+    std::string result = PyUnicode_AsUTF8(str);
+    Py_DECREF(str);
+    return result;
+  }
+
+  PyObject *repr = PyObject_Repr(value);
+  if (repr != nullptr) {
+    std::string result = PyUnicode_AsUTF8(repr);
+    Py_DECREF(repr);
+    return result;
+  }
+
+  if (value->ob_type != nullptr) {
+    PyObject *typestr = PyObject_Str((PyObject *)(value->ob_type));
+    if (typestr != nullptr) {
+      std::string result = PyUnicode_AsUTF8(typestr);
+      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

+ 5 - 3
direct/src/dcparser/dcPacker.I

@@ -1124,7 +1124,9 @@ operator new(size_t size) {
  */
 INLINE void DCPacker::StackElement::
 operator delete(void *ptr) {
-  StackElement *obj = (StackElement *)ptr;
-  obj->_next = _deleted_chain;
-  _deleted_chain = obj;
+  if (ptr != nullptr) {
+    StackElement *obj = (StackElement *)ptr;
+    obj->_next = _deleted_chain;
+    _deleted_chain = obj;
+  }
 }

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

+ 1 - 3
direct/src/dcparser/dcPackerCatalog.cxx

@@ -44,9 +44,7 @@ DCPackerCatalog(const DCPackerCatalog &copy) :
  */
 DCPackerCatalog::
 ~DCPackerCatalog() {
-  if (_live_catalog != nullptr) {
-    delete _live_catalog;
-  }
+  delete _live_catalog;
 
   SwitchCatalogs::iterator si;
   for (si = _switch_catalogs.begin(); si != _switch_catalogs.end(); ++si) {

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

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

+ 1 - 3
direct/src/dcparser/dcPackerInterface.cxx

@@ -60,9 +60,7 @@ DCPackerInterface(const DCPackerInterface &copy) :
  */
 DCPackerInterface::
 ~DCPackerInterface() {
-  if (_catalog != nullptr) {
-    delete _catalog;
-  }
+  delete _catalog;
 }
 
 /**

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

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

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

@@ -0,0 +1,447 @@
+/**
+ * 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;
+    }
+    break;
+
+  case PT_uint64:
+    if (PyLong_Check(object)) {
+      _this->pack_uint64(PyLong_AsUnsignedLongLong(object));
+      return;
+    }
+    break;
+
+  case PT_int:
+    if (PyLong_Check(object)) {
+      _this->pack_int(PyLong_AsLong(object));
+      return;
+    }
+    break;
+
+  case PT_uint:
+    if (PyLong_Check(object)) {
+      _this->pack_uint(PyLong_AsUnsignedLong(object));
+      return;
+    }
+    break;
+
+  default:
+    break;
+  }
+
+  if (PyLong_Check(object)) {
+    _this->pack_int(PyLong_AsLong(object));
+  } else if (PyFloat_Check(object)) {
+    _this->pack_double(PyFloat_AS_DOUBLE(object));
+  } else if (PyLong_Check(object)) {
+    _this->pack_int64(PyLong_AsLongLong(object));
+  } 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 {
+    // 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();
+      object = PyLong_FromLong(value);
+    }
+    break;
+
+  case PT_uint:
+    {
+      unsigned int value = _this->unpack_uint();
+      object = PyLong_FromLong(value);
+    }
+    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:
+    {
+      std::string str;
+      _this->unpack_string(str);
+      object = PyBytes_FromStringAndSize(str.data(), str.size());
+    }
+    break;
+
+  case PT_string:
+    {
+      std::string str;
+      _this->unpack_string(str);
+      object = PyUnicode_FromStringAndSize(str.data(), str.size());
+    }
+    break;
+
+  case PT_class:
+    {
+      const DCClassParameter *class_param = _this->get_current_field()->as_class_parameter();
+      if (class_param != nullptr) {
+        const DCClass *dclass = class_param->get_class();
+        if (invoke_extension(dclass).has_class_def()) {
+          // If we know what kind of class object this is and it has a valid
+          // constructor, create the class object instead of just a tuple.
+          object = unpack_class_object(dclass);
+          if (object == nullptr) {
+            std::cerr << "Unable to construct object of class "
+                 << dclass->get_name() << "\n";
+          } else {
+            break;
+          }
+        }
+      }
+    }
+    // Fall through (if no constructor)
+
+    // If we don't know what kind of class object it is, or it doesn't have a
+    // constructor, fall through and make a tuple.
+  default:
+    {
+      // First, build up a list from the nested objects.
+      object = PyList_New(0);
+
+      _this->push();
+      while (_this->more_nested_fields()) {
+        PyObject *element = unpack_object();
+        PyList_Append(object, element);
+        Py_DECREF(element);
+      }
+      _this->pop();
+
+      if (pack_type != PT_array) {
+        // For these other kinds of objects, we'll convert the list into a
+        // tuple.
+        PyObject *tuple = PyList_AsTuple(object);
+        Py_DECREF(object);
+        object = tuple;
+      }
+    }
+    break;
+  }
+
+  nassertr(object != nullptr, nullptr);
+  return object;
+}
+
+/**
+ * Given that the current element is a ClassParameter for a Python class
+ * object, try to extract the appropriate values from the class object and
+ * pack in.
+ */
+void Extension<DCPacker>::
+pack_class_object(const DCClass *dclass, PyObject *object) {
+  _this->push();
+  while (_this->more_nested_fields() && !_this->_pack_error) {
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertv(field != nullptr);
+    get_class_element(dclass, object, field);
+  }
+  _this->pop();
+}
+
+/**
+ * Given that the current element is a ClassParameter for a Python class for
+ * which we have a valid constructor, unpack it and fill in its values.
+ */
+PyObject *Extension<DCPacker>::
+unpack_class_object(const DCClass *dclass) {
+  PyObject *class_def = invoke_extension(dclass).get_class_def();
+  nassertr(class_def != nullptr, nullptr);
+
+  PyObject *object = nullptr;
+
+  if (!dclass->has_constructor()) {
+    // If the class uses a default constructor, go ahead and create the Python
+    // object for it now.
+    object = PyObject_CallObject(class_def, nullptr);
+    if (object == nullptr) {
+      return nullptr;
+    }
+  }
+
+  _this->push();
+  if (object == nullptr && _this->more_nested_fields()) {
+    // The first nested field will be the constructor.
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertr(field != nullptr, object);
+    nassertr(field == dclass->get_constructor(), object);
+
+    set_class_element(class_def, object, field);
+
+    // By now, the object should have been constructed.
+    if (object == nullptr) {
+      return nullptr;
+    }
+  }
+  while (_this->more_nested_fields()) {
+    const DCField *field = _this->get_current_field()->as_field();
+    nassertr(field != nullptr, object);
+
+    set_class_element(class_def, object, field);
+  }
+  _this->pop();
+
+  return object;
+}
+
+/**
+ * Unpacks the current element and stuffs it on the Python class object in
+ * whatever way is appropriate.
+ */
+void Extension<DCPacker>::
+set_class_element(PyObject *class_def, PyObject *&object,
+                  const DCField *field) {
+  std::string field_name = field->get_name();
+  DCPackType pack_type = _this->get_pack_type();
+
+  if (field_name.empty()) {
+    switch (pack_type) {
+    case PT_class:
+    case PT_switch:
+      // If the field has no name, but it is one of these container objects,
+      // we want to unpack its nested objects directly into the class.
+      _this->push();
+      while (_this->more_nested_fields()) {
+        const DCField *field = _this->get_current_field()->as_field();
+        nassertv(field != nullptr);
+        nassertv(object != nullptr);
+        set_class_element(class_def, object, field);
+      }
+      _this->pop();
+      break;
+
+    default:
+      // Otherwise, we just skip over the field.
+      _this->unpack_skip();
+    }
+
+  } else {
+    // If the field does have a name, we will want to store it on the class,
+    // either by calling a method (for a PT_field pack_type) or by setting a
+    // value (for any other kind of pack_type).
+
+    PyObject *element = unpack_object();
+
+    if (pack_type == PT_field) {
+      if (object == nullptr) {
+        // If the object hasn't been constructed yet, assume this is the
+        // constructor.
+        object = PyObject_CallObject(class_def, element);
+
+      } else {
+        if (PyObject_HasAttrString(object, (char *)field_name.c_str())) {
+          PyObject *func = PyObject_GetAttrString(object, (char *)field_name.c_str());
+          if (func != nullptr) {
+            PyObject *result = PyObject_CallObject(func, element);
+            Py_XDECREF(result);
+            Py_DECREF(func);
+          }
+        }
+      }
+
+    } else {
+      nassertv(object != nullptr);
+      PyObject_SetAttrString(object, (char *)field_name.c_str(), element);
+    }
+
+    Py_DECREF(element);
+  }
+}
+
+/**
+ * Gets the current element from the Python object and packs it.
+ */
+void Extension<DCPacker>::
+get_class_element(const DCClass *dclass, PyObject *object,
+                  const DCField *field) {
+  std::string field_name = field->get_name();
+  DCPackType pack_type = _this->get_pack_type();
+
+  if (field_name.empty()) {
+    switch (pack_type) {
+    case PT_class:
+    case PT_switch:
+      // If the field has no name, but it is one of these container objects,
+      // we want to get its nested objects directly from the class.
+      _this->push();
+      while (_this->more_nested_fields() && !_this->_pack_error) {
+        const DCField *field = _this->get_current_field()->as_field();
+        nassertv(field != nullptr);
+        get_class_element(dclass, object, field);
+      }
+      _this->pop();
+      break;
+
+    default:
+      // Otherwise, we just pack the default value.
+      _this->pack_default_value();
+    }
+
+  } else {
+    // If the field does have a name, we will want to get it from the class
+    // and pack it.  It just so happens that there's already a method that
+    // does this on DCClass.
+
+    if (!invoke_extension(dclass).pack_required_field(*_this, object, field)) {
+      _this->_pack_error = true;
+    }
+  }
+}
+
+#endif  // HAVE_PYTHON

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

@@ -0,0 +1,45 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file dcPacker_ext.h
+ * @author CFSworks
+ * @date 2019-07-03
+ */
+
+#ifndef DCPACKER_EXT_H
+#define DCPACKER_EXT_H
+
+#include "dtoolbase.h"
+
+#ifdef HAVE_PYTHON
+
+#include "extension.h"
+#include "dcPacker.h"
+#include "py_panda.h"
+
+/**
+ * This class defines the extension methods for DCPacker, which are called
+ * instead of any C++ methods with the same prototype.
+ */
+template<>
+class Extension<DCPacker> : public ExtensionBase<DCPacker> {
+public:
+  void pack_object(PyObject *object);
+  PyObject *unpack_object();
+
+  void pack_class_object(const DCClass *dclass, PyObject *object);
+  PyObject *unpack_class_object(const DCClass *dclass);
+  void set_class_element(PyObject *class_def, PyObject *&object,
+                         const DCField *field);
+  void get_class_element(const DCClass *dclass, PyObject *object,
+                         const DCField *field);
+};
+
+#endif  // HAVE_PYTHON
+
+#endif  // DCPACKER_EXT_H

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

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

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

@@ -1,42 +0,0 @@
-/**
- * PANDA 3D SOFTWARE
- * Copyright (c) Carnegie Mellon University.  All rights reserved.
- *
- * All use of this software is subject to the terms of the revised BSD
- * license.  You should have received a copy of this license along
- * with this source code in a file named "LICENSE."
- *
- * @file dcPython.h
- * @author drose
- * @date 2004-06-22
- */
-
-#ifndef DCPYTHON_H
-#define DCPYTHON_H
-
-// The only purpose of this file is to serve as a common place to put the
-// nonsense associated with #including <Python.h>.
-
-#ifdef HAVE_PYTHON
-
-#define PY_SSIZE_T_CLEAN 1
-
-#undef _POSIX_C_SOURCE
-#undef _XOPEN_SOURCE
-#include <Python.h>
-
-// Python 2.5 adds Py_ssize_t; earlier versions don't have it.
-#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
-typedef int Py_ssize_t;
-#define PY_SSIZE_T_MAX INT_MAX
-#define PY_SSIZE_T_MIN INT_MIN
-#endif
-
-// Several interfaces in this module that use Python also require these header
-// files, so we might as well pick them up too.
-#include "datagram.h"
-#include "datagramIterator.h"
-
-#endif  // HAVE_PYTHON
-
-#endif

+ 3 - 9
direct/src/dcparser/dcSwitch.cxx

@@ -42,21 +42,15 @@ DCSwitch::
   nassertv(_key_parameter != nullptr);
   delete _key_parameter;
 
-  Cases::iterator ci;
-  for (ci = _cases.begin(); ci != _cases.end(); ++ci) {
-    SwitchCase *dcase = (*ci);
+  for (SwitchCase *dcase : _cases) {
     delete dcase;
   }
 
-  CaseFields::iterator fi;
-  for (fi = _case_fields.begin(); fi != _case_fields.end(); ++fi) {
-    SwitchFields *fields = (*fi);
+  for (SwitchFields *fields : _case_fields) {
     delete fields;
   }
 
-  Fields::iterator ni;
-  for (ni = _nested_fields.begin(); ni != _nested_fields.end(); ++ni) {
-    DCField *field = (*ni);
+  for (DCField *field : _nested_fields) {
     delete field;
   }
 }

+ 7 - 3
direct/src/dcparser/dcbase.h

@@ -32,15 +32,16 @@
 #include "pvector.h"
 #include "pmap.h"
 #include "pset.h"
+#include "vector_uchar.h"
 
 #else  // WITHIN_PANDA
 
-#ifdef WIN32
+#ifdef _MSC_VER
 /* C4786: 255 char debug symbols */
 #pragma warning (disable : 4786)
 /* C4503: decorated name length exceeded */
 #pragma warning (disable : 4503)
-#endif  /* WIN32_VC */
+#endif  /* _MSC_VER */
 
 #include <iostream>
 #include <fstream>
@@ -53,7 +54,7 @@
 // These header files are needed to compile dcLexer.cxx, the output from flex.
 // flex doesn't create a perfectly windows-friendly source file right out of
 // the box.
-#ifdef WIN32
+#ifdef _WIN32
 #include <io.h>
 #include <malloc.h>
 #else
@@ -81,6 +82,7 @@
 #define nassertr_always(condition, return_value) assert(condition)
 #define nassertv(condition) assert(condition)
 #define nassertv_always(condition) assert(condition)
+#define nassert_raise(message) {std::cerr << message << std::endl; abort();}
 
 // Panda defines a special Filename class.  We'll use an ordinary string
 // instead.
@@ -97,8 +99,10 @@ typedef std::string Filename;
 #define pvector std::vector
 #define pmap std::map
 #define pset std::set
+#define vector_uchar std::vector<unsigned char>
 
 #include <stdint.h>
+#include <string.h>
 
 typedef std::ifstream pifstream;
 typedef std::ofstream pofstream;

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

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

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

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

+ 4 - 3
direct/src/deadrec/CMakeLists.txt

@@ -16,8 +16,9 @@ target_interrogate(p3deadrec ALL)
 if(NOT BUILD_METALIBS)
   install(TARGETS p3deadrec
     EXPORT Direct COMPONENT Direct
-    DESTINATION lib
-    RUNTIME DESTINATION bin
+    DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
     ARCHIVE COMPONENT DirectDevel)
 endif()
-install(FILES ${P3DEADREC_HEADERS} COMPONENT DirectDevel DESTINATION include/panda3d)
+install(FILES ${P3DEADREC_HEADERS} COMPONENT DirectDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 1 - 1
direct/src/directbase/CMakeLists.txt

@@ -11,4 +11,4 @@ add_library(p3directbase INTERFACE)
 target_link_libraries(p3directbase INTERFACE panda)
 
 install(TARGETS p3directbase EXPORT Direct COMPONENT Direct)
-install(FILES ${P3DIRECTBASE_HEADERS} COMPONENT DirectDevel DESTINATION include/panda3d)
+install(FILES ${P3DIRECTBASE_HEADERS} COMPONENT DirectDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

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

+ 6 - 9
direct/src/directscripts/extract_docs.py

@@ -9,7 +9,7 @@ from __future__ import print_function
 
 __all__ = []
 
-import os, sys
+import os
 from distutils import sysconfig
 import panda3d, pandac
 from panda3d.interrogatedb import *
@@ -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)
 
@@ -307,13 +309,8 @@ if __name__ == "__main__":
     processModule(handle, "core")
 
     # Determine the suffix for the extension modules.
-    if sys.version_info >= (3, 0):
-        import _imp
-        ext_suffix = _imp.extension_suffixes()[0]
-    elif sys.platform == "win32":
-        ext_suffix = ".pyd"
-    else:
-        ext_suffix = ".so"
+    import _imp
+    ext_suffix = _imp.extension_suffixes()[0]
 
     for lib in os.listdir(os.path.dirname(panda3d.__file__)):
         if lib.endswith(ext_suffix) and not lib.startswith('core.'):

+ 1 - 5
direct/src/directtools/DirectSession.py

@@ -1,5 +1,4 @@
 import math
-import sys
 
 from panda3d.core import *
 from .DirectUtil import *
@@ -942,10 +941,7 @@ class DirectSession(DirectObject):
 
     def getAndSetName(self, nodePath):
         """ Prompt user for new node path name """
-        if sys.version_info >= (3, 0):
-            from tkinter.simpledialog import askstring
-        else:
-            from tkSimpleDialog import askstring
+        from tkinter.simpledialog import askstring
         newName = askstring('Node Path: ' + nodePath.getName(),
                             'Enter new name:')
         if newName:

+ 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

+ 119 - 51
direct/src/dist/FreezeTool.py

@@ -11,6 +11,7 @@ import struct
 import io
 import distutils.sysconfig as sysconf
 import zipfile
+import importlib
 
 from . import pefile
 
@@ -35,21 +36,15 @@ isDebugBuild = (python.lower().endswith('_d'))
 # NB. if encodings are removed, be sure to remove them from the shortcut in
 # deploy-stub.c.
 startupModules = [
-    'imp', 'encodings', 'encodings.*',
+    'imp', 'encodings', 'encodings.*', 'io', 'marshal', 'importlib.machinery',
+    'importlib.util',
 ]
-if sys.version_info >= (3, 0):
-    # Modules specific to Python 3
-    startupModules += ['io', 'marshal', 'importlib.machinery', 'importlib.util']
-else:
-    # Modules specific to Python 2
-    startupModules += []
 
 # These are some special init functions for some built-in Python modules that
 # deviate from the standard naming convention.  A value of None means that a
 # dummy entry should be written to the inittab.
 builtinInitFuncs = {
     'builtins': None,
-    '__builtin__': None,
     'sys': None,
     'exceptions': None,
     '_warnings': '_PyWarnings_Init',
@@ -75,6 +70,7 @@ hiddenImports = {
     'datetime': ['_strptime'],
     'keyring.backends': ['keyring.backends.*'],
     'matplotlib.font_manager': ['encodings.mac_roman'],
+    'matplotlib.backends._backend_tk': ['tkinter'],
     'direct.particles': ['direct.particles.ParticleManagerGlobal'],
     'numpy.core._multiarray_umath': [
         'numpy.core._internal',
@@ -83,12 +79,6 @@ hiddenImports = {
     ],
 }
 
-if sys.version_info >= (3,):
-    hiddenImports['matplotlib.backends._backend_tk'] = ['tkinter']
-else:
-    hiddenImports['matplotlib.backends._backend_tk'] = ['Tkinter']
-
-
 # These are overrides for specific modules.
 overrideModules = {
     # Used by the warnings module, among others, to get line numbers.  Since
@@ -785,7 +775,7 @@ class Freezer:
         # already-imported modules.  (Some of them might do their own
         # special path mangling.)
         for moduleName, module in list(sys.modules.items()):
-            if module and hasattr(module, '__path__'):
+            if module and getattr(module, '__path__', None) is not None:
                 path = list(getattr(module, '__path__'))
                 if path:
                     modulefinder.AddPackagePath(moduleName, path[0])
@@ -800,29 +790,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 +842,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 +876,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 +891,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 +915,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 +928,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 +985,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 +1021,7 @@ class Freezer:
         directories within a particular directory.
         """
 
-        assert self.mf == None
+        assert self.mf is None
 
         if not newName:
             newName = moduleName
@@ -1046,7 +1043,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.
@@ -1303,11 +1300,7 @@ class Freezer:
 
     def __addPyc(self, multifile, filename, code, compressionLevel):
         if code:
-            data = imp.get_magic() + b'\0\0\0\0'
-
-            if sys.version_info >= (3, 0):
-                data += b'\0\0\0\0'
-
+            data = imp.get_magic() + b'\0\0\0\0\0\0\0\0'
             data += marshal.dumps(code)
 
             stream = StringStream(data)
@@ -1664,10 +1657,7 @@ class Freezer:
                     # initmodule or PyInit_module function.
                     modname = mod.split('.')[-1]
                     libfile = modname + '.lib'
-                    if sys.version_info >= (3, 0):
-                        symbolName = 'PyInit_' + modname
-                    else:
-                        symbolName = 'init' + modname
+                    symbolName = 'PyInit_' + modname
                     os.system('lib /nologo /def /export:%s /name:%s.pyd /out:%s' % (symbolName, modname, libfile))
                     extraLink.append(libfile)
                     cleanFiles += [libfile, modname + '.exp']
@@ -1771,10 +1761,7 @@ class Freezer:
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
                 else:
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(os.path.dirname(sys.executable), "%s%s"))' % (moduleName, moduleName, moduleName, modext)
-                if sys.version_info >= (3, 2):
-                    code = compile(code, moduleName, 'exec', optimize=2)
-                else:
-                    code = compile(code, moduleName, 'exec')
+                code = compile(code, moduleName, 'exec', optimize=2)
                 code = marshal.dumps(code)
                 moduleList.append((moduleName, len(pool), len(code)))
                 pool += code
@@ -2214,6 +2201,10 @@ class Freezer:
 
         return True
 
+
+_PKG_NAMESPACE_DIRECTORY = object()
+
+
 class PandaModuleFinder(modulefinder.ModuleFinder):
 
     def __init__(self, *args, **kw):
@@ -2262,7 +2253,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 except KeyError:
                     return None
 
-                if sys.version_info >= (3, 0) and 'b' not in mode:
+                if 'b' not in mode:
                     return io.TextIOWrapper(fp, encoding='utf8')
                 return fp
 
@@ -2272,6 +2263,44 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         return None
 
+    def _dir_exists(self, path):
+        """Returns True if the given directory exists, either on disk or inside
+        a wheel."""
+
+        if os.path.isdir(path):
+            return True
+
+        # Is there a zip file along the path?
+        dir, dirname = os.path.split(path.rstrip(os.path.sep + '/'))
+        fn = dirname
+        while dirname:
+            if os.path.isfile(dir):
+                # Okay, this is actually a file.  Is it a zip file?
+                if dir in self._zip_files:
+                    # Yes, and we've previously opened this.
+                    zip = self._zip_files[dir]
+                elif zipfile.is_zipfile(dir):
+                    zip = zipfile.ZipFile(dir)
+                    self._zip_files[dir] = zip
+                else:
+                    # It's a different kind of file.  Stop looking.
+                    return None
+
+                # (Most) zip files do not store directories; check instead for a
+                # file whose path starts with this directory name.
+                prefix = fn.replace(os.path.sep, '/') + '/'
+                for name in zip.namelist():
+                    if name.startswith(prefix):
+                        return True
+
+                return False
+
+            # Look at the parent directory.
+            dir, dirname = os.path.split(dir)
+            fn = os.path.join(dirname, fn)
+
+        return False
+
     def load_module(self, fqname, fp, pathname, file_info):
         """Copied from ModuleFinder.load_module with fixes to handle sending bytes
         to compile() for PY_SOURCE types. Sending bytes to compile allows it to
@@ -2284,6 +2313,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             self.msgout(2, "load_module ->", m)
             return m
 
+        if type is _PKG_NAMESPACE_DIRECTORY:
+            m = self.add_module(fqname)
+            m.__code__ = compile('', '', 'exec')
+            m.__path__ = pathname
+            return m
+
         if type == imp.PY_SOURCE:
             if fqname in overrideModules:
                 # This module has a custom override.
@@ -2294,12 +2329,23 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             code += b'\n' if isinstance(code, bytes) else '\n'
             co = compile(code, pathname, 'exec')
         elif type == imp.PY_COMPILED:
-            try:
-                marshal_data = importlib._bootstrap_external._validate_bytecode_header(fp.read())
-            except ImportError as exc:
-                self.msgout(2, "raise ImportError: " + str(exc), pathname)
-                raise
-            co = marshal.loads(marshal_data)
+            if sys.version_info >= (3, 7):
+                try:
+                    data = fp.read()
+                    importlib._bootstrap_external._classify_pyc(data, fqname, {})
+                except ImportError as exc:
+                    self.msgout(2, "raise ImportError: " + str(exc), pathname)
+                    raise
+
+                co = marshal.loads(memoryview(data)[16:])
+            else:
+                try:
+                    marshal_data = importlib._bootstrap_external._validate_bytecode_header(fp.read())
+                except ImportError as exc:
+                    self.msgout(2, "raise ImportError: " + str(exc), pathname)
+                    raise
+
+                co = marshal.loads(marshal_data)
         else:
             co = None
 
@@ -2365,7 +2411,20 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
             path = self.path
 
+            if fullname == 'distutils' and hasattr(sys, 'real_prefix'):
+                # The PyPI version of virtualenv inserts a special version of
+                # distutils that does some bizarre stuff that won't work in our
+                # deployed application.  Force it to find the regular one.
+                try:
+                    fp, fn, stuff = self.find_module('opcode')
+                    if fn:
+                        path = [os.path.dirname(fn)] + path
+                except ImportError:
+                    pass
+
         # Look for the module on the search path.
+        ns_dirs = []
+
         for dir_path in path:
             basename = os.path.join(dir_path, name.split('.')[-1])
 
@@ -2382,6 +2441,10 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 if self._open_file(init, mode):
                     return (None, basename, ('', '', imp.PKG_DIRECTORY))
 
+            # This may be a namespace package.
+            if self._dir_exists(basename):
+                ns_dirs.append(basename)
+
         # It wasn't found through the normal channels.  Maybe it's one of
         # ours, or maybe it's frozen?
         if not path:
@@ -2390,6 +2453,11 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 # It's a frozen module.
                 return (None, name, ('', '', imp.PY_FROZEN))
 
+        # If we found folders on the path with this module name without an
+        # __init__.py file, we should consider this a namespace package.
+        if ns_dirs:
+            return (None, ns_dirs, ('', '', _PKG_NAMESPACE_DIRECTORY))
+
         raise ImportError(name)
 
     def find_all_submodules(self, m):

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

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

+ 170 - 128
direct/src/dist/commands.py

@@ -1,10 +1,15 @@
+"""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
 
 from pathlib import Path
 from modulefinder import ModuleFinder
 import collections
 import os
-import pip
 import plistlib
 import sys
 import subprocess
@@ -17,7 +22,6 @@ import imp
 import string
 import time
 import tempfile
-import glob
 
 from distutils.version import StrictVersion, LooseVersion
 from wheel import wheelfile, pep425tags
@@ -26,22 +30,18 @@ import distutils.log
 
 from . import FreezeTool
 from . import pefile
+from .icon import Icon
 import panda3d.core as p3d
 
 
-if 'basestring' not in globals():
-    basestring = str
-
-
 if sys.version_info < (3, 0):
     # Python 3 defines these subtypes of IOError, but Python 2 doesn't.
     FileNotFoundError = IOError
 
     # Warn the user.  They might be using Python 2 by accident.
     print("=================================================================")
-    print("WARNING: You are using Python 2, which will soon be discontinued.")
-    print("WARNING: Please use Python 3 for best results and continued")
-    print("WARNING: support after the EOL date of December 31st, 2019.")
+    print("WARNING: You are using Python 2, which has reached the end of its")
+    print("WARNING: life as of January 1, 2020.  Please upgrade to Python 3.")
     print("=================================================================")
     sys.stdout.flush()
     time.sleep(4.0)
@@ -53,7 +53,7 @@ def version_string_from_abi(abi):
     return '.'.join(abi.lstrip('cp').rstrip('mdu'))
 
 def _parse_list(input):
-    if isinstance(input, basestring):
+    if isinstance(input, str):
         input = input.strip().replace(',', '\n')
         if input:
             return [item.strip() for item in input.split('\n') if item.strip()]
@@ -75,6 +75,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([
@@ -114,6 +116,7 @@ PACKAGE_DATA_DIRS = {
         ('cefpython3/subprocess*', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
         ('cefpython3/locals/*', 'locals', {}),
         ('cefpython3/Chromium Embedded Framework.framework/Resources', 'Chromium Embedded Framework.framework/Resources', {}),
+        ('cefpython3/Chromium Embedded Framework.framework/Chromium Embedded Framework', '', {'PKG_DATA_MAKE_EXECUTABLE'}),
     ],
 }
 
@@ -125,45 +128,7 @@ PACKAGE_LIB_DIRS = {
     'scipy':  ['scipy/extra-dll'],
 }
 
-# site.py for Python 2.
-SITE_PY2 = u"""
-import sys
-
-sys.frozen = True
-
-# Override __import__ to set __file__ for frozen modules.
-prev_import = __import__
-def __import__(*args, **kwargs):
-    mod = prev_import(*args, **kwargs)
-    if mod:
-        mod.__file__ = sys.executable
-    return mod
-
-# Add our custom __import__ version to the global scope, as well as a builtin
-# definition for __file__ so that it is available in the module itself.
-import __builtin__
-__builtin__.__import__ = __import__
-__builtin__.__file__ = sys.executable
-del __builtin__
-
-# Set the TCL_LIBRARY directory to the location of the Tcl/Tk/Tix files.
-import os
-tcl_dir = os.path.join(os.path.dirname(sys.executable), 'tcl')
-if os.path.isdir(tcl_dir):
-    for dir in os.listdir(tcl_dir):
-        sub_dir = os.path.join(tcl_dir, dir)
-        if os.path.isdir(sub_dir):
-            if dir.startswith('tcl'):
-                os.environ['TCL_LIBRARY'] = sub_dir
-            if dir.startswith('tk'):
-                os.environ['TK_LIBRARY'] = sub_dir
-            if dir.startswith('tix'):
-                os.environ['TIX_LIBRARY'] = sub_dir
-del os
-"""
-
-# site.py for Python 3.
-SITE_PY3 = u"""
+SITE_PY = u"""
 import sys
 from _frozen_importlib import _imp, FrozenImporter
 
@@ -211,8 +176,6 @@ if os.path.isdir(tcl_dir):
 del os
 """
 
-SITE_PY = SITE_PY3 if sys.version_info >= (3,) else SITE_PY2
-
 
 class build_apps(setuptools.Command):
     description = 'build Panda3D applications'
@@ -235,9 +198,10 @@ class build_apps(setuptools.Command):
         self.exclude_patterns = []
         self.include_modules = {}
         self.exclude_modules = {}
+        self.icons = {}
         self.platforms = [
             'manylinux1_x86_64',
-            'macosx_10_6_x86_64',
+            'macosx_10_9_x86_64',
             'win_amd64',
         ]
         self.plugins = []
@@ -272,6 +236,7 @@ class build_apps(setuptools.Command):
             'libbz2.so.*', 'libz.so.*', 'liblzma.so.*', 'librt.so.*', 'libutil.so.*',
 
             # macOS
+            '/usr/lib/libc++.1.dylib',
             '/usr/lib/libstdc++.*.dylib',
             '/usr/lib/libz.*.dylib',
             '/usr/lib/libobjc.*.dylib',
@@ -310,6 +275,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)
@@ -365,10 +331,22 @@ class build_apps(setuptools.Command):
         tmp.update(self.file_handlers)
         self.file_handlers = tmp
 
-        tmp = self.package_data_dirs.copy()
+        tmp = PACKAGE_DATA_DIRS.copy()
         tmp.update(self.package_data_dirs)
         self.package_data_dirs = tmp
 
+        self.icon_objects = {}
+        for app, iconpaths in self.icons.items():
+            if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple):
+                iconpaths = (iconpaths,)
+
+            iconobj = Icon()
+            for iconpath in iconpaths:
+                iconobj.addImage(iconpath)
+
+            iconobj.generateMissingImages()
+            self.icon_objects[app] = iconobj
+
     def run(self):
         self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO)
 
@@ -381,29 +359,22 @@ class build_apps(setuptools.Command):
         directory containing the Python runtime libraries, which will be added
         to sys.path."""
 
-        self.announce('Gathering wheels for platform: {}'.format(platform), distutils.log.INFO)
-
-        whldir = os.path.join(self.build_base, '__whl_cache__')
+        import pip
 
-        #TODO find a better way to get abi tag than from internal/private pip APIs
-        if hasattr(pip, 'pep425tags'):
-            pep425tags = pip.pep425tags
-            wheel = pip.wheel
-        else:
-            from pip._internal import pep425tags, wheel
+        self.announce('Gathering wheels for platform: {}'.format(platform), distutils.log.INFO)
 
-        abi_tag = pep425tags.get_abi_tag()
+        whlcache = os.path.join(self.build_base, '__whl_cache__')
 
-        if 'u' in abi_tag and (platform.startswith('win') or platform.startswith('macosx')):
-            abi_tag = abi_tag.replace('u', '')
+        pip_version = int(pip.__version__.split('.')[0])
+        if pip_version < 9:
+            raise RuntimeError("pip 9.0 or greater is required, but found {}".format(pip.__version__))
 
-        # For these distributions, we need to append 'u' on Linux
-        if abi_tag in ('cp26m', 'cp27m', 'cp32m') and not platform.startswith('win') and not platform.startswith('macosx'):
-            abi_tag += 'u'
+        abi_tag = 'cp%d%d' % (sys.version_info[:2])
+        if sys.version_info < (3, 8):
+            abi_tag += 'm'
 
-        pip_version = pip.__version__.split('.')
-        if int(pip_version[0]) < 9:
-            raise RuntimeError("pip 9.0 or greater is required, but found {}".format(pip.__version__))
+        whldir = os.path.join(whlcache, '_'.join((platform, abi_tag)))
+        os.makedirs(whldir, exist_ok=True)
 
         # Remove any .zip files. These are built from a VCS and block for an
         # interactive prompt on subsequent downloads.
@@ -434,19 +405,28 @@ class build_apps(setuptools.Command):
 
         subprocess.check_call([sys.executable, '-m', 'pip'] + pip_args)
 
-        # Now figure out which of the downloaded wheels are relevant to us.
-        tags = pep425tags.get_supported(platform=platform, abi=abi_tag)
-        wheelpaths = []
-        for filename in os.listdir(whldir):
-            try:
-                whl = wheel.Wheel(filename)
-            except wheel.InvalidWheelFilename:
-                continue
+        # Return a list of paths to the downloaded whls
+        return [
+            os.path.join(whldir, filename)
+            for filename in os.listdir(whldir)
+            if filename.endswith('.whl')
+        ]
+
+    def update_pe_resources(self, appname, runtime):
+        """Update resources (e.g., icons) in windows PE file"""
 
-            if whl.supported(tags):
-                wheelpaths.append(os.path.join(whldir, filename))
+        icon = self.icon_objects.get(
+            appname,
+            self.icon_objects.get('*', None),
+        )
 
-        return wheelpaths
+        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"""
@@ -489,6 +469,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)
@@ -547,15 +536,19 @@ class build_apps(setuptools.Command):
             libdir = os.path.dirname(dtool_fn.to_os_specific())
             etcdir = os.path.join(libdir, '..', 'etc')
 
-            for fn in os.listdir(etcdir):
+            etcfiles = os.listdir(etcdir)
+            etcfiles.sort(reverse=True)
+            for fn in etcfiles:
                 if fn.lower().endswith('.prc'):
                     with open(os.path.join(etcdir, fn)) as f:
                         prcstring += f.read()
         else:
             etcfiles = [i for i in p3dwhl.namelist() if i.endswith('.prc')]
+            etcfiles.sort(reverse=True)
             for fn in etcfiles:
                 with p3dwhl.open(fn) as f:
                     prcstring += f.read().decode('utf8')
+
         user_prcstring = self.extra_prc_data
         for fn in self.extra_prc_files:
             with open(fn) as f:
@@ -574,12 +567,33 @@ class build_apps(setuptools.Command):
             for ln in prcstr.split('\n'):
                 ln = ln.strip()
                 useline = True
+
                 if ln.startswith('#') or not ln:
                     continue
-                if 'model-cache-dir' in ln:
-                    ln = ln.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
+
+                words = ln.split(None, 1)
+                if not words:
+                    continue
+                var = words[0]
+                value = words[1] if len(words) > 1 else ''
+
+                # Strip comment after value.
+                c = value.find(' #')
+                if c > 0:
+                    value = value[:c].rstrip()
+
+                if var == 'model-cache-dir' and value:
+                    value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
+
+                if var == 'audio-library-name':
+                    # We have the default set to p3fmod_audio on macOS in 1.10,
+                    # but this can be unexpected as other platforms use OpenAL
+                    # by default.  Switch it up if FMOD is not included.
+                    if value not in self.plugins and value == 'p3fmod_audio' and 'p3openal_audio' in self.plugins:
+                        self.warn("Missing audio plugin p3fmod_audio referenced in PRC data, replacing with p3openal_audio")
+
                 for plugin in check_plugins:
-                    if plugin in ln and plugin not in self.plugins:
+                    if plugin in value and plugin not in self.plugins:
                         useline = False
                         if warn_on_missing_plugin:
                             self.warn(
@@ -587,7 +601,10 @@ class build_apps(setuptools.Command):
                             )
                         break
                 if useline:
-                    out.append(ln)
+                    if value:
+                        out.append(var + ' ' + value)
+                    else:
+                        out.append(var)
             return out
         prcexport = parse_prc(prcstring, 0) + parse_prc(user_prcstring, 1)
 
@@ -605,6 +622,28 @@ class build_apps(setuptools.Command):
         freezer_modules = set()
         freezer_modpaths = set()
         ext_suffixes = set()
+
+        def get_search_path_for(source_path):
+            search_path = [os.path.dirname(source_path)]
+            if use_wheels:
+                search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
+
+                # If the .whl containing this file has a .libs directory, add
+                # it to the path.  This is an auditwheel/numpy convention.
+                if '.whl' + os.sep in source_path:
+                    whl, wf = source_path.split('.whl' + os.path.sep)
+                    whl += '.whl'
+                    rootdir = wf.split(os.path.sep, 1)[0]
+                    search_path.append(os.path.join(whl, rootdir, '.libs'))
+
+                    # Also look for more specific per-package cases, defined in
+                    # PACKAGE_LIB_DIRS at the top of this file.
+                    whl_name = os.path.basename(whl).split('-', 1)[0]
+                    extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
+                    for extra_dir in extra_dirs:
+                        search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
+            return search_path
+
         def create_runtime(appname, mainscript, use_console):
             freezer = FreezeTool.Freezer(platform=platform, path=path)
             freezer.addModule('__main__', filename=mainscript)
@@ -633,6 +672,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,
@@ -648,6 +699,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:
@@ -717,11 +771,10 @@ class build_apps(setuptools.Command):
                     basename = module.rsplit('.', 1)[0] + '.' + basename
 
                 # Remove python version string
-                if sys.version_info >= (3, 0):
-                    parts = basename.split('.')
-                    if len(parts) >= 3 and '-' in parts[-2]:
-                        parts = parts[:-2] + parts[-1:]
-                        basename = '.'.join(parts)
+                parts = basename.split('.')
+                if len(parts) >= 3 and '-' in parts[-2]:
+                    parts = parts[:-2] + parts[-1:]
+                    basename = '.'.join(parts)
             else:
                 # Builtin module, but might not be builtin in wheel libs, so double check
                 if module in whl_modules:
@@ -732,35 +785,16 @@ class build_apps(setuptools.Command):
                     continue
 
             # If this is a dynamic library, search for dependencies.
-            search_path = [os.path.dirname(source_path)]
-            if use_wheels:
-                search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
-
-                # If the .whl containing this file has a .libs directory, add
-                # it to the path.  This is an auditwheel/numpy convention.
-                if '.whl' + os.sep in source_path:
-                    whl, wf = source_path.split('.whl' + os.path.sep)
-                    whl += '.whl'
-                    rootdir = wf.split(os.path.sep, 1)[0]
-                    search_path.append(os.path.join(whl, rootdir, '.libs'))
-
-                    # Also look for more specific per-package cases, defined in
-                    # PACKAGE_LIB_DIRS at the top of this file.
-                    whl_name = os.path.basename(whl).split('-', 1)[0]
-                    extra_dirs = PACKAGE_LIB_DIRS.get(whl_name, [])
-                    for extra_dir in extra_dirs:
-                        search_path.append(os.path.join(whl, extra_dir.replace('/', os.path.sep)))
-
             target_path = os.path.join(builddir, basename)
+            search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_path)
 
         # Copy over the tcl directory.
         #TODO: get this to work on non-Windows platforms.
         if sys.platform == "win32" and platform.startswith('win'):
             tcl_dir = os.path.join(sys.prefix, 'tcl')
-            tkinter_name = 'tkinter' if sys.version_info >= (3, 0) else 'Tkinter'
 
-            if os.path.isdir(tcl_dir) and tkinter_name in freezer_modules:
+            if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
                 self.announce('Copying Tcl files', distutils.log.INFO)
                 os.makedirs(os.path.join(builddir, 'tcl'))
 
@@ -783,7 +817,7 @@ class build_apps(setuptools.Command):
                 whlfile = self._get_zip_file(whl)
                 filenames = whlfile.namelist()
                 for source_pattern, target_dir, flags in datadesc:
-                    srcglob = p3d.GlobPattern(source_pattern)
+                    srcglob = p3d.GlobPattern(source_pattern.lower())
                     source_dir = os.path.dirname(source_pattern)
                     # Relocate the target dir to the build directory.
                     target_dir = target_dir.replace('/', os.sep)
@@ -797,12 +831,15 @@ class build_apps(setuptools.Command):
                             relpath = wf[len(source_dir) + 1:]
                             source_path = os.path.join(whl, wf)
                             target_path = os.path.join(target_dir, relpath)
-                            self.copy(source_path, target_path)
 
                             if 'PKG_DATA_MAKE_EXECUTABLE' in flags:
+                                search_path = get_search_path_for(source_path)
+                                self.copy_with_dependencies(source_path, target_path, search_path)
                                 mode = os.stat(target_path).st_mode
                                 mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
                                 os.chmod(target_path, mode)
+                            else:
+                                self.copy(source_path, target_path)
 
         # Copy Game Files
         self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO)
@@ -854,6 +891,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)
 
@@ -1127,18 +1167,20 @@ class build_apps(setuptools.Command):
                     dylib = dylib.replace('@loader_path/../Frameworks/', '')
                 elif dylib.startswith('@executable_path/../Frameworks/'):
                     dylib = dylib.replace('@executable_path/../Frameworks/', '')
-                elif dylib.startswith('@loader_path/'):
-                    dylib = dylib.replace('@loader_path/', '')
-
-                    # Do we need to flatten the relative reference?
-                    if '/' in dylib and flatten:
-                        new_dylib = '@loader_path/' + os.path.basename(dylib)
-                        str_size = len(cmd_data) - 16
-                        if len(new_dylib) < str_size:
-                            fp.seek(-str_size, os.SEEK_CUR)
-                            fp.write(new_dylib.encode('ascii').ljust(str_size, b'\0'))
-                        else:
-                            self.warn('Unable to rewrite dependency {}'.format(orig))
+                else:
+                    for prefix in ('@loader_path/', '@rpath/'):
+                        if dylib.startswith(prefix):
+                            dylib = dylib.replace(prefix, '')
+
+                            # Do we need to flatten the relative reference?
+                            if '/' in dylib and flatten:
+                                new_dylib = prefix + os.path.basename(dylib)
+                                str_size = len(cmd_data) - 16
+                                if len(new_dylib) < str_size:
+                                    fp.seek(-str_size, os.SEEK_CUR)
+                                    fp.write(new_dylib.encode('ascii').ljust(str_size, b'\0'))
+                                else:
+                                    self.warn('Unable to rewrite dependency {}'.format(orig))
 
                 load_dylibs.append(dylib)
 
@@ -1656,4 +1698,4 @@ class make_xcodeproj(setuptools.Command):
         shutil.copytree(panda_app_target_dir, proj_folder)
         shutil.copytree(panda_app_template_dir, proj_name)
 
-        shutil.rmtree(self.tmp_dir)
+        shutil.rmtree(self.tmp_dir)

+ 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

+ 6 - 10
direct/src/dist/pefile.py

@@ -10,11 +10,7 @@ from collections import namedtuple
 from array import array
 import time
 from io import BytesIO
-import sys
 
-if sys.version_info >= (3, 0):
-    unicode = str
-    unichr = chr
 
 # Define some internally used structures.
 RVASize = namedtuple('RVASize', ('addr', 'size'))
@@ -38,7 +34,7 @@ def _unpack_wstring(mem, offs=0):
     name = ""
     for i in range(name_len):
         offs += 2
-        name += unichr(*unpack('<H', mem[offs:offs+2]))
+        name += chr(*unpack('<H', mem[offs:offs+2]))
     return name
 
 def _padded(n, boundary):
@@ -208,7 +204,7 @@ class VersionInfoResource(object):
         if isinstance(value, dict):
             type = 1
             value_length = 0
-        elif isinstance(value, bytes) or isinstance(value, unicode):
+        elif isinstance(value, bytes) or isinstance(value, str):
             type = 1
             value_length = len(value) * 2 + 2
         else:
@@ -227,7 +223,7 @@ class VersionInfoResource(object):
         if isinstance(value, dict):
             for key2, value2 in sorted(value.items(), key=lambda x:x[0]):
                 self._pack_info(data, key2, value2)
-        elif isinstance(value, bytes) or isinstance(value, unicode):
+        elif isinstance(value, bytes) or isinstance(value, str):
             for c in value:
                 data += pack('<H', ord(c))
             data += b'\x00\x00'
@@ -294,7 +290,7 @@ class VersionInfoResource(object):
         c, = unpack('<H', data[offset:offset+2])
         offset += 2
         while c:
-            key += unichr(c)
+            key += chr(c)
             c, = unpack('<H', data[offset:offset+2])
             offset += 2
 
@@ -309,7 +305,7 @@ class VersionInfoResource(object):
                 c, = unpack('<H', data[offset:offset+2])
                 offset += 2
                 while c:
-                    value += unichr(c)
+                    value += chr(c)
                     c, = unpack('<H', data[offset:offset+2])
                     offset += 2
             else:
@@ -705,7 +701,7 @@ class PEFile(object):
 
         Returns the newly created Section object. """
 
-        if isinstance(name, unicode):
+        if isinstance(name, str):
             name = name.encode('ascii')
 
         section = Section()

+ 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

+ 11 - 7
direct/src/distributed/CMakeLists.txt

@@ -4,28 +4,32 @@ endif()
 
 set(P3DISTRIBUTED_HEADERS
   config_distributed.h
-  cConnectionRepository.I
   cConnectionRepository.h
-  cDistributedSmoothNodeBase.I
+  cConnectionRepository.I
   cDistributedSmoothNodeBase.h
+  cDistributedSmoothNodeBase.I
 )
 
 set(P3DISTRIBUTED_SOURCES
   config_distributed.cxx
+)
+
+set(P3DISTRIBUTED_IGATEEXT
   cConnectionRepository.cxx
   cDistributedSmoothNodeBase.cxx
 )
 
 add_component_library(p3distributed NOINIT SYMBOL BUILDING_DIRECT_DISTRIBUTED
-  ${P3DISTRIBUTED_HEADERS})
+  ${P3DISTRIBUTED_HEADERS} ${P3DISTRIBUTED_SOURCES})
 target_link_libraries(p3distributed p3directbase p3dcparser panda)
-target_interrogate(p3distributed ALL EXTENSIONS ${P3DISTRIBUTED_SOURCES})
+target_interrogate(p3distributed ALL EXTENSIONS ${P3DISTRIBUTED_IGATEEXT})
 
 if(NOT BUILD_METALIBS)
   install(TARGETS p3distributed
     EXPORT Direct COMPONENT Direct
-    DESTINATION lib
-    RUNTIME DESTINATION bin
+    DESTINATION ${CMAKE_INSTALL_LIBDIR}
+    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d
     ARCHIVE COMPONENT DirectDevel)
 endif()
-install(FILES ${P3DISTRIBUTED_HEADERS} COMPONENT DirectDevel DESTINATION include/panda3d)
+install(FILES ${P3DISTRIBUTED_HEADERS} COMPONENT DirectDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 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 - 6
direct/src/distributed/NetMessenger.py

@@ -2,12 +2,7 @@
 from direct.directnotify import DirectNotifyGlobal
 from direct.distributed.PyDatagram import PyDatagram
 from direct.showbase.Messenger import Messenger
-
-import sys
-if sys.version_info >= (3, 0):
-    from pickle import dumps, loads
-else:
-    from cPickle import dumps, loads
+from pickle import dumps, loads
 
 
 # Messages do not need to be in the MESSAGE_TYPES list.

部分文件因文件數量過多而無法顯示