Ver código fonte

Merge branch 'master' into collision-heightfield

hecris 5 anos atrás
pai
commit
c80017b6ec
100 arquivos alterados com 6933 adições e 1829 exclusões
  1. 22 0
      .editorconfig
  2. 2 0
      .github/FUNDING.yml
  3. 25 0
      .github/ISSUE_TEMPLATE/bug.md
  4. 15 0
      .github/ISSUE_TEMPLATE/enhancement.md
  5. 15 0
      .github/PULL_REQUEST_TEMPLATE.md
  6. 16 0
      .github/codecov.yml
  7. 384 0
      .github/workflows/ci.yml
  8. 6 0
      .gitignore
  9. 2 3
      .travis.yml
  10. 8 0
      BACKERS.md
  11. 190 0
      CMakeLists.txt
  12. 55 0
      CONTRIBUTING.md
  13. 27 23
      README.md
  14. 78 0
      cmake/README.md
  15. 157 0
      cmake/install/Panda3DConfig.cmake
  16. 75 0
      cmake/macros/AddBisonTarget.cmake
  17. 77 0
      cmake/macros/AddFlexTarget.cmake
  18. 426 0
      cmake/macros/BuildMetalib.cmake
  19. 167 0
      cmake/macros/CompositeSources.cmake
  20. 400 0
      cmake/macros/Interrogate.cmake
  21. 506 0
      cmake/macros/PackageConfig.cmake
  22. 50 0
      cmake/macros/PerConfigOption.cmake
  23. 163 0
      cmake/macros/Python.cmake
  24. 9 0
      cmake/macros/README.md
  25. 37 0
      cmake/macros/RunPzip.cmake
  26. 21 0
      cmake/macros/Versioning.cmake
  27. 33 0
      cmake/modules/FindARToolKit.cmake
  28. 33 0
      cmake/modules/FindAssimp.cmake
  29. 175 0
      cmake/modules/FindCg.cmake
  30. 104 0
      cmake/modules/FindDirect3D9.cmake
  31. 21 0
      cmake/modules/FindEGL.cmake
  32. 25 0
      cmake/modules/FindEigen3.cmake
  33. 88 0
      cmake/modules/FindFCollada.cmake
  34. 116 0
      cmake/modules/FindFFMPEG.cmake
  35. 107 0
      cmake/modules/FindFFTW3.cmake
  36. 83 0
      cmake/modules/FindFMODEx.cmake
  37. 23 0
      cmake/modules/FindHarfBuzz.cmake
  38. 78 0
      cmake/modules/FindLibSquish.cmake
  39. 157 0
      cmake/modules/FindODE.cmake
  40. 20 0
      cmake/modules/FindOgg.cmake
  41. 92 0
      cmake/modules/FindOpenCV.cmake
  42. 88 0
      cmake/modules/FindOpenEXR.cmake
  43. 21 0
      cmake/modules/FindOpenGLES1.cmake
  44. 21 0
      cmake/modules/FindOpenGLES2.cmake
  45. 34 0
      cmake/modules/FindOpusFile.cmake
  46. 50 0
      cmake/modules/FindSWResample.cmake
  47. 50 0
      cmake/modules/FindSWScale.cmake
  48. 40 0
      cmake/modules/FindVRPN.cmake
  49. 34 0
      cmake/modules/FindVorbisFile.cmake
  50. 7 0
      cmake/modules/README.md
  51. 70 0
      cmake/scripts/ConcatenateToCXX.cmake
  52. 27 0
      cmake/scripts/CopyPattern.cmake
  53. 78 0
      cmake/scripts/CopyPython.cmake
  54. 28 0
      cmake/scripts/MakeComposite.cmake
  55. 10 0
      cmake/scripts/README.md
  56. 7 0
      cmake/templates/METADATA.in
  57. 9 0
      cmake/templates/metalib_init.cxx.in
  58. 9 0
      cmake/templates/metalib_init.h.in
  59. 23 0
      cmake/templates/win32_python/__init__.py
  60. 18 0
      contrib/CMakeLists.txt
  61. 54 0
      contrib/src/ai/CMakeLists.txt
  62. 3 0
      contrib/src/ai/aiBehaviors.h
  63. 14 0
      contrib/src/contribbase/CMakeLists.txt
  64. 52 0
      contrib/src/rplight/CMakeLists.txt
  65. 75 0
      direct/CMakeLists.txt
  66. 117 104
      direct/src/actor/Actor.py
  67. 2 1
      direct/src/cluster/ClusterClient.py
  68. 20 17
      direct/src/cluster/ClusterConfig.py
  69. 6 8
      direct/src/cluster/ClusterMsgs.py
  70. 17 16
      direct/src/controls/ControlManager.py
  71. 8 6
      direct/src/controls/DevWalker.py
  72. 8 6
      direct/src/controls/GhostWalker.py
  73. 8 6
      direct/src/controls/GravityWalker.py
  74. 20 18
      direct/src/controls/InputState.py
  75. 8 6
      direct/src/controls/NonPhysicsWalker.py
  76. 8 6
      direct/src/controls/ObserverWalker.py
  77. 0 2
      direct/src/controls/PhysicsRoller.py
  78. 8 6
      direct/src/controls/PhysicsWalker.py
  79. 1 1
      direct/src/controls/TwoDWalker.py
  80. 3 0
      direct/src/dcparse/CMakeLists.txt
  81. 87 0
      direct/src/dcparser/CMakeLists.txt
  82. 7 691
      direct/src/dcparser/dcClass.cxx
  83. 60 45
      direct/src/dcparser/dcClass.h
  84. 657 0
      direct/src/dcparser/dcClass_ext.cxx
  85. 93 0
      direct/src/dcparser/dcClass_ext.h
  86. 6 313
      direct/src/dcparser/dcField.cxx
  87. 13 15
      direct/src/dcparser/dcField.h
  88. 293 0
      direct/src/dcparser/dcField_ext.cxx
  89. 48 0
      direct/src/dcparser/dcField_ext.h
  90. 3 0
      direct/src/dcparser/dcLexer.cxx.prebuilt
  91. 3 1
      direct/src/dcparser/dcLexer.lxx
  92. 5 3
      direct/src/dcparser/dcPacker.I
  93. 0 508
      direct/src/dcparser/dcPacker.cxx
  94. 17 14
      direct/src/dcparser/dcPacker.h
  95. 1 3
      direct/src/dcparser/dcPackerCatalog.cxx
  96. 3 3
      direct/src/dcparser/dcPackerInterface.I
  97. 1 3
      direct/src/dcparser/dcPackerInterface.cxx
  98. 0 1
      direct/src/dcparser/dcPackerInterface.h
  99. 447 0
      direct/src/dcparser/dcPacker_ext.cxx
  100. 45 0
      direct/src/dcparser/dcPacker_ext.h

+ 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,T}]
+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
+

+ 25 - 0
.github/ISSUE_TEMPLATE/bug.md

@@ -0,0 +1,25 @@
+---
+name: Bug report
+about: Report a defect
+title:
+labels:
+assignees:
+---
+
+## Description
+<!-- Provide a clear and concise description of the bug: what you expected to
+     happen, and what actually happened instead.  If you have a log file or
+     crash report, please attach those! -->
+
+## Steps to Reproduce
+<!-- The most minimal but complete set of steps to reproduce the bug, with a
+     code snippet that clearly demonstrates the bug.  We must be able to run
+     the code, ie. you must include any required assets. -->
+
+## Environment
+ * Operating system:
+ * System architecture:
+ * Panda3D version: <!-- import panda3d; print(panda3d.__version__) -->
+ * Installation method: (eg. pip, wheel, SDK, built from source)
+ * Python version (if using Python):
+ * Compiler (if using C++):

+ 15 - 0
.github/ISSUE_TEMPLATE/enhancement.md

@@ -0,0 +1,15 @@
+---
+name: Enhancement request
+about: Request new functionality or improvement to existing functionality
+title:
+labels: enhancement
+assignees:
+---
+
+## Description
+<!-- Please clearly describe which change you want to be made. -->
+
+## Use Case
+<!-- Describe why you need this change, how it will help you, including any
+     other imagined scenarios in which it will be useful.  If this request is
+     urgent and you have no alternatives, please state so as well. -->

+ 15 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,15 @@
+## Issue description
+<!-- What is this change intended to accomplish?  What problem does it solve?
+     If this change resolves a GitHub issue, include the issue number. -->
+
+## Solution description
+<!-- Explain here how your PR solves the problem, what approach it takes. -->
+
+## Checklist
+I have done my best to ensure that…
+* [ ] …I have familiarized myself with the CONTRIBUTING.md file
+* [ ] …this change follows the coding style and design patterns of the codebase
+* [ ] …I own the intellectual property rights to this code
+* [ ] …the intent of this change is clearly explained
+* [ ] …existing uses of the Panda3D API are not broken
+* [ ] …the changed code is adequately covered by the test suite, where possible.

+ 16 - 0
.github/codecov.yml

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

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

@@ -0,0 +1,384 @@
+name: Continuous Integration
+on: [push, pull_request]
+
+jobs:
+  cmake:
+    name: CMake Buildsystem
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
+
+    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:
+    if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')"
+    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

+ 2 - 3
.travis.yml

@@ -5,9 +5,9 @@ matrix:
     - compiler: clang
       env: PYTHONV=python3 FLAGS=--installer
     - compiler: clang
-      env: PYTHONV=python2.7 FLAGS=--override=STDFLOAT_DOUBLE=1
+      env: PYTHONV=python3 FLAGS=--override=STDFLOAT_DOUBLE=1
     - compiler: gcc
-      env: PYTHONV=python2.7 FLAGS=--optimize=4
+      env: PYTHONV=python3 FLAGS=--optimize=4
       before_install:
         - export CC=gcc-4.7
         - export CXX=g++-4.7
@@ -44,7 +44,6 @@ install:
     - $PYTHONV -m pip install pytest
 script:
     - $PYTHONV makepanda/makepanda.py --everything --git-commit $TRAVIS_COMMIT $FLAGS --threads 4
-    - test -n "$SKIP_TESTS" || LD_LIBRARY_PATH=built/lib PYTHONPATH=built $PYTHONV makepanda/test_imports.py
     - test -n "$SKIP_TESTS" || LD_LIBRARY_PATH=built/lib PYTHONPATH=built $PYTHONV -m pytest -v tests
 notifications:
   irc:

+ 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
 

+ 190 - 0
CMakeLists.txt

@@ -0,0 +1,190 @@
+cmake_minimum_required(VERSION 3.0.2)
+set(CMAKE_DISABLE_SOURCE_CHANGES ON) # Must go before project() below
+set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) # Must go before project() below
+
+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)
+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()
+
+# 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
+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()
+
+string(REPLACE "$(EFFECTIVE_PLATFORM_NAME)" "" PANDA_CFG_INTDIR "${CMAKE_CFG_INTDIR}")
+
+# Add generic modules to cmake module path,
+# and add Panda3D specific modules to cmake module path
+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/")
+
+if(CMAKE_VERSION VERSION_GREATER "3.8")
+  # 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)
+endif()
+
+# 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)
+include(dtool/PandaVersion.cmake)
+include(dtool/Package.cmake)
+include(dtool/Config.cmake)
+
+# Include global modules
+include(AddBisonTarget)     # Defines add_bison_target function
+include(AddFlexTarget)      # Defines add_flex_target function
+include(BuildMetalib)       # Defines add_component_library AND add_metalib
+include(CompositeSources)   # Defines composite_sources function
+include(Python)             # Defines add_python_target AND install_python_package
+include(Interrogate)        # Defines target_interrogate AND add_python_module
+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_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)
+option(BUILD_MODELS "Build/install the built-in models." ON)
+
+# Include Panda3D packages
+if(BUILD_DTOOL)
+  add_subdirectory(dtool "${CMAKE_BINARY_DIR}/dtool")
+endif()
+
+if(BUILD_PANDA)
+  add_subdirectory(panda "${CMAKE_BINARY_DIR}/panda")
+endif()
+
+if(BUILD_DIRECT)
+  add_subdirectory(direct "${CMAKE_BINARY_DIR}/direct")
+endif()
+
+if(BUILD_PANDATOOL)
+  add_subdirectory(pandatool "${CMAKE_BINARY_DIR}/pandatool")
+endif()
+
+if(BUILD_CONTRIB)
+  add_subdirectory(contrib "${CMAKE_BINARY_DIR}/contrib")
+endif()
+
+if(BUILD_MODELS)
+  run_pzip(models
+    "${CMAKE_CURRENT_SOURCE_DIR}/models/"
+    "${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR}/models"
+    *.egg)
+  run_pzip(dmodels
+    "${CMAKE_CURRENT_SOURCE_DIR}/dmodels/src/"
+    "${PROJECT_BINARY_DIR}/${PANDA_CFG_INTDIR}/models"
+    *.egg)
+
+  add_custom_command(TARGET models
+                     POST_BUILD
+                     COMMAND ${CMAKE_COMMAND}
+                             -DSOURCE="${CMAKE_CURRENT_SOURCE_DIR}/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="${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 "${PANDA_OUTPUT_DIR}/models"
+    COMPONENT Models DESTINATION ${CMAKE_INSTALL_DATADIR}/panda3d)
+endif()
+
+if(INTERROGATE_PYTHON_INTERFACE)
+  # If we built the Python interface, run the test suite.  Note, we do NOT test
+  # for pytest before adding this test.  If the user doesn't have pytest, we'd
+  # like for the tests to fail.
+
+  # In the Coverage configuration, we also require pytest-cov
+
+  add_test(NAME pytest
+    COMMAND "${PYTHON_EXECUTABLE}" -m pytest "${PROJECT_SOURCE_DIR}/tests"
+    $<$<CONFIG:Coverage>:--cov=.>
+    WORKING_DIRECTORY "${PANDA_OUTPUT_DIR}")
+endif()
+
+# Generate the Panda3DConfig.cmake file so find_package(Panda3D) works, and
+# also register the build directory with CMake's package registry.
+
+file(COPY "${PROJECT_SOURCE_DIR}/cmake/install/Panda3DConfig.cmake"
+  DESTINATION "${PROJECT_BINARY_DIR}")
+install(FILES "${PROJECT_SOURCE_DIR}/cmake/install/Panda3DConfig.cmake"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D")
+
+include(CMakePackageConfigHelpers)
+write_basic_package_version_file(
+  "${PROJECT_BINARY_DIR}/Panda3DConfigVersion.cmake"
+  VERSION "${PROJECT_VERSION}"
+  COMPATIBILITY AnyNewerVersion)
+install(FILES "${PROJECT_BINARY_DIR}/Panda3DConfigVersion.cmake"
+  DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D")
+
+if(NOT CMAKE_CROSSCOMPILING)
+  export(PACKAGE Panda3D)
+endif()

+ 55 - 0
CONTRIBUTING.md

@@ -0,0 +1,55 @@
+# Contributing to Panda3D
+
+Panda3D is an open-source, community-driven project, completely dependent on the
+contribution of volunteers.  As such we highly welcome you to contribute code to
+the project.  This document aims to outline some guidelines for doing so.
+
+If you would like to contribute but aren't sure where to start, please visit the
+[Get Involved](https://www.panda3d.org/get-involved/) page on our website, or
+the excellent [Open Source Guide](https://opensource.guide/how-to-contribute/).
+
+## Before implementing a change
+
+We highly recommend that you file issues before making a change.  Issues are
+used to track bugs and feature requests but also to get feedback from the other
+developers about design decisions or a specific implementation strategy.
+
+It is important for acceptance that the change is implemented in a way that fits
+the general design principles of the Panda3D API, and fits well with the general
+priorities of the team.  Therefore, prior discussion with other developers is
+critical.  Issues can be used to facilitate this, but we also invite you to
+visit the #development channel on Discord (or #panda3d-devel on FreeNode IRC).
+
+We also recommend that you familiarize yourself with the established coding
+style and design patterns of Panda3D, to reduce the amount of changes that have
+to be made during the review process.
+
+## Submitting a change
+
+All changes from non-core contributors are made via pull requests.  This
+requires you to fork the Panda3D repository, create a branch for your change,
+push your changes to this branch, and request that this branch is merged into
+the upstream branch.  Each pull request is reviewed by a maintainer and
+automatically tested for regressions and unit test coverage.  The maintainer
+will suggest any changes, which you can add by committing more code to the same
+branch (you can do a force push if necessary).  Once the change is deemed
+acceptable, the maintainer will merge your change into the appropriate branch of
+the repository.
+
+To make it easier for the maintainer to review your changes, we highly recommend
+that you give a clear and concise description of intent (linking to any issues
+that are resolved by the change), as well as the inclusion of unit tests, which
+contribute to a high level of confidence that this change does not break any
+existing behaviours.  We also recommend breaking up separate changes into
+separate PRs, rather than submitting one big PR with several unrelated changes.
+
+If your change is still a work in progress, please mark the PR as "draft".  This
+will prevent other contributors from receiving an email every time you push a
+new change to your branch.  Draft PRs can also be used to invite early feedback
+on your change, especially if you are uncertain about whether you are going in
+the right direction.
+
+The code in the Panda3D repository is copyrighted to Carnegie Mellon University
+and licensed under the Modified BSD License.  By submitting your changes, you
+accept that your code becomes placed under the same license.  Except in specific
+agreed-upon cases, we do not accept code contributions under alternate licenses.

+ 27 - 23
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
@@ -99,7 +100,7 @@ for you to install, depending on your distribution).
 The following command illustrates how to build Panda3D with some common
 options:
 ```bash
-python makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2 --no-opencv
+python3 makepanda/makepanda.py --everything --installer --no-egl --no-gles --no-gles2 --no-opencv
 ```
 
 You will probably see some warnings saying that it's unable to find several
@@ -112,11 +113,11 @@ If you are on Ubuntu, this command should cover the most frequently
 used third-party packages:
 
 ```bash
-sudo apt-get install build-essential pkg-config fakeroot python-dev libpng-dev libjpeg-dev libtiff-dev zlib1g-dev libssl-dev libx11-dev libgl1-mesa-dev libxrandr-dev libxxf86dga-dev libxcursor-dev bison flex libfreetype6-dev libvorbis-dev libeigen3-dev libopenal-dev libode-dev libbullet-dev nvidia-cg-toolkit libgtk2.0-dev libassimp-dev libopenexr-dev
+sudo apt-get install build-essential pkg-config fakeroot python3-dev libpng-dev libjpeg-dev libtiff-dev zlib1g-dev libssl-dev libx11-dev libgl1-mesa-dev libxrandr-dev libxxf86dga-dev libxcursor-dev bison flex libfreetype6-dev libvorbis-dev libeigen3-dev libopenal-dev libode-dev libbullet-dev nvidia-cg-toolkit libgtk2.0-dev libassimp-dev libopenexr-dev
 ```
 
 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.
 

+ 78 - 0
cmake/README.md

@@ -0,0 +1,78 @@
+Building with CMake
+-------------------
+
+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 ..
+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 .`),
+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>=YES/NO   # Example: USE_JPEG
+```
+Panda subpackage building is handled by:
+```
+	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=YES
+
+	# ... etc ...
+
+```
+
+For example, `makepanda.py --distributor X` becomes `cmake -DPANDA_DISTRIBUTOR=X`
+
+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).
+
+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.

+ 157 - 0
cmake/install/Panda3DConfig.cmake

@@ -0,0 +1,157 @@
+# 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."
+#
+# Author: CFSworks (Dec. 11, 2018)
+
+# This file is installed to CMake's package search path, and is invoked for
+# find_package(Panda3D [COMPONENTS ...])
+#
+# The following components are available for importing:
+#
+#   Core      - The core Panda3D libraries; this component is always included.
+#
+#               Panda3D::Core::panda
+#               Panda3D::Core::pandaexpress
+#               etc.
+#
+#
+#   Python    - Python targets, which can be used for linking against the Python
+#               extension modules directly.  Note that this also imports the
+#               Python bindings for other requested components that have them.
+#
+#               Panda3D::Python::panda3d.core
+#               Panda3D::Python::panda3d.physics
+#               etc.
+#
+#
+#   Tools     - Various tools used in asset manipulation and debugging.
+#
+#               Panda3D::Tools::egg2bam
+#               Panda3D::Tools::egg-optchar
+#               Panda3D::Tools::pview
+#               etc.
+#
+#
+#   Direct    - Panda's "direct" Python framework; C++ support library.
+#
+#               Panda3D::Direct::p3direct
+#
+#
+#   Contrib   - Extensions not part of the Panda3D core, but contributed by the
+#               community.
+#
+#               Panda3D::Contrib::p3ai
+#               Panda3D::Contrib::p3rplight
+#
+#
+#   Framework - Panda's "p3framework" C++ framework.
+#
+#               Panda3D::Framework::p3framework
+#
+#
+#   Egg       - Support for the Egg file format.
+#
+#               Panda3D::Egg::pandaegg
+#
+#
+#   Bullet    - Support for Bullet physics.
+#
+#               Panda3D::Bullet::p3bullet
+#
+#
+#   ODE       - Support for the ODE physics engine.
+#
+#               Panda3D::ODE::p3ode
+#
+#
+#   FFmpeg    - Support for FFmpeg media format loading.
+#
+#               Panda3D::FFmpeg::p3ffmpeg
+#
+#
+#   OpenAL    - Support for OpenAL audio output.
+#
+#               Panda3D::OpenAL::p3openal_audio
+#
+#
+#   FMOD      - Support for FMOD audio output.
+#
+#               Panda3D::FMOD::p3fmod_audio
+#
+#
+#   OpenGL    - Support for OpenGL rendering.
+#
+#               Panda3D::OpenGL::pandagl
+#
+#
+#   DX9       - Support for Direct3D 9 rendering.
+#
+#               Panda3D::DX9::pandadx9
+#
+#
+#   OpenGLES1 - Support for OpenGL ES 1.x rendering.
+#
+#               Panda3D::OpenGLES1::pandagles
+#
+#
+#   OpenGLES2 - Support for OpenGL ES 2.x+ rendering.
+#
+#               Panda3D::OpenGLES2::pandagles2
+#
+#
+#   Vision    - Support for vision processing.
+#
+#               Panda3D::Vision::p3vision
+#
+#
+#   VRPN      - Support for connecting to a VRPN virtual reality server.
+#
+#               Panda3D::VRPN::p3vrpn
+
+if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 3.0)
+  message(FATAL_ERROR "CMake >= 3.0.2 required")
+endif()
+
+get_filename_component(_panda_config_prefix "${CMAKE_CURRENT_LIST_FILE}" PATH)
+
+include("${_panda_config_prefix}/Panda3DPackages.cmake")
+
+set(_panda_components
+  Core Python Tools
+  Direct Contrib Framework Egg
+  Bullet ODE
+  FFmpeg
+  OpenAL FMOD
+  OpenGL DX9 OpenGLES1 OpenGLES2
+  Vision VRPN
+)
+
+set(Panda3D_FIND_REQUIRED_Core ON)
+
+foreach(_comp Core ${Panda3D_FIND_COMPONENTS})
+  if(";${_panda_components};" MATCHES ";${_comp};" AND
+     EXISTS "${_panda_config_prefix}/Panda3D${_comp}Targets.cmake")
+
+    include("${_panda_config_prefix}/Panda3D${_comp}Targets.cmake")
+
+    if(";${Panda3D_FIND_COMPONENTS};" MATCHES ";Python;" AND
+       EXISTS "${_panda_config_prefix}/Panda3D${_comp}PythonTargets.cmake")
+
+      include("${_panda_config_prefix}/Panda3D${_comp}PythonTargets.cmake")
+
+    endif()
+
+  elseif(Panda3D_FIND_REQUIRED_${_comp})
+
+    message(FATAL_ERROR "Panda3D REQUIRED component ${_comp} not found")
+
+  endif()
+
+endforeach(_comp)
+unset(_comp)
+
+unset(_panda_components)

+ 75 - 0
cmake/macros/AddBisonTarget.cmake

@@ -0,0 +1,75 @@
+# Filename: AddBisonTarget.cmake
+# Description: This file defines the function add_bison_target which instructs
+#   cmake to use bison on an input .yxx file.  If bison is not available on
+#   the system, add_bison_target tries to use .prebuilt .cxx files instead.
+#
+# Usage:
+#   add_bison_target(output_cxx input_yxx [DEFINES output_h] [PREFIX prefix])
+#
+
+# Define add_bison_target()
+function(add_bison_target output_cxx input_yxx)
+  set(arguments "")
+  set(outputs "${output_cxx}")
+  set(keyword "")
+
+  # Parse the extra arguments to the function.
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "DEFINES")
+      set(keyword "DEFINES")
+
+    elseif(arg STREQUAL "PREFIX")
+      set(keyword "PREFIX")
+
+    elseif(keyword STREQUAL "PREFIX")
+      list(APPEND arguments -p "${arg}")
+
+    elseif(keyword STREQUAL "DEFINES")
+      list(APPEND arguments --defines="${arg}")
+      list(APPEND outputs "${arg}")
+
+    else()
+      message(SEND_ERROR "Unexpected argument ${arg} to add_bison_target")
+
+    endif()
+  endforeach()
+
+  if(keyword STREQUAL arg AND NOT keyword STREQUAL "")
+    message(SEND_ERROR "Expected argument after ${keyword}")
+  endif()
+
+  if(HAVE_BISON)
+    get_source_file_property(input_yxx "${input_yxx}" LOCATION)
+
+    # If we have bison, we can of course just run it.
+    add_custom_command(
+      OUTPUT ${outputs}
+      COMMAND ${BISON_EXECUTABLE}
+        -o "${output_cxx}" ${arguments}
+        "${input_yxx}"
+      MAIN_DEPENDENCY "${input_yxx}"
+    )
+
+  else()
+    # Look for prebuilt versions of the outputs.
+    set(commands "")
+    set(depends "")
+
+    foreach(output ${outputs})
+      set(prebuilt_file "${output}.prebuilt")
+      get_filename_component(prebuilt_file "${prebuilt_file}" ABSOLUTE)
+
+      if(NOT EXISTS "${prebuilt_file}")
+        message(SEND_ERROR "Bison was not found and ${prebuilt_file} does not exist!")
+      endif()
+
+      list(APPEND depends "${prebuilt_file}")
+      list(APPEND commands COMMAND ${CMAKE_COMMAND} -E copy ${prebuilt_file} ${output})
+    endforeach()
+
+    add_custom_command(
+      OUTPUT ${outputs}
+      ${commands}
+      DEPENDS ${depends})
+  endif()
+endfunction(add_bison_target)

+ 77 - 0
cmake/macros/AddFlexTarget.cmake

@@ -0,0 +1,77 @@
+# Filename: AddFlexTarget.cmake
+# Description: This file defines the function add_flex_target which instructs
+#   cmake to use flex on an input .lxx file.  If flex is not available on
+#   the system, add_flex_target tries to use .prebuilt .cxx files instead.
+#
+# Usage:
+#   add_flex_target(output_cxx input_lxx [DEFINES output_h] [PREFIX prefix])
+#
+
+# Define add_flex_target()
+function(add_flex_target output_cxx input_lxx)
+  set(arguments "")
+  set(outputs "${output_cxx}")
+  set(keyword "")
+
+  # Parse the extra arguments to the function.
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "DEFINES")
+      set(keyword "DEFINES")
+
+    elseif(arg STREQUAL "PREFIX")
+      set(keyword "PREFIX")
+
+    elseif(arg STREQUAL "CASE_INSENSITIVE")
+      list(APPEND arguments "-i")
+
+    elseif(keyword STREQUAL "PREFIX")
+      list(APPEND arguments "-P${arg}")
+
+    elseif(keyword STREQUAL "DEFINES")
+      list(APPEND arguments "--header-file=${arg}")
+      list(APPEND outputs "${arg}")
+
+    else()
+      message(SEND_ERROR "Unexpected argument ${arg} to add_flex_target")
+
+    endif()
+  endforeach()
+
+  if(keyword STREQUAL arg AND NOT keyword STREQUAL "")
+    message(SEND_ERROR "Expected argument after ${keyword}")
+  endif()
+
+  if(HAVE_FLEX)
+    get_source_file_property(input_lxx "${input_lxx}" LOCATION)
+
+    # If we have flex, we can of course just run it.
+    add_custom_command(
+      OUTPUT ${outputs}
+      COMMAND ${FLEX_EXECUTABLE}
+        "-o${output_cxx}" ${arguments}
+        "${input_lxx}"
+      MAIN_DEPENDENCY "${input_lxx}")
+
+  else()
+    # Look for prebuilt versions of the outputs.
+    set(commands "")
+    set(depends "")
+
+    foreach(output ${outputs})
+      set(prebuilt_file "${output}.prebuilt")
+      get_filename_component(prebuilt_file "${prebuilt_file}" ABSOLUTE)
+
+      if(NOT EXISTS "${prebuilt_file}")
+        message(SEND_ERROR "Flex was not found and ${prebuilt_file} does not exist!")
+      endif()
+
+      list(APPEND depends "${prebuilt_file}")
+      list(APPEND commands COMMAND ${CMAKE_COMMAND} -E copy ${prebuilt_file} ${output})
+    endforeach()
+
+    add_custom_command(
+      OUTPUT ${outputs}
+      ${commands}
+      DEPENDS ${depends})
+  endif()
+endfunction(add_flex_target)

+ 426 - 0
cmake/macros/BuildMetalib.cmake

@@ -0,0 +1,426 @@
+# Filename: BuildMetalib.cmake
+#
+# Description: This file contains macros to build Panda3D's "metalibs" - these
+#   are special libraries that contain no unique code themselves and are
+#   instead just an agglomeration of the various component libraries that get
+#   linked into them. A library of libraries - a "metalibrary."
+
+#
+# Function: target_link_libraries(...)
+#
+# Overrides CMake's target_link_libraries() to support "linking" object
+# libraries. This is a partial reimplementation of CMake commit dc38970f83,
+# which is only available in CMake 3.12+
+#
+if(CMAKE_VERSION VERSION_LESS "3.12")
+  function(target_link_libraries target)
+    get_target_property(target_type "${target}" TYPE)
+    if(NOT target_type STREQUAL "OBJECT_LIBRARY")
+      _target_link_libraries("${target}" ${ARGN})
+      return()
+    endif()
+
+    foreach(library ${ARGN})
+      # This is a quick and dirty regex to tell targets apart from other stuff.
+      # It just checks if it's alphanumeric and starts with p3/panda.
+      if(library MATCHES "^(PKG::|p3|panda)[A-Za-z0-9]*$")
+        # We need to add "library"'s include directories to "target"
+        # (and transitively to INTERFACE_INCLUDE_DIRECTORIES so further
+        # dependencies will work)
+        set(include_directories "$<TARGET_PROPERTY:${library},INTERFACE_INCLUDE_DIRECTORIES>")
+        set_property(TARGET "${target}" APPEND PROPERTY INCLUDE_DIRECTORIES "${include_directories}")
+        set_property(TARGET "${target}" APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${include_directories}")
+
+        # SYSTEM include directories should still be reported as SYSTEM, so
+        # that warnings from those includes are suppressed
+        set(sys_include_directories
+          "$<TARGET_PROPERTY:${library},INTERFACE_SYSTEM_INCLUDE_DIRECTORIES>")
+        target_include_directories("${target}" SYSTEM PUBLIC "${sys_include_directories}")
+
+        # And for INTERFACE_COMPILE_DEFINITIONS as well
+        set(compile_definitions "$<TARGET_PROPERTY:${library},INTERFACE_COMPILE_DEFINITIONS>")
+        set_property(TARGET "${target}" APPEND PROPERTY COMPILE_DEFINITIONS "${compile_definitions}")
+        set_property(TARGET "${target}" APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS "${compile_definitions}")
+
+        # Build up some generator expressions for determining whether `library`
+        # is a component library or not.
+        if(library MATCHES ".*::.*")
+          # "::" messes up CMake's genex parser; fortunately, a library whose
+          # name contains that is either an interface library or alias, and
+          # definitely not a component
+          set(is_component 0)
+          set(name_of_component "")
+          set(name_of_non_component "${library}")
+
+        else()
+          set(is_component "$<TARGET_PROPERTY:${library},IS_COMPONENT>")
+
+          # CMake complains if we lookup IS_COMPONENT on an INTERFACE library :(
+          set(is_object "$<STREQUAL:$<TARGET_PROPERTY:${library},TYPE>,OBJECT_LIBRARY>")
+          set(is_component "$<BOOL:$<${is_object}:${is_component}>>")
+
+          set(name_of_component "$<${is_component}:$<TARGET_NAME:${library}>>")
+          set(name_of_non_component "$<$<NOT:${is_component}>:$<TARGET_NAME:${library}>>")
+
+        endif()
+
+        # Libraries are only linked transitively if they aren't components.
+        set_property(TARGET "${target}" APPEND PROPERTY
+          INTERFACE_LINK_LIBRARIES "${name_of_non_component}")
+
+      else()
+        # This is a file path to an out-of-tree library - this needs to be
+        # recorded so that the metalib can link them. (They aren't needed at
+        # all for the object libraries themselves, so they don't have to work
+        # transitively.)
+        set_property(TARGET "${target}" APPEND PROPERTY INTERFACE_LINK_LIBRARIES "${library}")
+
+      endif()
+
+    endforeach(library)
+
+  endfunction(target_link_libraries)
+endif()
+
+#
+# Function: add_component_library(target [SYMBOL building_symbol]
+#                                 [SOURCES] [[NOINIT]/[INIT func [header]]])
+#
+# Used very similarly to add_library.  You can specify a symbol with SYMBOL,
+# which works like CMake's own DEFINE_SYMBOL property: it's defined when
+# building the library, but not when building something that links against the
+# library.
+#
+# INIT specifies the init function that should be called from a metalib's init
+# function when this is added to a metalib.  The header parameter can further
+# clarify what header declares this function.  By default, this is
+# init_libTARGET and config_TARGET.h, respectively, where TARGET is the
+# target name (with 'p3' stripped off, if applicable).  The NOINIT keyword
+# suppresses this default.
+#
+# Note that this function gets to decide whether the component library is
+# OBJECT or SHARED, and whether the library is installed or not.  Also, as
+# a rule, component libraries may only be linked by other component libraries
+# in the same metalib - outside of the metalib, you must link the metalib
+# itself.
+#
+function(add_component_library target_name)
+  set(sources)
+  unset(symbol)
+
+  if(target_name MATCHES "^p3.*")
+    string(SUBSTRING "${target_name}" 2 -1 name_without_prefix)
+
+  else()
+    set(name_without_prefix "${target_name}")
+
+  endif()
+
+  set(init_func "init_lib${name_without_prefix}")
+  set(init_header "config_${name_without_prefix}.h")
+
+  set(symbol_keyword OFF)
+  set(init_keyword 0)
+  foreach(source ${ARGN})
+    if(source STREQUAL "SYMBOL")
+      set(symbol_keyword ON)
+      set(init_keyword 0)
+
+    elseif(source STREQUAL "INIT")
+      set(symbol_keyword OFF)
+      set(init_keyword 2)
+
+    elseif(source STREQUAL "NOINIT")
+      set(init_func)
+      set(init_header)
+
+    elseif(symbol_keyword)
+      set(symbol_keyword OFF)
+      set(symbol "${source}")
+
+    elseif(init_keyword EQUAL 2)
+      set(init_func "${source}")
+      set(init_keyword 1)
+
+    elseif(init_keyword EQUAL 1)
+      set(init_header "${source}")
+      set(init_keyword 0)
+
+    else()
+      list(APPEND sources "${source}")
+
+    endif()
+  endforeach()
+
+  if(BUILD_METALIBS)
+    # CMake 3.0.2 doesn't like .I/.N/.T files!  We let it know that they're only
+    # headers.
+    foreach(source ${sources})
+      if(source MATCHES "\\.[INT]$")
+        set_source_files_properties(${source} PROPERTIES HEADER_FILE_ONLY ON)
+      endif()
+    endforeach(source)
+
+    add_library("${target_name}" OBJECT ${sources})
+
+  else()
+    add_library("${target_name}" ${sources})
+
+  endif()
+
+  set_target_properties("${target_name}" PROPERTIES 
+    IS_COMPONENT ON
+    INIT_FUNCTION "${init_func}"
+    INIT_HEADER "${init_header}")
+
+  if(symbol)
+    set_property(TARGET "${target_name}" PROPERTY DEFINE_SYMBOL "${symbol}")
+
+    if(BUILD_METALIBS)
+      # ... DEFINE_SYMBOL is apparently not respected for object libraries?
+      set_property(TARGET "${target_name}" APPEND PROPERTY COMPILE_DEFINITIONS "${symbol}")
+
+      # Make sure other component libraries relying on this one inherit the
+      # symbol
+      set_property(TARGET "${target_name}" APPEND PROPERTY
+        INTERFACE_COMPILE_DEFINITIONS "$<$<BOOL:$<TARGET_PROPERTY:IS_COMPONENT>>:${symbol}>")
+    endif()
+  endif()
+
+  if(BUILD_METALIBS)
+    # Apparently neither is CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE?
+    set_property(TARGET "${target_name}" PROPERTY
+      INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}")
+
+    # If we're building dynamic libraries, the object library needs to be -fPIC
+    if(BUILD_SHARED_LIBS)
+      set_property(TARGET "${target_name}" PROPERTY
+        POSITION_INDEPENDENT_CODE ON)
+    endif()
+  endif()
+
+endfunction(add_component_library)
+
+#
+# Function: add_metalib(target [source1 source2]
+#                       [INCLUDE header1.h ...]
+#                       [INIT initfunc [initheader.h] [EXPORT type name expr]]
+#                       [COMPONENTS component1 ...])
+#
+# This is add_library, but for metalibs.
+#
+# The INIT keyword can specify an initialization function/header (which will be
+# autogenerated by this function) that calls the underlying component libs'
+# init functions.
+#
+# The EXPORT keyword exports the expression `expr`, which yields a value of
+# type `type`, as the undecorated (extern "C") symbol `name`
+#
+# The INCLUDE keyword allows the init file to pull in additional headers, which
+# may be useful for EXPORT.
+#
+function(add_metalib target_name)
+  set(components_keyword OFF)
+  set(init_keyword 0)
+  set(init_func)
+  set(init_header "${target_name}.h")
+  set(export_keyword 0)
+  set(export_declarations)
+  set(export_definitions)
+  set(component_init_headers)
+  set(components)
+  set(sources)
+
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "COMPONENTS")
+      set(components_keyword ON)
+      set(include_keyword OFF)
+      set(init_keyword 0)
+      set(export_keyword 0)
+
+    elseif(arg STREQUAL "INCLUDE")
+      set(include_keyword ON)
+      set(components_keyword OFF)
+      set(init_keyword 0)
+      set(export_keyword 0)
+
+    elseif(arg STREQUAL "INIT")
+      set(init_keyword 2)
+      set(components_keyword OFF)
+      set(include_keyword OFF)
+      set(export_keyword 0)
+
+    elseif(arg STREQUAL "EXPORT")
+      if(NOT init_func)
+        message(FATAL_ERROR "EXPORT cannot be used before INIT")
+      endif()
+
+      set(export_keyword 3)
+      set(components_keyword OFF)
+      set(include_keyword OFF)
+      set(init_keyword 0)
+
+    elseif(components_keyword)
+      list(APPEND components "${arg}")
+
+    elseif(include_keyword)
+      set(component_init_headers
+        "${component_init_headers}#include \"${arg}\"\n")
+
+    elseif(init_keyword EQUAL 2)
+      set(init_func "${arg}")
+      set(init_keyword 1)
+
+    elseif(init_keyword EQUAL 1)
+      set(init_header "${arg}")
+      set(init_keyword 0)
+
+    elseif(export_keyword EQUAL 3)
+      set(_export_type "${arg}")
+      set(export_keyword 2)
+
+    elseif(export_keyword EQUAL 2)
+      set(_export_name "${arg}")
+      set(export_keyword 1)
+
+    elseif(export_keyword EQUAL 1)
+      set(export_declarations
+        "${export_declarations}\nextern \"C\" IMPORT_CLASS ${_export_type} ${_export_name}();")
+      set(export_definitions
+        "${export_definitions}\nextern \"C\" EXPORT_CLASS ${_export_type} ${_export_name}() { return ${arg}; }")
+      unset(_export_type)
+      unset(_export_name)
+      set(export_keyword 0)
+
+    else()
+      list(APPEND sources "${arg}")
+
+    endif()
+  endforeach()
+
+  string(REPLACE ";" "|" piped_components "${components}")
+  set(component_genex_regex ".*TARGET_PROPERTY:(${piped_components}),.*")
+
+  set(private_defines)
+  set(interface_defines)
+  set(includes)
+  set(libs)
+  set(component_init_funcs "")
+  foreach(component ${components})
+    if(NOT TARGET "${component}")
+      message(FATAL_ERROR
+        "Missing component library ${component} referenced by metalib ${target_name}!
+        (Component library targets must be created BEFORE add_metalib.)")
+    endif()
+
+    get_target_property(is_component "${component}" IS_COMPONENT)
+    if(NOT is_component)
+      message(FATAL_ERROR
+        "Attempted to metalink non-component ${component} into ${target_name}!")
+    endif()
+
+    get_target_property(component_init_header "${component}" INIT_HEADER)
+    get_target_property(component_init_func "${component}" INIT_FUNCTION)
+
+    if(component_init_header)
+      set(component_init_headers
+        "${component_init_headers}#include \"${component_init_header}\"\n")
+    endif()
+
+    if(component_init_func)
+      set(component_init_funcs
+        "${component_init_funcs}  ${component_init_func}();\n")
+    endif()
+
+    if(BUILD_METALIBS)
+      # This will be an object library we're pulling in; rather than link,
+      # let's try to "flatten" all of its properties into ours.
+
+      # Private defines: Just reference using a generator expression
+      list(APPEND private_defines "$<TARGET_PROPERTY:${component},COMPILE_DEFINITIONS>")
+
+      # Interface defines: Copy those, but filter out generator expressions
+      # referencing a component library
+      get_target_property(component_defines "${component}" INTERFACE_COMPILE_DEFINITIONS)
+      if(component_defines)
+        foreach(component_define ${component_defines})
+          if(component_define MATCHES "${component_genex_regex}")
+            # Filter, it's a genex referencing one of our components
+          elseif(component_define MATCHES ".*IS_COMPONENT.*")
+            # Filter, it's testing to see if the consumer is a component
+          else()
+            list(APPEND interface_defines ${component_define})
+          endif()
+        endforeach(component_define)
+      endif()
+
+      # Include directories: Filter out anything that references a component
+      # library or anything in the project path
+      get_target_property(component_includes "${component}" INTERFACE_INCLUDE_DIRECTORIES)
+      foreach(component_include ${component_includes})
+        if(component_include MATCHES "${component_genex_regex}")
+          # Ignore component references
+
+        elseif(component_include MATCHES "^${PROJECT_SOURCE_DIR}")
+          # Include path within project; should only be included when building
+          list(APPEND includes "$<BUILD_INTERFACE:${component_include}>")
+
+        else()
+          # Anything else gets included
+          list(APPEND includes "${component_include}")
+
+        endif()
+      endforeach(component_include)
+
+      # Link libraries: Filter out component libraries; we aren't linking
+      # against them, we're using their objects
+      get_target_property(component_libraries "${component}" INTERFACE_LINK_LIBRARIES)
+      foreach(component_library ${component_libraries})
+        if(NOT component_library)
+          # NOTFOUND - guess there are no INTERFACE_LINK_LIBRARIES
+
+        elseif(component_library MATCHES "${component_genex_regex}")
+          # Ignore component references
+
+        elseif(component_library MATCHES ".*(${piped_components}).*")
+          # Component library, ignore
+
+        else()
+          # Anything else gets included
+          list(APPEND libs "${component_library}")
+
+        endif()
+      endforeach(component_library)
+
+      # Consume this component's objects
+      list(APPEND sources "$<TARGET_OBJECTS:${component}>")
+
+    else() # NOT BUILD_METALIBS
+      list(APPEND libs "${component}")
+
+    endif()
+  endforeach()
+
+  if(init_func)
+    set(init_source_path "${CMAKE_CURRENT_BINARY_DIR}/init_${target_name}.cxx")
+    set(init_header_path "${CMAKE_CURRENT_BINARY_DIR}/${init_header}")
+
+    configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/metalib_init.cxx.in"
+      "${init_source_path}")
+    list(APPEND sources "${init_source_path}")
+
+    configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/metalib_init.h.in"
+      "${init_header_path}")
+    install(FILES "${init_header_path}" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)
+  endif()
+
+  add_library("${target_name}" ${sources})
+  target_compile_definitions("${target_name}"
+    PRIVATE ${private_defines}
+    INTERFACE ${interface_defines})
+  target_link_libraries("${target_name}" ${libs})
+  target_include_directories("${target_name}"
+    PUBLIC ${includes}
+    INTERFACE "$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}/panda3d>")
+
+endfunction(add_metalib)

+ 167 - 0
cmake/macros/CompositeSources.cmake

@@ -0,0 +1,167 @@
+# Filename: CompositeSources.cmake
+# Description: This file defines the function composite_sources which looks at
+#   a provided list of sources, generates _compositeN.cxx, and appends the
+#   composites to the list. The original files in the list are marked as headers
+#   so that they will be available in an IDE, but not compiled at build time.
+#
+# Usage:
+#   composite_sources(target source_var)
+#
+# Example:
+#   set(MY_SOURCES a.cxx b.cxx c.cxx)
+#   composite_sources(my_lib MY_SOURCES)
+#   add_library(my_lib ${MY_SOURCES})
+#
+
+
+# Settings for composite builds.  Should be moved to Config.cmake?
+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.")
+
+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_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})
+  list(LENGTH sources num_sources)
+
+  # 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 ${CMAKE_UNITY_BUILD_BATCH_SIZE} LESS 2 OR ${_index} GREATER -1)
+    return()
+  endif()
+
+  # Sort each source file into a list.
+  foreach(source ${sources})
+    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_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
+        ";${COMPOSITE_SOURCE_EXTENSIONS};" MATCHES ";${extension};")
+
+      if(NOT DEFINED sources_${extension})
+        set(sources_${extension})
+      endif()
+
+      # Append it to one of the lists.
+      list(APPEND sources_${extension} "${source}")
+    endif()
+  endforeach(source)
+
+  # Now, put it all into one big list!
+  set(sorted_sources)
+  foreach(extension ${COMPOSITE_SOURCE_EXTENSIONS})
+    if(DEFINED sources_${extension})
+      list(APPEND sorted_sources ${sources_${extension}})
+    endif()
+  endforeach(extension)
+
+  set(composite_files)
+  set(composite_sources)
+
+  # Fill in composite_ext so we can kick off the loop.
+  list(GET sorted_sources 0 first_source)
+  get_filename_component(first_source_ext "${first_source}" EXT)
+  set(composite_ext ${first_source_ext})
+
+  while(num_sources GREATER 0)
+    # Pop the first element and adjust the sorted_sources length accordingly.
+    list(GET sorted_sources 0 source)
+    list(REMOVE_AT sorted_sources 0)
+    list(LENGTH sorted_sources num_sources)
+
+    # Add this file to our composite_sources buffer.
+    list(APPEND composite_sources ${source})
+    list(LENGTH composite_sources num_composite_sources)
+
+    # Get the next source file's extension, so we can see if we're done with
+    # this set of source files.
+    if(num_sources GREATER 0)
+      list(GET sorted_sources 0 next_source)
+      get_filename_component(next_extension "${next_source}" EXT)
+    else()
+      set(next_extension "")
+    endif()
+
+    # Check if this is the point where we should cut the file.
+    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)
+
+        # Figure out the name of our composite file.
+        list(LENGTH composite_files index)
+        math(EXPR index "1+${index}")
+        set(composite_file "${CMAKE_CURRENT_BINARY_DIR}/${target}_composite${index}${composite_ext}")
+        list(APPEND composite_files "${composite_file}")
+
+        # Set HEADER_FILE_ONLY to prevent it from showing up in the
+        # compiler command, but still show up in the IDE environment.
+        set_source_files_properties(${composite_sources} PROPERTIES HEADER_FILE_ONLY ON)
+
+        # We'll interrogate the composite files, so exclude the original sources.
+        set_source_files_properties(${composite_sources} PROPERTIES WRAP_EXCLUDE YES)
+
+        # Finally, add the target that generates the composite file.
+        add_custom_command(
+          OUTPUT "${composite_file}"
+          COMMAND ${CMAKE_COMMAND}
+            -DCOMPOSITE_FILE="${composite_file}"
+            -DCOMPOSITE_SOURCES="${composite_sources}"
+            -P "${COMPOSITE_GENERATOR}"
+          DEPENDS ${composite_sources})
+      endif()
+
+      # Reset for the next composite file.
+      set(composite_sources "")
+      set(composite_ext ${next_extension})
+    endif()
+  endwhile()
+
+  set_source_files_properties(${composite_files} PROPERTIES GENERATED YES)
+
+  # The new files are added to the existing files, which means the old files
+  # are still there, but they won't be compiled due to the HEADER_FILE_ONLY setting.
+  set(${sources_var} ${orig_sources} ${composite_files} PARENT_SCOPE)
+
+endfunction(composite_sources)

+ 400 - 0
cmake/macros/Interrogate.cmake

@@ -0,0 +1,400 @@
+# Filename: Interrogate.cmake
+#
+# Description: This file contains macros and functions that are used to invoke
+#   interrogate, to generate wrappers for Python and/or other languages.
+#
+# Functions:
+#   target_interrogate(target [ALL] [source1 [source2 ...]])
+#   add_python_module(module [lib1 [lib2 ...]])
+#   add_python_target(target [source1 [source2 ...]])
+#
+
+set(IGATE_FLAGS -DCPPPARSER -D__cplusplus -Dvolatile -Dmutable)
+
+# In addition, Interrogate needs to know if this is a 64-bit build:
+include(CheckTypeSize)
+check_type_size(long CMAKE_SIZEOF_LONG)
+if(CMAKE_SIZEOF_LONG EQUAL 8)
+  list(APPEND IGATE_FLAGS "-D_LP64")
+endif()
+
+
+# This is a list of regexes that are applied to every filename. If one of the
+# regexes matches, that file will not be passed to Interrogate.
+set(INTERROGATE_EXCLUDE_REGEXES
+  ".*\\.I$"
+  ".*\\.N$"
+  ".*\\.c$"
+  ".*\\.lxx$"
+  ".*\\.yxx$"
+  ".*_src\\..*"
+)
+
+if(WIN32)
+  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")
+endif()
+
+set(IMOD_FLAGS -python-native)
+
+# This stores the names of every module added to the Interrogate system:
+set(ALL_INTERROGATE_MODULES CACHE INTERNAL "Internal variable")
+
+#
+# Function: target_interrogate(target [ALL] [source1 [source2 ...]])
+# NB. This doesn't actually invoke interrogate, but merely adds the
+# sources to the list of scan sources associated with the target.
+# Interrogate will be invoked when add_python_module is called.
+# If ALL is specified, all of the sources from the associated
+# target are added.
+#
+function(target_interrogate target)
+  set(sources)
+  set(extensions)
+  set(want_all OFF)
+  set(extensions_keyword OFF)
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "ALL")
+      set(want_all ON)
+
+    elseif(arg STREQUAL "EXTENSIONS")
+      set(extensions_keyword ON)
+
+    elseif(extensions_keyword)
+      list(APPEND extensions "${arg}")
+
+    else()
+      list(APPEND sources "${arg}")
+
+    endif()
+  endforeach()
+
+  # If ALL was specified, pull in all sources from the target.
+  if(want_all)
+    get_target_property(target_sources "${target}" SOURCES)
+    list(APPEND sources ${target_sources})
+  endif()
+
+  list(REMOVE_DUPLICATES sources)
+
+  # Now let's get everything's absolute path, so that it can be passed
+  # through a property while still preserving the reference.
+  set(absolute_sources)
+  foreach(source ${sources})
+    get_source_file_property(exclude "${source}" WRAP_EXCLUDE)
+    if(NOT exclude)
+      get_source_file_property(location "${source}" LOCATION)
+      list(APPEND absolute_sources ${location})
+    endif()
+  endforeach(source)
+
+  set(absolute_extensions)
+  foreach(extension ${extensions})
+    get_source_file_property(location "${extension}" LOCATION)
+    list(APPEND absolute_extensions ${location})
+  endforeach(extension)
+
+  set_target_properties("${target}" PROPERTIES
+    IGATE_SOURCES "${absolute_sources}")
+  set_target_properties("${target}" PROPERTIES
+    IGATE_EXTENSIONS "${absolute_extensions}")
+
+  # CMake has no property for determining the source directory where the
+  # target was originally added. interrogate_sources makes use of this
+  # property (if it is set) in order to make all paths on the command-line
+  # relative to it, thereby shortening the command-line even more.
+  # Since this is not an Interrogate-specific property, it is not named with
+  # an IGATE_ prefix.
+  set_target_properties("${target}" PROPERTIES
+    TARGET_SRCDIR "${CMAKE_CURRENT_SOURCE_DIR}")
+
+  # Also store where the build files are kept, so the Interrogate output can go
+  # there as well.
+  set_target_properties("${target}" PROPERTIES
+    TARGET_BINDIR "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}")
+
+endfunction(target_interrogate)
+
+#
+# Function: interrogate_sources(target output database language_flags module)
+#
+# This function actually runs a component-level interrogation against 'target'.
+# It generates the outfile.cxx (output) and dbfile.in (database) files, which
+# can then be used during the interrogate_module step to produce language
+# bindings.
+#
+# The target must first have had sources selected with target_interrogate.
+# Failure to do so will result in an error.
+#
+function(interrogate_sources target output database language_flags)
+  get_target_property(sources "${target}" IGATE_SOURCES)
+  get_target_property(extensions "${target}" IGATE_EXTENSIONS)
+
+  if(NOT sources)
+    message(FATAL_ERROR
+      "Cannot interrogate ${target} unless it's run through target_interrogate first!")
+  endif()
+
+  get_target_property(srcdir "${target}" TARGET_SRCDIR)
+  if(NOT srcdir)
+    # No TARGET_SRCDIR was set, so we'll do everything relative to our
+    # current binary dir instead:
+    set(srcdir "${CMAKE_CURRENT_BINARY_DIR}")
+  endif()
+
+  set(scan_sources)
+  set(nfiles)
+  foreach(source ${sources})
+    get_filename_component(source_basename "${source}" NAME)
+
+    # Only certain sources should actually be scanned by Interrogate. The
+    # rest are merely dependencies. This uses the exclusion regex above in
+    # order to determine what files are okay:
+    set(exclude OFF)
+    foreach(regex ${INTERROGATE_EXCLUDE_REGEXES})
+      if("${source_basename}" MATCHES "${regex}")
+        set(exclude ON)
+      endif()
+    endforeach(regex)
+
+    if(NOT exclude)
+      # This file is to be scanned by Interrogate. In order to avoid
+      # cluttering up the command line, we should first make it relative:
+      file(RELATIVE_PATH rel_source "${srcdir}" "${source}")
+      list(APPEND scan_sources "${rel_source}")
+
+      # Also see if this file has a .N counterpart, which has directives
+      # specific for Interrogate. If there is a .N file, we add it as a dep,
+      # so that CMake will rerun Interrogate if the .N files are modified:
+      get_filename_component(source_path "${source}" PATH)
+      get_filename_component(source_name_we "${source}" NAME_WE)
+      set(nfile "${source_path}/${source_name_we}.N")
+      if(EXISTS "${nfile}")
+        list(APPEND nfiles "${nfile}")
+      endif()
+    endif()
+  endforeach(source)
+
+  # Also add extensions, in relative-path form
+  foreach(extension ${extensions})
+    file(RELATIVE_PATH rel_extension "${srcdir}" "${extension}")
+    list(APPEND scan_sources "${rel_extension}")
+  endforeach(extension)
+
+  # Interrogate also needs the include paths, so we'll extract them from the
+  # target. These are available via a generator expression.
+
+  # When we read the INTERFACE_INCLUDE_DIRECTORIES property, we need to read it
+  # from a target that has the IS_INTERROGATE=1 property.
+  # (See PackageConfig.cmake for an explanation why.)
+  # The problem is, custom commands are not targets, so we can't put target
+  # properties on them. And if you try to use the $<TARGET_PROPERTY:prop>
+  # generator expression from the context of a custom command, it'll instead
+  # read the property from the most recent actual target. As a workaround for
+  # this, we create a fake target with the IS_INTERROGATE property set and pull
+  # the INTERFACE_INCLUDE_DIRECTORIES property out through that.
+  # I hate it, but such is CMake.
+  add_custom_target(${target}_igate_internal)
+  set_target_properties(${target}_igate_internal PROPERTIES
+    IS_INTERROGATE 1
+    INTERFACE_INCLUDE_DIRECTORIES "$<TARGET_PROPERTY:${target},INTERFACE_INCLUDE_DIRECTORIES>")
+
+  # Note, the \t is a workaround for a CMake bug where using a plain space in
+  # a JOIN will cause it to be escaped. Tabs are not escaped and will
+  # separate correctly.
+  set(include_flags "-I$<JOIN:$<TARGET_PROPERTY:${target}_igate_internal,INTERFACE_INCLUDE_DIRECTORIES>,\t-I>")
+
+  # Get the compiler definition flags. These must be passed to Interrogate
+  # in the same way that they are passed to the compiler so that Interrogate
+  # will preprocess each file in the same way.
+  set(_compile_defs "$<TARGET_PROPERTY:${target},COMPILE_DEFINITIONS>")
+  if(NOT CMAKE_HOST_WIN32)
+    # Win32's command-line parser doesn't understand "'"
+    # that's fine, it also ignores '"'
+    set(_q "'")
+  endif()
+  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(_config STREQUAL "<ALL>")
+      list(APPEND define_flags "${igate_flags}")
+    else()
+      list(APPEND define_flags "$<$<CONFIG:${_config}>:${igate_flags}>")
+    endif()
+  endforeach(_config)
+
+  get_filename_component(output_directory "${output}" DIRECTORY)
+  get_filename_component(database_directory "${database}" DIRECTORY)
+
+  add_custom_command(
+    OUTPUT "${output}" "${database}"
+    COMMAND ${CMAKE_COMMAND} -E
+      make_directory "${output_directory}"
+    COMMAND ${CMAKE_COMMAND} -E
+      make_directory "${database_directory}"
+    COMMAND host_interrogate
+      -oc "${output}"
+      -od "${database}"
+      -srcdir "${srcdir}"
+      -library ${target}
+      ${INTERROGATE_OPTIONS}
+      ${IGATE_FLAGS}
+      ${language_flags}
+      ${define_flags}
+      -S "${PROJECT_SOURCE_DIR}/dtool/src/interrogatedb"
+      -S "${PROJECT_SOURCE_DIR}/dtool/src/parser-inc"
+      -S "${PYTHON_INCLUDE_DIRS}"
+      ${include_flags}
+      ${scan_sources}
+
+    DEPENDS host_interrogate ${sources} ${extensions} ${nfiles}
+    COMMENT "Interrogating ${target}")
+
+  # Propagate the target's compile definitions to the output file
+  set_source_files_properties("${output}" PROPERTIES
+    COMPILE_DEFINITIONS "$<TARGET_PROPERTY:${target},INTERFACE_COMPILE_DEFINITIONS>")
+
+endfunction(interrogate_sources)
+
+#
+# Function: add_python_module(module [lib1 [lib2 ...]] [LINK lib1 ...]
+#    [IMPORT mod1 ...])
+# Uses interrogate to create a Python module. If the LINK keyword is specified,
+# the Python module is linked against the specified libraries instead of those
+# listed before. The IMPORT keyword makes the output module import another
+# Python module when it's initialized.
+#
+function(add_python_module module)
+  if(NOT INTERROGATE_PYTHON_INTERFACE)
+    return()
+  endif()
+
+  set(targets)
+  set(component "Python")
+  set(link_targets)
+  set(import_flags)
+  set(infiles_rel)
+  set(infiles_abs)
+  set(sources_abs)
+  set(extensions)
+
+  set(keyword)
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "LINK" OR arg STREQUAL "IMPORT" OR arg STREQUAL "COMPONENT")
+      set(keyword "${arg}")
+
+    elseif(keyword STREQUAL "LINK")
+      list(APPEND link_targets "${arg}")
+      set(keyword)
+
+    elseif(keyword STREQUAL "IMPORT")
+      list(APPEND import_flags "-import" "${arg}")
+      set(keyword)
+
+    elseif(keyword STREQUAL "COMPONENT")
+      set(component "${arg}")
+      set(keyword)
+
+    else()
+      list(APPEND targets "${arg}")
+
+    endif()
+  endforeach(arg)
+
+  if(NOT link_targets)
+    set(link_targets ${targets})
+  endif()
+
+  string(REGEX REPLACE "^.*\\." "" modname "${module}")
+
+  foreach(target ${targets})
+    get_target_property(workdir_abs "${target}" TARGET_BINDIR)
+    if(NOT workdir_abs)
+      # No TARGET_BINDIR was set, so we'll just use our current directory:
+      set(workdir_abs "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}")
+    endif()
+    # Keep command lines short
+    file(RELATIVE_PATH workdir_rel "${CMAKE_CURRENT_BINARY_DIR}" "${workdir_abs}")
+
+    interrogate_sources(${target}
+      "${workdir_abs}/${target}_igate.cxx"
+      "${workdir_abs}/${target}.in"
+      "-python-native;-module;${module}")
+
+    get_target_property(target_extensions "${target}" IGATE_EXTENSIONS)
+    list(APPEND infiles_rel "${workdir_rel}/${target}.in")
+    list(APPEND infiles_abs "${workdir_abs}/${target}.in")
+    list(APPEND sources_abs "${workdir_abs}/${target}_igate.cxx")
+    list(APPEND extensions ${target_extensions})
+  endforeach(target)
+
+  add_custom_command(
+    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}/${module}_module.cxx"
+    COMMAND ${CMAKE_COMMAND} -E
+      make_directory "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}"
+    COMMAND host_interrogate_module
+      -oc "${CMAKE_CURRENT_BINARY_DIR}/${PANDA_CFG_INTDIR}/${module}_module.cxx"
+      -module ${modname} -library ${modname}
+      ${import_flags}
+      ${INTERROGATE_MODULE_OPTIONS}
+      ${IMOD_FLAGS} ${infiles_rel}
+    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})
+  target_link_libraries(${module} ${link_targets})
+
+  if(CMAKE_VERSION VERSION_LESS "3.11")
+    # CMake <3.11 doesn't allow generator expressions on source files, so we
+    # need to copy them to our target, which does allow them.
+
+    foreach(source ${sources_abs})
+      get_source_file_property(compile_definitions "${source}" COMPILE_DEFINITIONS)
+      if(compile_definitions)
+        set_property(TARGET ${module} APPEND PROPERTY
+          COMPILE_DEFINITIONS ${compile_definitions})
+
+        set_source_files_properties("${source}" PROPERTIES COMPILE_DEFINITIONS "")
+      endif()
+    endforeach(source)
+  endif()
+
+  list(APPEND ALL_INTERROGATE_MODULES "${module}")
+  set(ALL_INTERROGATE_MODULES "${ALL_INTERROGATE_MODULES}" CACHE INTERNAL "Internal variable")
+endfunction(add_python_module)

+ 506 - 0
cmake/macros/PackageConfig.cmake

@@ -0,0 +1,506 @@
+# Filename: PackageConfig.cmake
+#
+# This module defines functions which find and configure libraries
+# and packages for Panda3D.
+#
+# Assumes an attempt to find the package has already been made with
+# find_package(). (i.e. relies on packagename_FOUND variable)
+#
+# The packages are added as imported/interface libraries in the PKG::
+# namespace.  If the package is not found (or disabled by the user),
+# a dummy package will be created instead.  Therefore, it is safe
+# to link against the PKG::PACKAGENAME target unconditionally.
+#
+# Function: package_option
+# Usage:
+#   package_option(package_name package_doc_string
+#                  [DEFAULT ON | OFF]
+#                  [IMPORTED_AS CMake::Imported::Target [...]]
+#                  [FOUND_AS find_name]
+#                  [LICENSE license])
+#
+# Examples:
+#   package_option(LIBNAME "Enables LIBNAME support." DEFAULT OFF)
+#
+#       If no default is given, the default in normal
+#       builds is to enable all found third-party packages.
+#       In builds for redistribution, there is the additional requirement that
+#       the package be suitably-licensed.
+#
+#       FOUND_AS indicates the name of the CMake find_package() module, which
+#       may differ from Panda3D's internal name for that package.
+#
+#       IMPORTED_AS is used to indicate that the find_package() may have
+#       provided one or more IMPORTED targets, and that if at least one is
+#       found, the IMPORTED target(s) should be used instead of the
+#       variables provided by find_package()
+#
+#
+# Function: package_status
+# Usage:
+#   package_status(package_name "Package description" ["Config summary"])
+#
+# Examples:
+#   package_status(OpenAL "OpenAL Audio Output")
+#   package_status(ROCKET "Rocket" "without Python bindings")
+#
+#
+# Function: show_packages
+# Usage:
+#   show_packages()
+#
+#   This prints the package usage report using the information provided in
+#   calls to package_status above.
+#
+
+set(_ALL_PACKAGE_OPTIONS CACHE INTERNAL "Internal variable")
+
+#
+# package_option
+#
+# In order to make sure no third-party licenses are inadvertently violated,
+# this imposes a few rules regarding license:
+# 1) If there is no license, no special restrictions.
+# 2) If there is a license, but the build is not flagged for redistribution,
+#    no special restrictions.
+# 3) If there is a license, and this is for redistribution, the package is
+#    forcibly defaulted off and must be explicitly enabled, unless the license
+#    matches a list of licenses suitable for redistribution.
+#
+function(package_option name)
+  # Parse the arguments.
+  set(command)
+  set(default)
+  set(found_as "${name}")
+  set(imported_as)
+  set(license "")
+  set(cache_string)
+
+  string(TOUPPER "${name}" name)
+
+  foreach(arg ${ARGN})
+    if(command STREQUAL "DEFAULT")
+      set(default "${arg}")
+      set(command)
+
+    elseif(command STREQUAL "FOUND_AS")
+      set(found_as "${arg}")
+      set(command)
+
+    elseif(command STREQUAL "LICENSE")
+      set(license "${arg}")
+      set(command)
+
+    elseif(arg STREQUAL "DEFAULT")
+      set(command "DEFAULT")
+
+    elseif(arg STREQUAL "FOUND_AS")
+      set(command "FOUND_AS")
+
+    elseif(arg STREQUAL "LICENSE")
+      set(command "LICENSE")
+
+    elseif(arg STREQUAL "IMPORTED_AS")
+      set(command "IMPORTED_AS")
+
+    elseif(command STREQUAL "IMPORTED_AS")
+      list(APPEND imported_as "${arg}")
+
+    else()
+      # Yes, a list, because semicolons can be in there, and
+      # that gets split up into multiple args, so we have to
+      # join it back together here.
+      list(APPEND cache_string "${arg}")
+
+    endif()
+  endforeach()
+
+  if(command AND NOT command STREQUAL "IMPORTED_AS")
+    message(SEND_ERROR "${command} in package_option takes an argument")
+  endif()
+
+  # If the default is not set, we set it.
+  if(NOT DEFINED default)
+    if(IS_DIST_BUILD)
+      # Accept things that don't have a configured license
+      if(license STREQUAL "")
+        set(default "${${found_as}_FOUND}")
+
+      else()
+        list(FIND PANDA_DIST_USE_LICENSES ${license} license_index)
+        # If the license isn't in the accept listed, don't use the package
+        if(${license_index} EQUAL "-1")
+          set(default OFF)
+
+        else()
+          set(default "${${found_as}_FOUND}")
+
+        endif()
+
+      endif()
+
+    else()
+      set(default "${${found_as}_FOUND}")
+
+    endif()
+  endif()
+
+  # If it was set by the user but not found, display an error.
+  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()
+
+  # Prevent the function from being called twice.
+  #   This would indicate a cmake error.
+  if(";${_ALL_PACKAGE_OPTIONS};" MATCHES ";${name};")
+    message(SEND_ERROR "package_option(${name}) was called twice.
+                        This is a bug in the cmake build scripts.")
+
+  else()
+    list(APPEND _ALL_PACKAGE_OPTIONS "${name}")
+    set(_ALL_PACKAGE_OPTIONS "${_ALL_PACKAGE_OPTIONS}" CACHE INTERNAL "Internal variable")
+
+  endif()
+
+  set(PANDA_PACKAGE_DEFAULT_${name} "${default}" PARENT_SCOPE)
+
+  # Create the INTERFACE library used to depend on this package.
+  add_library(PKG::${name} INTERFACE IMPORTED GLOBAL)
+
+  # Explicitly record the package's include directories as system include
+  # directories.  CMake does do this automatically for INTERFACE libraries, but
+  # it does it by discovering all transitive links first, then reading
+  # INTERFACE_INCLUDE_DIRECTORIES for those which are INTERFACE libraries.  So,
+  # this would be broken for the metalib system (pre CMake 3.12) which doesn't
+  # "link" the object libraries.
+  if(CMAKE_VERSION VERSION_LESS "3.12")
+    set_target_properties(PKG::${name} PROPERTIES
+      INTERFACE_SYSTEM_INCLUDE_DIRECTORIES
+      "$<TARGET_PROPERTY:PKG::${name},INTERFACE_INCLUDE_DIRECTORIES>")
+  endif()
+
+  # Create the option, and if it actually is enabled, populate the INTERFACE
+  # library created above
+  option("HAVE_${name}" "${cache_string}" "${default}")
+  if(HAVE_${name})
+    set(use_variables ON)
+
+    # This is gross, but we actually want to hide package include directories
+    # from Interrogate to make sure it relies on parser-inc instead, so we'll
+    # use some generator expressions to do that.
+    set(_is_not_interface_lib
+      "$<NOT:$<STREQUAL:$<TARGET_PROPERTY:TYPE>,INTERFACE_LIBRARY>>")
+    set(_is_not_interrogate
+      "$<NOT:$<BOOL:$<${_is_not_interface_lib}:$<TARGET_PROPERTY:IS_INTERROGATE>>>>")
+
+    foreach(implib ${imported_as})
+      if(TARGET ${implib})
+        # We found one of the implibs, so we don't need to use variables
+        # (below) anymore
+        set(use_variables OFF)
+
+        # Hide it from Interrogate
+        target_link_libraries(PKG::${name} INTERFACE
+          "$<${_is_not_interrogate}:$<TARGET_NAME:${implib}>>")
+      endif()
+    endforeach(implib)
+
+    if(use_variables)
+      if(DEFINED ${found_as}_INCLUDE_DIRS)
+        set(includes ${${found_as}_INCLUDE_DIRS})
+      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(DEFINED ${found_as}_LIBRARIES)
+        set(libs ${${found_as}_LIBRARIES})
+      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})
+
+      # Hide it from Interrogate
+      set_target_properties(PKG::${name} PROPERTIES
+        INTERFACE_INCLUDE_DIRECTORIES "$<${_is_not_interrogate}:${includes}>")
+    endif()
+  endif()
+endfunction(package_option)
+
+set(_ALL_CONFIG_PACKAGES CACHE INTERNAL "Internal variable")
+
+#
+# package_status
+#
+function(package_status name desc)
+  set(note "")
+  foreach(arg ${ARGN})
+    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.")
+    return()
+  endif()
+
+  if(";${_ALL_CONFIG_PACKAGES};" MATCHES ";${name};")
+    message(SEND_ERROR "package_status(${name}) was called twice.
+                        This is a bug in the cmake build scripts.")
+
+  else()
+    list(APPEND _ALL_CONFIG_PACKAGES "${name}")
+    set(_ALL_CONFIG_PACKAGES "${_ALL_CONFIG_PACKAGES}" CACHE INTERNAL "Internal variable")
+
+  endif()
+
+  set(PANDA_PACKAGE_DESC_${name} "${desc}" PARENT_SCOPE)
+  set(PANDA_PACKAGE_NOTE_${name} "${note}" PARENT_SCOPE)
+endfunction()
+
+#
+# show_packages
+#
+function(show_packages)
+  message("")
+  message("Configuring support for the following optional third-party packages:")
+
+  foreach(package ${_ALL_CONFIG_PACKAGES})
+    set(desc "${PANDA_PACKAGE_DESC_${package}}")
+    set(note "${PANDA_PACKAGE_NOTE_${package}}")
+    if(HAVE_${package})
+      if(NOT note STREQUAL "")
+        message("+ ${desc} (${note})")
+      else()
+        message("+ ${desc}")
+      endif()
+
+    else()
+      if(NOT ${package}_FOUND)
+        set(reason "not found")
+      elseif(NOT PANDA_PACKAGE_DEFAULT_${package})
+        set(reason "not requested")
+      else()
+        set(reason "disabled")
+      endif()
+
+      message("- ${desc} (${reason})")
+
+    endif()
+  endforeach()
+endfunction()
+
+#
+# export_packages(filename)
+#
+# Generates an includable CMake file that contains definitions for every PKG::
+# package defined.
+#
+function(export_packages filename)
+  set(exports "# Exports for Panda3D PKG:: packages\n")
+
+  foreach(pkg ${_ALL_PACKAGE_OPTIONS})
+    set(exports "${exports}\n# Create imported target PKG::${pkg}\n")
+    set(exports "${exports}add_library(PKG::${pkg} INTERFACE IMPORTED)\n\n")
+
+    set(exports "${exports}set_target_properties(PKG::${pkg} PROPERTIES\n")
+    foreach(prop
+        INTERFACE_COMPILE_DEFINITIONS
+        INTERFACE_COMPILE_FEATURES
+        INTERFACE_COMPILE_OPTIONS
+        INTERFACE_INCLUDE_DIRECTORIES
+        INTERFACE_LINK_DEPENDS
+        INTERFACE_LINK_DIRECTORIES
+        INTERFACE_LINK_OPTIONS
+        INTERFACE_POSITION_INDEPENDENT_CODE
+       #INTERFACE_SYSTEM_INCLUDE_DIRECTORIES  # Let the consumer dictate this
+        INTERFACE_SOURCES)
+
+      set(prop_ex "$<TARGET_PROPERTY:PKG::${pkg},${prop}>")
+      set(exports "${exports}$<$<BOOL:${prop_ex}>:  ${prop} \"${prop_ex}\"\n>")
+
+    endforeach(prop)
+
+    # Ugh, INTERFACE_LINK_LIBRARIES isn't transitive.  Fine.  Take care of it
+    # by hand:
+    set(libraries)
+    set(stack "PKG::${pkg}")
+    set(history)
+    while(stack)
+      # Remove head item from stack
+      unset(head)
+      while(NOT DEFINED head)
+        if(NOT stack)
+          break()
+        endif()
+
+        list(GET stack 0 head)
+        list(REMOVE_AT stack 0)
+
+        # Don't visit anything twice
+        list(FIND history "${head}" _index)
+        if(_index GREATER -1)
+          unset(head)
+        endif()
+      endwhile()
+
+      if(head)
+        list(APPEND history "${head}")
+      else()
+        break()
+      endif()
+
+      # If head isn't a target, add it to `libraries`, else recurse
+      if(TARGET "${head}")
+        get_target_property(link_libs "${head}" INTERFACE_LINK_LIBRARIES)
+        if(link_libs)
+          list(APPEND stack ${link_libs})
+        endif()
+
+        get_target_property(type "${head}" TYPE)
+        if(NOT type STREQUAL "INTERFACE_LIBRARY")
+          get_target_property(imported_location "${head}" IMPORTED_LOCATION)
+          get_target_property(imported_implib "${head}" IMPORTED_IMPLIB)
+          if(imported_implib)
+            list(APPEND libraries ${imported_implib})
+          elseif(imported_location)
+            list(APPEND libraries ${imported_location})
+          endif()
+
+          get_target_property(configs "${head}" IMPORTED_CONFIGURATIONS)
+          if(configs AND NOT imported_location)
+            foreach(config ${configs})
+              get_target_property(imported_location "${head}" IMPORTED_LOCATION_${config})
+
+              # Prefer IMPORTED_IMPLIB where present
+              get_target_property(imported_implib "${head}" IMPORTED_IMPLIB_${config})
+              if(imported_implib)
+                set(imported_location "${imported_implib}")
+              endif()
+
+              if(imported_location)
+                if(configs MATCHES ".*;.*")
+                  set(_bling "$<1:$>") # genex-escaped $
+                  list(APPEND libraries "${_bling}<${_bling}<CONFIG:${config}>:${imported_location}>")
+
+                else()
+                  list(APPEND libraries ${imported_location})
+
+                endif()
+              endif()
+            endforeach(config)
+          endif()
+
+        elseif(CMAKE_VERSION VERSION_GREATER "3.8")
+          # This is an INTERFACE_LIBRARY, and CMake is new enough to support
+          # IMPORTED_IMPLIB
+          get_target_property(imported_libname "${head}" IMPORTED_LIBNAME)
+          if(imported_libname)
+            list(APPEND libraries ${imported_libname})
+          endif()
+
+        endif()
+
+      elseif("${head}" MATCHES "\\$<TARGET_NAME:\([^>]+\)>")
+        string(REGEX REPLACE ".*\\$<TARGET_NAME:\([^>]+\)>.*" "\\1" match "${head}")
+        list(APPEND stack "${match}")
+
+      else()
+        list(APPEND libraries "${head}")
+
+      endif()
+    endwhile(stack)
+
+    set(exports "${exports}  INTERFACE_LINK_LIBRARIES \"${libraries}\"\n")
+
+    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)
+
+#
+# export_targets(set [NAMESPACE namespace] [COMPONENT component])
+#
+# Export targets in the export set named by "set"
+#
+# NAMESPACE overrides the namespace prefixed to the exported targets; it
+# defaults to "Panda3D::[set]::" if no explicit override is given.
+#
+# COMPONENT overrides the install component for the generated .cmake file; it
+# defaults to "[set]" if no explicit override is given.
+#
+function(export_targets set)
+  set(namespace "Panda3D::${set}::")
+  set(component "${set}")
+  set(keyword)
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "NAMESPACE" OR
+       arg STREQUAL "COMPONENT")
+
+      set(keyword "${arg}")
+
+    elseif(keyword STREQUAL "NAMESPACE")
+      set(namespace "${arg}")
+
+    elseif(keyword STREQUAL "COMPONENT")
+      set(component "${arg}")
+
+    else()
+      message(FATAL_ERROR "export_targets() given unexpected arg: ${arg}")
+
+    endif()
+  endforeach(arg)
+
+  export(EXPORT "${set}" NAMESPACE "${namespace}"
+    FILE "${PROJECT_BINARY_DIR}/Panda3D${set}Targets.cmake")
+  install(EXPORT "${set}" NAMESPACE "${namespace}"
+    FILE "Panda3D${set}Targets.cmake"
+    COMPONENT "${component}" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Panda3D)
+
+endfunction(export_targets)
+
+#
+# find_package
+#
+# This override implements CMAKE_FIND_PACKAGE_PREFER_CONFIG on versions of
+# CMake too old to include it.
+#
+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})
+
+    elseif(CMAKE_FIND_PACKAGE_PREFER_CONFIG)
+      # Try CONFIG
+      _find_package("${name}" CONFIG ${ARGN})
+
+      if(NOT ${name}_FOUND)
+        # CONFIG didn't work, fall back to MODULE
+        _find_package("${name}" MODULE ${ARGN})
+      endif()
+
+    else()
+      # Default behavior
+      _find_package(${ARGV})
+
+    endif()
+  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)

+ 163 - 0
cmake/macros/Python.cmake

@@ -0,0 +1,163 @@
+# Filename: Python.cmake
+#
+# Description: This file provides support functions for building/installing
+#   Python extension modules and/or pure-Python packages.
+#
+# Functions:
+#   add_python_target(target [source1 [source2 ...]])
+#   install_python_package(path [ARCH/LIB])
+#
+
+#
+# Function: add_python_target(target [EXPORT exp] [COMPONENT comp]
+#                                    [source1 [source2 ...]])
+# Build the provided source(s) as a Python extension module, linked against the
+# Python runtime library.
+#
+# Note that this also takes care of installation, unlike other target creation
+# commands in CMake.  The EXPORT and COMPONENT keywords allow passing the
+# corresponding options to install(), but default to "Python" otherwise.
+#
+function(add_python_target target)
+  if(NOT HAVE_PYTHON)
+    return()
+  endif()
+
+  string(REGEX REPLACE "^.*\\." "" basename "${target}")
+  set(sources)
+  set(component "Python")
+  set(export "Python")
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "COMPONENT")
+      set(keyword "component")
+
+    elseif(arg STREQUAL "EXPORT")
+      set(keyword "export")
+
+    elseif(keyword)
+      set(${keyword} "${arg}")
+      unset(keyword)
+
+    else()
+      list(APPEND sources "${arg}")
+
+    endif()
+  endforeach(arg)
+
+  string(REGEX REPLACE "\\.[^.]+$" "" namespace "${target}")
+  string(REPLACE "." "/" slash_namespace "${namespace}")
+
+  add_library(${target} ${MODULE_TYPE} ${sources})
+  target_link_libraries(${target} PKG::PYTHON)
+
+  if(BUILD_SHARED_LIBS)
+    set(_outdir "${PANDA_OUTPUT_DIR}/${slash_namespace}")
+
+    set_target_properties(${target} PROPERTIES
+      LIBRARY_OUTPUT_DIRECTORY "${_outdir}"
+      OUTPUT_NAME "${basename}"
+      PREFIX ""
+      SUFFIX "${PYTHON_EXTENSION_SUFFIX}")
+
+    # This is explained over in CompilerFlags.cmake
+    foreach(_config ${CMAKE_CONFIGURATION_TYPES})
+      string(TOUPPER "${_config}" _config)
+      set_target_properties(${target} PROPERTIES
+        LIBRARY_OUTPUT_DIRECTORY_${_config} "${_outdir}")
+    endforeach(_config)
+
+    if(PYTHON_ARCH_INSTALL_DIR)
+      install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION "${PYTHON_ARCH_INSTALL_DIR}/${slash_namespace}")
+    endif()
+
+  else()
+    set_target_properties(${target} PROPERTIES
+      OUTPUT_NAME "${basename}"
+      PREFIX "libpy.${namespace}.")
+
+    install(TARGETS ${target} EXPORT "${export}" COMPONENT "${component}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
+
+  endif()
+
+endfunction(add_python_target)
+
+#
+# Function: install_python_package(name [SOURCE path] [ARCH/LIB] [COMPONENT component])
+#
+# Installs the Python package `name` (which may have its source at `path`).
+#
+# The package is copied to (or created in) the build directory so that the user
+# may import it before the install step.
+#
+# Note that this handles more than just installation; it will also invoke
+# Python's compileall utility to pregenerate .pyc/.pyo files.  This will only
+# happen if the Python interpreter is found.
+#
+# The ARCH or LIB keyword may be used to specify whether this package should be
+# installed into Python's architecture-dependent or architecture-independent
+# package path.  The default, if unspecified, is LIB.
+#
+# The COMPONENT keyword overrides the install component (see CMake's
+# documentation for more information on what this does).  The default is
+# "Python".
+#
+function(install_python_package package_name)
+  set(type "LIB")
+  unset(keyword)
+  set(component "Python")
+  unset(src_path)
+  foreach(arg ${ARGN})
+    if(arg STREQUAL "ARCH")
+      set(type "ARCH")
+
+    elseif(arg STREQUAL "LIB")
+      set(type "LIB")
+
+    elseif(arg STREQUAL "COMPONENT")
+      set(keyword "${arg}")
+
+    elseif(keyword STREQUAL "COMPONENT")
+      set(component "${arg}")
+      unset(keyword)
+
+    elseif(arg STREQUAL "SOURCE")
+      set(keyword "${arg}")
+
+    elseif(keyword STREQUAL "SOURCE")
+      set(src_path "${arg}")
+      unset(keyword)
+
+    else()
+      message(FATAL_ERROR "install_python_package got unexpected argument: ${ARGN}")
+
+    endif()
+  endforeach(arg)
+
+  if(NOT DEFINED src_path AND type STREQUAL "ARCH" AND WIN32 AND NOT CYGWIN)
+    # Win32 needs a special fixup so the DLLs in "bin" can be on the path;
+    # let's set src_path to the directory containing our fixup __init__.py
+    set(src_path "${CMAKE_SOURCE_DIR}/cmake/templates/win32_python")
+  endif()
+
+  set(path "${PANDA_OUTPUT_DIR}/${package_name}")
+
+  set(args -D "OUTPUT_DIR=${path}")
+  if(src_path)
+    list(APPEND args -D "SOURCE_DIR=${src_path}")
+  endif()
+  if(PYTHON_EXECUTABLE)
+    list(APPEND args -D "PYTHON_EXECUTABLES=${PYTHON_EXECUTABLE}")
+  endif()
+  add_custom_target(${package_name} ALL
+    COMMAND ${CMAKE_COMMAND}
+      ${args}
+      -P "${CMAKE_SOURCE_DIR}/cmake/scripts/CopyPython.cmake")
+
+  set(dir "${PYTHON_${type}_INSTALL_DIR}")
+  if(dir)
+    install(DIRECTORY "${path}" DESTINATION "${dir}"
+      COMPONENT "${component}"
+      FILES_MATCHING REGEX "\\.py[co]?$")
+  endif()
+
+endfunction(install_python_package)

+ 9 - 0
cmake/macros/README.md

@@ -0,0 +1,9 @@
+Directory Info
+--------------
+**Directory:** /cmake/macros  
+**License:** Unlicense  
+**Description:** This directory is used for CMake modules which may be _unsafe_
+to use outside of a Panda3D project.  These modules may rely on Panda3D specific
+cmake variables, Panda3D's directory structure, or some other dependency.
+They are not intended to be included in other projects directly, though you
+are free to copy and adapt them to your own needs.

+ 37 - 0
cmake/macros/RunPzip.cmake

@@ -0,0 +1,37 @@
+function(run_pzip target_name source destination glob)
+  file(GLOB_RECURSE files RELATIVE "${source}" "${source}/${glob}")
+
+  set(dstfiles "")
+  foreach(filename ${files})
+    string(REGEX REPLACE "^/" "" filename "${filename}")
+
+    get_filename_component(dstdir "${destination}/${filename}" DIRECTORY)
+
+    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 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)
+
+  add_custom_target(${target_name} ALL
+    DEPENDS ${dstfiles}
+    WORKING_DIRECTORY "${destination}")
+
+endfunction(run_pzip)

+ 21 - 0
cmake/macros/Versioning.cmake

@@ -0,0 +1,21 @@
+# Filename: Versioning.cmake
+#
+# Description: Contains an override for add_library to set the
+#   VERSION and SOVERSION properties on all shared libraries, automatically, to
+#   the project version.
+#
+# Functions:
+#   add_library(...)
+#
+
+function(add_library target_name)
+  _add_library("${target_name}" ${ARGN})
+
+  get_target_property(type "${target_name}" TYPE)
+  if(type STREQUAL "SHARED_LIBRARY")
+    set_target_properties("${target_name}" PROPERTIES
+      VERSION "${PROJECT_VERSION}"
+      SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
+  endif()
+
+endfunction(add_library)

+ 33 - 0
cmake/modules/FindARToolKit.cmake

@@ -0,0 +1,33 @@
+# Filename: FindARToolKit.cmake
+# Authors: CFSworks (3 Nov, 2018)
+#
+# Usage:
+#   find_package(ARToolKit [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   ARTOOLKIT_FOUND       - system has ARToolKit
+#   ARTOOLKIT_INCLUDE_DIR - the include directory containing ARToolKit header files
+#   ARTOOLKIT_LIBRARIES   - the paths to the ARToolKit client libraries
+#
+
+find_path(ARTOOLKIT_INCLUDE_DIR "AR/ar.h")
+
+find_library(ARTOOLKIT_AR_LIBRARY
+  NAMES "AR" "libAR")
+
+find_library(ARTOOLKIT_ARMulti_LIBRARY
+  NAMES "ARMulti" "libARMulti")
+
+mark_as_advanced(ARTOOLKIT_INCLUDE_DIR ARTOOLKIT_AR_LIBRARY ARTOOLKIT_ARMulti_LIBRARY)
+
+set(ARTOOLKIT_LIBRARIES)
+if(ARTOOLKIT_AR_LIBRARY)
+  list(APPEND ARTOOLKIT_LIBRARIES "${ARTOOLKIT_AR_LIBRARY}")
+endif()
+if(ARTOOLKIT_ARMulti_LIBRARY)
+  list(APPEND ARTOOLKIT_LIBRARIES "${ARTOOLKIT_ARMulti_LIBRARY}")
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(ARToolKit DEFAULT_MSG
+  ARTOOLKIT_INCLUDE_DIR ARTOOLKIT_LIBRARIES)

+ 33 - 0
cmake/modules/FindAssimp.cmake

@@ -0,0 +1,33 @@
+# Filename: FindAssimp.cmake
+# Authors: CFSworks (9 Nov, 2018)
+#
+# Usage:
+#   find_package(Assimp [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   ASSIMP_FOUND        - system has Assimp
+#   ASSIMP_INCLUDE_DIR  - the path to the location of the assimp/ directory
+#   ASSIMP_LIBRARIES    - the libraries to link against for Assimp
+#
+
+find_path(ASSIMP_INCLUDE_DIR
+  NAMES "assimp/Importer.hpp")
+
+find_library(ASSIMP_ASSIMP_LIBRARY
+  NAMES "assimp")
+
+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_LIBRARIES)

+ 175 - 0
cmake/modules/FindCg.cmake

@@ -0,0 +1,175 @@
+# Filename: FindCg.cmake
+# Author: kestred (8 Dec, 2013)
+#
+# Usage:
+#   find_package(Cg [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   CG_FOUND         - system has NvidiaCg
+#   CG_INCLUDE_DIR   - the NvidiaCg include directory
+#   CG_INCLUDE_DIRS  - directories for all NvidiaCg components
+#   CG_LIBRARY_DIR   - the NvidiaCg library directory
+#   CG_LIBRARY       - the path to the library binary
+#   CG_LIBRARIES     - the paths to the Cg library and each library below.
+#
+#   CGGL_FOUND       - system has CgGL
+#   CGGL_INCLUDE_DIR - the CgGL include directory
+#   CGGL_LIBRARY_DIR - the CgGL library directory
+#   CGGL_LIBRARY     - the path to the library binary
+#
+
+
+### Define macros to find each sublibrary ###
+
+# 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()
+  if(NOT CGGL_LIBRARY_DIR OR NOT CGGL_INCLUDE_DIR)
+    # Find the include directory
+    find_path(CGGL_INCLUDE_DIR
+      NAMES "cgGL.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 NvidiaCgGL's include directory."
+    )
+
+    # Find the library directory
+    find_library(CGGL_LIBRARY
+      NAMES "CgGL" "libCgGL"
+      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"
+      DOC "The filepath to NvidiaCgGL's libary binary."
+    )
+    get_filename_component(CGGL_LIBRARY_DIR "${CGGL_LIBRARY}" PATH)
+    set(CGGL_LIBRARY_DIR "${CGGL_LIBRARY_DIR}" CACHE PATH "The path to the CgGL library directory.") # Library path
+
+    mark_as_advanced(CGGL_INCLUDE_DIR)
+    mark_as_advanced(CGGL_LIBRARY_DIR)
+    mark_as_advanced(CGGL_LIBRARY)
+  endif()
+
+  find_package_handle_standard_args(CgGL DEFAULT_MSG CGGL_LIBRARY CGGL_INCLUDE_DIR CGGL_LIBRARY_DIR)
+
+endmacro()
+
+
+# Find Cg for Direct3D 9
+macro(find_cgd3d9)
+  if(Cg_FIND_QUIETLY)
+    set(CgD3D9_FIND_QUIETLY TRUE)
+  endif()
+  if(NOT CGD3D9_LIBRARY_DIR OR NOT CGD3D9_INCLUDE_DIR)
+    # Find the include directory
+    find_path(CGD3D9_INCLUDE_DIR
+      NAMES "cgD3D9.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 NvidiaCgD3D9's include directory."
+    )
+
+    # Find the library directory
+    find_library(CGD3D9_LIBRARY
+      NAMES "CgD3D9" "libCgD3D9"
+      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"
+      DOC "The filepath to NvidiaCgD3D9's libary binary."
+    )
+    get_filename_component(CGD3D9_LIBRARY_DIR "${CGD3D9_LIBRARY}" PATH)
+    set(CGD3D9_LIBRARY_DIR "${CGD3D9_LIBRARY_DIR}" CACHE PATH "The path to the CgD3D9 library directory.") # Library path
+
+    mark_as_advanced(CGD3D9_INCLUDE_DIR)
+    mark_as_advanced(CGD3D9_LIBRARY_DIR)
+    mark_as_advanced(CGD3D9_LIBRARY)
+  endif()
+
+  find_package_handle_standard_args(CgD3D9 DEFAULT_MSG CGD3D9_LIBRARY CGD3D9_INCLUDE_DIR CGD3D9_LIBRARY_DIR)
+
+endmacro()
+
+
+
+# Find base Nvidia Cg
+if(NOT CG_LIBRARY_DIR OR NOT CG_INCLUDE_DIRS)
+  # 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})
+
+  mark_as_advanced(CG_INCLUDE_DIRS)
+  mark_as_advanced(CG_INCLUDE_DIR)
+  mark_as_advanced(CG_LIBRARY_DIR)
+  mark_as_advanced(CG_LIBRARY)
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Cg DEFAULT_MSG CG_LIBRARY CG_INCLUDE_DIRS CG_LIBRARY_DIR)
+
+if(CG_INCLUDE_DIR AND CG_LIBRARY_DIR)
+  find_cggl()
+  find_cgd3d9()
+
+  set(CG_LIBRARIES ${CG_LIBRARY})
+  if(CGGL_LIBRARY)
+    list(APPEND CG_LIBRARIES "${CGGL_LIBRARY}")
+  endif()
+  if(CGD3D9_LIBRARY)
+    list(APPEND CG_LIBRARIES "${CGD3D9_LIBRARY}")
+  endif()
+endif()

+ 104 - 0
cmake/modules/FindDirect3D9.cmake

@@ -0,0 +1,104 @@
+# Filename: FindDirect3D9.cmake
+# Authors: CFSworks (26 Oct, 2018)
+#
+# Usage:
+#   find_package(Direct3D9 [REQUIRED] [QUIET])
+#
+# This supports the following components:
+#   d3dx9
+#   dxerr
+#   dxguid
+#
+# Once done this will define:
+#   DIRECT3D9_FOUND       - system has Direct3D 9.x
+#   DIRECT3D9_INCLUDE_DIR - the include directory containing d3d9.h - note that
+#                           this will be empty if it's part of the Windows SDK.
+#   DIRECT3D9_LIBRARY     - the path to d3d9.lib
+#   DIRECT3D9_LIBRARIES   - the path to d3d9.lib and all extra component
+#                           libraries
+#
+
+include(CheckIncludeFile)
+
+if(Direct3D9_FIND_QUIETLY)
+  if(DEFINED CMAKE_REQUIRED_QUIET)
+    set(_OLD_CMAKE_REQUIRED_QUIET ${CMAKE_REQUIRED_QUIET})
+  endif()
+  # Suppress check_include_file messages
+  set(CMAKE_REQUIRED_QUIET ON)
+endif()
+
+check_include_file("d3d9.h" SYSTEM_INCLUDE_D3D9_H)
+mark_as_advanced(SYSTEM_INCLUDE_D3D9_H)
+
+if(Direct3D9_FIND_QUIETLY)
+  if(DEFINED _OLD_CMAKE_REQUIRED_QUIET)
+    set(CMAKE_REQUIRED_QUIET ${_OLD_CMAKE_REQUIRED_QUIET})
+    unset(_OLD_CMAKE_REQUIRED_QUIET)
+  else()
+    unset(CMAKE_REQUIRED_QUIET)
+  endif()
+endif()
+
+if(SYSTEM_INCLUDE_D3D9_H
+    AND NOT Direct3D9_FIND_REQUIRED_d3dx9
+    AND NOT Direct3D9_FIND_REQUIRED_dxerr)
+  # It's available as #include <d3d9.h> - easy enough.  We'll use "." as a way
+  # of saying "We found it, but please erase this variable later."
+  set(DIRECT3D9_INCLUDE_DIR ".")
+
+  # Since d3d9.h is on the search path, we can pretty much assume d3d9.lib is
+  # as well.
+  set(DIRECT3D9_LIBRARY "d3d9.lib")
+
+  # And dxguid.lib, why not
+  set(DIRECT3D9_dxguid_LIBRARY "dxguid.lib")
+
+else()
+  # We could not find it easily - maybe it's installed separately as part of
+  # the DirectX SDK?
+
+  find_path(DIRECT3D9_INCLUDE_DIR
+    NAMES d3d9.h
+    PATHS "$ENV{DXSDK_DIR}/Include")
+
+  if(CMAKE_SIZEOF_VOID_P EQUAL 8)
+    set(dx_lib_path "$ENV{DXSDK_DIR}/Lib/x64/")
+  else()
+    set(dx_lib_path "$ENV{DXSDK_DIR}/Lib/x86/")
+  endif()
+
+  find_library(DIRECT3D9_LIBRARY d3d9 "${dx_lib_path}" NO_DEFAULT_PATH)
+
+  find_library(DIRECT3D9_d3dx9_LIBRARY d3dx9 "${dx_lib_path}" NO_DEFAULT_PATH)
+  find_library(DIRECT3D9_dxerr_LIBRARY dxerr "${dx_lib_path}" NO_DEFAULT_PATH)
+  find_library(DIRECT3D9_dxguid_LIBRARY dxguid "${dx_lib_path}" NO_DEFAULT_PATH)
+
+  unset(dx_lib_path)
+endif()
+
+mark_as_advanced(DIRECT3D9_INCLUDE_DIR DIRECT3D9_LIBRARY)
+set(DIRECT3D9_LIBRARIES "${DIRECT3D9_LIBRARY}")
+
+foreach(_component d3dx9 dxerr dxguid)
+  if(DIRECT3D9_${_component}_LIBRARY)
+    set(Direct3D9_${_component}_FOUND ON)
+    list(FIND Direct3D9_FIND_COMPONENTS "${_component}" _index)
+    if(${_index} GREATER -1)
+      list(APPEND DIRECT3D9_LIBRARIES "${DIRECT3D9_${_component}_LIBRARY}")
+    endif()
+    unset(_index)
+  endif()
+endforeach(_component)
+unset(_component)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Direct3D9 HANDLE_COMPONENTS
+  REQUIRED_VARS DIRECT3D9_INCLUDE_DIR DIRECT3D9_LIBRARY DIRECT3D9_LIBRARIES)
+
+# See above - if we found the include as part of the system path, we don't want
+# to actually modify the include search path, but we need a non-empty string to
+# satisfy find_package_handle_standard_args()
+if(DIRECT3D9_INCLUDE_DIR STREQUAL ".")
+  set(DIRECT3D9_INCLUDE_DIR "")
+endif()

+ 21 - 0
cmake/modules/FindEGL.cmake

@@ -0,0 +1,21 @@
+# Filename: FindEGL.cmake
+# Authors: CFSworks (21 Oct, 2018)
+#
+# Usage:
+#   find_package(EGL [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   EGL_FOUND        - system has EGL
+#   EGL_INCLUDE_DIR  - the include directory containing EGL/egl.h
+#   EGL_LIBRARY      - the library to link against for EGL
+#
+
+find_path(EGL_INCLUDE_DIR "EGL/egl.h")
+
+find_library(EGL_LIBRARY
+  NAMES "EGL")
+
+mark_as_advanced(EGL_INCLUDE_DIR EGL_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(EGL DEFAULT_MSG EGL_INCLUDE_DIR EGL_LIBRARY)

+ 25 - 0
cmake/modules/FindEigen3.cmake

@@ -0,0 +1,25 @@
+# Filename: FindEigen3.cmake
+# Authors: kestred (13 Dec, 2013)
+#
+# Usage:
+#   find_package(Eigen3 [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   EIGEN_FOUND        - system has any version of Eigen
+#   EIGEN3_FOUND       - system has Eigen3
+#   EIGEN3_INCLUDE_DIR - the Eigen3 include directory
+#
+
+find_path(EIGEN3_INCLUDE_DIR
+  NAMES signature_of_eigen3_matrix_library
+  PATH_SUFFIXES eigen3 eigen)
+
+mark_as_advanced(EIGEN3_INCLUDE_DIR)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Eigen3 DEFAULT_MSG EIGEN3_INCLUDE_DIR)
+
+if(EIGEN3_FOUND)
+  set(EIGEN_FOUND TRUE)
+  set(EIGEN_INCLUDE_DIR ${EIGEN3_INCLUDE_DIR})
+endif()

+ 88 - 0
cmake/modules/FindFCollada.cmake

@@ -0,0 +1,88 @@
+# Filename: FindFCollada.cmake
+# Author: CFSworks (17 Mar, 2019)
+#
+# Usage:
+#   find_package(FCollada [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   FCOLLADA_FOUND       - system has FCollada
+#   FCOLLADA_INCLUDE_DIR - the FCollada include directory
+#
+#   FCOLLADA_RELEASE_LIBRARY - the filepath of the FCollada release library
+#   FCOLLADA_DEBUG_LIBRARY   - the filepath of the FCollada debug library
+#
+#   FCollada::FCollada - The recommended FCollada library to link against
+#
+
+# Find the FCollada include files
+find_path(FCOLLADA_INCLUDE_DIR "FCollada.h" PATH_SUFFIXES "FCollada")
+
+# Find the library built for release
+find_library(FCOLLADA_RELEASE_LIBRARY
+  NAMES "FCollada" "libFCollada"
+  "FColladaS" "libFColladaS"
+  "FColladaU" "libFColladaU"
+  "FColladaSU" "libFColladaSU"
+)
+
+# Find the library built for debug
+find_library(FCOLLADA_DEBUG_LIBRARY
+  NAMES "FColladaD" "libFColladaD"
+  "FColladaSD" "libFColladaSD"
+  "FColladaUD" "libFColladaUD"
+  "FColladaSUD" "libFColladaSUD"
+)
+
+mark_as_advanced(FCOLLADA_INCLUDE_DIR)
+mark_as_advanced(FCOLLADA_RELEASE_LIBRARY)
+mark_as_advanced(FCOLLADA_DEBUG_LIBRARY)
+
+set(_defines)
+if(FCOLLADA_RELEASE_LIBRARY MATCHES "FCollada[^/]*U" OR
+    FCOLLADA_DEBUG_LIBRARY MATCHES "FCollada[^/]*U")
+  list(APPEND _defines "UNICODE")
+endif()
+if(NOT MSVC AND
+    NOT FCOLLADA_RELEASE_LIBRARY MATCHES "FCollada[^/]*S" AND
+    NOT FCOLLADA_DEBUG_LIBRARY MATCHES "FCollada[^/]*S")
+  list(APPEND _defines "FCOLLADA_DLL")
+endif()
+
+# Identify the configs which we have available
+set(_configs)
+if(FCOLLADA_INCLUDE_DIR)
+  if(FCOLLADA_RELEASE_LIBRARY)
+    list(APPEND _configs RELEASE)
+  endif()
+  if(FCOLLADA_DEBUG_LIBRARY)
+    list(APPEND _configs DEBUG)
+  endif()
+
+  if(_configs)
+    set(_HAS_FCOLLADA_LIBRARY ON)
+    add_library(FCollada::FCollada UNKNOWN IMPORTED GLOBAL)
+
+    set_target_properties(FCollada::FCollada PROPERTIES
+      INTERFACE_COMPILE_DEFINITIONS "${_defines}"
+      INTERFACE_INCLUDE_DIRECTORIES "${FCOLLADA_INCLUDE_DIR}")
+
+  endif()
+
+endif()
+
+foreach(_config ${_configs})
+  set_property(TARGET FCollada::FCollada
+    APPEND PROPERTY IMPORTED_CONFIGURATIONS "${_config}")
+
+  set_target_properties(FCollada::FCollada PROPERTIES
+    IMPORTED_LOCATION_${_config} "${FCOLLADA_${_config}_LIBRARY}")
+
+endforeach(_config)
+unset(_config)
+unset(_configs)
+unset(_defines)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(FCollada DEFAULT_MSG FCOLLADA_INCLUDE_DIR _HAS_FCOLLADA_LIBRARY)
+
+unset(_HAS_FCOLLADA_LIBRARY)

+ 116 - 0
cmake/modules/FindFFMPEG.cmake

@@ -0,0 +1,116 @@
+# Filename: FindFFMPEG.cmake
+# Author: CFSworks (10 Apr, 2014)
+#
+# Usage:
+#   find_package(FFMPEG [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   FFMPEG_FOUND       - system has ffmpeg
+#   FFMPEG_INCLUDE_DIR - the ffmpeg include directory
+#   FFMPEG_LIBRARIES   - the path to the library binary
+#
+#   FFMPEG_LIBAVCODEC  - the path to the libavcodec library binary
+#   FFMPEG_LIBAVFORMAT - the path to the libavformat library binary
+#   FFMPEG_LIBAVUTIL   - the path to the libavutil library binary
+#
+
+# Find the libffmpeg include files
+find_path(FFMPEG_INCLUDE_DIR
+  NAMES "libavcodec/avcodec.h"
+  PATHS "/usr/include"
+        "/usr/local/include"
+        "/sw/include"
+        "/opt/include"
+        "/opt/local/include"
+        "/opt/csw/include"
+  PATH_SUFFIXES "libav" "ffmpeg"
+)
+
+# Find the libavcodec library
+find_library(FFMPEG_LIBAVCODEC
+  NAMES "avcodec"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+# Find the libavformat library
+find_library(FFMPEG_LIBAVFORMAT
+  NAMES "avformat"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+# Find the libavutil library
+find_library(FFMPEG_LIBAVUTIL
+  NAMES "avutil"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+mark_as_advanced(FFMPEG_INCLUDE_DIR)
+mark_as_advanced(FFMPEG_LIBAVCODEC)
+mark_as_advanced(FFMPEG_LIBAVFORMAT)
+mark_as_advanced(FFMPEG_LIBAVUTIL)
+
+# Translate library into library directory
+if(FFMPEG_LIBAVCODEC)
+  unset(FFMPEG_LIBRARY_DIR CACHE)
+  get_filename_component(FFMPEG_LIBRARY_DIR "${FFMPEG_LIBAVCODEC}" PATH)
+  set(FFMPEG_LIBRARY_DIR "${FFMPEG_LIBRARY_DIR}" CACHE PATH "The path to libffmpeg's library directory.") # Library path
+endif()
+
+set(FFMPEG_LIBRARIES)
+if(FFMPEG_LIBAVCODEC)
+  list(APPEND FFMPEG_LIBRARIES "${FFMPEG_LIBAVCODEC}")
+endif()
+if(FFMPEG_LIBAVFORMAT)
+  list(APPEND FFMPEG_LIBRARIES "${FFMPEG_LIBAVFORMAT}")
+endif()
+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)
+find_package_handle_standard_args(FFMPEG DEFAULT_MSG FFMPEG_LIBRARIES FFMPEG_LIBAVCODEC FFMPEG_LIBAVFORMAT FFMPEG_LIBAVUTIL FFMPEG_INCLUDE_DIR FFMPEG_LIBRARY_DIR)

+ 107 - 0
cmake/modules/FindFFTW3.cmake

@@ -0,0 +1,107 @@
+# Filename: FindFFTW.cmake
+# Author: Unknown (???), kestred (29 Nov, 2013)
+#
+# Usage:
+#   find_package(FFTW [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   FFTW3_FOUND       - true if fftw is found on the system
+#   FFTW3_INCLUDE_DIR - the fftw include directory
+#   FFTW3_LIBRARY_DIR - the fftw library directory
+#   FFTW3_LIBRARY     - the path to the library binary
+#
+# The following variables will be checked by the function
+#   FFTW3_ROOT - if set, the libraries are exclusively searched under this path
+#
+
+# Check if we can use PkgConfig
+find_package(PkgConfig QUIET)
+
+#Determine from PKG
+if(PKG_CONFIG_FOUND AND NOT FFTW3_ROOT)
+  pkg_check_modules(PKG_FFTW QUIET "fftw3")
+endif()
+
+if(FFTW3_ROOT)
+  # Try to find headers under root
+  find_path(FFTW3_INCLUDE_DIR
+    NAMES "fftw3.h"
+    PATHS ${FFTW3_ROOT}
+    PATH_SUFFIXES "include"
+    NO_DEFAULT_PATH
+  )
+
+  # Try to find library under root
+  find_library(FFTW3_LIBRARY
+    NAMES "fftw3"
+    PATHS ${FFTW3_ROOT}
+    PATH_SUFFIXES "lib" "lib64"
+    NO_DEFAULT_PATH
+  )
+
+  find_library(FFTW3_FFTWF_LIBRARY
+    NAMES "fftw3f"
+    PATHS ${FFTW3_ROOT}
+    PATH_SUFFIXES "lib" "lib64"
+    NO_DEFAULT_PATH
+  )
+
+  find_library(FFTW3_FFTWL_LIBRARY
+    NAMES "fftw3l"
+    PATHS ${FFTW3_ROOT}
+    PATH_SUFFIXES "lib" "lib64"
+    NO_DEFAULT_PATH
+  )
+else()
+  # Find headers the normal way
+  find_path(FFTW3_INCLUDE_DIR
+    NAMES "fftw3.h"
+    PATHS ${PKG_FFTW3_INCLUDE_DIRS}
+          ${INCLUDE_INSTALL_DIR}
+          "/usr/include"
+          "/usr/local/include"
+    PATH_SUFFIXES "" "fftw"
+  )
+
+  # Find library the normal way
+  find_library(FFTW3_LIBRARY
+    NAMES "fftw3"
+    PATHS ${PKG_FFTW3_LIBRARY_DIRS}
+          ${LIB_INSTALL_DIR}
+          "/usr"
+          "/usr/local"
+    PATH_SUFFIXES "" "lib" "lib32" "lib64"
+  )
+
+  find_library(FFTW3_FFTWF_LIBRARY
+    NAMES "fftw3f"
+    PATHS ${PKG_FFTW3_LIBRARY_DIRS}
+          ${LIB_INSTALL_DIR}
+          "/usr"
+          "/usr/local"
+    PATH_SUFFIXES "" "lib" "lib32" "lib64"
+  )
+
+
+  find_library(FFTW3_FFTWL_LIBRARY
+    NAMES "fftw3l"
+    PATHS ${PKG_FFTW3_LIBRARY_DIRS}
+          ${LIB_INSTALL_DIR}
+          "/usr"
+          "/usr/local"
+    PATH_SUFFIXES "" "lib" "lib32" "lib64"
+  )
+endif()
+
+# Get the directory conaining FFTW3_LIBRARY
+get_filename_component(FFTW3_LIBRARY_DIR "${FFTW3_LIBRARY}" PATH)
+set(FFTW3_LIBRARY_DIR "${FFTW3_LIBRARY_DIR}" CACHE PATH "The path to fftw's library directory.") # Library path
+
+mark_as_advanced(FFTW3_INCLUDE_DIR)
+mark_as_advanced(FFTW3_LIBRARY_DIR)
+mark_as_advanced(FFTW3_LIBRARY)
+mark_as_advanced(FFTW3_FFTWF_LIBRARY)
+mark_as_advanced(FFTW3_FFTWL_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(FFTW3 DEFAULT_MSG FFTW3_LIBRARY FFTW3_INCLUDE_DIR FFTW3_LIBRARY_DIR)

+ 83 - 0
cmake/modules/FindFMODEx.cmake

@@ -0,0 +1,83 @@
+# Filename: FindFMODEx.cmake
+# Author: kestred (8 Dec, 2013)
+#
+# Usage:
+#   find_pcackage(FMODEx [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   FMODEX_FOUND       - system has FMOD Ex
+#   FMODEX_INCLUDE_DIR - the FMOD Ex include directory
+#   FMODEX_LIBRARY_DIR - the FMOD Ex library directory
+#   FMODEX_LIBRARY     - the path to the library binary
+#
+#   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
+find_path(FMODEX_INCLUDE_DIR
+  NAMES "fmod.h"
+  PATHS "/usr/include"
+        "/usr/local/include"
+        "/sw/include"
+        "/opt/include"
+        "/opt/local/include"
+        "/opt/csw/include"
+        "/opt/fmodex/include"
+        "/opt/fmodex/api/inc"
+        "C:/Program Files (x86)/FMOD SoundSystem/FMOD Programmers API Win32/api/inc"
+  PATH_SUFFIXES "" "fmodex/fmod" "fmodex/fmod3" "fmod" "fmod3"
+  DOC "The path to FMOD Ex's include directory."
+)
+
+# Find the 32-bit library
+find_library(FMODEX_32_LIBRARY
+  NAMES "fmodex_vc" "fmodex_bc" "fmodex" "fmodexL" "libfmodex" "libfmodexL" "fmodex_vc" "fmodexL_vc"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/X11R6"
+        "/usr/local/X11R6"
+        "/sw"
+        "/opt"
+        "/opt/local"
+        "/opt/csw"
+        "/opt/fmodex"
+        "/opt/fmodex/api"
+        "C:/Program Files (x86)/FMOD SoundSystem/FMOD Programmers API Win32/api/lib"
+  PATH_SUFFIXES "" "lib" "lib32"
+)
+
+# Find the 64-bit library
+find_library(FMODEX_64_LIBRARY
+  NAMES "fmodex64" "libfmodex64" "fmodexL64" "libfmodexL64" "fmodex64_vc" "fmodexL64_vc"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/X11R6"
+        "/usr/local/X11R6"
+        "/sw"
+        "/opt"
+        "/opt/local"
+        "/opt/csw"
+        "/opt/fmodex"
+        "/opt/fmodex/api"
+        "/usr/freeware"
+  PATH_SUFFIXES "" "lib" "lib64"
+)
+
+if(FMODEX_32_LIBRARY)
+  set(FMODEX_LIBRARY ${FMODEX_32_LIBRARY} CACHE FILEPATH "The filepath to FMOD Ex's library binary.")
+elseif(FMODEX_64_LIBRARY)
+  set(FMODEX_LIBRARY ${FMODEX_64_LIBRARY} CACHE FILEPATH "The filepath to FMOD Ex's library binary.")
+endif()
+
+get_filename_component(FMODEX_LIBRARY_DIR "${FMODEX_LIBRARY}" PATH)
+set(FMODEX_LIBRARY_DIR "${FMODEX_LIBRARY_DIR}" CACHE PATH "The path to FMOD Ex's library directory.")
+
+mark_as_advanced(FMODEX_INCLUDE_DIR)
+mark_as_advanced(FMODEX_LIBRARY_DIR)
+mark_as_advanced(FMODEX_LIBRARY)
+mark_as_advanced(FMODEX_32_LIBRARY)
+mark_as_advanced(FMODEX_64_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(FMODEx DEFAULT_MSG FMODEX_LIBRARY FMODEX_INCLUDE_DIR FMODEX_LIBRARY_DIR)

+ 23 - 0
cmake/modules/FindHarfBuzz.cmake

@@ -0,0 +1,23 @@
+# Filename: FindHarfBuzz.cmake
+# Authors: CFSworks (2 Nov, 2018)
+#
+# Usage:
+#   find_package(HarfBuzz [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   HARFBUZZ_FOUND       - system has HarfBuzz
+#   HARFBUZZ_INCLUDE_DIR - the include directory containing hb.h
+#   HARFBUZZ_LIBRARY     - the path to the HarfBuzz library
+#
+
+find_path(HARFBUZZ_INCLUDE_DIR
+  NAMES "hb.h"
+  PATH_SUFFIXES "harfbuzz")
+
+find_library(HARFBUZZ_LIBRARY
+  NAMES "harfbuzz")
+
+mark_as_advanced(HARFBUZZ_INCLUDE_DIR HARFBUZZ_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(HarfBuzz DEFAULT_MSG HARFBUZZ_INCLUDE_DIR HARFBUZZ_LIBRARY)

+ 78 - 0
cmake/modules/FindLibSquish.cmake

@@ -0,0 +1,78 @@
+# Filename: FindLibSquish.cmake
+# Author: kestred (7 Dec, 2013)
+#
+# Usage:
+#   find_package(LibSquish [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   LIBSQUISH_FOUND       - system has libsquish
+#   LIBSQUISH_INCLUDE_DIR - the libsquish include directory
+#   LIBSQUISH_LIBRARY_DIR - the libsquish library directory
+#   LIBSQUISH_LIBRARY     - the path to the library binary
+#
+#   LIBSQUISH_RELEASE_LIBRARY - the filepath of the libsquish release library
+#   LIBSQUISH_DEBUG_LIBRARY   - the filepath of the libsquish debug library
+#
+
+# Find the libsquish include files
+find_path(LIBSQUISH_INCLUDE_DIR
+  NAMES "squish.h"
+  PATHS "/usr/include"
+        "/usr/local/include"
+        "/sw/include"
+        "/opt/include"
+        "/opt/local/include"
+        "/opt/csw/include"
+  PATH_SUFFIXES "" "cppunit"
+)
+
+# Find the libsquish library built for release
+find_library(LIBSQUISH_RELEASE_LIBRARY
+  NAMES "squish" "libsquish"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+# Find the libsquish library built for debug
+find_library(LIBSQUISH_DEBUG_LIBRARY
+  NAMES "squishd" "libsquishd"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+
+mark_as_advanced(LIBSQUISH_INCLUDE_DIR)
+mark_as_advanced(LIBSQUISH_RELEASE_LIBRARY)
+mark_as_advanced(LIBSQUISH_DEBUG_LIBRARY)
+
+# Choose library
+if(CMAKE_BUILD_TYPE MATCHES "Debug" AND LIBSQUISH_DEBUG_LIBRARY)
+	set(LIBSQUISH_LIBRARY ${LIBSQUISH_DEBUG_LIBRARY} CACHE FILEPATH "The filepath to libsquish's library binary.")
+elseif(LIBSQUISH_RELEASE_LIBRARY)
+	set(LIBSQUISH_LIBRARY ${LIBSQUISH_RELEASE_LIBRARY} CACHE FILEPATH "The filepath to libsquish's library binary.")
+elseif(LIBSQUISH_DEBUG_LIBRARY)
+	set(LIBSQUISH_LIBRARY ${LIBSQUISH_DEBUG_LIBRARY} CACHE FILEPATH "The filepath to libsquish's library binary.")
+endif()
+
+# Translate library into library directory
+if(LIBSQUISH_LIBRARY)
+	unset(LIBSQUISH_LIBRARY_DIR CACHE)
+	get_filename_component(LIBSQUISH_LIBRARY_DIR "${LIBSQUISH_LIBRARY}" PATH)
+	set(LIBSQUISH_LIBRARY_DIR "${LIBSQUISH_LIBRARY_DIR}" CACHE PATH "The path to libsquish's library directory.") # Library path
+endif()
+
+mark_as_advanced(LIBSQUISH_LIBRARY)
+mark_as_advanced(LIBSQUISH_LIBRARY_DIR)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(LibSquish DEFAULT_MSG LIBSQUISH_LIBRARY LIBSQUISH_INCLUDE_DIR LIBSQUISH_LIBRARY_DIR)

+ 157 - 0
cmake/modules/FindODE.cmake

@@ -0,0 +1,157 @@
+# Filename: FindODE.cmake
+# Author: CFSworks (7 Feb, 2014)
+#
+# Usage:
+#   find_package(ODE [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   ODE_FOUND       - system has ode
+#   ODE_INCLUDE_DIR - the ode include directory
+#
+#   ODE_RELEASE_LIBRARY - the filepath of the ode release library
+#   ODE_DEBUG_LIBRARY   - the filepath of the ode debug library
+#
+#   ODE_SINGLE_DEBUG_LIBRARY   - the filepath of the single-precision ode debug library
+#   ODE_SINGLE_RELEASE_LIBRARY   - the filepath of the single-precision ode release library
+#   ODE_DOUBLE_DEBUG_LIBRARY   - the filepath of the double-precision ode debug library
+#   ODE_DOUBLE_RELEASE_LIBRARY   - the filepath of the double-precision ode release library
+#
+#   ODE::ODE        - The recommended ODE library to link against
+#   ODE::ODE_single - If available, this links against single-precision ODE
+#   ODE::ODE_double - If available, this links against double-precision ODE
+#
+
+# Find the libode include files
+find_path(ODE_INCLUDE_DIR "ode/ode.h")
+
+# Find the libode library built for release
+find_library(ODE_RELEASE_LIBRARY
+  NAMES "ode" "libode")
+
+# Find the libode library built for debug
+find_library(ODE_DEBUG_LIBRARY
+  NAMES "oded" "liboded")
+
+# Find the single-precision library built for release
+find_library(ODE_SINGLE_RELEASE_LIBRARY
+  NAMES "ode_single" "libode_single")
+
+# Find the single-precision library built for debug
+find_library(ODE_SINGLE_DEBUG_LIBRARY
+  NAMES "ode_singled" "libode_singled")
+
+# Find the double-precision library built for release
+find_library(ODE_DOUBLE_RELEASE_LIBRARY
+  NAMES "ode_double" "libode_double")
+
+# Find the double-precision library built for debug
+find_library(ODE_DOUBLE_DEBUG_LIBRARY
+  NAMES "ode_doubled" "libode_doubled")
+
+# Find libccd, which ODE sometimes links against, so we want to let the linker
+# know about it if it's present.
+find_library(ODE_LIBCCD_LIBRARY
+  NAMES "ccd" "libccd")
+
+mark_as_advanced(ODE_INCLUDE_DIR)
+mark_as_advanced(ODE_RELEASE_LIBRARY)
+mark_as_advanced(ODE_DEBUG_LIBRARY)
+mark_as_advanced(ODE_SINGLE_RELEASE_LIBRARY)
+mark_as_advanced(ODE_SINGLE_DEBUG_LIBRARY)
+mark_as_advanced(ODE_DOUBLE_RELEASE_LIBRARY)
+mark_as_advanced(ODE_DOUBLE_DEBUG_LIBRARY)
+mark_as_advanced(ODE_LIBCCD_LIBRARY)
+
+# Define targets for both precisions (and unspecified)
+foreach(_precision _single _double "")
+  string(TOUPPER "${_precision}" _PRECISION)
+
+  if(EXISTS "${ODE${_PRECISION}_RELEASE_LIBRARY}" OR
+      EXISTS "${ODE${_PRECISION}_DEBUG_LIBRARY}")
+    if(NOT TARGET ODE::ODE${_precision})
+      add_library(ODE::ODE${_precision} UNKNOWN IMPORTED GLOBAL)
+
+      set_target_properties(ODE::ODE${_precision} PROPERTIES
+        INTERFACE_INCLUDE_DIRECTORIES "${ODE_INCLUDE_DIR}")
+
+      if(ODE_LIBCCD_LIBRARY)
+        set_target_properties(ODE::ODE${_precision} PROPERTIES
+          INTERFACE_LINK_LIBRARIES "${ODE_LIBCCD_LIBRARY}")
+      endif()
+
+      if(EXISTS "${ODE${_PRECISION}_RELEASE_LIBRARY}")
+        set_property(TARGET ODE::ODE${_precision} APPEND PROPERTY
+          IMPORTED_CONFIGURATIONS RELEASE)
+        set_target_properties(ODE::ODE${_precision} PROPERTIES
+          IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "C"
+          IMPORTED_LOCATION_RELEASE "${ODE${_PRECISION}_RELEASE_LIBRARY}")
+      endif()
+
+      if(EXISTS "${ODE${_PRECISION}_DEBUG_LIBRARY}")
+        set_property(TARGET ODE::ODE${_precision} APPEND PROPERTY
+          IMPORTED_CONFIGURATIONS DEBUG)
+        set_target_properties(ODE::ODE${_precision} PROPERTIES
+          IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "C"
+          IMPORTED_LOCATION_DEBUG "${ODE${_PRECISION}_DEBUG_LIBRARY}")
+      endif()
+
+      # If this has a precision, we should be sure to define
+      # dIDESINGLE/dIDEDOUBLE to keep it consistent
+      if(_precision)
+        string(REPLACE "_" "dIDE" _precision_symbol "${_PRECISION}")
+
+        set_target_properties(ODE::ODE${_precision} PROPERTIES
+          INTERFACE_COMPILE_DEFINITIONS "${_precision_symbol}")
+
+        unset(_precision_symbol)
+      endif()
+
+    endif()
+  endif()
+endforeach(_precision)
+unset(_precision)
+unset(_PRECISION)
+
+# OKAY.  If everything went well, we have ODE::ODE_single and/or
+# ODE::ODE_double.  We might even have an ODE::ODE, but if not, we need to
+# alias one of the other two to it.
+if(NOT TARGET ODE::ODE)
+  if(TARGET ODE::ODE_single)
+    set(_copy_from "ODE::ODE_single")
+  elseif(TARGET ODE::ODE_double)
+    set(_copy_from "ODE::ODE_double")
+  endif()
+
+  if(DEFINED _copy_from)
+    add_library(ODE::ODE UNKNOWN IMPORTED GLOBAL)
+
+    foreach(_prop
+        INTERFACE_INCLUDE_DIRECTORIES
+        INTERFACE_COMPILE_DEFINITIONS
+        INTERFACE_LINK_LIBRARIES
+        IMPORTED_CONFIGURATIONS
+        IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE
+        IMPORTED_LOCATION_RELEASE
+        IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG
+        IMPORTED_LOCATION_DEBUG)
+
+      get_target_property(_value "${_copy_from}" "${_prop}")
+      if(_value)
+        set_target_properties(ODE::ODE PROPERTIES "${_prop}" "${_value}")
+      endif()
+      unset(_value)
+    endforeach(_prop)
+    unset(_prop)
+  endif()
+
+  unset(_copy_from)
+endif()
+
+if(TARGET ODE::ODE)
+  set(_HAS_ODE_LIBRARY ON)
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(ODE DEFAULT_MSG ODE_INCLUDE_DIR _HAS_ODE_LIBRARY)
+
+unset(_HAS_ODE_LIBRARY)

+ 20 - 0
cmake/modules/FindOgg.cmake

@@ -0,0 +1,20 @@
+# Filename: FindOgg.cmake
+# Authors: CFSworks (13 Jan, 2019)
+#
+# Usage:
+#   find_package(Ogg [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   OGG_FOUND       - system has Ogg
+#   OGG_INCLUDE_DIR - the include directory containing ogg/
+#   OGG_LIBRARY     - the path to the ogg library
+#
+
+find_path(OGG_INCLUDE_DIR NAMES "ogg/ogg.h")
+
+find_library(OGG_LIBRARY NAMES "ogg" "libogg_static")
+
+mark_as_advanced(OGG_INCLUDE_DIR OGG_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Ogg DEFAULT_MSG OGG_INCLUDE_DIR OGG_LIBRARY)

+ 92 - 0
cmake/modules/FindOpenCV.cmake

@@ -0,0 +1,92 @@
+# Filename: FindOpenCV.cmake
+# Authors: CFSworks (3 Nov, 2018)
+#
+# Usage:
+#   find_package(OpenCV [REQUIRED] [QUIET])
+#
+# This supports the following components:
+#   calib3d
+#   contrib
+#   core
+#   features2d
+#   flann
+#   gpu
+#   highgui
+#   imgproc
+#   legacy
+#   ml
+#   nonfree
+#   objdetect
+#   photo
+#   stitching
+#   superres
+#   video
+#   videoio
+#   videostab
+#
+# Once done this will define:
+#   OPENCV_FOUND        - system has OpenCV
+#   OpenCV_INCLUDE_DIRS - the include dir(s) containing OpenCV header files
+#   OpenCV_comp_LIBRARY - the path to the OpenCV library for the particular
+#                         component
+#   OpenCV_LIBS         - the paths to the OpenCV libraries for the requested
+#                         component(s)
+#   OpenCV_VERSION_MAJOR- a "best guess" of the major version (X.x)
+#   OpenCV_VERSION_MINOR- a "best guess" of the minor version (x.X)
+#
+
+set(OpenCV_INCLUDE_DIRS)
+
+find_path(OpenCV_V1_INCLUDE_DIR
+  NAMES "cv.h"
+  PATH_SUFFIXES "opencv")
+mark_as_advanced(OpenCV_V1_INCLUDE_DIR)
+if(OpenCV_V1_INCLUDE_DIR)
+  list(APPEND OpenCV_INCLUDE_DIRS "${OpenCV_V1_INCLUDE_DIR}")
+
+  # This is a wild guess:
+  set(OpenCV_VERSION_MAJOR 1)
+  set(OpenCV_VERSION_MINOR 0)
+endif()
+
+find_path(OpenCV_V2_INCLUDE_DIR "opencv2/core/version.hpp")
+mark_as_advanced(OpenCV_V2_INCLUDE_DIR)
+if(OpenCV_V2_INCLUDE_DIR)
+  list(APPEND OpenCV_INCLUDE_DIRS "${OpenCV_V2_INCLUDE_DIR}")
+
+  file(STRINGS "${OpenCV_V2_INCLUDE_DIR}/opencv2/core/version.hpp"
+    _version_major REGEX "#define CV_VERSION_EPOCH")
+  file(STRINGS "${OpenCV_V2_INCLUDE_DIR}/opencv2/core/version.hpp"
+    _version_minor REGEX "#define CV_VERSION_MAJOR")
+
+  string(REGEX REPLACE "[^0-9]" "" OpenCV_VERSION_MAJOR "${_version_major}")
+  string(REGEX REPLACE "[^0-9]" "" OpenCV_VERSION_MINOR "${_version_minor}")
+  unset(_version_major)
+  unset(_version_minor)
+endif()
+
+set(OpenCV_LIBS)
+foreach(_component calib3d contrib core features2d flann gpu highgui imgproc
+                  legacy ml nonfree objdetect photo stitching superres video
+                  videoio videostab)
+
+  list(FIND OpenCV_FIND_COMPONENTS "${_component}" _index)
+  if(_index GREATER -1 OR _component STREQUAL "core")
+    if(NOT OpenCV_${_component}_LIBRARY)
+      find_library(OpenCV_${_component}_LIBRARY
+        NAMES "opencv_${_component}")
+    endif()
+
+    if(OpenCV_${_component}_LIBRARY)
+      list(APPEND OpenCV_LIBS "${OpenCV_${_component}_LIBRARY}")
+      set(OpenCV_${_component}_FOUND ON)
+    endif()
+  endif()
+  unset(_index)
+endforeach(_component)
+unset(_component)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpenCV HANDLE_COMPONENTS
+  REQUIRED_VARS OpenCV_INCLUDE_DIRS OpenCV_LIBS
+  OpenCV_VERSION_MAJOR OpenCV_VERSION_MINOR)

+ 88 - 0
cmake/modules/FindOpenEXR.cmake

@@ -0,0 +1,88 @@
+# Filename: FindOpenEXR.cmake
+# Authors: CFSworks (5 Nov, 2018)
+#
+# Usage:
+#   find_package(OpenEXR [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   OPENEXR_FOUND       - system has OpenEXR
+#   OPENEXR_INCLUDE_DIR - the include directory containing OpenEXR header files
+#   OPENEXR_LIBRARIES   - the path to the OpenEXR libraries
+#
+
+find_path(OPENEXR_INCLUDE_DIR
+  "ImfVersion.h"
+  PATH_SUFFIXES "OpenEXR")
+
+mark_as_advanced(OPENEXR_INCLUDE_DIR)
+
+find_library(OPENEXR_imf_LIBRARY
+  NAMES "IlmImf")
+
+if(OPENEXR_imf_LIBRARY)
+  get_filename_component(_imf_dir "${OPENEXR_imf_LIBRARY}" DIRECTORY)
+  find_library(OPENEXR_imfutil_LIBRARY
+    NAMES "IlmImfUtil"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  find_library(OPENEXR_ilmthread_LIBRARY
+    NAMES "IlmThread"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  find_library(OPENEXR_iex_LIBRARY
+    NAMES "Iex"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  find_library(OPENEXR_iexmath_LIBRARY
+    NAMES "IexMath"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  find_library(OPENEXR_imath_LIBRARY
+    NAMES "Imath"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  find_library(OPENEXR_half_LIBRARY
+    NAMES "Half"
+    PATHS "${_imf_dir}"
+    NO_DEFAULT_PATH)
+
+  unset(_imf_dir)
+endif()
+
+mark_as_advanced(
+  OPENEXR_imf_LIBRARY
+  OPENEXR_imfutil_LIBRARY
+  OPENEXR_ilmthread_LIBRARY
+  OPENEXR_iex_LIBRARY
+  OPENEXR_iexmath_LIBRARY
+  OPENEXR_imath_LIBRARY
+  OPENEXR_half_LIBRARY
+)
+
+set(OPENEXR_LIBRARIES
+  ${OPENEXR_imf_LIBRARY}
+  ${OPENEXR_imfutil_LIBRARY}
+  ${OPENEXR_ilmthread_LIBRARY}
+  ${OPENEXR_iex_LIBRARY}
+  ${OPENEXR_iexmath_LIBRARY}
+  ${OPENEXR_imath_LIBRARY}
+  ${OPENEXR_half_LIBRARY}
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpenEXR DEFAULT_MSG
+  OPENEXR_INCLUDE_DIR OPENEXR_LIBRARIES
+
+  OPENEXR_imf_LIBRARY
+  OPENEXR_imfutil_LIBRARY
+  OPENEXR_ilmthread_LIBRARY
+  OPENEXR_iex_LIBRARY
+  OPENEXR_iexmath_LIBRARY
+  OPENEXR_imath_LIBRARY
+  OPENEXR_half_LIBRARY
+)

+ 21 - 0
cmake/modules/FindOpenGLES1.cmake

@@ -0,0 +1,21 @@
+# Filename: FindOpenGLES1.cmake
+# Authors: CFSworks (21 Oct, 2018)
+#
+# Usage:
+#   find_package(OpenGLES1 [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   OPENGLES1_FOUND       - system has OpenGL ES 1.x
+#   OPENGLES1_INCLUDE_DIR - the include directory containing GLES/gl.h
+#   OPENGLES1_LIBRARY     - the library to link against for OpenGL ES 1.x
+#
+
+find_path(OPENGLES1_INCLUDE_DIR "GLES/gl.h")
+
+find_library(OPENGLES1_LIBRARY
+  NAMES "GLESv1" "GLESv1_CM" "GLES_CM")
+
+mark_as_advanced(OPENGLES1_INCLUDE_DIR OPENGLES1_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpenGLES1 DEFAULT_MSG OPENGLES1_INCLUDE_DIR OPENGLES1_LIBRARY)

+ 21 - 0
cmake/modules/FindOpenGLES2.cmake

@@ -0,0 +1,21 @@
+# Filename: FindOpenGLES2.cmake
+# Authors: CFSworks (21 Oct, 2018)
+#
+# Usage:
+#   find_package(OpenGLES2 [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   OPENGLES2_FOUND        - system has OpenGL ES 2.x
+#   OPENGLES2_INCLUDE_DIR  - the include directory containing GLES2/gl2.h
+#   OPENGLES2_LIBRARY      - the library to link against for OpenGL ES 2.x
+#
+
+find_path(OPENGLES2_INCLUDE_DIR "GLES2/gl2.h")
+
+find_library(OPENGLES2_LIBRARY
+  NAMES "GLESv2")
+
+mark_as_advanced(OPENGLES2_INCLUDE_DIR OPENGLES2_LIBRARY)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpenGLES2 DEFAULT_MSG OPENGLES2_INCLUDE_DIR OPENGLES2_LIBRARY)

+ 34 - 0
cmake/modules/FindOpusFile.cmake

@@ -0,0 +1,34 @@
+# Filename: FindOpusFile.cmake
+# Authors: CFSworks (13 Jan, 2019)
+#
+# Usage:
+#   find_package(OpusFile [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   OPUSFILE_FOUND        - system has Ogg and opusfile
+#   OPUSFILE_INCLUDE_DIRS - the include directory/ies containing opus/ and ogg/
+#   OPUSFILE_LIBRARIES    - the paths to the opus and opusfile libraries
+#
+
+# Find Ogg
+find_package(Ogg QUIET)
+
+# Find Opus
+find_path(OPUS_INCLUDE_DIR NAMES "opus/opusfile.h")
+
+find_library(OPUS_opus_LIBRARY NAMES "opus")
+find_library(OPUS_opusfile_LIBRARY NAMES "opusfile")
+
+mark_as_advanced(OPUS_INCLUDE_DIR OPUS_opus_LIBRARY OPUS_opusfile_LIBRARY)
+
+# Define output variables
+set(OPUSFILE_INCLUDE_DIRS ${OPUS_INCLUDE_DIR} "${OPUS_INCLUDE_DIR}/opus")
+if(NOT OGG_INCLUDE_DIR STREQUAL OPUS_INCLUDE_DIR)
+  list(APPEND OPUSFILE_INCLUDE_DIRS ${OGG_INCLUDE_DIR})
+endif()
+set(OPUSFILE_LIBRARIES ${OPUS_opusfile_LIBRARY} ${OPUS_opus_LIBRARY} ${OGG_LIBRARY})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(OpusFile DEFAULT_MSG
+  Ogg_FOUND
+  OPUS_INCLUDE_DIR OPUS_opus_LIBRARY OPUS_opusfile_LIBRARY)

+ 50 - 0
cmake/modules/FindSWResample.cmake

@@ -0,0 +1,50 @@
+# Filename: FindSWResample.cmake
+# Author: CFSworks (10 Apr, 2014)
+#
+# Usage:
+#   find_package(SWResample [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   SWRESAMPLE_FOUND       - system has ffmpeg's libswresample
+#   SWRESAMPLE_INCLUDE_DIR - the libswresample include directory
+#   SWRESAMPLE_LIBRARY     - the path to the library binary
+#
+
+# Find the libswresample include files
+find_path(SWRESAMPLE_INCLUDE_DIR
+  NAMES "libswresample/swresample.h"
+  PATHS "/usr/include"
+        "/usr/local/include"
+        "/sw/include"
+        "/opt/include"
+        "/opt/local/include"
+        "/opt/csw/include"
+  PATH_SUFFIXES "libav" "ffmpeg"
+)
+
+# Find the libswresample library
+find_library(SWRESAMPLE_LIBRARY
+  NAMES "swresample"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+mark_as_advanced(SWRESAMPLE_INCLUDE_DIR)
+mark_as_advanced(SWRESAMPLE_LIBRARY)
+
+# Translate library into library directory
+if(SWRESAMPLE_LIBRARY)
+  unset(SWRESAMPLE_LIBRARY_DIR CACHE)
+  get_filename_component(SWRESAMPLE_LIBRARY_DIR "${SWRESAMPLE_LIBRARY}" PATH)
+  set(SWRESAMPLE_LIBRARY_DIR "${SWRESAMPLE_LIBRARY_DIR}" CACHE PATH "The path to libffmpeg's library directory.") # Library path
+endif()
+
+mark_as_advanced(SWRESAMPLE_LIBRARY_DIR)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(SWResample DEFAULT_MSG SWRESAMPLE_LIBRARY SWRESAMPLE_INCLUDE_DIR SWRESAMPLE_LIBRARY_DIR)

+ 50 - 0
cmake/modules/FindSWScale.cmake

@@ -0,0 +1,50 @@
+# Filename: FindSWScale.cmake
+# Author: CFSworks (10 Apr, 2014)
+#
+# Usage:
+#   find_package(SWScale [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   SWSCALE_FOUND       - system has ffmpeg's libswscale
+#   SWSCALE_INCLUDE_DIR - the libswscale include directory
+#   SWSCALE_LIBRARY     - the path to the library binary
+#
+
+# Find the libswscale include files
+find_path(SWSCALE_INCLUDE_DIR
+  NAMES "libswscale/swscale.h"
+  PATHS "/usr/include"
+        "/usr/local/include"
+        "/sw/include"
+        "/opt/include"
+        "/opt/local/include"
+        "/opt/csw/include"
+  PATH_SUFFIXES "libav" "ffmpeg"
+)
+
+# Find the libswscale library
+find_library(SWSCALE_LIBRARY
+  NAMES "swscale"
+  PATHS "/usr"
+        "/usr/local"
+        "/usr/freeware"
+        "/sw"
+        "/opt"
+        "/opt/csw"
+  PATH_SUFFIXES "lib" "lib32" "lib64"
+)
+
+mark_as_advanced(SWSCALE_INCLUDE_DIR)
+mark_as_advanced(SWSCALE_LIBRARY)
+
+# Translate library into library directory
+if(SWSCALE_LIBRARY)
+  unset(SWSCALE_LIBRARY_DIR CACHE)
+  get_filename_component(SWSCALE_LIBRARY_DIR "${SWSCALE_LIBRARY}" PATH)
+  set(SWSCALE_LIBRARY_DIR "${SWSCALE_LIBRARY_DIR}" CACHE PATH "The path to libffmpeg's library directory.") # Library path
+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)

+ 40 - 0
cmake/modules/FindVRPN.cmake

@@ -0,0 +1,40 @@
+# Filename: FindVRPN.cmake
+# Authors: CFSworks (2 Nov, 2018)
+#
+# Usage:
+#   find_package(VRPN [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   VRPN_FOUND       - system has VRPN
+#   VRPN_INCLUDE_DIR - the include directory containing VRPN header files
+#   VRPN_LIBRARIES   - the path to the VRPN client libraries
+#
+
+find_path(VRPN_INCLUDE_DIR "vrpn_Connection.h")
+
+find_library(VRPN_vrpn_LIBRARY
+  NAMES "vrpn")
+
+mark_as_advanced(VRPN_INCLUDE_DIR VRPN_vrpn_LIBRARY)
+
+if(VRPN_vrpn_LIBRARY)
+  get_filename_component(_vrpn_dir "${VRPN_vrpn_LIBRARY}" DIRECTORY)
+  find_library(VRPN_quat_LIBRARY
+    NAMES "quat"
+    PATHS "${_vrpn_dir}"
+    NO_DEFAULT_PATH)
+
+  unset(_vrpn_dir)
+  mark_as_advanced(VRPN_quat_LIBRARY)
+endif()
+
+set(VRPN_LIBRARIES)
+if(VRPN_vrpn_LIBRARY)
+  list(APPEND VRPN_LIBRARIES "${VRPN_vrpn_LIBRARY}")
+endif()
+if(VRPN_quat_LIBRARY)
+  list(APPEND VRPN_LIBRARIES "${VRPN_quat_LIBRARY}")
+endif()
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(VRPN DEFAULT_MSG VRPN_INCLUDE_DIR VRPN_LIBRARIES)

+ 34 - 0
cmake/modules/FindVorbisFile.cmake

@@ -0,0 +1,34 @@
+# Filename: FindVorbisFile.cmake
+# Authors: CFSworks (13 Jan, 2019)
+#
+# Usage:
+#   find_package(VorbisFile [REQUIRED] [QUIET])
+#
+# Once done this will define:
+#   VORBISFILE_FOUND        - system has Ogg and vorbisfile
+#   VORBISFILE_INCLUDE_DIRS - the include directory/ies containing vorbis/ and ogg/
+#   VORBISFILE_LIBRARIES    - the paths to the vorbis and vorbisfile libraries
+#
+
+# Find Ogg
+find_package(Ogg QUIET)
+
+# Find Vorbis
+find_path(VORBIS_INCLUDE_DIR NAMES "vorbis/vorbisfile.h")
+
+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)
+
+# Define output variables
+set(VORBISFILE_INCLUDE_DIRS ${VORBIS_INCLUDE_DIR})
+if(NOT OGG_INCLUDE_DIR STREQUAL VORBIS_INCLUDE_DIR)
+  list(APPEND VORBISFILE_INCLUDE_DIRS ${OGG_INCLUDE_DIR})
+endif()
+set(VORBISFILE_LIBRARIES ${VORBIS_vorbisfile_LIBRARY} ${VORBIS_vorbis_LIBRARY} ${OGG_LIBRARY})
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(VorbisFile DEFAULT_MSG
+  Ogg_FOUND
+  VORBIS_INCLUDE_DIR VORBIS_vorbis_LIBRARY VORBIS_vorbisfile_LIBRARY)

+ 7 - 0
cmake/modules/README.md

@@ -0,0 +1,7 @@
+Directory Info
+--------------
+**Directory:** /cmake/modules
+**License:** Unlicense  
+**Description:** This directory is used for standard CMake modules which are safe
+to use outside of a Panda3D project.  This should be the normal location for
+most FindXYZZY packages that are written for Panda3D.

+ 70 - 0
cmake/scripts/ConcatenateToCXX.cmake

@@ -0,0 +1,70 @@
+# Filename: ConcatenateToCXX.cmake
+#
+# Description: When run, creates a single C++ file which includes a const char[]
+#   containing the bytes from one or more files.
+#
+#   There is a {SYMBOL_NAME}_size symbol defined as well, storing the total
+#   number of bytes in the concatenated input files.
+#
+#   A single null terminator byte is added for the benefit of programs that
+#   simply treat the data array as a string.
+#
+# Usage:
+#   This script is invoked via add_custom_target, like this:
+#   cmake -D OUTPUT_FILE="out.cxx" -D SYMBOL_NAME=data -D INPUT_FILES="a.bin b.bin" -P ConcatenateToCXX.cmake
+#
+
+if(NOT CMAKE_SCRIPT_MODE_FILE)
+  message(FATAL_ERROR "ConcatenateToCXX.cmake should not be included but run in script mode.")
+  return()
+endif()
+
+if(NOT DEFINED OUTPUT_FILE)
+  message(FATAL_ERROR "OUTPUT_FILE should be defined when running ConcatenateToCXX.cmake!")
+  return()
+endif()
+
+if(NOT DEFINED SYMBOL_NAME)
+  set(SYMBOL_NAME "data")
+endif()
+
+file(WRITE "${OUTPUT_FILE}" "/* Generated by CMake.  DO NOT EDIT. */
+
+extern const char ${SYMBOL_NAME}[];
+extern const int ${SYMBOL_NAME}_size;
+
+const char ${SYMBOL_NAME}[] = {\n")
+
+set(byte_count 0)
+separate_arguments(INPUT_FILES)
+foreach(infile ${INPUT_FILES})
+  file(APPEND "${OUTPUT_FILE}" "  /* ${infile} */\n")
+
+  set(offset 0)
+  while(1)
+    # Read up to 1024 bytes from the input file
+    file(READ "${infile}" data LIMIT 1024 OFFSET ${offset} HEX)
+    math(EXPR offset "${offset} + 1024")
+
+    # If 'data' is empty, we're done
+    if(NOT data)
+      break()
+    endif()
+
+    # Count the bytes we're adding
+    string(LENGTH "${data}" strlen)
+    math(EXPR byte_count "${byte_count} + (${strlen} / 2)")
+
+    # Format runs of up to 32 hex chars by indenting and giving a newline
+    string(REGEX REPLACE
+      "(...?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?)" " \\1\n"
+      data "${data}")
+    # Format each byte (2 hex chars) in each line with 0x prefix and comma suffix
+    string(REGEX REPLACE "([0-9a-fA-F][0-9a-fA-F])" " 0x\\1," data "${data}")
+    file(APPEND "${OUTPUT_FILE}" "${data}")
+  endwhile()
+endforeach(infile)
+
+file(APPEND "${OUTPUT_FILE}" "  0\n};
+
+extern const int ${SYMBOL_NAME}_size = ${byte_count};\n")

+ 27 - 0
cmake/scripts/CopyPattern.cmake

@@ -0,0 +1,27 @@
+# Filename: CopyPattern.cmake
+#
+# Description: This is a standalone version of CMake's file(COPY) command so we
+#              can use all of its features during build-time instead of
+#              config-time.
+#
+# Usage:
+#   This script is invoked via add_custom_target, like this:
+#   cmake -D SOURCE=[source directory]
+#         -D DESTINATION=[destination directory]
+#         -D FILES_MATCHING="[globbing patterns passed to file(COPY)]"
+#         -P CopyPattern.cmake
+if(NOT DEFINED SOURCE OR NOT DEFINED DESTINATION)
+    message(SEND_ERROR "CopyPattern.cmake requires SOURCE and DESTINATION to be
+defined.")
+endif()
+
+if(DEFINED FILES_MATCHING)
+  separate_arguments(FILES_MATCHING UNIX_COMMAND ${FILES_MATCHING})
+
+  file(COPY "${SOURCE}"
+       DESTINATION "${DESTINATION}"
+       FILES_MATCHING ${FILES_MATCHING})
+else()
+  file(COPY "${SOURCE}"
+       DESTINATION "${DESTINATION}")
+endif()

+ 78 - 0
cmake/scripts/CopyPython.cmake

@@ -0,0 +1,78 @@
+# Filename: CopyPython.cmake
+#
+# Description: When run, copies Python files from a source directory to a
+#   build directory located somewhere in the project's binary path. If
+#   PYTHON_EXECUTABLES is provided, it will also invoke compileall in the
+#   destination dir(s) to precache .pyc/.pyo files.
+#
+# Usage:
+#   This script is invoked via add_custom_target, like this:
+#   cmake -D OUTPUT_DIR="/out/dir/Release"
+#         -D SOURCE_DIR="/panda3d/direct/src/"
+#         -D PYTHON_EXECUTABLES="/usr/bin/python3.6"
+#         -P CopyPython.cmake
+#
+
+if(NOT CMAKE_SCRIPT_MODE_FILE)
+  message(FATAL_ERROR "CopyPython.cmake should not be included but run in script mode.")
+  return()
+endif()
+
+if(NOT DEFINED OUTPUT_DIR)
+  message(FATAL_ERROR "OUTPUT_DIR should be defined when running CopyPython.cmake!")
+  return()
+endif()
+
+# Ensure OUTPUT_DIR exists
+file(MAKE_DIRECTORY ${OUTPUT_DIR})
+
+# If there's a SOURCE_DIR, glob for .py files and copy
+# (this is done by hand to avoid the CMake bug where it creates empty dirs)
+if(DEFINED SOURCE_DIR)
+  file(GLOB_RECURSE py_files RELATIVE "${SOURCE_DIR}" "${SOURCE_DIR}/*.py")
+  foreach(py_file ${py_files})
+    get_filename_component(py_file_parent "${py_file}" DIRECTORY)
+    file(TIMESTAMP "${SOURCE_DIR}/${py_file}" src_stamp)
+    file(TIMESTAMP "${OUTPUT_DIR}/${py_file}" dst_stamp)
+
+    # The file is only copied if:
+    # - 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((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)
+    endif()
+
+  endforeach(py_file)
+
+else()
+  # No SOURCE_DIR; assume we're outdated since our caller might be populating
+  # the OUTPUT_DIR themselves
+  set(changed YES)
+
+endif()
+
+# Make sure Python recognizes OUTPUT_DIR as a package
+if(NOT EXISTS "${OUTPUT_DIR}/__init__.py")
+  file(WRITE "${OUTPUT_DIR}/__init__.py" "")
+  set(changed YES)
+endif()
+
+# Generate .pyc files for each Python version, if our caller wants
+if(changed AND DEFINED PYTHON_EXECUTABLES)
+  foreach(interp ${PYTHON_EXECUTABLES})
+    execute_process(
+      COMMAND "${interp}" -m compileall .
+      OUTPUT_QUIET
+      WORKING_DIRECTORY "${OUTPUT_DIR}")
+
+    execute_process(
+      COMMAND "${interp}" -O -m compileall .
+      OUTPUT_QUIET
+      WORKING_DIRECTORY "${OUTPUT_DIR}")
+
+  endforeach(interp)
+endif()

+ 28 - 0
cmake/scripts/MakeComposite.cmake

@@ -0,0 +1,28 @@
+# Filename: MakeComposite.cmake
+#
+# Description: When run, creates a single C++ file which includes multiple
+#   other C++ files, to help facilitate unity builds.
+#   Unity builds provide significantly increased compile speed.
+#
+# Usage:
+#   This script is invoked via add_custom_target, like this:
+#   cmake -P MakeComposite.cmake -D COMPOSITE_FILE="x_composite1.cxx" -D COMPOSITE_SOURCES="a.cxx b.cxx"
+#
+
+if(CMAKE_SCRIPT_MODE_FILE)
+  if(NOT DEFINED COMPOSITE_FILE)
+    message(FATAL_ERROR "COMPOSITE_FILE should be defined when running MakeComposite.cmake!")
+    return()
+  endif()
+
+  file(WRITE "${COMPOSITE_FILE}" "/* Generated by CMake.  DO NOT EDIT. */\n")
+
+  separate_arguments(COMPOSITE_SOURCES)
+  foreach(source ${COMPOSITE_SOURCES})
+    file(APPEND "${COMPOSITE_FILE}" "#include \"${source}\"\n")
+  endforeach()
+
+else()
+  message(SEND_ERROR "MakeComposite.cmake should not be included but run in script mode.")
+
+endif()

+ 10 - 0
cmake/scripts/README.md

@@ -0,0 +1,10 @@
+Directory Info
+--------------
+**Directory:** /cmake/scripts  
+**License:** Unlicense  
+**Description:** This directory is used for cmake files which are not meant to
+be included using CMake's normal include() directive.  Typically, files in this
+directory will be invoked as a custom command/target in the form of:
+```
+ cmake -P <CustomScriptName.cmake> [... other options ...]
+```

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

+ 9 - 0
cmake/templates/metalib_init.cxx.in

@@ -0,0 +1,9 @@
+#include "dtoolbase.h"
+
+@component_init_headers@
+
+EXPORT_CLASS void
+@init_func@() {
+@component_init_funcs@
+}
+@export_definitions@

+ 9 - 0
cmake/templates/metalib_init.h.in

@@ -0,0 +1,9 @@
+#ifndef _METALIB_INIT_@target_name@
+#define _METALIB_INIT_@target_name@
+
+#include "dtoolbase.h"
+
+IMPORT_CLASS void @init_func@();
+@export_declarations@
+
+#endif

+ 23 - 0
cmake/templates/win32_python/__init__.py

@@ -0,0 +1,23 @@
+def _fixup_dlls():
+    try:
+        path = __path__[0]
+    except (NameError, IndexError):
+        return # Not a package, or not on filesystem
+
+    import os
+
+    relpath = os.path.relpath(path, __path__[-1])
+    dll_path = os.path.abspath(os.path.join(__path__[-1], '../bin', relpath))
+    if not os.path.isdir(dll_path):
+        return
+
+    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

+ 18 - 0
contrib/CMakeLists.txt

@@ -0,0 +1,18 @@
+if(NOT BUILD_PANDA)
+  message(FATAL_ERROR "Cannot build contrib without panda!  Please enable the BUILD_PANDA option.")
+endif()
+
+# Include source directories
+add_subdirectory(src/ai)
+add_subdirectory(src/contribbase)
+add_subdirectory(src/rplight)
+
+if(HAVE_PYTHON)
+  add_python_module(panda3d.ai p3ai
+    IMPORT panda3d.core COMPONENT ContribPython)
+
+  add_python_module(panda3d._rplight p3rplight
+    IMPORT panda3d.core COMPONENT ContribPython)
+endif()
+
+export_targets(Contrib COMPONENT ContribDevel)

+ 54 - 0
contrib/src/ai/CMakeLists.txt

@@ -0,0 +1,54 @@
+set(P3AI_HEADERS
+  aiBehaviors.h
+  aiCharacter.h
+  aiGlobals.h
+  aiNode.h
+  aiPathFinder.h
+  aiWorld.h
+  arrival.h
+  config_ai.h
+  evade.h
+  flee.h
+  flock.h
+  meshNode.h
+  obstacleAvoidance.h
+  pathFind.h
+  pathFollow.h
+  pursue.h
+  seek.h
+  wander.h
+)
+
+set(P3AI_SOURCES
+  aiBehaviors.cxx
+  aiCharacter.cxx
+  aiNode.cxx
+  aiPathFinder.cxx
+  aiWorld.cxx
+  arrival.cxx
+  config_ai.cxx
+  evade.cxx
+  flee.cxx
+  flock.cxx
+  meshNode.cxx
+  obstacleAvoidance.cxx
+  pathFind.cxx
+  pathFollow.cxx
+  pursue.cxx
+  seek.cxx
+  wander.cxx
+)
+
+composite_sources(p3ai P3AI_SOURCES)
+add_library(p3ai ${P3AI_HEADERS} ${P3AI_SOURCES})
+set_target_properties(p3ai PROPERTIES DEFINE_SYMBOL BUILDING_PANDAAI)
+target_link_libraries(p3ai p3contribbase)
+target_interrogate(p3ai ALL)
+
+install(TARGETS p3ai
+  EXPORT Contrib COMPONENT Contrib
+  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 ${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;
 

+ 14 - 0
contrib/src/contribbase/CMakeLists.txt

@@ -0,0 +1,14 @@
+set(P3CONTRIBBASE_SOURCES
+  contribbase.cxx
+)
+
+set(P3CONTRIBBASE_HEADERS
+  contribbase.h contribsymbols.h
+)
+
+# Yes, INTERFACE: don't build it, there's no code!
+add_library(p3contribbase INTERFACE)
+target_link_libraries(p3contribbase INTERFACE panda)
+
+install(TARGETS p3contribbase EXPORT Contrib COMPONENT Contrib)
+install(FILES ${P3CONTRIBBASE_HEADERS} COMPONENT ContribDevel DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 52 - 0
contrib/src/rplight/CMakeLists.txt

@@ -0,0 +1,52 @@
+set(P3RPLIGHT_HEADERS
+  config_rplight.h
+  gpuCommand.h gpuCommand.I
+  gpuCommandList.h
+  iesDataset.h
+  internalLightManager.h internalLightManager.I
+  pointerSlotStorage.h
+  pssmCameraRig.h pssmCameraRig.I
+  rpLight.h rpLight.I
+  rpPointLight.h rpPointLight.I
+  rpSpotLight.h rpSpotLight.I
+  shadowAtlas.h shadowAtlas.I
+  shadowManager.h shadowManager.I
+  shadowSource.h shadowSource.I
+  tagStateManager.h tagStateManager.I
+)
+
+set(P3RPLIGHT_SOURCES
+  config_rplight.cxx
+  gpuCommand.cxx
+  gpuCommandList.cxx
+  iesDataset.cxx
+  internalLightManager.cxx
+  pssmCameraRig.cxx
+  rpLight.cxx
+  rpPointLight.cxx
+  rpSpotLight.cxx
+  shadowAtlas.cxx
+  shadowManager.cxx
+  shadowSource.cxx
+  tagStateManager.cxx
+)
+
+composite_sources(p3rplight P3RPLIGHT_SOURCES)
+# STATIC, because it doesn't contain any EXPCL_ stuff
+add_library(p3rplight STATIC ${P3RPLIGHT_HEADERS} ${P3RPLIGHT_SOURCES})
+target_link_libraries(p3rplight p3contribbase)
+target_interrogate(p3rplight ALL)
+
+if(MODULE_TYPE STREQUAL "MODULE")
+  # Because it's STATIC (see above), we need to make it PIC so it can link into
+  # a Python module
+  set_target_properties(p3rplight PROPERTIES POSITION_INDEPENDENT_CODE ON)
+endif()
+
+install(TARGETS p3rplight
+  EXPORT Contrib COMPONENT Contrib
+  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 ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

+ 75 - 0
direct/CMakeLists.txt

@@ -0,0 +1,75 @@
+if(NOT BUILD_PANDA)
+  message(FATAL_ERROR "Cannot build direct without panda!  Please enable the BUILD_PANDA option.")
+endif()
+
+# Include source directories which have C++ components:
+add_subdirectory(src/dcparse)
+add_subdirectory(src/dcparser)
+add_subdirectory(src/deadrec)
+add_subdirectory(src/directbase)
+#add_subdirectory(src/directd)
+#add_subdirectory(src/directdServer)
+add_subdirectory(src/distributed)
+add_subdirectory(src/interval)
+add_subdirectory(src/motiontrail)
+add_subdirectory(src/showbase)
+
+set(P3DIRECT_COMPONENTS
+  p3dcparser p3deadrec
+  p3interval p3motiontrail p3showbase
+)
+if(HAVE_PYTHON)
+  list(APPEND P3DIRECT_COMPONENTS p3distributed)
+endif()
+
+set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "DirectDevel")
+add_metalib(p3direct INIT init_libdirect direct.h COMPONENTS ${P3DIRECT_COMPONENTS})
+unset(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME)
+set_property(TARGET p3direct PROPERTY LINKER_LANGUAGE "CXX")
+
+# Installation:
+install(TARGETS p3direct
+  EXPORT Direct COMPONENT Direct
+  DESTINATION ${CMAKE_INSTALL_LIBDIR}
+  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+  ARCHIVE COMPONENT DirectDevel)
+
+if(HAVE_PYTHON)
+  # Now for the Python side of everything
+
+  add_python_module(panda3d.direct
+    p3dcparser p3deadrec p3distributed p3interval
+    p3motiontrail p3showbase LINK p3direct IMPORT panda3d.core COMPONENT Direct)
+
+  # Copy Python source files into the build directory
+  install_python_package(direct SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/src" LIB COMPONENT Direct)
+
+  # This bit is to generate the 'pandac' compatibility shim. It's deprecated now,
+  # but in older versions of Panda3D, one would use
+  # from pandac.PandaModules import *
+  # instead of
+  # from panda3d.FOO import *
+  # Generate PandaModules:
+  file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/pandac/PandaModules.py"
+    "\"This module is deprecated.  Import from panda3d.core and other panda3d.* modules instead.\"
+
+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_name}\" in str(err)):
+        raise
+")
+  endforeach()
+
+  # Now install ourselves:
+  install_python_package(pandac SOURCE "${CMAKE_CURRENT_BINARY_DIR}/pandac" LIB COMPONENT Direct)
+endif()
+
+# "Direct" component contains both the Python stuff and the non-Python stuff,
+# because direct has a pretty solid dependency on Python.
+export_targets(Direct NAMESPACE "Panda3D::Direct::" COMPONENT DirectDevel)

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

+ 3 - 0
direct/src/dcparse/CMakeLists.txt

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

+ 87 - 0
direct/src/dcparser/CMakeLists.txt

@@ -0,0 +1,87 @@
+set(P3DCPARSER_HEADERS
+  dcAtomicField.h dcAtomicField.I
+  dcClass.h dcClass.I
+  dcDeclaration.h
+  dcField.h dcField.I
+  dcFile.h dcFile.I
+  dcKeyword.h dcKeywordList.h
+  dcLexer.lxx dcLexerDefs.h
+  dcMolecularField.h
+  dcParser.yxx dcParserDefs.h
+  dcSubatomicType.h
+  dcPackData.h dcPackData.I
+  dcPacker.h dcPacker.I
+  dcPackerCatalog.h dcPackerCatalog.I
+  dcPackerInterface.h dcPackerInterface.I
+  dcParameter.h
+  dcClassParameter.h
+  dcArrayParameter.h
+  dcSimpleParameter.h
+  dcSwitchParameter.h
+  dcNumericRange.h dcNumericRange.I
+  dcSwitch.h
+  dcTypedef.h
+  dcbase.h
+  dcindent.h
+  dcmsgtypes.h
+  hashGenerator.h
+  primeNumberGenerator.h
+)
+
+set(P3DCPARSER_SOURCES
+  dcAtomicField.cxx
+  dcClass.cxx
+  dcDeclaration.cxx
+  dcField.cxx
+  dcFile.cxx
+  dcKeyword.cxx
+  dcKeywordList.cxx
+  dcMolecularField.cxx
+  dcSubatomicType.cxx
+  dcPackData.cxx
+  dcPacker.cxx
+  dcPackerCatalog.cxx
+  dcPackerInterface.cxx
+  dcParameter.cxx
+  dcClassParameter.cxx
+  dcArrayParameter.cxx
+  dcSimpleParameter.cxx
+  dcSwitchParameter.cxx
+  dcSwitch.cxx
+  dcTypedef.cxx
+  dcindent.cxx
+  hashGenerator.cxx
+  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)
+
+# These cannot be interrogated, and are excluded from the composites.
+set(P3DCPARSER_PARSER_SOURCES
+    dcParser.cxx
+    dcLexer.cxx)
+
+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)
+target_interrogate(p3dcparser ${P3DCPARSER_HEADERS} ${P3DCPARSER_SOURCES}
+  EXTENSIONS ${P3DCPARSER_IGATEEXT})
+
+if(NOT BUILD_METALIBS)
+  install(TARGETS p3dcparser
+    EXPORT Direct COMPONENT Direct
+    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 ${CMAKE_INSTALL_INCLUDEDIR}/panda3d)

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

+ 60 - 45
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,18 @@ private:
   typedef pmap<int, DCField *> FieldsByIndex;
   FieldsByIndex _fields_by_index;
 
-#ifdef HAVE_PYTHON
-  PyObject *_class_def;
-  PyObject *_owner_class_def;
-#endif
-
   friend class DCField;
+
+#ifdef WITHIN_PANDA
+  // See pandaNode.h for an explanation of this trick
+  class PythonClassDefs : public ReferenceCount {
+  public:
+    virtual ~PythonClassDefs() {};
+  };
+  PT(PythonClassDefs) _python_class_defs;
+
+  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

+ 3 - 0
direct/src/dcparser/dcLexer.cxx.prebuilt

@@ -599,7 +599,10 @@ char *dcyytext;
 #include "dcParser.h"
 #include "dcFile.h"
 #include "dcindent.h"
+
+#ifdef WITHIN_PANDA
 #include "pstrtod.h"
+#endif
 
 
 static int yyinput(void);        // declared by flex.

+ 3 - 1
direct/src/dcparser/dcLexer.lxx

@@ -11,8 +11,10 @@
 #include "dcParser.h"
 #include "dcFile.h"
 #include "dcindent.h"
-#include "pstrtod.h"
 
+#ifdef WITHIN_PANDA
+#include "pstrtod.h"
+#endif
 
 static int yyinput(void);        // declared by flex.
 extern "C" int dcyywrap();

+ 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

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff