ya2 · news · projects · code · about

first commit
authorFlavio Calva <f.calva@gmail.com>
Sun, 14 Nov 2021 18:20:49 +0000 (19:20 +0100)
committerFlavio Calva <f.calva@gmail.com>
Sun, 14 Nov 2021 18:20:49 +0000 (19:20 +0100)
99 files changed:
.gitignore [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README [new file with mode: 0644]
lib/__init__.py [new file with mode: 0644]
lib/build/__init__.py [new file with mode: 0644]
lib/build/blend2gltf.py [new file with mode: 0644]
lib/build/build.py [new file with mode: 0644]
lib/build/commands.patch [new file with mode: 0644]
lib/build/docs.py [new file with mode: 0644]
lib/build/images.py [new file with mode: 0644]
lib/build/in_venv.sh [new file with mode: 0755]
lib/build/lang.py [new file with mode: 0644]
lib/build/models.py [new file with mode: 0644]
lib/build/mtprocesser.py [new file with mode: 0644]
lib/computer_proxy.py [new file with mode: 0644]
lib/decorator.py [new file with mode: 0644]
lib/dictfile.py [new file with mode: 0644]
lib/engine/__init__.py [new file with mode: 0644]
lib/engine/audio.py [new file with mode: 0644]
lib/engine/cbmux.py [new file with mode: 0644]
lib/engine/clock.py [new file with mode: 0644]
lib/engine/configuration.py [new file with mode: 0644]
lib/engine/engine.py [new file with mode: 0644]
lib/engine/enginefacade.py [new file with mode: 0644]
lib/engine/event.py [new file with mode: 0644]
lib/engine/font.py [new file with mode: 0644]
lib/engine/functional.py [new file with mode: 0644]
lib/engine/gfx.py [new file with mode: 0755]
lib/engine/gui/__init__.py [new file with mode: 0644]
lib/engine/gui/browser.py [new file with mode: 0644]
lib/engine/gui/circle.py [new file with mode: 0644]
lib/engine/gui/cursor.py [new file with mode: 0644]
lib/engine/gui/gui.py [new file with mode: 0755]
lib/engine/gui/imgbtn.py [new file with mode: 0644]
lib/engine/gui/mainpage.py [new file with mode: 0644]
lib/engine/gui/menu.py [new file with mode: 0644]
lib/engine/gui/page.py [new file with mode: 0755]
lib/engine/joystick.py [new file with mode: 0644]
lib/engine/lang.py [new file with mode: 0644]
lib/engine/log.py [new file with mode: 0755]
lib/engine/logic.py [new file with mode: 0755]
lib/engine/network/__init__.py [new file with mode: 0644]
lib/engine/network/binary.py [new file with mode: 0644]
lib/engine/network/client.py [new file with mode: 0644]
lib/engine/network/network.py [new file with mode: 0644]
lib/engine/network/server.py [new file with mode: 0644]
lib/engine/particle.py [new file with mode: 0755]
lib/engine/pause.py [new file with mode: 0644]
lib/engine/phys.py [new file with mode: 0644]
lib/engine/profiler.py [new file with mode: 0644]
lib/engine/shader.py [new file with mode: 0644]
lib/engine/vec.py [new file with mode: 0644]
lib/facade.py [new file with mode: 0644]
lib/game.py [new file with mode: 0644]
lib/gameobject.py [new file with mode: 0644]
lib/lib/__init__.py [new file with mode: 0644]
lib/lib/builder.py [new file with mode: 0644]
lib/lib/bullet/__init__.py [new file with mode: 0644]
lib/lib/bullet/bullet.py [new file with mode: 0644]
lib/lib/gui.py [new file with mode: 0644]
lib/lib/ivals.py [new file with mode: 0644]
lib/lib/p3d/__init__.py [new file with mode: 0644]
lib/lib/p3d/audio.py [new file with mode: 0644]
lib/lib/p3d/gfx.py [new file with mode: 0755]
lib/lib/p3d/gui.py [new file with mode: 0755]
lib/lib/p3d/ivals.py [new file with mode: 0644]
lib/lib/p3d/joystick.py [new file with mode: 0644]
lib/lib/p3d/p3d.py [new file with mode: 0755]
lib/lib/p3d/particle.py [new file with mode: 0755]
lib/lib/p3d/pause.py [new file with mode: 0644]
lib/lib/p3d/shader.py [new file with mode: 0644]
lib/lib/p3d/vec.py [new file with mode: 0755]
lib/lib/p3d/widget.py [new file with mode: 0755]
lib/observer.py [new file with mode: 0644]
lib/tools/apply_gloss.py [new file with mode: 0644]
lib/tools/build_metal_texture.py [new file with mode: 0644]
lib/tools/fix_mask_texture.py [new file with mode: 0644]
lib/tools/kill_yorg.py [new file with mode: 0644]
lib/tools/kill_yorg_server.py [new file with mode: 0644]
lib/tools/pdfsingle.py [new file with mode: 0755]
lib/tools/process_models.py [new file with mode: 0644]
lib/tools/set_diffuse.py [new file with mode: 0755]
licenses/bsd.txt [new file with mode: 0644]
licenses/cc_by-sa.txt [new file with mode: 0644]
licenses/ffmpeg/COPYING.LGPLv2.1.txt [new file with mode: 0644]
licenses/freetype/FTL.TXT [new file with mode: 0644]
licenses/libjpg/README.txt [new file with mode: 0644]
licenses/libtiff/libtiff.txt [new file with mode: 0644]
licenses/licenses.txt [new file with mode: 0644]
licenses/openal_soft/COPYING [new file with mode: 0644]
licenses/opencv/license.txt [new file with mode: 0644]
licenses/openssl/license.txt [new file with mode: 0644]
licenses/panda3d/license.txt [new file with mode: 0644]
main.py [new file with mode: 0644]
pmachines/pmachines.py [new file with mode: 0755]
prj.org [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..840f817
--- /dev/null
@@ -0,0 +1,9 @@
+TAGS
+*.bam
+*.txo
+*.dds
+*.mo
+*.pyc
+*.blend1
+/build/
+/dist/
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..93161e1
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,5 @@
+* first commit
+       * current rc *
+       * current *
+       * previous rc *
+       * previous *
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..78e4364
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+The source code (*.py, *.glsl and generally every text file) is licensed under
+3-clause BSD License (see the file licenses/bsd.txt for details).
+
+Several assets are from OpenGameArt.com, they are licensed under several forms
+of CC, look at licenses/licenses.txt for details.
+
+The remaining assets (art, audio files) are licensed under CC BY-SA.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..0c431ca
--- /dev/null
+++ b/README
@@ -0,0 +1,27 @@
+pmachines
+
+pmachines is an open source puzzle game based on Rube Goldberg devices
+developed by Ya2 using Panda3D (http://www.panda3d.org) for Windows, and Linux.
+More information can be found on http://www.ya2.it/pages/pmachines.html.
+
+It requires Python 3.x.
+
+To run it you should create assets:
+
+* python setup.py images lang models
+
+To create the builds, you can use the awesome Panda3D's deployment tools:
+
+* python setup.py bdist_apps
+
+Here's a short guide about installing and preparing your environment for pmachines.
+
+* clone the repository:
+  git clone http://www.ya2tech.it/git/pmachines.git
+* go into the directory: cd pmachines
+* create a python3 virtualenv: python3 -m venv venv
+* activate the virtualenv: . ./venv/bin/activate
+  (now you should see (venv) before your prompt)
+* install the prerequisites: pip install -r requirements.txt
+* build the required assets: python setup.py lang images models
+* launch the game: python main.py
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644 (file)
index 0000000..a65c163
--- /dev/null
@@ -0,0 +1,3 @@
+from pathlib import Path
+import sys
+sys.path.append(str(Path(__file__).parent.parent))
diff --git a/lib/build/__init__.py b/lib/build/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/build/blend2gltf.py b/lib/build/blend2gltf.py
new file mode 100644 (file)
index 0000000..b43f09f
--- /dev/null
@@ -0,0 +1,9 @@
+import bpy, sys, os
+
+filepath = os.path.abspath(sys.argv[-1])
+bpy.ops.export_scene.gltf(
+    filepath=filepath,
+    export_format='GLTF_SEPARATE',
+    export_extras=True,
+    export_tangents=True,
+    use_selection=True)
diff --git a/lib/build/build.py b/lib/build/build.py
new file mode 100644 (file)
index 0000000..6170b4f
--- /dev/null
@@ -0,0 +1,143 @@
+'''Tools for making the builds.'''
+from os import walk, chdir, getcwd
+from os.path import join, getsize, exists, dirname, getmtime, sep
+from subprocess import Popen, PIPE, run
+from time import strftime
+from pathlib import Path
+from hashlib import md5
+
+
+#TODO refactor: make class BuilderTools
+
+
+def exec_cmd(cmd):
+    '''Synchronously executes a command and returns its output.'''
+    #ret = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
+    #return ret[0].decode('utf-8').strip()
+    proc = run(cmd, shell=True, stdout=PIPE, stderr=PIPE, universal_newlines=True)
+    return proc.stdout.strip() + proc.stderr.strip()
+
+
+def _branch():
+    '''Returns the current branch.'''
+    git_branch = exec_cmd('git symbolic-ref HEAD').split('/')[-1].strip()
+    branches = ['master', 'rc', 'stable']
+    if git_branch in branches:
+        return git_branch
+    root = str(Path(dirname(dirname(__file__))).parent) + '/'
+    if 'itch' in __file__.split(sep):
+        root = str(Path(dirname(__file__))) + '/'
+    if __file__ == '/app/bin/yocto':  # flatpak
+        root = '/app/bin/'
+    for branch in branches:
+        try:
+            with open(root + 'assets/bld_version.txt') as fver:
+                ver = fver.read()
+            if branch in ver:
+                return branch
+        except FileNotFoundError:
+            print('file not found: %s' % root + 'assets/bld_version.txt')
+
+
+def _version():
+    '''Computes the version of the current build.'''
+    day = strftime('%y%m%d')
+    root = str(Path(dirname(dirname(__file__))).parent) + '/'
+    if __file__ == '/app/bin/yocto':  # flatpak
+        root = '/app/bin/'
+    if _branch() == 'stable':
+        pref, _ver = '', ''
+        if exists(root + 'assets/version.txt'):
+            with open(root + 'assets/version.txt') as fver:
+                pref = fver.read().strip() + '-'  # + _branch() + '-'
+                _ver = fver.read().strip()
+        ret_ver = _ver or ('0.' + day)
+    else:
+        try:
+            pref = {'master': 'a', 'rc': 'rc', '': 'runtime'}[_branch()]
+        except KeyError:
+            pref = 'notfound'
+        ret_ver = '0%s%s' % (pref, day)
+        pref = ret_ver
+    bld_ver = pref + '-' + exec_cmd('git rev-parse HEAD')[:7]
+    try:
+        with open(root + 'assets/bld_version.txt', 'w') as fver:
+            fver.write(bld_ver)
+    except OSError:
+        print("we can't write inside flatpaks, but we don't need it")
+    return ret_ver
+
+
+def files(_extensions, excl_dirs=None, excl_ends_with=None, root_path='.'):
+    '''Retrieves filenames in root_path with _extensions, with filters.'''
+    return [join(root, fname)
+            for root, _, fnames in walk(root_path)
+            for fname in __files_ext(fnames, _extensions)
+            if not any(e_d in root.split('/') for e_d in excl_dirs or []) and
+            not any(fname.endswith(e_e) for e_e in excl_ends_with or [])]
+
+
+def __files_ext(fnames, _extensions):
+    return [fname for fname in fnames
+            if any(fname.endswith('.' + ext) for ext in _extensions)]
+
+
+def __to_be_built_single(src, tgt):
+    if getmtime(tgt) > getmtime(src):
+        print('%s is newer than %s: do not build' % (tgt, src))
+        return False
+    with open(src, 'rb') as f:
+        src_content = f.read()
+    with open(tgt, 'rb') as f:
+        tgt_content = f.read()
+    hash_src = md5(src_content).hexdigest()
+    hash_tgt = md5(tgt_content).hexdigest()
+    cache = {}
+    lines = []
+    if exists('hash_cache.txt'):
+        with open('hash_cache.txt') as f:
+            lines = f.readlines()
+    for line in lines:
+        line_spl = line.split()
+        hash = line_spl[-1]
+        fname = ' '.join(line_spl[:-1])
+        cache[fname] = hash
+    if src in cache and tgt in cache:
+        if hash_src == cache[src] and \
+           hash_tgt == cache[tgt]:
+            print('%s and %s are in the cache: do not build' % (tgt, src))
+            return False
+    print('%s and %s are not up-to-date: building...' % (src, tgt))
+    return True
+
+
+def to_be_built(tgt, srcs):
+    '''Should tgt be built (i.e. is it older?) from sources srcs?'''
+    if not exists(tgt):
+        print(tgt + ' does not exist: building...')
+        return True
+    return any(__to_be_built_single(src, tgt) for src in srcs)
+
+
+class InsideDir:
+    '''Context manager for working inside a directory.'''
+
+    def __init__(self, dir_):
+        self.dir = dir_
+        self.old_dir = getcwd()
+
+    def __enter__(self):
+        chdir(self.dir)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        chdir(self.old_dir)
+
+
+bld_dpath = 'build/'
+branch = _branch()
+ver = _version()
+win_fpath = '{dst_dir}{appname}-%s-windows.exe' % branch
+#osx_fpath = '{dst_dir}{appname}-%s-osx.zip' % branch
+#flatpak_fpath = '{dst_dir}{appname}-%s-flatpak' % branch
+appimage_fpath = '{dst_dir}{appname}-%s-appimage' % branch
+docs_fpath = '{dst_dir}{appname}-%s-docs.tar.gz' % branch
diff --git a/lib/build/commands.patch b/lib/build/commands.patch
new file mode 100644 (file)
index 0000000..2548880
--- /dev/null
@@ -0,0 +1,13 @@
+--- /home/flavio/yocto/builder/venv/lib/python3.7/site-packages/direct/dist/commands_src.py    2021-02-16 07:15:48.000000000 +0100
++++ /home/flavio/yocto/builder/venv/lib/python3.7/site-packages/direct/dist/commands.py        2021-03-11 10:17:03.770413010 +0100
+@@ -1532,4 +1532,8 @@
+         nsi.write('  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n')
+         nsi.write('  RMDir /r "$SMPROGRAMS\\$StartMenuFolder"\n')
+         nsi.write('SectionEnd\n')
++
++        nsi.write('Function .onInit\n')
++        nsi.write('    ExecWait \'\"$INSTDIR\\Uninstall.exe /S\"\'\n')
++        nsi.write('FunctionEnd\n')
+         nsi.close()
+
+         cmd = ['makensis']
diff --git a/lib/build/docs.py b/lib/build/docs.py
new file mode 100644 (file)
index 0000000..83f4351
--- /dev/null
@@ -0,0 +1,25 @@
+'''This module creates the documentation using pydoc.'''
+from os import system, mkdir, remove, rename
+from os.path import exists
+from shutil import rmtree, make_archive
+from lib.build.build import InsideDir
+from pathlib import Path
+from glob import glob
+
+
+#TODO refactor: make class DocsBuilder
+
+
+def bld_docs():
+    '''Builds the docs (inside a zip file).'''
+    system('python -m pydoc -w ./')
+    rmtree('docs', ignore_errors=True)
+    Path('docs').mkdir(exist_ok=True)
+    [rename(fname, 'docs/' + fname) for fname in glob('*.html')]
+    Path('build').mkdir(exist_ok=True)
+    rmtree('build/docs', ignore_errors=True)
+    rename('docs', 'build/docs')
+    with InsideDir('build'):
+        exists('docs.zip') and remove('docs.zip')
+        make_archive('docs', 'zip', '.', 'docs')
+        rmtree('docs')
diff --git a/lib/build/images.py b/lib/build/images.py
new file mode 100644 (file)
index 0000000..53e3021
--- /dev/null
@@ -0,0 +1,18 @@
+from os.path import dirname
+from sys import executable
+from lib.build.mtprocesser import ProcesserMgr
+
+
+#TODO refactor: make class ImagesBuilder
+
+
+def bld_images(files, cores):
+    mp_mgr = ProcesserMgr(cores)
+    list(map(__bld_img, [(str(src), mp_mgr) for src in files]))
+    mp_mgr.run()
+
+
+def __bld_img(fname_mp_mgr):
+    fname, mp_mgr = fname_mp_mgr
+    curr_path = dirname(__file__)
+    mp_mgr.add('convert "%s" "%s"' % (fname, fname[:-3] + 'dds'))
diff --git a/lib/build/in_venv.sh b/lib/build/in_venv.sh
new file mode 100755 (executable)
index 0000000..89665dd
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+#virtualenv venv
+#. ./venv/bin/activate
+#pip install --upgrade pip
+#pip install --upgrade setuptools
+#pip install panda3d -i https://archive.panda3d.org/branches/deploy-ng --upgrade
+python setup.py "$@"
diff --git a/lib/build/lang.py b/lib/build/lang.py
new file mode 100644 (file)
index 0000000..c9c631f
--- /dev/null
@@ -0,0 +1,73 @@
+'''Tools for l10n.'''
+from os import system, makedirs, remove
+from os.path import exists
+from shutil import move, copy
+from lib.build.build import files
+
+
+class LanguageBuilder:
+    '''Tools for building files for l10n.'''
+
+    @staticmethod
+    def mo(tgt, lng_dir_code, appname):
+        '''Builds the mo file in the lng_dir_code directory.'''
+        lng_code = tgt[len(lng_dir_code):].split('/')[0]
+        lng_dir = lng_dir_code + lng_code + '/LC_MESSAGES/'
+        cmd = 'msgfmt -o {lng_dir}{appname}.mo assets/po/{lng_code}.po'
+        system(cmd.format(lng_dir=lng_dir, appname=appname, lng_code=lng_code))
+
+    @staticmethod
+    def pot(appname, pot_path):
+        '''Builds the pot file in the lng_dir_code directory.'''
+        src_files = ' '.join(files(['py']))
+        cmd_tmpl = 'xgettext -ci18n -d {appname} -L python ' + \
+            '-o {pot_path}{appname}.pot '
+        system(cmd_tmpl.format(appname=appname, pot_path=pot_path) + src_files)
+
+    @staticmethod
+    def merge(lng_code, tgt_path, lng_dir, appname):
+        '''Merges the new strings with the previous ones.'''
+        lng_base_dir = LanguageBuilder.__prepare(lng_dir, lng_code, appname)
+        LanguageBuilder.__merge(lng_base_dir, lng_code, appname, tgt_path)
+        LanguageBuilder.__postprocess(lng_code)
+
+    @staticmethod
+    def __prepare(lng_base_dir, lng, appname):
+        '''Prepares a directory for working with languages.'''
+        makedirs(lng_base_dir + lng + '/LC_MESSAGES', exist_ok=True)
+        lng_dir = lng_base_dir + lng + '/LC_MESSAGES/'
+        if not exists('assets/po/' + lng + '.po'):
+            lines_to_fix = ['CHARSET/UTF-8', 'ENCODING/8bit']
+            [LanguageBuilder.__fix_line(line, lng_dir, appname) for line in lines_to_fix]
+            copy(lng_dir + appname + '.pot', lng_dir + appname + '.po')
+        return lng_dir
+
+    @staticmethod
+    def __fix_line(line, lng_dir, appname):
+        '''Fixes po files (misaligned entries).'''
+        cmd_tmpl = "sed 's/{line}/' {lng_dir}{appname}.pot > " + \
+            "{lng_dir}{appname}tmp.po"
+        system(cmd_tmpl.format(line=line, lng_dir=lng_dir, appname=appname))
+        move(lng_dir + appname + 'tmp.po', lng_dir + appname + '.pot')
+
+    @staticmethod
+    def __merge(lng_dir, lng_code, appname, tgt_path):
+        '''Manages the msgmerge's invokation.'''
+        print('merge', lng_dir)
+        cmd = 'msgmerge -o {lng_dir}{appname}merge.po ' + \
+            '{tgt_path}{lng_code}.po {tgt_path}{appname}.pot'
+        cmd = cmd.format(lng_dir=lng_dir, lng_code=lng_code, appname=appname,
+                         tgt_path=tgt_path)
+        system(cmd)
+        copy(lng_dir + appname + 'merge.po', 'assets/po/%s.po' % lng_code)
+        poname_tmpl = '{lng_dir}{appname}merge.po'
+        remove(poname_tmpl.format(lng_dir=lng_dir, appname=appname))
+
+    @staticmethod
+    def __postprocess(lng_code):
+        '''Fixes po files at the end of the building process.'''
+        lines = open('assets/po/%s.po' % lng_code, 'r').readlines()
+        with open('assets/po/%s.po' % lng_code, 'w') as outf:
+            for line in lines:
+                po_str = '"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n'
+                outf.write(po_str if line.startswith(po_str[:20]) else line)
diff --git a/lib/build/models.py b/lib/build/models.py
new file mode 100644 (file)
index 0000000..3c8a048
--- /dev/null
@@ -0,0 +1,112 @@
+'''Provides tools for building models.'''
+from logging import info
+from os import system, walk, makedirs
+from os.path import exists, basename, dirname
+from glob import glob
+from hashlib import md5
+from shutil import copyfile, move, rmtree
+from lib.build.mtprocesser import ProcesserMgr
+from lib.build.build import to_be_built
+
+
+class ModelsBuilder():
+
+    def __init__(self):
+        self._cache_files = []  # for avoiding rebuilding the same file
+
+    def build(self, blend_path, cores):
+        '''Builds the models i.e. creates glTF and bam files from blend
+        ones.'''
+        mp_mgr = ProcesserMgr(cores)
+        for root, _, fnames in walk(blend_path):
+            for fname in [fname for fname in fnames if fname.endswith('.blend')]:
+                if '/prototypes/' not in root:
+                    self._export_blend(root, fname, mp_mgr)
+        mp_mgr.run()
+        cache, lines = [], []
+        if exists('hash_cache.txt'):
+            with open('hash_cache.txt') as fhash:
+                lines = fhash.readlines()
+        for line in lines:  # line's e.g. assets/path/to/gltf_or_png.ext 68ced1
+            line_spl = line.split()
+            hashval = line_spl[-1]
+            fname = ' '.join(line_spl[:-1])
+            if fname not in self._cache_files:
+                cache += [(fname, hashval)]
+        for cfile in self._cache_files:
+            cache += [(cfile, md5(open(cfile, 'rb').read()).hexdigest())]
+        cache = cache[-2048:]
+        with open('hash_cache.txt', 'w') as fhash:
+            fhash.write('\n'.join([' '.join(line) for line in cache]))
+
+    def _export_blend(self, root, fname, mp_mgr):
+        '''Exports blend files to glTF and bam formats.'''
+        if self._export_gltf(root, fname):
+            self._export_bam(root, fname, mp_mgr)
+
+    def _export_gltf(self, root, fname):
+        '''Exports glTF files from blend ones.'''
+        _fname = '%s/%s' % (root, fname)
+        files_before = [basename(gname) for gname in glob('./*')]
+        cmd = 'blender %s --background --python lib/build/blend2gltf.py '
+        cmd += '-- %s.gltf'
+        cmd = cmd % (_fname, fname[:-6])
+        pgltf = 'assets/gltf/'
+        gltf_name = _fname.replace('assets/models/', pgltf)
+        gltf_name = gltf_name.replace('.blend', '.gltf')
+        if not to_be_built(gltf_name, [_fname]):
+            return False
+        system(cmd)
+        self._cache_files += [_fname, gltf_name]
+        files_after = [basename(gname) for gname in glob('./*')]
+        new_files = [nnm for nnm in files_after if nnm not in files_before]
+        new_dir = root.replace('assets/models/', pgltf)
+        rmtree(new_dir, ignore_errors=True)
+        makedirs(new_dir)
+        for mname in new_files:
+            new_name = '%s/%s' % (new_dir, mname)
+            move(mname, new_name)
+            info('move %s %s' % (mname, new_name))
+        # blender rewrites metal files: let's restore them
+        metal_files = [fnm for fnm in glob(new_dir + '/*') if 'metal' in fnm]
+        for metal_file in metal_files:
+            src = metal_file.replace(pgltf, 'assets/models/')
+            if not exists(src):
+                src = metal_file.replace(pgltf, 'assets/models/prototypes/')
+                src_split = src.split('/')
+                src_tracks_idx = src_split.index('tracks')
+                before = src_split[:src_tracks_idx]
+                after = src_split[src_tracks_idx + 2:]
+                src = '/'.join(before + after)
+            copyfile(src, metal_file)
+        return True
+
+    def _export_bam(self, root, fname, mp_mgr):
+        '''Exports bam files from glTF ones.'''
+        _fname = '%s/%s' % (root, fname)
+        gltf_name = (_fname[:-5] + 'gltf').replace('/models/', '/gltf/', 1)
+        bam_name = (_fname[:-5] + 'bam').replace('/models/', '/bam/', 1)
+        cmd_args = gltf_name, bam_name
+        # use dds files in place of png/jpg in gltf2bam
+        copyfile(gltf_name, gltf_name + '.tmp')
+        with open(gltf_name) as fgltf:
+            lines = fgltf.readlines()
+        deps = []
+        for line in lines:
+            if ('.png' in line or '.jpg' in line) and '"uri"' in line:
+                rln = line[line.index('"uri"') + 9:].rstrip(',\n"')
+                tname = '%s/%s' % (root, rln)
+                deps += [tname.replace('/models/', '/gltf/', 1)]
+        for dep in deps:
+            tgt = dep.replace('/gltf/', '/bam/', 1)
+            tgt = tgt.replace('.png', '.dds').replace('.jpg', '.dds')
+            makedirs(dirname(tgt), exist_ok=True)
+            info('convert %s %s' % (dep, tgt))
+            system('convert %s %s' % (dep, tgt))
+        rpl = lambda lin: lin.replace('.png', '.dds').replace('.jpg', '.dds')
+        with open(gltf_name, 'w') as fgltf:
+            fgltf.write(''.join([rpl(line) for line in lines]))
+        makedirs(dirname(bam_name), exist_ok=True)
+        if to_be_built(bam_name, deps):
+            mp_mgr.add('gltf2bam %s %s' % cmd_args)
+        self._cache_files += [gltf_name] + deps
diff --git a/lib/build/mtprocesser.py b/lib/build/mtprocesser.py
new file mode 100644 (file)
index 0000000..5bdb6a4
--- /dev/null
@@ -0,0 +1,77 @@
+'''A multi-threaded command dispatcher.'''
+from datetime import datetime
+from multiprocessing import cpu_count
+from threading import Thread, RLock
+from os import system
+from logging import info, debug
+
+
+class ProcesserThread(Thread):
+    '''A thread which asynchronously processes commands from a command list.'''
+
+    def __init__(self, cmd_lst, lock):
+        '''The constructor.
+        cmd_lst: commands that can be exectuted from serveral processers
+        lock: shared lock between processers for accessing the command list'''
+        Thread.__init__(self)
+        self.cmd_lst = cmd_lst
+        self.lock = lock
+
+    def run(self):
+        '''The thread's logics.'''
+        while True:
+            with self.lock:
+                if not self.cmd_lst:
+                    return
+                cmd = self.cmd_lst.pop(0)
+            info('%s %s' % (datetime.now().strftime("%H:%M:%S"), cmd))
+            system(cmd)
+
+
+class SyncProcesser:
+    '''Synchronously processes a command list.'''
+
+    def __init__(self, cmd_lst):
+        '''The constructor.
+        cmd_lst: the list that must be executed'''
+        self.cmd_lst = cmd_lst
+
+    def run(self):
+        '''The processer's logics.'''
+        for cmd in self.cmd_lst:
+            before_str = datetime.now().strftime("(executing) %H:%M:%S")
+            info('%s %s' % (before_str, cmd))
+            system(cmd)
+            after_str = datetime.now().strftime("(executed) %H:%M:%S")
+            debug('%s %s' % (after_str, cmd))
+
+
+class ProcesserMgr:
+    '''Synchronously processes commands that are submitted (eventually) using
+    multiple threads.'''
+
+    def __init__(self, cores):
+        '''The constructor.
+        cores: how many cpu cores are used to process the commands'''
+        try:
+            self.cores = cpu_count()
+        except NotImplementedError:
+            self.cores = 1
+        self.cores = cores if cores else int(self.cores / 4 + 1)
+        debug('processer-mgr: using %s cores' % self.cores)
+        self.cmd_lst = []
+
+    def add(self, cmd):
+        '''Adds cmd to the list that will be processed.'''
+        self.cmd_lst += [cmd]
+
+    def run(self):
+        '''Performs the commands that have been added.'''
+        if self.cores != 1:
+            threads, lock = [], RLock()
+            threads = [ProcesserThread(self.cmd_lst, lock)
+                       for _ in range(self.cores)]
+            [thread.start() for thread in threads]
+            [thread.join() for thread in threads]
+        else:
+            SyncProcesser(self.cmd_lst).run()
diff --git a/lib/computer_proxy.py b/lib/computer_proxy.py
new file mode 100644 (file)
index 0000000..505a273
--- /dev/null
@@ -0,0 +1,38 @@
+from functools import wraps
+
+
+def compute_once(fun):
+    @wraps(fun)
+    def wrapper(*args, **kwargs):
+        self = args[0]
+        key = fun.__name__, args  # add support for kwargs
+        if key not in self.buffered_vals:
+            self.buffered_vals[key] = fun(*args, **kwargs)
+        return self.buffered_vals[key]
+    return wrapper
+
+
+def once_a_frame(fun):
+    @wraps(fun)
+    def wrapper(*args, **kwargs):
+        self = args[0]
+        key = fun.__name__, args  # add support for kwargs
+        if key not in self.buffered_vals_frm:
+            self.buffered_vals_frm[key] = fun(*args, **kwargs)
+        return self.buffered_vals_frm[key]
+    return wrapper
+
+
+class ComputerProxy:
+
+    def __init__(self):
+        self.eng.attach_obs(self.on_start_frame)
+        # there are issues if the object has another on_start_frame
+        self.buffered_vals, self.buffered_vals_frm = {}, {}
+
+    def on_start_frame(self):
+        self.buffered_vals_frm = {}
+
+    def destroy(self):
+        self.eng.detach_obs(self.on_start_frame)
+        self.buffered_vals = self.buffered_vals_frm = None
diff --git a/lib/decorator.py b/lib/decorator.py
new file mode 100644 (file)
index 0000000..18f8911
--- /dev/null
@@ -0,0 +1,9 @@
+# class Decorator:\r
+\r
+#     def __init__(self, decorated):\r
+#         self.__dict__['_decorated'] = decorated\r
+\r
+#     def __getattr__(self, attr): return getattr(self._decorated, attr)\r
+\r
+#     def __setattr__(self, attr, value):\r
+#         return setattr(self._decorated, attr, value)\r
diff --git a/lib/dictfile.py b/lib/dictfile.py
new file mode 100644 (file)
index 0000000..7199423
--- /dev/null
@@ -0,0 +1,96 @@
+import sys
+from logging import info
+from os import makedirs
+from os.path import dirname
+from collections import Mapping
+from configparser import ConfigParser
+from json import load, dumps
+from lib.gameobject import GameObject
+from lib.lib.builder import LibP3d
+
+
+class DctFile(GameObject):
+
+    def __init__(self, fpath, default_dct=None, persistent=True):
+        GameObject.__init__(self)
+        default_dct = default_dct or {}
+        if sys.platform == 'darwin' and LibP3d.runtime():
+            fpath = dirname(__file__) + '/' + fpath
+        self.fpath = fpath
+        self.persistent = persistent
+        try:
+            #with open(fpath) as json: fdct = load(json)
+            config = ConfigParser()
+            config.read(fpath)
+            fdct = {section: dict(config.items(section)) for section in config.sections()}
+            fdct = self.__typed_dct(fdct)
+            self.dct = self.__add_default(default_dct, fdct)
+        except IOError: self.dct = default_dct
+
+    @staticmethod
+    def __typed_dct(dct):
+        def convert_single_val(val):
+            try: return int(val)
+            except ValueError:
+                try: return float(val)
+                except ValueError:
+                    if not val or val[0] != '[':
+                        return val
+                    else:
+                        raise ValueError
+        def converted(val):
+            try: return convert_single_val(val)
+            except ValueError:
+                return [elm.strip() for elm in val[1:-1].split(',')]
+        new_dct = {}
+        for section, sec_dct in dct.items():
+            for key, val in sec_dct.items():
+                if section not in new_dct:
+                    new_dct[section] = {}
+                new_dct[section][key] = converted(val)
+        return new_dct
+
+    @staticmethod
+    def __add_default(dct, upd):
+        for key, val in upd.items():
+            if isinstance(val, Mapping):
+                dct[key] = DctFile.__add_default(dct.get(key, {}), val)
+            else: dct[key] = upd[key]
+        return dct
+
+    @staticmethod
+    def deepupdate(dct, new_dct):
+        for key, val in new_dct.items():
+            if isinstance(val, Mapping):
+                dct[key] = DctFile.deepupdate(dct.get(key, {}), val)
+            else: dct[key] = val
+        return dct
+
+    def store(self):
+        info('storing %s' % self.fpath)
+        if not self.persistent: return
+        #json_str = dumps(self.dct, sort_keys=True, indent=4,
+        #                 separators=(',', ': '))
+        #with open(self.fpath, 'w') as json: json.write(json_str)
+        fdct = {}
+        for section, sec_dct in self.dct.items():
+            if section not in fdct:
+                fdct[section] = {}
+            for key, val in sec_dct.items():
+                if type(val) == list:
+                    fdct[section][key] = '[%s]' % ', '.join(val)
+                else:
+                    fdct[section][key] = val
+        config = ConfigParser()
+        for key in self.dct:
+            config[key] = fdct[key]
+        if dirname(self.fpath):
+            makedirs(dirname(self.fpath), exist_ok=True)
+        with open(self.fpath, 'w') as ini_file:
+            config.write(ini_file)
+
+    def __getitem__(self, arg): return self.dct[arg]
+
+    def __setitem__(self, arg, val): self.dct[arg] = val
+
+    def __delitem__(self, arg): del self.dct[arg]
diff --git a/lib/engine/__init__.py b/lib/engine/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/engine/audio.py b/lib/engine/audio.py
new file mode 100644 (file)
index 0000000..8cbe80c
--- /dev/null
@@ -0,0 +1,14 @@
+from lib.gameobject import AudioColleague
+from lib.lib.p3d.audio import P3dSound
+
+
+AudioSound = P3dSound
+
+
+class EngineAudio(AudioColleague):
+
+    def __init__(self, mediator, vol=1.0):
+        AudioColleague.__init__(self, mediator)
+        self.set_volume(vol)
+
+    def set_volume(self, vol): self.eng.lib.volume = vol
diff --git a/lib/engine/cbmux.py b/lib/engine/cbmux.py
new file mode 100644 (file)
index 0000000..5004757
--- /dev/null
@@ -0,0 +1,22 @@
+from threading import Lock
+
+
+class CallbackMux:
+    # this is a sort of "multiplexer" i.e. it manages callbacks from threads
+    # and redirect them to the main thread (this prevents deadlocks)
+
+    def __init__(self):
+        self.lock = Lock()
+        self.callbacks = []
+        taskMgr.add(self.process_callbacks, 'processing callbacks')
+
+    def add_cb(self, func, args=None):
+        args = args or []
+        with self.lock: self.callbacks += [(func, args)]
+
+    def process_callbacks(self, task):
+        with self.lock:
+            callbacks = self.callbacks[:]
+            self.callbacks = []
+        for func, args in callbacks: func(*args)
+        return task.cont
diff --git a/lib/engine/clock.py b/lib/engine/clock.py
new file mode 100644 (file)
index 0000000..feefaa2
--- /dev/null
@@ -0,0 +1,16 @@
+class Clock:
+
+    def __init__(self, pause):
+        self.__paused_time = 0
+        self.__curr_stopped_time = 0
+        pause.logic.attach(self.on_pause)
+        pause.logic.attach(self.on_resume)
+
+    @property
+    def time(self): return globalClock.get_frame_time() - self.__paused_time
+
+    def on_pause(self): self.__curr_stopped_time = globalClock.get_frame_time()
+
+    def on_resume(self):
+        self.__paused_time += \
+            globalClock.get_frame_time() - self.__curr_stopped_time
diff --git a/lib/engine/configuration.py b/lib/engine/configuration.py
new file mode 100644 (file)
index 0000000..f26ef0d
--- /dev/null
@@ -0,0 +1,178 @@
+from panda3d.core import load_prc_file_data
+from lib.lib.builder import LibP3d
+
+
+class GuiCfg:
+
+    def __init__(self, fps=False, win_size='1280 720', win_orig=None,
+                 win_title='lib', fullscreen=False, sync_video=None,
+                 antialiasing=False, shaders=True, volume=1, fixed_fps=0):
+        self.fps = fps
+        self.win_size = win_size
+        self.win_title = win_title
+        self.win_orig = win_orig
+        self.fullscreen = fullscreen
+        self.sync_video = LibP3d.runtime() if sync_video is None \
+            else sync_video
+        self.antialiasing = antialiasing
+        self.shaders = shaders
+        self.volume = volume
+        self.fixed_fps = fixed_fps
+
+
+class ProfilingCfg:
+
+    def __init__(self, profiling=False, pyprof_percall=False):
+        self.profiling = profiling  # profiling with panda3d's tools
+        self.pyprof_percall = pyprof_percall
+
+
+class LangCfg:
+
+    def __init__(self, lang='en', lang_path='assets/locale',
+                 lang_domain='lib_game', languages=[('English', 'en')]):
+        self.lang = lang
+        self.lang_path = lang_path
+        self.lang_domain = lang_domain
+        self.languages = languages
+
+
+class CursorCfg:
+
+    def __init__(self, cursor_hidden=False, cursor_path='',
+                 cursor_scale=(1, 1, 1), cursor_color=(1, 1, 1, 1),
+                 cursor_hotspot=(0, 0)):
+        self.cursor_hidden = cursor_hidden
+        self.cursor_path = cursor_path
+        self.cursor_scale = cursor_scale
+        self.cursor_color = cursor_color
+        self.cursor_hotspot = cursor_hotspot
+
+
+class DevCfg:
+
+    def __init__(
+            self, mt_render=False, model_path='assets/models', excluded='',
+            shaders_dev=False, pbr=False, gamma=1.0, menu_joypad=True,
+            verbose='', verbose_log=False, xmpp_server='', start_wp='',
+            port=9099, server='localhost:9098', srgb=False, opengl_3_2=False,
+            gl_debug=False, async_textures=False, show_buffers=False,
+            anisotropic=8, pbr_normal=True, pbr_occlusion=True,
+            pbr_shadows=True, pbr_exposure=True, pbr_msaa_samples=4,
+            pbr_fog=True, offscreen=False, directx=False,
+            functional_test=False, functional_ref=False):
+        self.multithreaded_render = mt_render  # multithreaded rendering
+        self.model_path = model_path
+        self.excluded = excluded
+        self.shaders_dev = shaders_dev
+        self.pbr = pbr
+        self.pbr_normal = pbr_normal
+        self.pbr_occlusion = pbr_occlusion
+        self.pbr_shadows = pbr_shadows
+        self.pbr_exposure = pbr_exposure
+        self.pbr_msaa_samples = pbr_msaa_samples
+        self.pbr_fog = pbr_fog
+        self.gamma = gamma
+        self.directx = directx
+        self.menu_joypad = menu_joypad
+        self.verbose = verbose
+        self.verbose_log = verbose_log
+        self.xmpp_server = xmpp_server
+        self.port = port
+        self.server = server
+        self.start_wp = start_wp
+        self.srgb = srgb
+        self.opengl_3_2 = opengl_3_2
+        self.gl_debug = gl_debug
+        self.async_textures = async_textures
+        self.show_buffers = show_buffers
+        self.anisotropic = anisotropic
+        self.offscreen = offscreen
+        self.functional_test = functional_test
+        self.functional_ref = functional_ref
+
+
+class Cfg:
+
+    def __init__(self, gui_cfg=None, profiling_cfg=None, lang_cfg=None,
+                 cursor_cfg=None, dev_cfg=None):
+        self.gui_cfg = gui_cfg or GuiCfg()
+        self.profiling_cfg = profiling_cfg or ProfilingCfg()
+        self.lang_cfg = lang_cfg or LangCfg()
+        self.cursor_cfg = cursor_cfg or CursorCfg()
+        self.dev_cfg = dev_cfg or DevCfg()
+        self.__configure()
+
+    @staticmethod
+    def __set(key, val): load_prc_file_data('', key + ' ' + str(val))
+
+    def __configure(self):
+        cfginfo = [
+            ('texture-anisotropic-degree', self.dev_cfg.anisotropic),
+            # ('client-sleep', 0.001),
+            ('texture-minfilter', 'linear_mipmap_linear'),
+            ('gl-coordinate-system', 'default'),
+            ('textures-power-2', 'down'),
+            ('textures-auto-power-2', 1),
+            ('show-frame-rate-meter', int(self.gui_cfg.fps)),
+            ('hardware-animated-vertices', 'true'),
+            ('x-init-threads', 'true'),
+            # temp workaround for mtrendering (linux)
+            ('basic-shaders-only', 'false'),
+            ('default-model-extension', '.bam'),
+            #('compressed-textures',  1),  # particles don't work
+            #('model-cache-textures', 1),
+            #('model-cache-compressed-textures', 1),
+            #('bam-version', '6 45'),
+            ('audio-ouput-rate', 44100)]
+        if self.gui_cfg.win_size:
+            cfginfo += [('win-size', self.gui_cfg.win_size)]
+        if self.gui_cfg.win_orig:
+            cfginfo += [('win-origin', self.gui_cfg.win_orig)]
+        if self.dev_cfg.srgb:
+            cfginfo += [('framebuffer-srgb', 'true')]
+        if self.dev_cfg.opengl_3_2:
+            cfginfo += [('gl-version', '3 2')]
+        if self.dev_cfg.gl_debug:
+            cfginfo += [('gl-debug', 1)]
+        if self.dev_cfg.show_buffers:
+            cfginfo += [('show-buffers', 'true')]
+        if self.dev_cfg.async_textures:
+            cfginfo += [
+                ('preload-textures', 0),
+                ('preload-simple-textures', 1),
+                ('texture-compression', 1),
+                ('allow-incomplete-render', 1)]
+        if self.dev_cfg.directx:
+            cfginfo += [
+                ('load-display', 'pandadx9')]
+        cfginfo += [
+            ('window-title', self.gui_cfg.win_title),
+            ('cursor-hidden', int(self.cursor_cfg.cursor_hidden)),
+            ('sync-video', int(self.gui_cfg.sync_video)),
+            ('framebuffer-multisample', 1),
+            ('multisamples', 2)]
+        if self.dev_cfg.multithreaded_render:
+            cfginfo += [('threading-model', '/Draw')]
+        if self.dev_cfg.offscreen:
+            cfginfo += [('window-type', 'offscreen')]
+        if self.profiling_cfg.profiling:
+            cfginfo += [
+                ('want-pstats', 1),
+                ('task-timer-verbose', 1),
+                ('pstats-tasks', 1),
+                ('gl-finish', 1),
+                ('pstats-host', '127.0.0.1')]
+        for verb in self.dev_cfg.verbose.split(';'):
+            if not verb: continue
+            verb_el = verb.strip().split()
+            if verb_el[0] == 'direct':
+                cfginfo += [
+                    ('default-directnotify-level', verb_el[1])]
+            elif verb_el[0] == 'panda':
+                cfginfo += [
+                    ('notify-level', verb_el[1])]
+            else:
+                cfginfo += [
+                    ('notify-level-' + verb_el[0], verb_el[1])]
+        list(map(lambda args: self.__set(*args), cfginfo))
diff --git a/lib/engine/engine.py b/lib/engine/engine.py
new file mode 100644 (file)
index 0000000..3c2ce96
--- /dev/null
@@ -0,0 +1,83 @@
+from sys import path
+from os.path import dirname, realpath
+path.append(dirname(realpath(__file__)) + '/../thirdparty')
+
+from lib.lib.builder import LibBuilder
+from lib.engine.pause import PauseMgr
+from lib.engine.profiler import AbsProfiler
+from lib.engine.shader import ShaderMgr
+from lib.engine.log import LogMgr
+from lib.engine.font import FontMgr
+from lib.engine.phys import PhysMgr
+from lib.engine.gfx import EngineGfx
+from lib.engine.network.server import Server
+from lib.engine.network.client import Client
+from lib.engine.gui.gui import EngineGui
+from lib.engine.logic import EngineLogic
+from lib.engine.event import EngineEvent
+from lib.engine.audio import EngineAudio
+from lib.engine.lang import LangMgr
+from lib.gameobject import GameObject, Colleague
+from lib.engine.enginefacade import EngineFacade
+from lib.engine.configuration import Cfg
+from lib.engine.cbmux import CallbackMux
+from lib.engine.clock import Clock
+
+
+class Engine(GameObject, EngineFacade):
+
+    network_priority = -39
+
+    def __init__(self, cfg=None, end_cb=None, client_cls=None):
+        self.lib = LibBuilder.build()
+        self.lib.configure()
+        self.lib.init(end_cb=end_cb)
+        Colleague.eng = GameObject.eng = self
+        cfg = cfg or Cfg()  # use a default conf if not provided
+        self.shader_mgr = ShaderMgr(cfg.dev_cfg.shaders_dev, cfg.dev_cfg.gamma)
+        self.profiler = AbsProfiler.build(cfg.profiling_cfg.pyprof_percall)
+        self.font_mgr = FontMgr()
+        self.server = Server(cfg.dev_cfg.port)
+        client_cls = client_cls or Client
+        self.client = client_cls(cfg.dev_cfg.port, cfg.dev_cfg.server)
+        self.cb_mux = CallbackMux()
+        self.logic = EngineLogic(self, cfg)
+        self.log_mgr = LogMgr.init_cls()(self)
+        self.gfx = EngineGfx(self, cfg.dev_cfg.model_path,
+                             cfg.gui_cfg.antialiasing,
+                             cfg.gui_cfg.shaders,
+                             cfg.gui_cfg.fixed_fps,
+                             cfg.dev_cfg.srgb)
+        self.phys_mgr = PhysMgr(self)
+        self.event = EngineEvent(self, cfg.dev_cfg.menu_joypad,
+                                 cfg.dev_cfg.functional_test)
+        self.gui = EngineGui.init_cls()(self)
+        self.audio = EngineAudio(self, cfg.gui_cfg.volume)
+        self.pause = PauseMgr(self)
+        self.lang_mgr = LangMgr(cfg.lang_cfg.lang,
+                                cfg.lang_cfg.lang_domain,
+                                cfg.lang_cfg.lang_path)
+        GameObject.__init__(self)
+        self.clock = Clock(self.pause)
+
+    def destroy(self):
+        GameObject.destroy(self)
+        self.lib.destroy()
+        self.shader_mgr.destroy()
+        self.profiler.destroy()
+        self.font_mgr.destroy()
+        self.server.destroy()
+        self.client.destroy()
+        # self.xmpp.destroy()
+        self.logic.destroy()
+        self.log_mgr.destroy()
+        self.gfx.destroy()
+        self.phys_mgr.destroy()
+        self.event.destroy()
+        self.gui.destroy()
+        self.audio.destroy()
+        self.pause.destroy()
+        self.lang_mgr.destroy()
+        self.lib = self.shader_mgr = self.profiler = self.font_mgr = \
+            self.server = self.client = None
+        base.destroy()
diff --git a/lib/engine/enginefacade.py b/lib/engine/enginefacade.py
new file mode 100644 (file)
index 0000000..9a76f5a
--- /dev/null
@@ -0,0 +1,102 @@
+from math import pi
+
+
+class EngineFacade:
+
+    @property
+    def version(self): return self.logic.version
+
+    @property
+    def curr_path(self): return self.logic.curr_path
+
+    @property
+    def is_appimage(self): return self.logic.is_appimage
+
+    @property
+    def cfg(self): return self.logic.cfg
+
+    @property
+    def is_runtime(self): return self.logic.is_runtime
+
+    @property
+    def languages(self): return self.logic.cfg.lang_cfg.languages
+
+    @property
+    def resolutions(self): return self.gui.resolutions
+
+    @property
+    def closest_resolution(self): return self.gui.closest_resolution
+
+    @property
+    def joystick_mgr(self): return self.event.joystick_mgr
+
+    @property
+    def curr_time(self): return self.clock.time
+
+    def attach_obs(self, obs_meth, sort=10, rename='', args=None):
+        args = args or []
+        return self.event.attach(obs_meth, sort, rename, args)
+
+    def detach_obs(self, obs_meth, lambda_call=None):
+        return self.event.detach(obs_meth, lambda_call)
+    def attach_node(self, name): return self.gfx.root.attach_node(name)
+    def particle(
+            self, parent, texture, color=(1, 1, 1, 1), ampl=pi/6, ray=.5,
+            rate=.0001, gravity=-.85, vel=3.8, part_duration=1.0,
+            autodestroy=None, size=10):
+        return self.gfx.particle(
+            parent, texture, color, ampl, ray, rate, gravity, vel,
+            part_duration, autodestroy, size)
+    def init_gfx(self): return self.gfx.init()
+    def clean_gfx(self): return self.gfx.clean()
+
+    @staticmethod
+    def set_cam_pos(pos): return base.camera.set_pos(pos)
+
+    def load_font(self, fpath, outline=True):
+        return self.eng.font_mgr.load_font(fpath, outline)
+    def open_browser(self, url): return self.gui.open_browser(url)
+
+    def toggle_pause(self, show_frm=True):
+        return self.pause.logic.toggle(show_frm)
+
+    def play(self): return self.audio.play()
+    def set_volume(self, vol): return self.audio.set_volume(vol)
+    def show_cursor(self): return self.gui.cursor.show()
+    def show_standard_cursor(self): return self.gui.cursor.show_standard()
+    def hide_cursor(self): return self.gui.cursor.hide()
+    def hide_standard_cursor(self): return self.gui.cursor.hide_standard()
+    def cursor_top(self): return self.gui.cursor.cursor_top()
+    def set_amb_lgt(self, col): return self.shader_mgr.set_amb_lgt(col)
+
+    def set_dir_lgt(self, col, direction):
+        return self.shader_mgr.set_dir_lgt(col, direction)
+
+    def set_shadow_lgt(self, direction):
+        return self.shader_mgr.set_shadow_lgt(direction)
+
+    def clear_lights(self): return self.shader_mgr.clear_lights()
+    def toggle_shader(self): return self.shader_mgr.toggle_shader()
+
+    def set_resolution(self, res, fullscreen=None):
+        return self.gui.set_resolution(res, fullscreen)
+
+    def toggle_fullscreen(self): return self.gui.toggle_fullscreen()
+    def send(self, msg): return self.lib.send(msg)
+
+    def do_later(self, time, meth, args=None):
+        return self.lib.do_later(time, meth, args)
+
+    def add_task(self, mth, priority=0):
+        return self.lib.add_task(mth, priority)
+
+    def remove_task(self, tsk): return self.lib.remove_task(tsk)
+    def log(self, msg, verbose=False): return self.log_mgr.log(msg, verbose)
+    def log_tasks(self): return self.log_mgr.log_tasks()
+
+    def rm_do_later(self, tsk):
+        self.pause.remove_task(tsk)
+        return self.lib.remove_task(tsk)
+
+    def load_model(self, filename, callback=None, anim=None):
+        return self.gfx.load_model(filename, callback, anim)
diff --git a/lib/engine/event.py b/lib/engine/event.py
new file mode 100644 (file)
index 0000000..bff03a2
--- /dev/null
@@ -0,0 +1,51 @@
+from time import time
+from logging import info
+from lib.gameobject import EventColleague
+from lib.engine.joystick import JoystickMgr
+
+
+class EngineEvent(EventColleague):
+
+    def __init__(self, mediator, emulate_keyboard, functional_test):
+        EventColleague.__init__(self, mediator)
+        self.unpaused_time = 0
+        self._prev_time = time()
+        self.eng.add_task(self.__on_frame)
+        taskMgr.setupTaskChain('unpausable')
+        mth = self.__on_frame_unpausable
+        taskMgr.add(mth, 'unpausable', taskChain='unpausable')
+        self.joystick_mgr = JoystickMgr(emulate_keyboard, functional_test)
+
+    def __on_frame(self, task):  # unused task
+        self.notify('on_start_frame')
+        self.notify('on_frame')
+        self.notify('on_end_frame')
+        return self.eng.lib.task_cont
+
+    def __on_frame_unpausable(self, task):
+        self.unpaused_time += time() - self._prev_time
+        self._prev_time = time()
+        try:
+            self.notify('on_frame_unpausable')
+            return task.cont
+        except AttributeError: info("engine has been destroyed")
+
+    @staticmethod
+    def key2desc(keystr):
+        if not keystr.startswith('raw-'): return keystr
+        keystr = keystr[4:]
+        kmap = base.win.get_keyboard_map()
+        virt_key = kmap.get_mapped_button(keystr)
+        return (kmap.get_mapped_button_label(keystr) or str(virt_key)).lower()
+
+    @staticmethod
+    def desc2key(desc):
+        kmap = base.win.get_keyboard_map()
+        for i in range(kmap.get_num_buttons()):
+            if kmap.get_mapped_button_label(i).lower() == desc:
+                return str(kmap.get_mapped_button(i))
+            return desc
+
+    def destroy(self):
+        self.joystick_mgr.destroy()
+        EventColleague.destroy(self)
diff --git a/lib/engine/font.py b/lib/engine/font.py
new file mode 100644 (file)
index 0000000..4ab9d01
--- /dev/null
@@ -0,0 +1,17 @@
+from lib.gameobject import GameObject
+
+
+class FontMgr(GameObject):
+
+    def __init__(self):
+        GameObject.__init__(self)
+        self.__fonts = {}
+
+    def load_font(self, fpath, outline=True):
+        if fpath not in self.__fonts:
+            self.__fonts[fpath] = self.eng.lib.load_font(fpath, outline)
+        return self.__fonts[fpath]
+
+    def destroy(self):
+        self.__fonts = None
+        GameObject.destroy(self)
diff --git a/lib/engine/functional.py b/lib/engine/functional.py
new file mode 100644 (file)
index 0000000..3492f41
--- /dev/null
@@ -0,0 +1,403 @@
+'''Create ref:
+* M-x fla-set-fun-test
+* rm options.ini
+* python main.py --functional-test 1 --functional-ref
+* python main.py --functional-test 2 --functional-ref
+* M-x fla-unset-fun-test'''
+from logging import debug, info
+from pathlib import Path
+from shutil import rmtree
+from os import makedirs
+from os.path import join, exists
+from glob import glob
+from sys import exit
+from panda3d.core import Filename
+from direct.gui.OnscreenText import OnscreenText
+from lib.gameobject import GameObject
+from lib.build.build import _branch
+
+
+class FunctionalTest(GameObject):
+
+    screenshot_time = 1.2
+    evt_time = 1.0
+    start_time = 5
+
+    def __init__(self, idx, ref):
+        super().__init__()
+        self.txt = OnscreenText('', fg=(1, 0, 0, 1), scale=.16)
+        #self._path = ''
+        #if self.eng.is_appimage:
+        self._path = str(Filename().get_user_appdata_directory())
+        self._path += '/yocto_racer/'
+        self._path += 'tests/functional%s/' % ('_ref' if ref else '')
+        home = '/home/flavio'  # we must force this for wine
+        # if self._path.startswith('/c/users/') and exists(str(Path.home()) + '/.local/share/flatpak-wine601/default/'):
+        #     self._path = str(Path.home()) + '/.local/share/flatpak-wine601/default/drive_' + self._path[1:]
+        if self._path.startswith('/c/users/') and exists(home + '/.wine/'):
+            self._path = home + '/.wine/drive_' + self._path[1:]
+        if ref:
+            self._path = join(
+                Filename().get_user_appdata_directory(),
+                'yocto_racer/tests/functional_ref_%s/' % _branch())
+        self._curr_time = 0
+        if int(idx) == 1:
+            rmtree(self._path, ignore_errors=True)
+        info('creating dir: %s' % self._path)
+        makedirs(self._path, exist_ok=True)
+        self._fnames = []
+        self._tasks = []
+        self._prev_time = 0
+        self.eng.attach_obs(self.on_frame_unpausable)
+        self._do_screenshots(idx)
+
+    def _screenshot(self, time, name):
+        self._fnames += [self._path + name + '.png']
+        self._tasks += [(
+            self._curr_time + time,
+            lambda: self.eng.gfx.gfx_mgr.screenshot(self._path + name + '.png'),
+            'screenshot: %s' % name)]
+        def txt(show_hide):
+            self.txt['text'] = name
+            (self.txt.show if show_hide else self.txt.hide)()
+        self._tasks += [(
+            self._curr_time + time + .1,
+            lambda: txt(True),
+            'screenshot: %s (show)' % name)]
+        self._tasks += [(
+            self._curr_time + time + FunctionalTest.evt_time - .1,
+            lambda: txt(False),
+            'screenshot: %s (hide)' % name)]
+        self._curr_time += time
+
+    def __keypress(self, key):
+        '''Emulates a keypress'''
+        dev = base.win.getInputDevice(0)
+        dev.buttonDown(key)
+        dev.buttonUp(key)
+
+    def __char_entered(self, char):
+        '''Emulates a character being entered.'''
+        dev = base.win.getInputDevice(0)
+        dev.keystroke(ord(char))
+
+    def _event(self, time, evt, messenger_evt=False, append_up=True):
+        def _append_up(evt_name):
+            return evt + ('' if evt.endswith('-up') or not append_up else '-up')
+        def cback_char(_evt):
+            self.__char_entered(_evt)
+        def cback_keyp(_evt):
+            self.__keypress(_evt)
+            self.__keypress('raw-' + _evt)
+        cback = lambda: (cback_char(evt) if len(evt) == 1 else cback_keyp(evt))
+        if messenger_evt:
+            cback = lambda: messenger.send(_append_up(evt))
+        self._tasks += [(
+            self._curr_time + time,
+            cback,
+            'event: %s' % evt)]
+        def txt(show_hide):
+            self.txt['text'] = evt
+            (self.txt.show if show_hide else self.txt.hide)()
+        self._tasks += [(
+            self._curr_time + time + .2,
+            lambda: txt(True),
+            'event: %s (show)' % evt)]
+        self._tasks += [(
+            self._curr_time + time + .8,
+            lambda: txt(False),
+            'event: %s (hide)' % evt)]
+        self._curr_time += time
+
+    def _verify(self):
+        def __verify():
+            files = glob(self._path + '*')
+            for fname in self._fnames:
+                info('verifying %s' % fname)
+                assert exists(fname)
+        self._tasks += [(
+            self._curr_time + 3,
+            lambda: __verify(),
+            'verify')]
+        self._curr_time += 3
+
+    def _exit(self):
+        self._tasks += [(
+            self._curr_time + 3,
+            lambda: exit(),
+            'exit')]
+
+    def on_frame_unpausable(self):
+        for tsk in self._tasks:
+            if self._prev_time <= tsk[0] < self.eng.event.unpaused_time:
+                debug('%s %s' % (tsk[0], tsk[2]))
+                tsk[1]()
+        self._prev_time = self.eng.event.unpaused_time
+
+    def _do_screenshots_1(self):
+        info('_do_screenshots_1')
+        self._screenshot(FunctionalTest.start_time, 'main_menu')
+        self._do_screenshots_credits()
+        self._do_screenshots_options()
+        self._do_screenshots_exit()
+
+    def _do_screenshots_credits(self):
+        # go to credits
+        self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._screenshot(FunctionalTest.screenshot_time, 'main_menu_highlight')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'credits_menu')
+        # go to supporters
+        self._event(FunctionalTest.evt_time, 'joypad0-face_a', True)
+        self._screenshot(FunctionalTest.screenshot_time, 'supporters_menu')
+        # back to main
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'joypad0-face_b', True)
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+
+    def _do_screenshots_options(self):
+        # go to options
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'options_menu')
+        # language
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'language_open')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._screenshot(FunctionalTest.screenshot_time, 'language_highlight')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'language_it')
+        # volume
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_right')
+        self._event(FunctionalTest.evt_time, 'arrow_right')
+        self._screenshot(FunctionalTest.screenshot_time, 'volume')
+        # car's number
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'cars_open')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'cars_changed')
+        # back
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+
+    def _do_screenshots_exit(self):
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._verify()
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._exit()
+
+    def _do_screenshots_2(self):
+        info('_do_screenshots_2')
+        self._do_screenshots_restore_options()
+        self._do_screenshots_game()
+        self._do_screenshots_end()
+
+    def _do_screenshots_restore_options(self):
+        # go to options
+        self._event(FunctionalTest.evt_time, 'joypad0-dpad_down', True)
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'options_menu_restored')
+        # # language
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'language_en_restored')
+        # # volume
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_left')
+        self._event(FunctionalTest.evt_time, 'arrow_left')
+        self._screenshot(FunctionalTest.screenshot_time, 'volume_restored')
+        # car's number
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'cars_restored')
+        # graphics settings
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'graphics_settings')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'antialiasing')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'shadows')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'fog')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'normal_mapping')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'occlusion')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        # input
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'input')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_rec')
+        self._event(FunctionalTest.evt_time, '8', True, False)
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_changed')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up', True, False)
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_restored')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'w', True, False)
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_already')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p1_already_closed')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p2')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p3')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'keyboard_p4')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+
+    def _do_screenshots_game(self):
+        # single player
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'single_player_menu')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'track_page')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'car_page_start')
+        self._event(FunctionalTest.evt_time, 'arrow_left')
+        self._screenshot(FunctionalTest.screenshot_time, 'car_page_sel')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'driver_page_start')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_left')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_left')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(FunctionalTest.evt_time, 'arrow_up')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._event(FunctionalTest.evt_time, 'backspace')
+        self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry_empty')
+        self._event(FunctionalTest.evt_time, 'f')
+        self._event(FunctionalTest.evt_time, 'l')
+        self._event(FunctionalTest.evt_time, 'a')
+        self._event(FunctionalTest.evt_time, 'v')
+        self._event(FunctionalTest.evt_time, 'i')
+        self._event(FunctionalTest.evt_time, 'o')
+        self._event(FunctionalTest.evt_time, 'enter')
+        self._screenshot(FunctionalTest.screenshot_time, 'driver_page_entry_full')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_right')
+        self._screenshot(FunctionalTest.screenshot_time, 'driver_page_sel')
+        # some ai tests
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._event(40, 'escape-up')
+        self._screenshot(FunctionalTest.screenshot_time, 'ingame_menu')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'race_back')
+        self._event(FunctionalTest.evt_time, 'escape-up')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._screenshot(FunctionalTest.screenshot_time, 'ingame_sel')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'main_page_back_race')
+
+    def _do_screenshots_end(self):
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._screenshot(FunctionalTest.screenshot_time, 'exit_page')
+        self._event(FunctionalTest.evt_time, 'arrow_down')
+        self._screenshot(FunctionalTest.screenshot_time, 'exit_page_sel')
+        self._verify()
+        self._event(FunctionalTest.evt_time, 'rcontrol')
+        self._exit()
+
+    def _do_screenshots(self, idx):
+        [self._do_screenshots_1, self._do_screenshots_2][int(idx) - 1]()
diff --git a/lib/engine/gfx.py b/lib/engine/gfx.py
new file mode 100755 (executable)
index 0000000..0031c8f
--- /dev/null
@@ -0,0 +1,56 @@
+from math import pi
+from panda3d.core import ClockObject
+from lib.gameobject import GfxColleague
+from lib.engine.particle import Particle
+from lib.lib.p3d.gfx import P3dGfxMgr, P3dAnimNode, P3dAmbientLight, \
+    P3dSpotlight
+
+
+GfxMgr = P3dGfxMgr
+AnimNode = P3dAnimNode
+AmbientLight = P3dAmbientLight
+Spotlight = P3dSpotlight
+
+
+class EngineGfx(GfxColleague):
+
+    def __init__(self, mediator, model_path, antialiasing, shaders, fps, srgb):
+        GfxColleague.__init__(self, mediator)
+        self.gfx_mgr = GfxMgr(model_path, antialiasing, shaders, srgb)
+        self.root = None
+        self.part2eff = {}
+        if fps: self.set_frame_rate(fps)
+        # if self.mediator.cfg.gui_cfg.shaders and \
+        #         self.eng.lib.version.startswith('1.10'):
+        #     self.set_toon()
+        #     self.set_bloom()
+
+    def init(self):
+        self.root = self.gfx_mgr.root.attach_node('world')
+
+    def clean(self): self.root.remove_node()
+
+    def load_model(self, filename, callback=None, anim=None):
+        try: return self.gfx_mgr.load_model(filename, callback, anim)
+        except OSError:
+            return self.gfx_mgr.load_model(filename + '.bam', callback, anim)
+
+    def set_toon(self): self.gfx_mgr.set_toon()
+
+    def set_bloom(self): self.gfx_mgr.set_bloom()
+
+    def print_stats(self, two_d=True, three_d=True, analyze=True, ls=True):
+        self.gfx_mgr.print_stats(two_d, three_d, analyze, ls)
+
+    @staticmethod
+    def set_frame_rate(fps):
+        globalClock.setMode(ClockObject.MLimited)
+        globalClock.set_frame_rate(fps)
+        # base.set_sleep(.01)
+
+    @staticmethod
+    def particle(parent, texture, color=(1, 1, 1, 1), ampl=pi/6, ray=.5,
+                 rate=.0001, gravity=-.85, vel=3.8, part_duration=1.0,
+                 autodestroy=None, size=10):
+        return Particle(parent, texture, color, ampl, ray, rate, gravity, vel,
+                        part_duration, autodestroy, size)
diff --git a/lib/engine/gui/__init__.py b/lib/engine/gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/engine/gui/browser.py b/lib/engine/gui/browser.py
new file mode 100644 (file)
index 0000000..15ae5d1
--- /dev/null
@@ -0,0 +1,26 @@
+from sys import platform
+from os import environ, system
+from webbrowser import open_new_tab
+
+
+class BrowserStrategy:
+
+    @staticmethod
+    def open(url): open_new_tab(url)
+
+
+class BrowserStrategyLinux(BrowserStrategy):
+
+    @staticmethod
+    def open(url):
+        environ['LD_LIBRARY_PATH'] = ''
+        system('xdg-open ' + url)
+
+
+class Browser:
+
+    @staticmethod
+    def open(url):
+        cls = BrowserStrategyLinux if platform.startswith('linux') else \
+            BrowserStrategy
+        cls.open(url)
diff --git a/lib/engine/gui/circle.py b/lib/engine/gui/circle.py
new file mode 100644 (file)
index 0000000..351cb70
--- /dev/null
@@ -0,0 +1,35 @@
+from os import name
+from lib.lib.gui import Frame
+from lib.lib.p3d.shader import load_shader
+from lib.gameobject import GameObject
+
+
+class Circle(Frame, GameObject):
+
+    def __init__(self, size=.4, pos=(0, 0), parent=None, ray=.4, thickness=.05,
+                 col_start=(1, 1, 1, 1), col_end=(1, 1, 1, 1)):
+        GameObject.__init__(self)
+        Frame.__init__(self, pos=(pos[0], pos[1]), texture_coord=True,
+                       frame_size=(-size, size, -size, size), parent=parent)
+        path = 'assets/shaders/'
+        shader = load_shader(path + 'filter.vert', path + 'circle.frag')
+        drv_lst = [self.eng.lib.driver_vendor, self.eng.lib.driver_renderer,
+                   self.eng.lib.driver_version]
+        is_nvidia = any('nvidia' in drv.lower() for drv in drv_lst)
+        if shader and not (name == 'nt' and is_nvidia):
+            self.set_shader(shader)
+            args = [('ray', ray), ('width', thickness), ('progress', 0),
+                    ('color_start', col_start), ('color_end', col_end)]
+            list(map(lambda arg: self.set_shader_input(*arg), args))
+        else: self['frameColor'] = (1, 1, 1, 0)
+        self.set_transparency(True)
+
+    @property
+    def progress(self): return self.get_shader_input('progress')
+
+    @progress.setter
+    def progress(self, val): self.set_shader_input('progress', val)
+
+    def destroy(self):
+        Frame.destroy(self)
+        GameObject.destroy(self)
diff --git a/lib/engine/gui/cursor.py b/lib/engine/gui/cursor.py
new file mode 100644 (file)
index 0000000..580dd2e
--- /dev/null
@@ -0,0 +1,46 @@
+from lib.lib.gui import Img
+from lib.gameobject import GameObject
+
+
+class MouseCursorFacade:
+
+    def show(self):
+        if not self.eng.cfg.dev_cfg.functional_test:
+            return self.cursor_img.show()
+    def hide(self): return self.cursor_img.hide()
+
+
+class MouseCursor(GameObject, MouseCursorFacade):
+
+    def __init__(self, filepath, scale, color, hotspot):
+        GameObject.__init__(self)
+        MouseCursorFacade.__init__(self)
+        if not filepath: return
+        self.eng.lib.hide_std_cursor()
+        self.cursor_img = Img(filepath, scale=scale, foreground=True)
+        self.cursor_img.img.set_color(color)
+        if self.eng.cfg.dev_cfg.functional_test:
+            self.cursor_img.hide()
+        self.hotspot_dx = scale[0] * (1 - 2 * hotspot[0])
+        self.hotspot_dy = scale[2] * (1 - 2 * hotspot[1])
+        self.eng.attach_obs(self.on_frame)
+        self.eng.attach_obs(self.on_frame_unpausable)
+
+    def show_standard(self): self.eng.lib.show_std_cursor()
+
+    def hide_standard(self): self.eng.lib.hide_std_cursor()
+
+    def cursor_top(self):
+        self.cursor_img.reparent_to(self.cursor_img.parent)
+
+    def __on_frame(self):
+        mouse = self.eng.lib.mousepos
+        if not mouse: return
+        h_x = mouse[0] * self.eng.lib.aspect_ratio + self.hotspot_dx
+        self.cursor_img.set_pos((h_x, mouse[1] - self.hotspot_dy))
+
+    def on_frame(self):
+        if not self.eng.pause.paused: self.__on_frame()
+
+    def on_frame_unpausable(self):
+        if self.eng.pause.paused: self.__on_frame()
diff --git a/lib/engine/gui/gui.py b/lib/engine/gui/gui.py
new file mode 100755 (executable)
index 0000000..9395fba
--- /dev/null
@@ -0,0 +1,66 @@
+from logging import info
+from lib.gameobject import GuiColleague
+from lib.engine.gui.cursor import MouseCursor
+from lib.engine.gui.browser import Browser
+
+
+up = (0, 1)
+down = (0, -1)
+left = (-1, 0)
+right = (1, 0)
+
+
+class EngineGuiBase(GuiColleague):  # no win: EngineGui strictly manages win
+
+    @staticmethod
+    def init_cls():
+        return EngineGui if EngineGuiBase.eng.lib.has_window else EngineGuiBase
+
+    @staticmethod
+    def open_browser(url): Browser.open(url)
+
+    @property
+    def resolutions(self):
+        return sorted(list(set(self.eng.lib.resolutions)))
+
+    @property
+    def closest_resolution(self):
+        def distance(res):
+            curr_res = self.eng.lib.resolution
+            return abs(res[0] - curr_res[0]) + abs(res[1] - curr_res[1])
+
+        try:
+            return min(self.resolutions, key=distance)
+        except ValueError:  # sometimes we have empty resolutions
+            return self.eng.lib.resolution
+
+    def set_resolution_check(self, res):
+        res_msg = 'resolutions: {curr} (current), {res} (wanted)'
+        info(res_msg.format(curr=self.eng.lib.resolution, res=res))
+        if self.eng.lib.resolution == res: return
+        retry = 'second attempt: {curr} (current) {res} (wanted)'
+        info(retry.format(curr=self.eng.lib.resolution, res=res))
+        self.set_resolution(res, False)
+
+    def toggle_fullscreen(self):
+        self.set_resolution(self.closest_resolution)
+        self.eng.lib.toggle_fullscreen()
+
+
+class EngineGui(EngineGuiBase):
+
+    def __init__(self, mediator):
+        EngineGuiBase.__init__(self, mediator)
+        cfg = self.eng.cfg
+        res_strings = cfg.gui_cfg.win_size.split()
+        res_ints = tuple(int(size) for size in res_strings)
+        self.set_resolution(res_ints, fullscreen=cfg.gui_cfg.fullscreen)
+        cur_cfg = cfg.cursor_cfg
+        self.cursor = MouseCursor(
+            cur_cfg.cursor_path, cur_cfg.cursor_scale, cur_cfg.cursor_color,
+            cur_cfg.cursor_hotspot)
+
+    def set_resolution(self, res, check=True, fullscreen=None):
+        info('setting resolution ' + str(res))
+        self.eng.lib.set_resolution(res, fullscreen)
+        if check: self.eng.do_later(3.0, self.set_resolution_check, [res])
diff --git a/lib/engine/gui/imgbtn.py b/lib/engine/gui/imgbtn.py
new file mode 100644 (file)
index 0000000..99d914f
--- /dev/null
@@ -0,0 +1,30 @@
+from lib.lib.gui import Btn
+from lib.lib.p3d.shader import load_shader
+
+
+class ImgBtn(Btn):
+
+    def __init__(self, *args, **kwargs):
+        Btn.__init__(self, *args, **kwargs)
+        shader_dirpath = 'assets/shaders/'
+        shader = load_shader(shader_dirpath + 'filter.vert',
+                             shader_dirpath + 'imgbtn.frag')
+        if shader:
+            self.set_shader(shader)
+            shader_args = [('col_offset', 0), ('enable', 1)]
+            list(map(lambda args: self.set_shader_input(*args), shader_args))
+        self.set_transparency(True)
+
+    def _on_enter(self, pos):  # pos comes from mouse
+        self.set_shader_input('col_offset', .25)
+
+    def _on_exit(self, pos):  # pos comes from mouse
+        self.set_shader_input('col_offset', 0)
+
+    def enable(self):
+        super().enable()
+        self.set_shader_input('enable', 1)
+
+    def disable(self):
+        super().disable()
+        self.set_shader_input('enable', .2)
diff --git a/lib/engine/gui/mainpage.py b/lib/engine/gui/mainpage.py
new file mode 100644 (file)
index 0000000..5e85b2d
--- /dev/null
@@ -0,0 +1,56 @@
+from lib.lib.gui import Text, Img
+from lib.engine.gui.page import Page, PageGui, PageFacade
+from lib.engine.gui.imgbtn import ImgBtn
+
+
+class MainPageGui(PageGui):
+
+    def build(self, back_btn=True, exit_behav=False):
+        #self.__build_social()
+        self.__build_version()
+        self.__build_dependencies()
+        self._set_widgets()
+        self.transition_enter()
+
+    def __build_social(self):
+        sites = self.props.gameprops.social_sites
+        menu_props = self.props.gameprops.menu_props
+        left = (len(sites) - 1) / 2.0 * .15
+        buttons = [
+            ImgBtn(
+                parent='bottomcenter',
+                scale=(.06, .06),
+                pos=(-left + i*.15, .1),
+                frame_col=(1, 1, 1, 1),
+                frame_texture=menu_props.social_imgs_dirpath % site[0],
+                cmd=self.eng.open_browser,
+                extra_args=[site[1]],
+                **menu_props.imgbtn_args)
+            for i, site in enumerate(sites)]
+        self.add_widgets(buttons)
+
+    def __build_version(self):
+        txt = Text(
+            _('version: ') + self.eng.version, parent='bottomleft',
+            pos=(.02, .02), scale=.04, fg=(.8, .8, .8, 1), align='left',
+            font=self.props.gameprops.menu_props.font, wordwrap=256)
+        self.add_widgets([txt])
+
+    def __build_dependencies(self):
+        txt = Text(
+            _('made with heart with panda3d, panda3d-simplepbr, panda3d-gltf'),
+            parent='bottomright', pos=(-.09, .02), scale=.04, wordwrap=128,
+            fg=(.8, .8, .8, 1), align='right',
+            font=self.props.gameprops.menu_props.font)
+        img = Img(
+            'assets/images/gui/p3d.dds', scale=.04,
+            parent=base.a2dBottomRight, pos=(-.04, .04))
+        self.add_widgets([txt, img])
+
+
+class MainPage(Page, PageFacade):
+    gui_cls = MainPageGui
+
+    def __init__(self, mainpage_props):
+        Page.__init__(self, mainpage_props)
+        PageFacade.__init__(self)
diff --git a/lib/engine/gui/menu.py b/lib/engine/gui/menu.py
new file mode 100644 (file)
index 0000000..7ad105c
--- /dev/null
@@ -0,0 +1,208 @@
+from direct.gui.DirectGuiGlobals import FLAT
+from lib.gameobject import GuiColleague, LogicColleague, GameObject
+from lib.lib.gui import Img
+from lib.engine.audio import AudioSound
+
+
+class NavInfoPerPlayer:
+
+    def __init__(self, left, right, up, down, fire):
+        self.left = left
+        self.right = right
+        self.up = up
+        self.down = down
+        self.fire = fire
+
+
+class NavInfo:
+
+    def __init__(self, navinfo_lst):
+        self.navinfo_lst = navinfo_lst
+
+
+class MenuProps(GameObject):
+
+    def __init__(
+            self, font_path, text_active_col, text_normal_col, text_err_col,
+            text_scale, btn_size, btn_col, background_img_path, over_sfx_path,
+            click_sfx_path, social_imgs_dirpath, nav):
+        GameObject.__init__(self)
+        self.__font_path = font_path
+        self.text_active_col = text_active_col
+        self.text_normal_col = text_normal_col
+        self.text_err_col = text_err_col
+        self.text_scale = text_scale
+        self.btn_size = btn_size
+        self.btn_col = btn_col
+        self.background_img_path = background_img_path
+        self.__over_sfx_path = over_sfx_path
+        self.__click_sfx_path = click_sfx_path
+        self.social_imgs_dirpath = social_imgs_dirpath
+        self.nav = nav
+
+    @property
+    def font(self): return self.eng.font_mgr.load_font(self.__font_path)
+
+    @property
+    def over_sfx(self): return AudioSound(self.__over_sfx_path).snd
+
+    @property
+    def click_sfx(self): return AudioSound(self.__click_sfx_path).snd
+
+    @property
+    def btn_args(self):  # it may be used without a menu e.g. results
+        return {
+            'scale': (self.text_scale, self.text_scale),
+            'text_font': self.font,
+            'text_fg': self.text_active_col,
+            'frame_col': self.btn_col,
+            'frame_size': self.btn_size,
+            'over_snd': self.over_sfx,
+            'click_snd': self.click_sfx}
+
+    @property
+    def imgbtn_args(self):
+        return {
+            'over_snd': self.over_sfx,
+            'click_snd': self.click_sfx}
+
+    @property
+    def label_args(self):
+        return {
+            'scale': self.text_scale,
+            'text_fg': self.text_normal_col,
+            'text_font': self.font,
+            'frame_col': (1, 1, 1, 0)}
+
+    @property
+    def option_args(self):
+        tfg = self.text_active_col
+        return {
+            'scale': self.text_scale,
+            'text_font': self.font,
+            'text_fg': tfg,
+            'frame_col': self.btn_col,
+            'frame_size': self.btn_size,
+            'over_snd': self.over_sfx,
+            'click_snd': self.click_sfx,
+            'text_scale': .85,
+            'item_text_font': self.font,
+            'item_frame_col': tfg,
+            'item_relief': FLAT,
+            'popup_marker_col': self.btn_col,
+            'text_may_change': 1,
+            'highlight_col': (tfg[0] * 1.2, tfg[1] * 1.2, tfg[2] * 1.2, .2)}
+
+    @property
+    def checkbtn_args(self):
+        return {
+            'scale': self.text_scale,
+            'text_font': self.font,
+            'text_fg': self.text_active_col,
+            'frame_col': self.btn_col,
+            'over_snd': self.over_sfx,
+            'click_snd': self.click_sfx}
+
+    @property
+    def text_args(self):
+        return {
+            'scale': self.text_scale,
+            'fg': self.text_normal_col,
+            'font': self.font}
+
+
+class MenuGui(GuiColleague):
+
+    def __init__(self, mediator, menu_props):
+        GuiColleague.__init__(self, mediator)
+        self.menu_props = menu_props
+        self.background = None
+        if not self.menu_props.background_img_path: return
+        self.background = Img(self.menu_props.background_img_path,
+                              scale=(1.77778, 1, 1.0),
+                              background=True)
+
+    def destroy(self):
+        if self.background: self.background.destroy()
+        self.menu_props = self.background = None
+        GuiColleague.destroy(self)
+
+
+class MenuLogic(LogicColleague):
+
+    def __init__(self, mediator):
+        LogicColleague.__init__(self, mediator)
+        self.pages = []
+
+    def push_page(self, page):
+        if self.pages:
+            self.pages[-1].hide()
+            if len(self.pages) > 1:  # first page doesn't go back
+                self.pages[-1].detach_obs(self.on_back)
+                self.pages[-1].detach_obs(self.on_quit)
+        self.pages += [page]
+        list(map(
+            page.attach_obs, [self.on_back, self.on_quit, self.on_push_page]))
+
+    def enable(self): self.pages[-1].enable()
+
+    def enable_navigation(self): self.pages[-1].enable_navigation()
+
+    def disable(self): self.pages[-1].disable()
+
+    def disable_navigation(self): self.pages[-1].disable_navigation()
+
+    def on_push_page(self, page_code, args=None): pass
+
+    def __back_quit_tmpl(self, idx, fun):
+        page = self.pages.pop()
+        list(map(page.detach_obs, [self.on_back, self.on_quit]))
+        page.destroy()
+        fun()
+        self.pages[idx].show()
+        list(map(self.pages[idx].attach_obs, [self.on_back, self.on_quit]))
+
+    def on_back(self):
+        self.__back_quit_tmpl(-1, lambda: None)
+
+    def on_quit(self):
+        def __on_quit():
+            while len(self.pages) > 1:
+                page = self.pages.pop()
+                page.destroy()
+        self.__back_quit_tmpl(0, __on_quit)
+
+    def destroy(self):
+        list(map(lambda page: page.destroy(), self.pages))
+        self.pages = None
+        LogicColleague.destroy(self)
+
+
+class MenuFacade:
+
+    def push_page(self, page): return self.logic.push_page(page)
+    def attach_obs(self, obs_meth, sort=10, rename='', args=None):
+        return self.gui.attach(obs_meth, sort, rename, args or [])
+    def detach_obs(self, obs_meth, lambda_call=None):
+        return self.gui.detach(obs_meth, lambda_call)
+    def enable(self): return self.gui.enable()
+    def enable_navigation(self): return self.gui.enable_navigation()
+
+
+class Menu(GameObject, MenuFacade):
+    gui_cls = MenuGui
+    logic_cls = MenuLogic
+
+    def __init__(self, menu_props):
+        GameObject.__init__(self)
+        self.logic = self.logic_cls(self)
+        self.__menu_props = menu_props
+        self._build_gui()
+
+    def _build_gui(self):
+        self.gui = self.gui_cls(self, self.__menu_props)
+
+    def destroy(self):
+        self.logic.destroy()
+        self.gui.destroy()
+        GameObject.destroy(self)
diff --git a/lib/engine/gui/page.py b/lib/engine/gui/page.py
new file mode 100755 (executable)
index 0000000..f046c61
--- /dev/null
@@ -0,0 +1,273 @@
+from inspect import getmro
+from lib.lib.gui import Btn, Slider, CheckBtn, OptionMenu, Entry
+from lib.engine.vec import Vec2
+from lib.engine.gui.gui import left, right, up, down
+from lib.gameobject import GameObject, GuiColleague, EventColleague
+from lib.engine.gui.imgbtn import ImgBtn
+
+
+class PageGui(GuiColleague):
+
+    def __init__(self, mediator, menu_props, players=[0]):
+        GuiColleague.__init__(self, mediator)
+        self.enable_tsk = None
+        self._back_btn = None
+        self.menu_props = menu_props
+        self.players = players
+        self.widgets = []
+        self.build()
+        self.translate()
+        self.curr_wdgs = []
+        for player in players:
+            self.curr_wdgs += [
+                self.__next_wdg((-.1, -1), player, Vec2(-3.6, 1))]
+            if self.curr_wdgs[-1]:
+                self.curr_wdgs[-1].on_wdg_enter(None, player)
+
+    def build(self, back_btn=True, exit_behav=False):
+        if back_btn: self.__build_back_btn(exit_behav)
+        self._set_widgets()
+        self._set_entries()
+        self.transition_enter()
+        self.eng.cursor_top()
+
+    def add_widgets(self, widgets): self.widgets += widgets
+
+    def on_arrow(self, direction, player):
+        if not self.curr_wdgs[player]: return
+        if self.curr_wdgs[player].__class__.__name__ == 'P3dEntryWidget' and \
+            self.curr_wdgs[player].focused: return
+        processed_cmd = self.curr_wdgs[player].on_arrow(direction)
+        # e.g. up/down in a combobox or left/right in a slider
+        if processed_cmd: return
+        next_wdg = self.__next_wdg(direction, player)
+        if not next_wdg: return
+        self.focus(next_wdg, player)
+
+    def on_enter(self, player):
+        if not self.curr_wdgs[player]: return
+        arg = player if len(self.players) > 1 else None
+        if self.curr_wdgs[player].on_enter(arg): self.enable([player])
+        # wdg.on_enter returns True when it is an option widget
+
+    @property
+    def buttons(self):
+        is_btn = lambda wdg: Btn in getmro(wdg.__class__)
+        return [wdg for wdg in self.widgets if is_btn(wdg)]
+
+    def focus(self, wdg, player):
+        self.curr_wdgs[player].on_wdg_exit(None, player)
+        self.curr_wdgs[player] = wdg
+        self.curr_wdgs[player].on_wdg_enter(None, player)
+
+    def __direction_dot_dwg(self, wdg, direction, player, start=None):
+        if start: start_pos = start
+        else: start_pos = self.curr_wdgs[player].pos
+        return (wdg.pos - start_pos).normalized.dot(direction)
+
+    def __next_weight(self, wdg, direction, player, start=None):
+        if start: start_pos = start
+        else: start_pos = self.curr_wdgs[player].global_pos
+        dot = self.__direction_dot_dwg(wdg, direction, player, start)
+        if direction in [(-1, 0), (1, 0)]:
+            proj_dist = abs(wdg.global_pos.x - start_pos.x)
+        else: proj_dist = abs(wdg.global_pos.y - start_pos.y)
+        weights = [.5, .5] if direction in [left, right] else [.1, .9]
+        return weights[0] * (dot * dot) + weights[1] * (1 - proj_dist)
+
+    def __next_wdg(self, direction, player, start=None):
+        # interactive classes
+        iclss = [Btn, CheckBtn, Slider, OptionMenu, ImgBtn, Entry]
+        inter = lambda wdg: any(pcl in iclss for pcl in getmro(wdg.__class__))
+        allwdgs = [wdg for wdg in self.widgets if inter(wdg)]
+        wdgs = list(filter(lambda wdg: wdg.is_enabled, allwdgs))
+        if player < len(self.curr_wdgs) and self.curr_wdgs[player] \
+                and self.curr_wdgs[player] in wdgs:
+                # the last check for this case: multiple players appear on the
+                # same button, one player clicks it, another moves from it
+            wdgs.remove(self.curr_wdgs[player])
+        mth = self.__direction_dot_dwg
+        in_direction = lambda wdg: mth(wdg, direction, player, start) > .1
+        dirwdgs = list(filter(in_direction, wdgs))
+        if not dirwdgs: return None
+        nextweight = lambda wdg: \
+                     self.__next_weight(wdg, direction, player, start)
+        return max(dirwdgs, key=nextweight)
+
+    def _set_widgets(self):
+        list(map(lambda wdg: wdg.set_widget(), self.widgets))
+
+    def _set_entries(self):
+        for wdg in self.widgets:
+            if wdg.__class__.__name__ == 'P3dEntryWidget':
+                wdg.attach(self.on_entry_enter)
+                wdg.attach(self.on_entry_exit)
+
+    def on_entry_enter(self):
+        if self.menu_props:  # i.e. not destroyed
+            self.disable_navigation(self.players)
+
+    def on_entry_exit(self):
+        if self.menu_props:  # i.e. not destroyed
+            self.enable_navigation(self.players)
+
+    def transition_enter(self):
+        self.translate()
+        list(map(lambda wdg: wdg.set_enter_transition(), self.widgets))
+        self.enable(self.players)
+
+    def translate(self): list(map(lambda wdg: wdg.translate(), self.widgets))
+
+    def enable_navigation(self, players):
+        if self.enable_tsk: self.eng.rm_do_later(self.enable_tsk)
+        self.enable_tsk = self.eng.do_later(
+            .01, self.enable_navigation_aux, [players])
+
+    def update_navigation(self):
+        self.disable_navigation(self.players)
+        self.enable_navigation(self.players)
+
+    def enable_navigation_aux(self, players):
+        navs = []
+        for player in players:
+            nav = self.menu_props.nav.navinfo_lst[player]
+            evts = [
+                (self.eng.lib.remap_str(nav.left), self.on_arrow,
+                 [left, player]),
+                (self.eng.lib.remap_str(nav.right), self.on_arrow,
+                 [right, player]),
+                (self.eng.lib.remap_str(nav.up), self.on_arrow, [up, player]),
+                (self.eng.lib.remap_str(nav.down), self.on_arrow,
+                 [down, player]),
+                (self.eng.lib.remap_str(nav.fire), self.on_enter, [player])]
+            def append_up(evt): return evt + ('' if evt.endswith('-up') else '-up')
+            evts = [(append_up(evt[0]), evt[1], evt[2]) for evt in evts]
+            navs += [nav]
+            list(map(lambda args: self.mediator.event.accept(*args), evts))
+        self.eng.joystick_mgr.bind_keyboard(navs)
+        if self.eng.cfg.dev_cfg.menu_joypad and self._back_btn:
+            self.mediator.event.accept('joypad0-face_b-up', self.__back_wrapper)
+
+    def __back_wrapper(self):
+        if not self.eng.joystick_mgr.is_recording: self._back_btn['command']()
+
+    def disable_navigation(self, players):
+        if self.enable_tsk:
+            self.enable_tsk = self.eng.rm_do_later(self.enable_tsk)
+        for player in players:
+            nav = self.menu_props.nav.navinfo_lst[player]
+            # evts = [nav.left, nav.right, nav.up, nav.down, nav.fire]
+            evts = [
+                self.eng.lib.remap_str(nav.left),
+                self.eng.lib.remap_str(nav.right),
+                self.eng.lib.remap_str(nav.up),
+                self.eng.lib.remap_str(nav.down),
+                self.eng.lib.remap_str(nav.fire)]
+            def append_up(evt): return evt + ('' if evt.endswith('-up') else '-up')
+            evts = [append_up(evt) for evt in evts]
+            self.eng.joystick_mgr.unbind_keyboard()
+            list(map(self.mediator.event.ignore, evts))
+        self.mediator.event.ignore('joypad0-face_b-up')
+
+    def enable(self, players):
+        self.enable_navigation(players)
+        list(map(lambda wdg: wdg.enable(), self.widgets))
+
+    def disable(self, players):
+        if self.enable_tsk:
+            self.enable_tsk = self.eng.rm_do_later(self.enable_tsk)
+        self.disable_navigation(players)
+        list(map(lambda wdg: wdg.disable(), self.widgets))
+
+    def transition_exit(self, destroy=True):
+        list(map(lambda wdg: wdg.set_exit_transition(destroy), self.widgets))
+        self.disable(self.players)
+
+    def __build_back_btn(self, exit_behav):
+        tra_src = 'Quit' if exit_behav else 'Back'
+        tra_tra = _('Quit') if exit_behav else _('Back')
+        callback = self._on_quit if exit_behav else self._on_back
+        btn = Btn(text='', pos=(0, -.85), cmd=callback,
+                  tra_src=tra_src, tra_tra=tra_tra, **self.menu_props.btn_args)
+        self.widgets += [btn]
+        self._back_btn = btn
+
+    def _on_back(self, player=0):
+        self.notify('on_back', self.__class__.__name__)
+
+    def _on_quit(self): self.notify('on_quit', self.__class__.__name__)
+
+    def show(self):
+        visible_widgets = [wdg for wdg in self.widgets if wdg.was_visible]
+        list(map(lambda wdg: wdg.show(), visible_widgets))
+        self.transition_enter()
+
+    def hide(self):
+        for wdg in self.widgets: wdg.was_visible = not wdg.hidden
+        self.transition_exit(False)
+        self.notify('on_hide')
+
+    def destroy(self):
+        self.transition_exit()
+        self.menu_props = None
+
+
+class PageEvent(EventColleague):
+
+    def on_back(self): pass
+
+    def on_quit(self): pass
+
+
+class PageFacade:
+
+    def show(self): return self.gui.show()
+    def hide(self): return self.gui.hide()
+    def enable(self, players): return self.gui.enable(players)
+    def disable(self, players): return self.gui.disable(players)
+
+    def enable_navigation(self, players):
+        return self.gui.enable_navigation(players)
+
+    def disable_navigation(self, players):
+        return self.gui.disable_navigation(players)
+
+    def attach_obs(self, obs_meth, sort=10, rename='', args=None):
+        return self.gui.attach(obs_meth, sort, rename, args or [])
+
+    def detach_obs(self, obs_meth, lambda_call=None):
+        return self.gui.detach(obs_meth, lambda_call)
+
+
+class Page(GameObject, PageFacade):
+
+    gui_cls = PageGui
+    event_cls = PageEvent
+
+    def __init__(self, menu_props, players=[0]):
+        PageFacade.__init__(self)
+        self.menu_props = menu_props
+        self.players = players
+        GameObject.__init__(self)
+        self._build_event()
+        self._build_gui()
+        list(map(self.gui.attach, [self.on_hide, self.on_back, self.on_quit]))
+
+    def _build_event(self):
+        self.event = self.event_cls(self)
+
+    def _build_gui(self):
+        self.gui = self.gui_cls(self, self.menu_props, self.players)
+
+    def on_hide(self): self.event.ignoreAll()
+
+    def on_back(self, cls_name, args=None): self.event.on_back()  # unused arg
+
+    def on_quit(self, cls_name): self.event.on_quit()  # unused arg
+
+    def destroy(self):
+        self.event.destroy()
+        self.gui.destroy()
+        bases = [basecls for basecls in Page.__bases__
+                 if basecls != PageFacade]
+        for cls in bases: cls.destroy(self)
diff --git a/lib/engine/joystick.py b/lib/engine/joystick.py
new file mode 100644 (file)
index 0000000..fb86e5f
--- /dev/null
@@ -0,0 +1,164 @@
+from lib.gameobject import GameObject
+from lib.lib.p3d.joystick import P3dJoystickMgr as JoystickMgrLib
+
+
+class JoystickState:
+
+    def __init__(self):
+        self.x = self.y = self.b0 = self.b1 = self.b2 = self.b3 = \
+            self.dpad_l = self.dpad_r = self.dpad_u = self.dpad_d = \
+            self.ltrigger = self.rtrigger = self.ltrigger_known = \
+            self.rtrigger_known = self.lshoulder = self.rshoulder = \
+            self.lstick = self.rstick = 0
+        #TODO: rename bi to btni
+
+
+class JoystickMgr(GameObject):
+
+    def __init__(self, emulate_keyboard, functional_test):
+        GameObject.__init__(self)
+        self.emulate_keyboard = emulate_keyboard
+        self._fun_tst = functional_test
+        self.old = [JoystickState() for i in range(3)]
+        self.nav = None
+        self.is_recording = False
+        self.joystick_lib = JoystickMgrLib()
+        self.joystick_lib.init_joystick()
+        self.eng.do_later(.01, self.eng.attach_obs, [self.on_frame])
+        # eng.event doesn't exist
+        #if self.emulate_keyboard:
+        self.set_keyboard_emulation()
+
+    def set_keyboard_emulation(self):
+        num_joysticks =1 if self._fun_tst else self.joystick_lib.num_joysticks
+        for i in range(num_joysticks):
+            base.accept('joypad%s-dpad_left-up' % i, self.__keyb_evt, [i, 'left'])
+            base.accept('joypad%s-dpad_right-up' % i, self.__keyb_evt, [i, 'right'])
+            base.accept('joypad%s-dpad_up-up' % i, self.__keyb_evt, [i, 'up'])
+            base.accept('joypad%s-dpad_down-up' % i, self.__keyb_evt, [i, 'down'])
+            base.accept('joypad%s-face_a-up' % i, self.__keyb_evt, [i, 'fire'])
+
+    def __keyb_evt(self, i, evt):
+        if not self.is_recording and self.nav:
+            self.eng.send(str(getattr(self.nav[i], evt)) + '-up')
+            # if the user chose a number
+
+    def on_frame(self):
+        #if not self.emulate_keyboard: return
+        for i in range(self.joystick_lib.num_joysticks): self.__process(i)
+
+    def __process(self, i):
+        j_x, j_y, btn0, btn1, btn2, btn3, dpad_l, dpad_r, dpad_u, dpad_d, \
+            trigger_l, trigger_r, shoulder_l, shoulder_r, stick_l, stick_r, \
+            trigger_l_known, trigger_r_known = \
+            self.joystick_lib.get_joystick(i)
+        # if not self.is_recording:
+        #     if self.old[i].x <= -.4 <= j_x or self.old[i].dpad_l and \
+        #             not dpad_l:
+        #         if self.nav and i < len(self.nav) and self.nav[i]:
+        #             self.eng.send(self.nav[i].left)
+        #     if self.old[i].x >= .4 >= j_x or self.old[i].dpad_r and not dpad_r:
+        #         if self.nav and i < len(self.nav) and self.nav[i]:
+        #             self.eng.send(self.nav[i].right)
+        #     if self.old[i].y >= .4 >= j_y or self.old[i].dpad_d and not dpad_d:
+        #         if self.nav and i < len(self.nav) and self.nav[i]:
+        #             self.eng.send(self.nav[i].down)
+        #     if self.old[i].y <= -.4 <= j_y or self.old[i].dpad_u and not dpad_u:
+        #         if self.nav and i < len(self.nav) and self.nav[i]:
+        #             self.eng.send(self.nav[i].up)
+        # if self.old[i].b0 and not btn0:
+        #     if self.nav and i < len(self.nav) and self.nav[i] and \
+        #             not self.is_recording:
+        #         self.eng.send(self.nav[i].fire)
+        #     self.eng.send('joypad%s_face_x' % i)
+        # if self.old[i].b1 and not btn1:
+        #     self.eng.send('joypad%s_face_y' % i)
+        # if self.old[i].b2 and not btn2:
+        #     self.eng.send('joypad%s_face_a' % i)
+        # if self.old[i].b3 and not btn3:
+        #     self.eng.send('joypad%s_face_b' % i)
+        if self.old[i].ltrigger and not trigger_l and not trigger_l_known:
+            #self.eng.send('joypad_trigger_l')
+            self.eng.send('joypad%s-ltrigger-up' % i)
+        if self.old[i].rtrigger and not trigger_r and not trigger_r_known:
+            #self.eng.send('joypad_trigger_r')
+            self.eng.send('joypad%s-rtrigger-up' % i)
+        # if self.old[i].shoulder_l and not shoulder_l:
+        #     self.eng.send('joypad_shoulder_l')
+        #     self.eng.send('joypad%s_shoulder_l' % i)
+        # if self.old[i].shoulder_r and not shoulder_r:
+        #     self.eng.send('joypad_shoulder_r')
+        #     self.eng.send('joypad%s_shoulder_r' % i)
+        # if self.old[i].stick_l and not stick_l:
+        #     self.eng.send('joypad_stick_l')
+        #     self.eng.send('joypad%s_stick_l' % i)
+        # if self.old[i].stick_r and not stick_r:
+        #     self.eng.send('joypad_stick_r')
+        #     self.eng.send('joypad%s_stick_r' % i)
+        self.old[i].x, self.old[i].y, self.old[i].b0, self.old[i].b1, \
+            self.old[i].b2, self.old[i].b3, self.old[i].dpad_l, \
+            self.old[i].dpad_r, self.old[i].dpad_u, self.old[i].dpad_d, \
+            self.old[i].ltrigger, self.old[i].rtrigger, \
+            self.old[i].lshoulder, self.old[i].rshoulder, \
+            self.old[i].lstick, self.old[i].rstick = \
+            j_x, j_y, btn0, btn1, btn2, btn3, dpad_l, dpad_r, dpad_u, dpad_d, \
+            trigger_l, trigger_r, shoulder_l, shoulder_r, stick_l, stick_r
+
+    def get_joystick(self, player_idx):
+        x, y, face_a, face_b, face_x, face_y, dpadl, dpadr, dpadu, dpadd, triggl, \
+            triggr, shl, shr, st_l, st_r, trl_k, trr_k= \
+            self.joystick_lib.get_joystick(player_idx)
+        jstate = JoystickState()
+        jstate.x = x
+        jstate.y = y
+        jstate.face_a = face_a
+        jstate.face_b = face_b
+        jstate.face_x = face_x
+        jstate.face_y = face_y
+        jstate.dpad_l = dpadl
+        jstate.dpad_r = dpadr
+        jstate.dpad_u = dpadu
+        jstate.dpad_d = dpadd
+        jstate.ltrigger = triggl
+        jstate.ltrigger_known = trl_k
+        jstate.rtrigger = triggr
+        jstate.rtrigger_known = trr_k
+        jstate.lshoulder = shl
+        jstate.rshoulder = shr
+        jstate.lstick = st_l
+        jstate.rstick = st_r
+        return jstate
+
+    def get_joystick_val(self, player_idx, code):
+        j_x, j_y, btn0, btn1, btn2, btn3, dpad_l, dpad_r, dpad_u, dpad_d, \
+            trigger_l, trigger_r, shoulder_l, shoulder_r, stick_l, stick_r = \
+            self.joystick_lib.get_joystick(player_idx)
+        code2val = {
+            'face_x': btn0,
+            'face_y': btn1,
+            'face_a': btn2,
+            'face_b': btn3,
+            'dpad_l': dpad_l,
+            'dpad_r': dpad_r,
+            'dpad_u': dpad_u,
+            'dpad_d': dpad_d,
+            'trigger_l': trigger_l,
+            'trigger_r': trigger_r,
+            'shoulder_l': shoulder_l,
+            'shoulder_r': shoulder_r,
+            'stick_l': stick_l,
+            'stick_r': stick_r}
+        return code2val[code]
+
+    def bind_keyboard(self, nav): self.nav = nav
+
+    def unbind_keyboard(self): self.nav = None
+
+    def destroy(self):
+        try: self.eng.detach_obs(self.on_frame)
+        except KeyError: pass
+        # it happens in unit tests since it destroys in the same frame
+        # remove this catch when i've refactored the object's building
+        # and i don't use the director anymore
+        self.joystick_lib.destroy()
+        GameObject.destroy(self)
diff --git a/lib/engine/lang.py b/lib/engine/lang.py
new file mode 100644 (file)
index 0000000..e8ebb11
--- /dev/null
@@ -0,0 +1,26 @@
+from logging import info
+from os.path import join
+from gettext import translation
+from lib.gameobject import GameObject
+
+
+class LangMgr(GameObject):
+
+    def __init__(self, lang, domain, dpath):
+        GameObject.__init__(self)
+        self.lang = lang
+        self.domain = domain
+        self.dpath = join(self.eng.curr_path, dpath)
+        info('language: %s, %s' % (self.domain, self.dpath))
+        self.set_lang(lang)
+
+    @property
+    def lang_codes(self):
+        return [lang[1] for lang in self.eng.cfg.lang_cfg.languages]
+
+    def set_lang(self, lang):
+        self.lang = lang
+        args = lang, self.domain, self.dpath
+        info('setting language %s, %s, %s' % args)
+        tra = translation(self.domain, self.dpath, [lang], fallback=True)
+        tra.install()
diff --git a/lib/engine/log.py b/lib/engine/log.py
new file mode 100755 (executable)
index 0000000..515d2f5
--- /dev/null
@@ -0,0 +1,131 @@
+from logging import basicConfig, info, INFO, DEBUG, getLogger
+from configparser import ConfigParser
+from sys import platform, argv
+from json import load, dumps
+#from datetime import datetime
+from pprint import pprint
+from os import getcwd, environ
+from os.path import exists
+from traceback import print_stack
+from sys import version_info
+# from platform import system, release, architecture, platform, processor, \
+#     version, machine
+# from multiprocessing import cpu_count
+from panda3d.core import Filename, GraphicsWindow
+from lib.gameobject import Colleague
+from lib.lib.builder import LibP3d
+
+
+lev = INFO
+opt_path = ''
+if platform in ['win32', 'linux'] and not exists('main.py'):
+    # it is the deployed version for windows
+    opt_path = str(Filename.get_user_appdata_directory()) + '/yocto_racer'
+opath = LibP3d.fixpath(opt_path + '/options.ini') if opt_path else \
+        'options.ini'
+if exists(opath):
+    with open(opath) as json_file:
+        #optfile = load(json_file)
+        optfile = ConfigParser()
+        optfile.read(opath)
+        # optfile['development']['verbose'] and int(optfile['development']['verbose']) or \
+        if optfile['development']['verbose_log'] and int(optfile['development']['verbose_log']):
+            lev = DEBUG
+
+basicConfig(level=lev, format='%(asctime)s %(message)s', datefmt='%H:%M:%S')
+getLogger().setLevel(lev)  # it doesn't work otherwise
+
+
+class LogMgrBase(Colleague):  # headless log manager
+
+    @staticmethod
+    def init_cls():
+        return LogMgr if base.win else LogMgrBase
+
+    def __init__(self, mediator):
+        Colleague.__init__(self, mediator)
+        self.log_cfg()
+
+    def log(self, msg, verbose=False):
+        if verbose and not self.eng.cfg.dev_cfg.verbose_log: return
+        info(msg)
+
+    def log_cfg(self):
+        if '--version' in argv:
+            path = str(Filename.get_user_appdata_directory())
+            home = '/home/flavio'  # we must force this for wine
+            if path.startswith('/c/users/') and exists(home + '/.wine/'):
+                path = home + '/.wine/drive_' + path[1:]
+            info('writing %s' % path + '/yocto_racer/obs_version.txt')
+            with open(path + '/yocto_racer/obs_version.txt', 'w') as f:
+                f.write(self.eng.logic.version)
+            if not platform.startswith('win'):
+                from os import ttyname  # here because it doesn't work on windows
+                import sys
+                with open(ttyname(0), 'w') as fout:
+                    sys.stdout = fout
+                    print('version: ' + self.eng.logic.version)
+        messages = ['version: ' + self.eng.logic.version]
+        messages += ['argv[0]: %s' % argv[0]]
+        messages += ['getcwd: %s' % getcwd()]
+        messages += ['__file__: %s' % __file__]
+        for elm in environ.items():
+            messages += ['env::%s: %s' % elm]
+        # os_info = (system(), release(), version())
+        # messages += ['operative system: %s %s %s' % os_info]
+        # messages += ['architecture: ' + str(architecture())]
+        # messages += ['machine: ' + machine()]
+        # messages += ['platform: ' + platform()]
+        # messages += ['processor: ' + processor()]
+        # try:
+        #     messages += ['cores: ' + str(cpu_count())]
+        # except NotImplementedError:  # on Windows
+        #     messages += ['cores: not implemented']
+        lib_ver = self.eng.lib.version
+        try:
+            import psutil
+            mem = psutil.virtual_memory().total / 1000000000.0
+            messages += ['memory: %s GB' % round(mem, 2)]
+        except ImportError: info("can't import psutil")  # windows
+        lib_commit = self.eng.lib.lib_commit
+        py_ver = [str(elm) for elm in version_info[:3]]
+        messages += ['python version: %s' % '.'.join(py_ver)]
+        messages += ['panda version: %s %s' % (lib_ver, lib_commit)]
+        messages += ['bullet version: ' + str(self.eng.lib.phys_version)]
+        messages += ['appdata: ' + str(self.eng.lib.user_appdata_dir)]
+        if base.win and isinstance(base.win, GraphicsWindow):  # not headless
+            print(base.win.get_keyboard_map())
+        list(map(self.log, messages))
+
+    @staticmethod
+    def log_tasks():
+        info('tasks: %s' % taskMgr.getAllTasks())
+        info('do-laters: %s' % taskMgr.getDoLaters())
+
+    @staticmethod
+    def plog(obj):
+        print('\n\n')
+        print_stack()
+        pprint(obj)
+        print('\n\n')
+
+
+class LogMgr(LogMgrBase):
+
+    def log_cfg(self):
+        LogMgrBase.log_cfg(self)
+        messages = [self.eng.lib.driver_vendor]
+        messages += [self.eng.lib.driver_renderer]
+        shad_maj = self.eng.lib.driver_shader_version_major
+        shad_min = self.eng.lib.driver_shader_version_minor
+        messages += ['shader: {maj}.{min}'.format(maj=shad_maj, min=shad_min)]
+        messages += [self.eng.lib.driver_version]
+        drv_maj = self.eng.lib.driver_version_major
+        drv_min = self.eng.lib.driver_version_minor
+        drv = 'driver version: {maj}.{min}'
+        messages += [drv.format(maj=drv_maj, min=drv_min)]
+        messages += ['fullscreen: ' + str(self.eng.lib.fullscreen)]
+        res_x, res_y = self.eng.lib.resolution
+        res_tmpl = 'resolution: {res_x}x{res_y}'
+        messages += [res_tmpl.format(res_x=res_x, res_y=res_y)]
+        list(map(self.log, messages))
diff --git a/lib/engine/logic.py b/lib/engine/logic.py
new file mode 100755 (executable)
index 0000000..46affee
--- /dev/null
@@ -0,0 +1,77 @@
+from os.path import exists
+from logging import info
+from sys import argv
+from lib.gameobject import LogicColleague
+from lib.engine.configuration import Cfg
+from lib.gameobject import GameObject
+from lib.computer_proxy import ComputerProxy, compute_once
+
+
+class VersionChecker(GameObject, ComputerProxy):
+
+    def __init__(self):
+        GameObject.__init__(self)
+        ComputerProxy.__init__(self)
+
+    @compute_once
+    def is_uptodate(self):
+        return True  # currently the server part is not active
+        self.eng.client.register_rpc('srv_version')
+        try: ver = self.eng.client.srv_version()
+        except AttributeError:
+            print("can't retrieve the version")
+            return True
+        major, minor, build = ver.split('.')
+        major, minor, build = int(major), int(minor), int(build)
+        curr_ver = self.eng.version
+        if curr_ver == 'deploy-ng': return True
+        info('versions: %s %s' % (curr_ver, ver))
+        return curr_ver >= ver
+        # curr_major, curr_minor, curr_build = curr_ver.split('-')[0].split('.')
+        # curr_major = int(curr_major)
+        # curr_minor = int(curr_minor)
+        # curr_build = int(curr_build)
+        # return curr_major > major or \
+        #     curr_major == major and curr_minor > minor or \
+        #     curr_major == major and curr_minor == minor and curr_build >= build
+
+    def destroy(self):
+        GameObject.destroy(self)
+        # ComputerProxy.destroy(self)  # raises an exception
+
+
+class EngineLogic(LogicColleague):
+
+    @staticmethod
+    def cmd_line():
+        return [arg for arg in iter(argv[1:]) if not arg.startswith('-psn_')]
+
+    def __init__(self, mediator, cfg=None):
+        LogicColleague.__init__(self, mediator)
+        self.cfg = cfg or Cfg()  # use a default conf if not provided
+
+    @property
+    def version(self):
+        #if not self.is_runtime:
+        #    if not exists('assets/version.txt'): return '-'
+        #    with open('assets/version.txt') as fver:
+        #        return fver.read().strip() + '-source'
+        # ^ --version goes here
+        return self.mediator.lib.build_version
+
+    @property
+    def is_runtime(self):
+        return self.mediator.lib.runtime()
+
+    @property
+    def curr_path(self):
+        return self.mediator.lib.curr_path + '/' \
+            if self.is_runtime else ''
+
+    @property
+    def is_appimage(self):
+        return self.mediator.lib.is_appimage
+
+    def destroy(self):
+        self.cfg = None
+        LogicColleague.destroy(self)
diff --git a/lib/engine/network/__init__.py b/lib/engine/network/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/engine/network/binary.py b/lib/engine/network/binary.py
new file mode 100644 (file)
index 0000000..50126d4
--- /dev/null
@@ -0,0 +1,119 @@
+from struct import Struct, calcsize, unpack
+
+
+class BinaryData:
+
+    @staticmethod
+    def pack(lst):
+        acc_fmt = '!'
+        acc_elems = []
+        acc_header = ''
+        acc_header = BinaryData._header_lst(lst, acc_header)
+        lst = [acc_header] + lst
+        acc_fmt, acc_elems = BinaryData._pack_lst(lst, acc_fmt, acc_elems)
+        msg_struct = Struct(acc_fmt)
+        msg_data = msg_struct.pack(*acc_elems)
+        return msg_struct.size, msg_data
+
+    @staticmethod
+    def _header_elm(elm, acc_header):
+        if elm is None: add = 'n'
+        elif isinstance(elm, bool): add = 'b'
+        elif isinstance(elm, int): add = 'i'
+        elif isinstance(elm, float): add = 'f'
+        elif isinstance(elm, str): add = 's'
+        elif isinstance(elm, dict): add = '{}'
+        elif isinstance(elm, tuple) or isinstance(elm, list):
+            add = BinaryData._header_lst(elm, acc_header)
+        return acc_header + add
+
+    @staticmethod
+    def _header_lst(elm, acc_header):
+        add = '('
+        for sub_elm in elm:
+            add += BinaryData._header_elm(sub_elm, acc_header)
+        add += ')'
+        return acc_header + add
+
+    @staticmethod
+    def _pack_lst(lst, acc_fmt, acc_elems):
+        add_fmt, add_elems = '', []
+        for sub_elm in lst:
+            elm_fmt, elm_elems = BinaryData._pack_elm(sub_elm, '', [])
+            add_fmt += elm_fmt
+            add_elems += elm_elems
+        return acc_fmt + add_fmt, acc_elems + add_elems
+
+    @staticmethod
+    def _pack_elm(elm, acc_fmt, acc_elems):
+        if elm is None:
+            add_fmt = ''
+            add_elems = []
+        elif isinstance(elm, bool):
+            add_fmt = '?'
+            add_elems = [elm]
+        elif isinstance(elm, int):
+            add_fmt = 'i'
+            add_elems = [elm]
+        elif isinstance(elm, float):
+            add_fmt = 'f'
+            add_elems = [elm]
+        elif isinstance(elm, str):
+            b_str = bytes(elm, 'utf-8')
+            add_fmt = 'i%ds' % len(b_str)
+            add_elems = [len(b_str), b_str]
+        elif isinstance(elm, tuple) or isinstance(elm, list):
+            add_fmt, add_elems = BinaryData._pack_lst(elm, '', [])
+        elif isinstance(elm, dict): add_fmt, add_elems = '', []
+        return acc_fmt + add_fmt, acc_elems + add_elems
+
+    @staticmethod
+    def unpack(data):
+        header_length, data = BinaryData.unpack_helper('!i', data)
+        header_length = header_length[0]
+        header, data = BinaryData.unpack_helper('!%ds' % header_length, data)
+        header = header[0].decode('utf-8')
+        vals = []
+        curr_lst = vals
+
+        def parent(sublist, lst):
+            if sublist in lst: return lst
+            for _subl in [elm for elm in lst if isinstance(elm, list)]:
+                if parent(sublist, _subl): return parent(sublist, _subl)
+        for elm in header:
+            if elm == '(':
+                curr_lst += [[]]
+                curr_lst = curr_lst[-1]
+            elif elm == ')':
+                curr_lst = parent(curr_lst, vals)
+            elif elm == '{': pass
+            elif elm == '}': curr_lst += [{}]
+            else:
+                val, data = BinaryData._unpack_elm(elm, data)
+                curr_lst += [val]
+        return vals[0]
+
+    @staticmethod
+    def _unpack_elm(elm, data):
+        if elm == 'n':
+            val, data = [None], data
+            val = val[0]
+        elif elm == 'b':
+            val, data = BinaryData.unpack_helper('!?', data)
+            val = val[0]
+        elif elm == 'i':
+            val, data = BinaryData.unpack_helper('!i', data)
+            val = val[0]
+        elif elm == 'f':
+            val, data = BinaryData.unpack_helper('!f', data)
+            val = val[0]
+        elif elm == 's':
+            s_len, data = BinaryData.unpack_helper('!i', data)
+            val, data = BinaryData.unpack_helper('!%ds' % s_len, data)
+            val = val[0].decode('utf-8')
+        return val, data
+
+    @staticmethod
+    def unpack_helper(fmt, data):
+        size = calcsize(fmt)
+        return unpack(fmt, data[:size]), data[size:]
diff --git a/lib/engine/network/client.py b/lib/engine/network/client.py
new file mode 100644 (file)
index 0000000..4ae8852
--- /dev/null
@@ -0,0 +1,63 @@
+from queue import Queue
+from lib.engine.network.network import AbsNetwork, NetworkThread, msg_rpc_call
+from lib.engine.network.binary import BinaryData
+
+
+class ClientThread(NetworkThread):
+
+    def __init__(self, srv_addr, eng, port):
+        self.srv_addr = srv_addr
+        NetworkThread.__init__(self, eng, port)
+        self.msgs = Queue()
+        self.rpc_ret = Queue()
+
+    def _configure_socket(self):
+        self.tcp_sock.connect((self.srv_addr, self.port))
+
+    def _rpc_cb(self, data, sock):
+        self.rpc_ret.put(data)
+
+    def _queue(self, sock):
+        return self.msgs
+
+    def send_msg(self, msg, receiver=None): self.msgs.put(msg)
+
+    def do_rpc(self, funcname, *args, **kwargs):
+        args = list(args)
+        msg_size, msg_data = BinaryData.pack(
+            [msg_rpc_call, funcname, args, kwargs])
+        self.msgs.put((msg_size, msg_data))
+        return self.rpc_ret.get()
+
+
+class Client(AbsNetwork):
+
+    def __init__(self, port, srv_addr):
+        AbsNetwork.__init__(self, port)
+        self.srv_addr = srv_addr
+        self._functions = []
+
+    def start(self, read_cb):
+        return AbsNetwork.start(self, read_cb)
+
+    def _bld_netw_thr(self):
+        srv, port = self.srv_addr.split(':')
+        return ClientThread(srv, self.eng, int(port))
+
+    def _configure_udp(self): pass
+
+    def send_udp(self, data_lst, sender):
+        host, port = self.srv_addr.split(':')
+        msg_size, msg_data = BinaryData.pack([sender] + data_lst)
+        self.udp_sock.sendto(msg_data, (host, int(port)))
+
+    def register_rpc(self, funcname): self._functions += [funcname]
+
+    def unregister_rpc(self, funcname): self._functions.remove(funcname)
+
+    def __getattr__(self, attr):
+        if attr not in self._functions: raise AttributeError(attr)
+
+        def do_rpc(*args, **kwargs):
+            return self.netw_thr.do_rpc(attr, *args, **kwargs)
+        return do_rpc
diff --git a/lib/engine/network/network.py b/lib/engine/network/network.py
new file mode 100644 (file)
index 0000000..41b6180
--- /dev/null
@@ -0,0 +1,172 @@
+from socket import socket, AF_INET, SOCK_DGRAM, error, SOCK_STREAM, \
+    SOL_SOCKET, SO_REUSEADDR
+from traceback import print_exc
+from logging import info
+from select import select
+from time import sleep
+from queue import Empty
+from threading import Thread
+from struct import Struct, error as unpack_error
+from _thread import interrupt_main
+from lib.gameobject import GameObject
+from lib.engine.network.binary import BinaryData
+
+
+msg_rpc_call, msg_rpc_answ = range(2)
+
+
+class _ConnectionError(Exception): pass
+
+
+class NetworkThread(Thread):
+
+    def __init__(self, eng, port):
+        Thread.__init__(self)
+        self.port = port
+        self.daemon = True
+        self.eng = eng
+        self.is_running = True
+        self.size_struct = Struct('!I')
+        self.tcp_sock = socket(AF_INET, SOCK_STREAM)
+        self.tcp_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
+        self._configure_socket()
+        self.connections = [self.tcp_sock]
+
+    def run(self):
+        while self.is_running:
+            sleep(.001)
+            try:
+                readable, writable, exceptional = select(
+                    self.connections, self.connections, self.connections, 1)
+                for sock in readable: self._process_read(sock)
+                for sock in writable: self._process_write(sock)
+                for sock in exceptional: print('exception', sock.getpeername())
+            except (error, AttributeError) as exc: print_exc()
+            # AttributeError happens when the server user exits from a race,
+            # then destroy is being called but _process_read is still alive
+            # and self.eng.cb_mux.add_cb is invoked, but self.eng in None
+            except Exception as exc:
+                print_exc()
+                interrupt_main()
+
+    def _process_read(self, sock):
+        try:
+            data = self.recv_one_msg(sock)
+            if data:
+                try:
+                    msg = BinaryData.unpack(data)
+                    if msg[0] == msg_rpc_call:
+                        funcname, args, kwargs = msg[1:]
+                        self._rpc_cb(funcname, args, kwargs, sock)
+                    elif msg[0] == msg_rpc_answ:
+                        self._rpc_cb(msg[1], sock)
+                    else:
+                        args = [msg, sock]
+                        self.eng.cb_mux.add_cb(self.read_cb, args)
+                except unpack_error as exc:
+                    print(exc)
+                    print_exc()
+        except (_ConnectionError, TypeError) as exc:
+            print_exc()
+            self.notify('on_disconnected', sock)
+            self.connections.remove(sock)
+
+    def _process_write(self, sock):
+        try:
+            msg_size, msg_data = self._queue(sock).get_nowait()
+            sock.sendall(self.size_struct.pack(msg_size))
+            sock.sendall(msg_data)
+        except Empty: pass
+
+    def recv_one_msg(self, sock):
+        lengthbuf = self.recvall(sock, self.size_struct.size)
+        try: length = self.size_struct.unpack(lengthbuf)[0]
+        except unpack_error as exc:
+            print(exc)
+            raise _ConnectionError()
+        return self.recvall(sock, length)
+
+    @staticmethod
+    def recvall(sock, cnt):
+        buf = b''
+        while cnt:
+            newbuf = sock.recv(cnt)
+            if not newbuf: return None
+            buf, cnt = buf + newbuf, cnt - len(newbuf)
+        return buf
+
+    def destroy(self):
+        self.is_running = False
+        self.tcp_sock.close()
+        self.eng = self.tcp_sock = self.connections = None
+
+
+class AbsNetwork(GameObject):
+
+    rate = .1
+    _public_addr = None
+    _local_addr = None
+
+    def __init__(self, port):
+        GameObject.__init__(self)
+        self.netw_thr = self.read_cb = self.udp_sock = self.tcp_sock = \
+            self.udp_sock = None
+        self.port = port
+        self.addr2conn = {}
+
+    def start(self, read_cb):
+        self.eng.attach_obs(self.on_frame, 1)
+        self.read_cb = read_cb
+        self.udp_sock = socket(AF_INET, SOCK_DGRAM)
+        self.udp_sock.setblocking(0)
+        self._configure_udp()
+        try:
+            self.netw_thr = self._bld_netw_thr()
+            self.netw_thr.start()
+            self.netw_thr.read_cb = read_cb
+            args = self.__class__.__name__, self.port
+            info('%s is up, port %s' % args)
+            return True
+        except ValueError:  # e.g. empty server
+            info("can't start the network")
+
+    def register_cb(self, callback):
+        self.read_cb = callback
+        self.netw_thr.read_cb = callback
+
+    def send(self, data_lst, receiver=None):
+        dgram = BinaryData.pack(data_lst)
+        self.netw_thr.send_msg(dgram, receiver)
+
+    def on_frame(self): self.process_udp()
+
+    @property
+    def is_active(self):
+        observers = self.eng.event.observers.values()
+        return self.on_frame in [obs.mth for olst in observers for obs in olst]
+
+    def stop(self):
+        if not self.netw_thr:
+            info('%s was already stopped' % self.__class__.__name__)
+            return
+        self.udp_sock.close()
+        self.netw_thr.destroy()
+        self.udp_sock = self.tcp_sock = self.netw_thr = None
+        self.eng.detach_obs(self.on_frame)
+        self.addr2conn = {}
+        info('%s has been stopped' % self.__class__.__name__)
+
+    def process_udp(self):
+        try: dgram, conn = self.udp_sock.recvfrom(8192)
+        except error: return
+        self.on_udp_pck(dgram, conn)
+        dgram = BinaryData.unpack(dgram)
+        sender, payload = dgram[0], dgram[1:]
+        self.read_cb(payload, conn)
+
+    def on_udp_pck(self, dgram, conn): pass
+
+    def destroy(self):
+        self.stop()
+        info('%s has been destroyed' % self.__class__.__name__)
+        GameObject.destroy(self)
diff --git a/lib/engine/network/server.py b/lib/engine/network/server.py
new file mode 100644 (file)
index 0000000..c53ef88
--- /dev/null
@@ -0,0 +1,103 @@
+from socket import error
+from queue import Queue
+from lib.engine.network.network import AbsNetwork, NetworkThread, \
+    msg_rpc_answ
+from lib.engine.network.binary import BinaryData
+from lib.gameobject import GameObject
+
+
+class ServerThread(NetworkThread, GameObject):
+
+    def __init__(self, eng, rpc_cb, port):
+        NetworkThread.__init__(self, eng, port)
+        GameObject.__init__(self)
+        self.rpc_cb = rpc_cb
+        self.conn2msgs = {}
+
+    def _configure_socket(self):
+        self.tcp_sock.setblocking(0)
+        self.tcp_sock.bind(('', self.port))
+        self.tcp_sock.listen(1)
+
+    def _process_read(self, sock):
+        if sock is self.tcp_sock:
+            conn, addr = sock.accept()
+            conn.setblocking(1)  # required on osx
+            self.connections += [conn]
+            self.conn2msgs[conn] = Queue()
+            self.notify('on_connected', conn)
+        else:
+            NetworkThread._process_read(self, sock)
+
+    def _rpc_cb(self, funcname, args, kwargs, sock):
+        self.eng.cb_mux.add_cb(self.rpc_cb, [funcname, args, kwargs, sock])
+
+    def _queue(self, sock):
+        return self.conn2msgs[sock]
+
+    def send_msg(self, conn, msg):
+        self.conn2msgs[conn].put(msg)
+
+
+class Server(AbsNetwork):
+
+    def __init__(self, port):
+        AbsNetwork.__init__(self, port)
+        self.conn_cb = None
+        self.fname2ref = {}
+
+    @property
+    def connections(self): return self.netw_thr.connections[1:]
+
+    def start(self, read_cb, conn_cb):
+        #TODO: parameters differ from overridden start
+        AbsNetwork.start(self, read_cb)
+        self.conn_cb = conn_cb
+        self.netw_thr.attach(self.on_connected)
+        self.netw_thr.attach(self.on_disconnected)
+
+    def on_connected(self, conn):
+        self.notify('on_connected', conn)
+
+    def on_disconnected(self, conn):
+        self.notify('on_disconnected', conn)
+
+    def _bld_netw_thr(self):
+        return ServerThread(self.eng, self.rpc_cb, self.port)
+
+    def _configure_udp(self): self.udp_sock.bind(('', self.port))
+
+    def send(self, data_lst, receiver=None):
+        receivers = [cln for cln in self.connections if cln == receiver]
+        dests = receivers if receiver else self.connections
+        dgram = BinaryData.pack(data_lst)
+        list(map(lambda cln: self.netw_thr.send_msg(cln, dgram), dests))
+
+    def rpc_cb(self, funcname, args, kwargs, conn):
+        kwargs = kwargs or {}
+        kwargs['sender'] = conn
+        ret = self.fname2ref[funcname](*args, **kwargs)
+        msg_size, msg_data = BinaryData.pack([msg_rpc_answ, ret])
+        self.netw_thr.send_msg(conn, (msg_size, msg_data))
+
+    def register_rpc(self, func): self.fname2ref[func.__name__] = func
+
+    def unregister_rpc(self, func): del self.fname2ref[func.__name__]
+
+    def on_udp_pck(self, dgram, conn):
+        sender = BinaryData.unpack(dgram)[0]
+        if sender not in self.addr2conn: self.addr2conn[sender] = conn
+
+    def process_udp(self):
+        try: dgram, conn = self.udp_sock.recvfrom(8192)
+        except error: return
+        try:
+            dgram = BinaryData.unpack(dgram)
+            sender, payload = dgram[0], dgram[1:]
+            self.read_cb(payload, sender)
+        except IndexError as exc: print(exc)
+
+    def send_udp(self, data_lst, receiver):
+        if receiver[0] not in self.addr2conn: return
+        msg_size, msg_data = BinaryData.pack(['server'] + data_lst)
+        self.udp_sock.sendto(msg_data, self.addr2conn[receiver[0]])
diff --git a/lib/engine/particle.py b/lib/engine/particle.py
new file mode 100755 (executable)
index 0000000..462232d
--- /dev/null
@@ -0,0 +1,16 @@
+# from math import pi
+# from ..gameobject import GameObject
+from lib.lib.p3d.particle import P3dParticle
+Particle = P3dParticle
+
+
+# class Particle(GameObject):
+
+#    def __init__(self, emitter, texture, npart, color=(1, 1, 1, 1), ampl=pi/6,
+#                 ray=.5, rate=.0001, gravity=-.85, vel=3.8, part_lifetime=1.0,
+#                 autodestroy=None):
+#        if not self.eng.lib.version.startswith('1.10'): return
+#        GameObject.__init__(self)
+#        LibParticle(
+#            emitter, texture, npart, color, ampl, ray, rate, gravity, vel,
+#            part_lifetime, autodestroy)
diff --git a/lib/engine/pause.py b/lib/engine/pause.py
new file mode 100644 (file)
index 0000000..bd1f37c
--- /dev/null
@@ -0,0 +1,77 @@
+from direct.gui.DirectFrame import DirectFrame
+from lib.gameobject import GuiColleague, LogicColleague, GameObject, \
+    Colleague
+from lib.lib.p3d.pause import P3dPause
+LibPause = P3dPause
+
+
+class PauseGui(GuiColleague):
+
+    def __init__(self, mediator):
+        GuiColleague.__init__(self, mediator)
+        self.pause_frm = None
+
+    def toggle(self, show_frm=True):
+        if not self.mediator.logic._pause.paused:
+            #TODO: don't access protected members
+            if show_frm:
+                self.pause_frm = DirectFrame(frameColor=(.3, .3, .3, .7),
+                                             frameSize=(-1.8, 1.8, -1, 1))
+        else:
+            if self.pause_frm: self.pause_frm.destroy()
+
+    def destroy(self):
+        if self.pause_frm: self.pause_frm = self.pause_frm.destroy()
+        GuiColleague.destroy(self)
+
+
+class PauseLogic(LogicColleague):
+
+    def __init__(self, mediator):
+        LogicColleague.__init__(self, mediator)
+        self._pause = LibPause()
+
+    def remove_task(self, tsk):
+        self._pause.remove_task(tsk)
+
+    def pause(self):
+        self.notify('on_pause')
+        return self._pause.pause()
+
+    def resume(self):
+        self.notify('on_resume')
+        return self._pause.resume()
+
+    def toggle(self, show_frm=True):
+        self.mediator.gui.toggle(show_frm)
+        (self.resume if self._pause.paused else self.pause)()
+
+    def destroy(self):
+        self._pause.destroy()
+        LogicColleague.destroy(self)
+
+
+class PauseFacade:
+
+    @property
+    def paused(self):
+        return self.logic._pause.paused
+        #TODO: don't access protected members
+
+
+class PauseMgr(GameObject, Colleague, PauseFacade):
+
+    def __init__(self, mediator):
+        GameObject.__init__(self)
+        Colleague.__init__(self, mediator)
+        self.gui = PauseGui(self)
+        self.logic = PauseLogic(self)
+
+    def remove_task(self, tsk):
+        return self.logic.remove_task(tsk)
+
+    def destroy(self):
+        self.gui = self.gui.destroy()
+        self.logic = self.logic.destroy()
+        GameObject.destroy(self)
+        Colleague.destroy(self)
diff --git a/lib/engine/phys.py b/lib/engine/phys.py
new file mode 100644 (file)
index 0000000..13b1956
--- /dev/null
@@ -0,0 +1,113 @@
+from logging import info
+from lib.gameobject import Colleague
+from lib.lib.bullet.bullet import (
+    BulletPhysWorld, BulletTriangleMesh, BulletTriangleMeshShape,
+    BulletRigidBodyNode, BulletGhostNode)
+
+
+PhysWorld = BulletPhysWorld
+TriangleMesh = BulletTriangleMesh
+TriangleMeshShape = BulletTriangleMeshShape
+RigidBodyNode = BulletRigidBodyNode
+GhostNode = BulletGhostNode
+
+
+class CollInfo:
+
+    def __init__(self, node, time):
+        self.node = node
+        self.time = time
+
+
+class PhysFacade:
+
+    def attach_rigid_body(self, rbnode):
+        return self.root.attach_rigid_body(rbnode)
+
+    def remove_rigid_body(self, rbnode):
+        return self.root.remove_rigid_body(rbnode)
+
+    def attach_ghost(self, gnode): return self.root.attach_ghost(gnode)
+    def remove_ghost(self, gnode): return self.root.remove_ghost(gnode)
+    def attach_vehicle(self, vehicle): return self.root.attach_vehicle(vehicle)
+    def remove_vehicle(self, vehicle): return self.root.remove_vehicle(vehicle)
+
+    def ray_test_all(self, from_pos, to_pos, mask=None):
+        return self.root.ray_test_all(from_pos, to_pos, mask)
+
+    def ray_test_closest(self, from_pos, to_pos, mask=None):
+        return self.root.ray_test_closest(from_pos, to_pos, mask)
+
+
+class PhysMgr(Colleague, PhysFacade):
+
+    def __init__(self, mediator):
+        Colleague.__init__(self, mediator)
+        self.collision_objs = []  # objects to be processed
+        self.__obj2coll = {}  # {obj: [(node, coll_time), ...], ...}
+        self.root = None
+        self.__debug_np = None
+        PhysFacade.__init__(self)
+
+    def reset(self):
+        self.collision_objs = []
+        self.__obj2coll = {}
+        self.root = PhysWorld()
+        self.root.set_gravity((0, 0, -8.5))
+        self.root.init_debug()
+
+    def start(self):
+        self.eng.attach_obs(self.on_frame, 2)
+
+    def on_frame(self):
+        self.root.do_phys(self.eng.lib.last_frame_dt, 10, 1/180.0)
+        self.__do_collisions()
+
+    def ray_test_closest(self, top, bottom):
+        #TODO: differs from PhysFacade's signature
+        return self.root.ray_test_closest(top, bottom)
+
+    def add_collision_obj(self, node): self.collision_objs += [node]
+
+    def remove_collision_obj(self, node):
+        try: self.collision_objs.remove(node)
+        except ValueError:
+            info("can't remove collision object %s" % node)
+            # it may happen with weapons during pause
+
+    def stop(self):
+        self.root.stop()
+        self.root = None
+        self.eng.detach_obs(self.on_frame)
+
+    def __do_collisions(self):
+        to_clear = self.collision_objs[:]
+        # identical collisions are ignored for .25 seconds
+        for obj in self.collision_objs:
+            if obj not in self.__obj2coll: self.__obj2coll[obj] = []
+            # for contact in self.root.get_contacts(obj):
+            # this doesn't work in 1.9, the following works
+            # odd, this doesn't work too
+            # for contact in self.root.wld.contact_test(obj).get_contacts():
+            result = self.root._wld.contact_test(obj)
+            #TODO: access a protected member
+            for contact in result.get_contacts():
+                self.__process_contact(obj, contact.get_node0(), to_clear)
+                self.__process_contact(obj, contact.get_node1(), to_clear)
+        for obj in to_clear:
+            if obj in self.__obj2coll:  # it may be that it isn't here e.g.
+                # when you fire a rocket while you're very close to the prev
+                # car and the rocket is removed suddenly
+                for coll in self.__obj2coll[obj]:
+                    if self.eng.curr_time - coll.time > .25:
+                        self.__obj2coll[obj].remove(coll)
+
+    def __process_contact(self, obj, node, to_clear):
+        if node == obj: return
+        if obj in to_clear: to_clear.remove(obj)
+        if node in [coll.node for coll in self.__obj2coll[obj]]: return
+        self.__obj2coll[obj] += [CollInfo(node, self.eng.curr_time)]
+        self.eng.event.notify('on_collision', obj, node)
+
+    def toggle_dbg(self):
+        if self.root: self.root.toggle_dbg()
diff --git a/lib/engine/profiler.py b/lib/engine/profiler.py
new file mode 100644 (file)
index 0000000..9cdd54c
--- /dev/null
@@ -0,0 +1,72 @@
+from os.path import exists
+
+
+if not exists('main.pyo'):  # we don't deploy cProfile
+    from cProfile import Profile
+    from pstats import Stats
+    from io import StringIO
+
+
+class AbsProfiler:
+
+    @staticmethod
+    def build(percall):
+        prof_cls = AbsProfiler
+        if not exists('main.pyo'):
+            prof_cls = PerCallProfiler if percall else Profiler
+        return prof_cls(percall)
+
+    def __init__(self, percall): pass
+
+    def printstats(self): pass
+
+    def toggle(self): pass
+
+    def destroy(self): pass
+
+
+class Profiler(AbsProfiler):
+
+    def __init__(self, percall):
+        AbsProfiler.__init__(self, percall)
+        self.percall = percall
+        self.is_profiling = False  # we can't infer from cProfile
+        self.prof = Profile()
+        self.stats = None
+
+    def toggle(self):
+        if not self.is_profiling: self.__enable()
+        else:
+            self.__disable()
+            self.printstats()
+
+    def __enable(self):
+        self.prof.enable()
+        self.is_profiling = True
+
+    def __disable(self):
+        self.prof.disable()
+        self.is_profiling = False
+
+    def printstats(self):
+        self.prof.disable()
+        sio = StringIO()
+        self.stats = Stats(self.prof, stream=sio).sort_stats('cumulative')
+        self.stats.print_stats()
+        self._print_lines(sio)
+
+    @staticmethod
+    def _print_lines(sio): print(sio.getvalue())
+
+
+class PerCallProfiler(Profiler):
+
+    def _print_lines(self, sio):
+        lines = sio.getvalue().split('\n')
+        header_lines = lines[:5]
+        content_lines = [line.split() for line in lines[5:] if line]
+        sorted_lines = sorted(content_lines, key=lambda line: line[4])
+        sorted_lines = reversed(sorted_lines)
+        # line[4] is the percall value
+        joined_lines = ['\t'.join(line) for line in sorted_lines]
+        print('\n'.join(header_lines + joined_lines))
diff --git a/lib/engine/shader.py b/lib/engine/shader.py
new file mode 100644 (file)
index 0000000..d81742c
--- /dev/null
@@ -0,0 +1,4 @@
+from lib.lib.p3d.shader import P3dShaderMgr
+
+
+ShaderMgr = P3dShaderMgr
diff --git a/lib/engine/vec.py b/lib/engine/vec.py
new file mode 100644 (file)
index 0000000..9e94f55
--- /dev/null
@@ -0,0 +1,3 @@
+from lib.lib.p3d.vec import P3dVec2, P3dVec3
+Vec = P3dVec3
+Vec2 = P3dVec2
diff --git a/lib/facade.py b/lib/facade.py
new file mode 100644 (file)
index 0000000..bb9bd5a
--- /dev/null
@@ -0,0 +1,23 @@
+# class Facade(object):
+
+#     # def _fwd_mth(self, mth_name, tgt_mth):
+#     #     def fun(*args, **kwargs): return tgt_mth(*args, **kwargs)
+#     #     setattr(self, mth_name, fun)
+
+#     # def _fwd_prop(self, prop_name, tgt_prop):
+#     #     setattr(self.__class__, prop_name, property(lambda self: tgt_prop))
+
+#     def __init__(self, prop_lst=[], mth_lst=[]):
+#         list(map(lambda args: self.__fwd_prop(*args), prop_lst))
+#         list(map(lambda args: self.__fwd_mth(*args), mth_lst))
+#         # try detecting if the forwarded item is a prop or a method, so we
+#         # can pass only a single list
+
+#     def __fwd_mth(self, mth_name, tgt_mth):
+#         def fun(*args, **kwargs): return tgt_mth(self)(*args, **kwargs)
+#         setattr(self, mth_name, fun)
+
+#     def __fwd_prop(self, prop_name, tgt_prop):
+#         setattr(self.__class__, prop_name, property(tgt_prop))
+
+#     def destroy(self): pass
diff --git a/lib/game.py b/lib/game.py
new file mode 100644 (file)
index 0000000..490a843
--- /dev/null
@@ -0,0 +1,37 @@
+from abc import ABCMeta
+from lib.gameobject import LogicColleague, GameObject
+from lib.engine.engine import Engine
+
+
+class GameLogic(LogicColleague):
+
+    def on_start(self): pass
+
+
+class GameFacade:
+
+    def demand(self, tgt_state, *args):
+        return self.fsm.demand(tgt_state, *args)
+
+
+class GameBase(GameObject, GameFacade):  # it doesn't manage the window
+    __metaclass__ = ABCMeta
+
+    def __init__(self, cfg, client_cls=None):
+        self.logic = LogicColleague(self)
+        self.eng = Engine(cfg, self.destroy, client_cls)
+        GameObject.__init__(self)
+
+    def destroy(self):
+        self.logic.destroy()
+        GameObject.destroy(self)
+        # self.eng = self.eng.destroy()
+        self.eng.server.destroy()
+        self.eng.client.destroy()
+
+
+class Game(GameBase):  # it adds the window
+
+    def run(self):
+        self.logic.on_start()
+        base.run()
diff --git a/lib/gameobject.py b/lib/gameobject.py
new file mode 100644 (file)
index 0000000..58c69a6
--- /dev/null
@@ -0,0 +1,91 @@
+from direct.fsm.FSM import FSM
+from direct.showbase.DirectObject import DirectObject
+from lib.observer import Subject
+
+
+class Colleague(Subject):
+
+    eng = None
+
+    def __init__(self, mediator):
+        Subject.__init__(self)
+        self.mediator = mediator  # refactor: remove it
+
+    def destroy(self):
+        self.mediator = None
+        Subject.destroy(self)
+
+
+class FsmColleague(FSM, Colleague):
+
+    def __init__(self, mediator):
+        FSM.__init__(self, self.__class__.__name__)
+        Colleague.__init__(self, mediator)
+
+    def destroy(self):
+        if self.state: self.cleanup()
+        Colleague.destroy(self)
+
+
+class EventColleague(Colleague, DirectObject):
+
+    def destroy(self):
+        self.ignoreAll()
+        Colleague.destroy(self)
+
+
+class AudioColleague(Colleague): pass
+
+
+class AiColleague(Colleague): pass
+
+
+class GfxColleague(Colleague): pass
+
+
+class GuiColleague(Colleague): pass
+
+
+class LogicColleague(Colleague):
+
+    def on_start(self): pass
+
+
+class PhysColleague(Colleague): pass
+
+
+class GODirector:
+
+    def __init__(self, tgt_obj, init_lst, end_cb):
+        self.__obj = tgt_obj
+        tgt_obj.attach(self.on_comp_blt)
+        self.end_cb = end_cb
+        self.completed = [False for _ in init_lst]
+        self.pending = {}
+        self.__init_lst = init_lst
+        for idx, _ in enumerate(init_lst): self.__process_lst(tgt_obj, idx)
+
+    def __process_lst(self, obj, idx):
+        if not self.__init_lst[idx]:
+            self.end_lst(idx)
+            return
+        comp_info = self.__init_lst[idx].pop(0)
+        attr_name, cls, arguments = comp_info
+        self.pending[cls.__name__] = idx
+        setattr(obj, attr_name, cls(*arguments))
+
+    def on_comp_blt(self, obj):
+        self.__process_lst(obj.mediator, self.pending[obj.__class__.__name__])
+
+    def end_lst(self, idx):
+        self.completed[idx] = True
+        if all(self.completed):
+            if self.end_cb: self.end_cb()
+            self.destroy()
+
+    def destroy(self):
+        self.__obj.detach(self.on_comp_blt)
+        self.__obj = self.end_cb = self.__init_lst = None
+
+
+class GameObject(Subject): pass
diff --git a/lib/lib/__init__.py b/lib/lib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/lib/builder.py b/lib/lib/builder.py
new file mode 100644 (file)
index 0000000..287d95b
--- /dev/null
@@ -0,0 +1,13 @@
+from lib.lib.p3d.p3d import LibP3d
+
+
+class LibBuilder:
+    '''This classe builds the implementation of the library abstraction  (for
+    the Dependency Inversion Principle).'''
+
+    @staticmethod
+    def build():
+        '''This method actually builds the library implementation.
+        Now it builds Panda3D's implementation layer, but it may be used as a
+        dispatcher (e.g. for new Panda3D versions).'''
+        return LibP3d()
diff --git a/lib/lib/bullet/__init__.py b/lib/lib/bullet/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/lib/bullet/bullet.py b/lib/lib/bullet/bullet.py
new file mode 100644 (file)
index 0000000..b2f390d
--- /dev/null
@@ -0,0 +1,79 @@
+from panda3d.bullet import \
+    BulletWorld as BWorld, \
+    BulletDebugNode as BDebugNode, \
+    BulletTriangleMesh as BTriangleMesh, \
+    BulletTriangleMeshShape as BTriangleMeshShape, \
+    BulletRigidBodyNode as BRigidBodyNode, \
+    BulletGhostNode as BGhostNode
+
+
+class BulletPhysWorld:
+
+    def __init__(self):
+        self._wld = BWorld()
+        self.__debug_np = None
+
+    def attach_rigid_body(self, rbnode):
+        return self._wld.attach_rigid_body(rbnode)
+
+    def remove_rigid_body(self, rbnode):
+        return self._wld.remove_rigid_body(rbnode)
+
+    def attach_ghost(self, gnode): return self._wld.attach_ghost(gnode)
+    def remove_ghost(self, gnode): return self._wld.remove_ghost(gnode)
+    def attach_vehicle(self, vehicle): return self._wld.attach_vehicle(vehicle)
+    def remove_vehicle(self, vehicle): return self._wld.remove_vehicle(vehicle)
+
+    def ray_test_closest(self, from_pos, to_pos, mask=None):
+        if mask is not None:
+            res = self._wld.ray_test_closest(from_pos, to_pos, mask)
+        else: res = self._wld.ray_test_closest(from_pos, to_pos)
+        return res
+
+    def do_phys(self, dt, max_substeps, stepsize):
+        return self._wld.do_physics(dt, max_substeps, stepsize)
+
+    def set_gravity(self, vec): return self._wld.set_gravity(vec)
+
+    def init_debug(self):
+        debug_node = BDebugNode('Debug')
+        debug_node.show_bounding_boxes(True)
+        self.__debug_np = render.attach_new_node(debug_node)
+        self._wld.set_debug_node(self.__debug_np.node())
+
+    def stop(self): self.__debug_np.remove_node()
+
+    def ray_test_all(self, pt_a, pt_b, mask=None):
+        args = [pt_a._vec, pt_b._vec, mask] if mask else [pt_a._vec, pt_b._vec]
+        #TODO: access to protected member
+        return self._wld.ray_test_all(*args)
+
+    def toggle_dbg(self):
+        hidden = self.__debug_np.is_hidden()
+        (self.__debug_np.show if hidden else self.__debug_np.hide)()
+
+
+class BulletTriangleMesh:
+
+    def __init__(self):
+        self._mesh = BTriangleMesh()
+
+    def add_geom(self, geom, rm_dupl, xform):
+        return self._mesh.add_geom(geom, rm_dupl, xform)
+
+
+class BulletTriangleMeshShape:
+
+    def __init__(self, mesh, dynamic):
+        self._mesh_shape = BTriangleMeshShape(mesh._mesh, dynamic=dynamic)
+        #TODO: access to protected member
+
+
+class BulletRigidBodyNode:
+
+    def __init__(self, name): self._node = BRigidBodyNode(name)
+
+
+class BulletGhostNode:
+
+    def __init__(self, name): self._node = BGhostNode(name)
diff --git a/lib/lib/gui.py b/lib/lib/gui.py
new file mode 100644 (file)
index 0000000..aad6ddd
--- /dev/null
@@ -0,0 +1,16 @@
+'''This module binds abstract GUI classes and actual implementation classes
+(for the Dependency Inversion Principle).'''
+from lib.lib.p3d.gui import P3dImg, P3dBtn, P3dSlider, P3dCheckBtn, \
+    P3dOptionMenu, P3dEntry, P3dLabel, P3dTxt, P3dFrame, P3dScrolledFrame
+
+
+Img = P3dImg
+Btn = P3dBtn
+Slider = P3dSlider
+CheckBtn = P3dCheckBtn
+OptionMenu = P3dOptionMenu
+Entry = P3dEntry
+Label = P3dLabel
+Text = P3dTxt
+Frame = P3dFrame
+ScrolledFrame = P3dScrolledFrame
diff --git a/lib/lib/ivals.py b/lib/lib/ivals.py
new file mode 100644 (file)
index 0000000..0498328
--- /dev/null
@@ -0,0 +1,9 @@
+'''This module binds abstract interval classes and actual implementation
+classes (for the Dependency Inversion Principle).'''
+from lib.lib.p3d.ivals import P3dSeq, P3dWait, P3dPosIval, P3dFunc
+
+
+Seq = P3dSeq
+Wait = P3dWait
+PosIval = P3dPosIval
+Func = P3dFunc
diff --git a/lib/lib/p3d/__init__.py b/lib/lib/p3d/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/lib/p3d/audio.py b/lib/lib/p3d/audio.py
new file mode 100644 (file)
index 0000000..4d2b281
--- /dev/null
@@ -0,0 +1,19 @@
+from panda3d.core import AudioSound
+
+
+class P3dSound:
+
+    def __init__(self, filepath):
+        self.snd = loader.loadSfx(filepath)
+
+    def stop(self): return self.snd.stop()
+    def set_loop(self, val): return self.snd.set_loop(val)
+    def set_volume(self, vol): return self.snd.set_volume(vol)
+    def set_play_rate(self, rate): return self.snd.set_play_rate(rate)
+
+    def play(self):
+        if self.snd.status() != AudioSound.PLAYING: return self.snd.play()
+        return None
+
+    @property
+    def playing(self): return self.snd.status() == AudioSound.PLAYING
diff --git a/lib/lib/p3d/gfx.py b/lib/lib/p3d/gfx.py
new file mode 100755 (executable)
index 0000000..0e2fcad
--- /dev/null
@@ -0,0 +1,341 @@
+import datetime
+from logging import debug, info
+from os import getcwd
+from os.path import exists, dirname
+from panda3d.core import get_model_path, AntialiasAttrib, PandaNode, \
+    LightRampAttrib, Camera, OrthographicLens, NodePath, OmniBoundingVolume, \
+    AmbientLight as P3DAmbientLight, Spotlight as P3DSpotlight, Point2, \
+    Point3, Texture
+from direct.filter.CommonFilters import CommonFilters
+from direct.actor.Actor import Actor
+from lib.lib.p3d.p3d import LibP3d
+
+
+class RenderToTexture:
+
+    def __init__(self, size=(256, 256)):
+        self.__set_buffer(size)
+        self.__set_display_region()
+        self.__set_camera()
+        self.__set_root()
+        self.display_region.set_camera(self.camera)
+
+    def __set_buffer(self, size):
+        self.buffer = base.win.make_texture_buffer('result buffer', size[0],
+                                                   size[1])
+        self.buffer.set_sort(-100)
+
+    def __set_display_region(self):
+        self.display_region = self.buffer.make_display_region()
+        self.display_region.set_sort(20)
+
+    def __set_camera(self):
+        self.camera = NodePath(Camera('camera 2d'))
+        lens = OrthographicLens()
+        lens.set_film_size(1, 1)
+        lens.set_near_far(-1000, 1000)
+        self.camera.node().set_lens(lens)
+
+    def __set_root(self):
+        self.root = NodePath('root')
+        self.root.set_depth_test(False)
+        self.root.set_depth_write(False)
+        self.camera.reparent_to(self.root)
+
+    @property
+    def texture(self): return self.buffer.get_texture()
+
+    def destroy(self):
+        base.graphicsEngine.remove_window(self.buffer)
+        if base.win:  # if you close the window during a race
+            base.win.remove_display_region(self.display_region)
+        list(map(lambda node: node.remove_node(), [self.camera, self.root]))
+
+
+class P3dGfxMgr:
+
+    def __init__(self, model_path, antialiasing, shaders, srgb):
+        self.root = P3dNode(render)
+        self.__srgb = srgb
+        self.callbacks = {}
+        self.filters = None
+        get_model_path().append_directory(model_path)
+        if LibP3d.runtime():
+            root_dir = LibP3d.p3dpath(dirname(__file__))
+            paths = [root_dir + '/' + model_path, root_dir]
+            list(map(get_model_path().append_directory, paths))
+        render.set_shader_auto()
+        # render.set_two_sided(True)  # it breaks shadows
+        if antialiasing: render.set_antialias(AntialiasAttrib.MAuto)
+        if shaders and base.win:
+            self.filters = CommonFilters(base.win, base.cam)
+
+    def load_model(self, filename, callback=None, anim=None):
+        ext = '.bam' if exists(filename + '.bam') else ''
+        if anim:
+            anim_dct = {'anim': filename + '-Anim' + ext}
+            node = P3dNode(self.set_srgb(Actor(filename + ext, anim_dct)))
+        elif callback:
+            callb = lambda model: callback(P3dNode(self.set_srgb(model)))
+            node = loader.loadModel(filename + ext, callback=callb)
+        else:
+            node = P3dNode(self.set_srgb(
+                loader.loadModel(LibP3d.p3dpath(filename + ext))))
+        return node
+
+    def set_srgb(self, model):
+        if self.__srgb:
+            for texture in model.find_all_textures():
+                if texture.get_format() in [Texture.F_rgba, Texture.F_rgbm]:
+                    texture.set_format(Texture.F_srgb_alpha)
+                elif texture.get_format() in [Texture.F_rgb]:
+                    texture.set_format(Texture.F_srgb)
+        return model
+
+    @staticmethod
+    def toggle_aa():
+        aa_not_none = render.get_antialias() != AntialiasAttrib.MNone
+        if render.has_antialias() and aa_not_none:
+            render.clear_antialias()
+        else: render.set_antialias(AntialiasAttrib.MAuto, 1)
+
+    def set_toon(self):
+        tmp_node = NodePath(PandaNode('temp node'))
+        tmp_node.set_attrib(LightRampAttrib.make_single_threshold(.5, .4))
+        tmp_node.set_shader_auto()
+        base.cam.node().set_initial_state(tmp_node.get_state())
+        self.filters.set_cartoon_ink(separation=1)
+
+    def set_bloom(self):
+        if not base.win: return
+        self.filters.setBloom(
+            blend=(.3, .4, .3, 0), mintrigger=.6, maxtrigger=1.0, desat=.6,
+            intensity=1.0, size='medium')
+        # default: (.3, .4, .3, 0), .6, 1, .6, 1, 'medium'
+
+    @staticmethod
+    def pos2d(node):
+        p3d = base.cam.get_relative_point(node.node, Point3(0, 0, 0))
+        p2d = Point2()
+        return p2d if base.camLens.project(p3d, p2d) else None
+
+    @property
+    def shader_support(self):
+        return base.win.get_gsg().get_supports_basic_shaders()
+
+    def screenshot(self, path=None):
+        time = datetime.datetime.now().strftime('%y%m%d%H%M%S')
+        #res = base.win.save_screenshot(Filename(path or ("yocto%s.png" % time)))
+        #debug('screenshot %s (%s)' % (path or ("yocto%s.png" % time), res))
+        res = base.screenshot(path or ("yocto%s.png" % time), False)
+        info('screenshot %s (%s; %s)' % (path or ("yocto%s.png" % time), res, getcwd()))
+
+    @staticmethod
+    def enable_shader(): render.set_shader_auto()
+
+    @staticmethod
+    def disable_shader(): render.set_shader_off()
+
+    @staticmethod
+    def print_stats(two_d=True, three_d=True, analyze=True, ls=True):
+        '''Print graphics stats. They use standard output (from p3d).'''
+        info = []
+        if two_d and analyze:
+            info +=[('render2d.analyze', base.render2d.analyze)]
+        if three_d and analyze:
+            info +=[('render.analyze', base.render.analyze)]
+        if two_d and ls:
+            info +=[('render2d.ls', base.render2d.ls)]
+        if three_d and ls:
+            info +=[('render.ls', base.render.ls)]
+        for elm in info:
+            print('\n\n#####\n%s()' % elm[0])
+            elm[1]()
+
+
+class P3dNode:
+
+    def __init__(self, nodepath):
+        self.nodepath = nodepath
+        self.node.set_python_tag('libnode', self)
+
+    def set_collide_mask(self, mask): return self.node.set_collide_mask(mask)
+    def set_x(self, val): return self.node.set_x(val)
+    def set_y(self, val): return self.node.set_y(val)
+    def set_z(self, val): return self.node.set_z(val)
+    def set_hpr(self, val): return self.node.set_hpr(val)
+    def set_h(self, val): return self.node.set_h(val)
+    def set_p(self, val): return self.node.set_p(val)
+    def set_r(self, val): return self.node.set_r(val)
+    def set_scale(self, val): return self.node.set_scale(val)
+    def set_transparency(self, val): return self.node.set_transparency(val)
+    def set_alpha_scale(self, val): return self.node.set_alpha_scale(val)
+    def set_texture(self, texturestage, texture):
+        return self.node.set_texture(texturestage, texture)
+    def has_tag(self, name): return self.node.has_tag(name)
+    def get_tag(self, name): return self.node.get_tag(name)
+    def get_python_tag(self, name): return self.node.get_python_tag(name)
+    def remove_node(self): return self.node.remove_node()
+    def flatten_strong(self): return self.node.flatten_strong()
+    def clear_model_nodes(self): return self.node.clear_model_nodes()
+    def show(self): return self.node.show()
+    def set_depth_offset(self, val): return self.node.set_depth_offset(val)
+    def loop(self, val): return self.node.loop(val)
+    def cleanup(self): return self.node.cleanup()
+    def write_bam_file(self, fname): return self.node.write_bam_file(fname)
+
+    def attach_node(self, name):
+        return P3dNode(self.node.attach_new_node(name))
+
+    def add_shape(self, shape):
+        return self.node.node().add_shape(shape._mesh_shape)
+        #TODO: don't access a protected member
+
+    @property
+    def name(self): return self.node.get_name()
+
+    @property
+    def node(self): return self.nodepath
+
+    @property
+    def p3dnode(self): return self.node.node()
+
+    def set_pos(self, pos): return self.node.set_pos(pos._vec)
+        #TODO: don't access a protected member
+
+    def get_pos(self, other=None):
+        return self.node.get_pos(* [] if other is None else [other.node])
+
+    @property
+    def x(self): return self.node.get_x()
+
+    @property
+    def y(self): return self.node.get_y()
+
+    @property
+    def z(self): return self.node.get_z()
+
+    @property
+    def hpr(self): return self.node.get_hpr()
+
+    @property
+    def h(self): return self.node.get_h()
+
+    @property
+    def p(self): return self.node.get_p()
+
+    @property
+    def r(self): return self.node.get_r()
+
+    @property
+    def scale(self): return self.node.get_scale()
+
+    @property
+    def is_empty(self): return self.node.is_empty()
+
+    def get_relative_vector(self, node, vec):
+        return self.node.get_relative_vector(node.node, vec)
+
+    def set_material(self, mat): return self.node.set_material(mat, 1)
+
+    def set_python_tag(self, name, val):
+        return self.node.set_python_tag(name, val)
+
+    def get_distance(self, other_node):
+        return self.node.get_distance(other_node.node)
+
+    def reparent_to(self, parent): return self.node.reparent_to(parent.node)
+
+    def wrt_reparent_to(self, parent):
+        return self.node.wrt_reparent_to(parent.node)
+
+    @staticmethod
+    def __get_pandanode(nodepath):
+        if nodepath.has_python_tag('libnode'):
+            return nodepath.get_python_tag('libnode')
+        return P3dNode(nodepath)
+
+    def find_all_matches(self, name):
+        nodes = self.node.find_all_matches(name)
+        return [self.__get_pandanode(node) for node in nodes]
+
+    def find(self, name):
+        model = self.node.find(name)
+        if model: return self.__get_pandanode(model)
+
+    def optimize(self):
+        self.node.prepare_scene(base.win.get_gsg())  # crash with texture.set_format
+        self.node.premunge_scene(base.win.get_gsg())
+
+    def hide(self, mask=None):
+        return self.node.hide(*[] if mask is None else [mask])
+
+    @property
+    def tight_bounds(self): return self.node.get_tight_bounds()
+
+    @property
+    def parent(self): return self.node.get_parent()
+
+    @property
+    def children(self): return self.node.get_children()
+
+    def destroy(self): return self.node.remove_node()
+
+
+class P3dAnimNode:
+
+    def __init__(self, filepath, anim_dct):
+        self.node = Actor(filepath, anim_dct)
+
+    def loop(self, val): return self.node.loop(val)
+
+    def reparent_to(self, node): self.node.reparent_to(node)
+
+    @property
+    def name(self): return self.node.get_name()
+
+    def optimize(self):
+        self.node.prepare_scene(base.win.get_gsg())
+        self.node.premunge_scene(base.win.get_gsg())
+
+    def set_omni(self):
+        self.node.node().set_bounds(OmniBoundingVolume())
+        self.node.node().set_final(True)
+
+    def destroy(self): self.node.cleanup()
+
+
+class P3dAmbientLight:
+
+    def __init__(self, color):
+        ambient_lgt = P3DAmbientLight('ambient light')
+        ambient_lgt.set_color(color)
+        self.ambient_np = render.attach_new_node(ambient_lgt)
+        render.set_light(self.ambient_np)
+
+    def destroy(self):
+        render.clear_light(self.ambient_np)
+        self.ambient_np.remove_node()
+
+
+class P3dSpotlight:
+
+    def __init__(self, mask=None):
+        self.spot_lgt = render.attach_new_node(P3DSpotlight('spot'))
+        snode = self.spot_lgt.node()
+        snode.set_scene(render)
+        snode.set_shadow_caster(True, 1024, 1024)
+        snode.get_lens().set_fov(40)
+        snode.get_lens().set_near_far(20, 200)
+        if mask: snode.set_camera_mask(mask)
+        render.set_light(self.spot_lgt)
+
+    def set_pos(self, pos): return self.spot_lgt.set_pos(*pos)
+
+    def look_at(self, pos): return self.spot_lgt.look_at(*pos)
+
+    def set_color(self, color): return self.spot_lgt.set_color(*color)
+
+    def destroy(self):
+        render.clear_light(self.spot_lgt)
+        self.spot_lgt.remove_node()
diff --git a/lib/lib/p3d/gui.py b/lib/lib/p3d/gui.py
new file mode 100755 (executable)
index 0000000..1e702d2
--- /dev/null
@@ -0,0 +1,405 @@
+from inspect import getmro
+from panda3d.core import TextNode, Texture
+from direct.gui.DirectGuiGlobals import FLAT, ENTER, EXIT, DISABLED, NORMAL, \
+    B1PRESS
+from direct.showbase.DirectObject import DirectObject
+from direct.gui.DirectButton import DirectButton
+from direct.gui.DirectCheckButton import DirectCheckButton
+from direct.gui.DirectOptionMenu import DirectOptionMenu
+from direct.gui.OnscreenImage import OnscreenImage
+from direct.gui.DirectSlider import DirectSlider
+from direct.gui.DirectEntry import DirectEntry, ENTRY_FOCUS_STATE
+from direct.gui.DirectLabel import DirectLabel
+from direct.gui.DirectFrame import DirectFrame
+from direct.gui.OnscreenText import OnscreenText
+from direct.gui.DirectScrolledFrame import DirectScrolledFrame
+from lib.observer import Subject
+from lib.lib.ivals import Seq, Wait, PosIval, Func
+
+
+class CommonBase:
+
+    def set_widget(self):
+        from lib.lib.gui import Frame, Slider, Btn, Label, OptionMenu, \
+            CheckBtn, Entry, Img, Text, ScrolledFrame
+        from lib.lib.p3d.widget import FrameMixin, SliderMixin, BtnMixin, \
+            OptionMenuMixin, CheckBtnMixin, EntryMixin, ImgMixin, \
+            ScrolledFrameMixin
+        self.__class__ = self.__class__  # for pylint
+        libwdg2wdg = {
+            FrameMixin: [Frame],
+            ScrolledFrameMixin: [ScrolledFrame],
+            SliderMixin: [Slider],
+            BtnMixin: [Btn, Label],
+            OptionMenuMixin: [OptionMenu],
+            CheckBtnMixin: [CheckBtn],
+            EntryMixin: [Entry],
+            ImgMixin: [Img, Text]}
+        for libwdg, wdgcls in libwdg2wdg.items():
+            if any(cls in getmro(self.__class__) for cls in wdgcls):
+                par_cls = libwdg
+        clsname = self.__class__.__name__ + 'Widget'
+        self.__class__ = type(clsname, (self.__class__, par_cls), {})
+        self.init(self)
+        if not hasattr(self, 'bind'): return
+        bind_args = [(ENTER, self.on_wdg_enter), (EXIT, self.on_wdg_exit)]
+        list(map(lambda args: self.bind(*args), bind_args))
+
+    def set_enter_transition(self):
+        start_pos = self.get_pos()
+        pos = self.pos - (3.6, 0)
+        self.set_pos((pos.x, 1, pos.y))
+        Seq(
+            Wait(abs(pos.y - 1) / 4),
+            PosIval(self.get_np(), .5, start_pos)
+        ).start()
+
+    def set_exit_transition(self, destroy):
+        start_pos = self.get_pos()
+        end_pos = (self.pos.x + 3.6, 1, self.pos.y)
+        seq = Seq(
+            Wait(abs(self.pos.y - 1) / 4),
+            PosIval(self.get_np(), .5, end_pos),
+            Func(self.destroy if destroy else self.hide))
+        if not destroy: seq += Func(self.set_pos, start_pos)
+        seq.start()
+
+    def translate(self):
+        if hasattr(self, 'bind_transl'): self.wdg['text'] = self.bind_transl
+
+
+class P3dImg(CommonBase):
+
+    def __init__(self, filepath, pos=(0, 0), scale=1.0, background=False,
+                 foreground=False, parent=None):
+        self.img = OnscreenImage(
+            filepath, pos=(pos[0], 1, pos[1]), scale=scale, parent=parent)
+        if background: self.img.set_bin('background', 10)
+        alpha_formats = [12]  # panda3d.core.texture.Frgba
+        if self.img.get_texture().get_format() in alpha_formats:
+            self.img.set_transparency(True)
+        if foreground: self.img.set_bin('gui-popup', 50)
+
+    def reparent_to(self, node): return self.img.reparent_to(node)
+    def show(self): return self.img.show()
+    def hide(self): return self.img.hide()
+    def set_shader(self, shader): return self.img.set_shader(shader)
+    def set_shader_input(self, name, val):
+        return self.img.set_shader_input(name, val)
+    def set_texture(self, texturestage, texture):
+        return self.img.set_texture(texturestage, texture)
+
+    def set_exit_transition(self, destroy):
+        start_pos = self.get_pos()
+        end_pos = (self.pos.x + 3.6, 1, self.pos.y)
+        seq = Seq(
+            Wait(abs(self.pos.y - 1) / 4),
+            PosIval(self.get_np(), .5, end_pos),
+            Func(self.destroy if destroy else self.hide))
+        if not destroy: seq += Func(self.set_pos, (start_pos[0], start_pos[2]))
+        seq.start()
+
+    def set_pos(self, pos): return self.img.set_pos(pos[0], 1, pos[1])
+
+    def get_pos(self, pos=None): return self.img.get_pos(*[pos] if pos else [])
+
+    @property
+    def parent(self): return self.img.get_parent()
+
+    @property
+    def hidden(self): return self.img.is_hidden()
+
+    def set_transparent(self): return self.img.set_transparency(True)
+
+    def destroy(self): self.img = self.img.destroy()
+
+
+class P3dBase(CommonBase):
+
+    def __init__(self, tra_src=None, tra_tra=None):
+        # self.text_src_tra = None  # it breaks the gui
+        if tra_src and tra_tra: self.bind_tra(tra_src, tra_tra)
+
+    def set_pos(self, pos): return self.wdg.set_pos(pos)
+    def show(self): return self.wdg.show()
+    def hide(self): return self.wdg.hide()
+
+    def bind_tra(self, text_src, text_transl):
+        # text_transl is not used, anyway we need it since we have this kind of
+        # use: self.bind_transl('example str', _('example str'))
+        # this allows to change translations on the fly keeping the source
+        # text for remapping it later
+        # TODO: try reverse mapping? i.e. retrieve the src string from the
+        # translated one
+        self.text_src_tra = text_src
+        self.text_tra_tra = text_transl
+        tra = lambda self: _(self.text_tra_tra)
+        self.__class__.bind_transl = property(tra)
+        self['text'] = self.bind_transl
+
+    def get_pos(self, pos=None):
+        return self.wdg.get_pos(*[pos] if pos else [])
+
+    def __setitem__(self, key, value): self.wdg[key] = value
+
+    def __getitem__(self, key): return self.wdg[key]
+
+    def get_np(self): return self.wdg
+
+    @property
+    def hidden(self): return self.wdg.is_hidden()
+
+    def destroy(self): self.wdg.destroy()
+
+
+class P3dAbs(P3dBase):
+
+    def get_value(self): return self.wdg.getValue()
+    def initialiseoptions(self): return self.wdg.initialiseoptions()
+    def set_z(self, val): return self.wdg.set_z(val)
+    def set_shader(self, shader): return self.wdg.set_shader(shader)
+    def set_shader_input(self, name, val):
+        return self.wdg.set_shader_input(name, val)
+    def set_transparency(self, val): return self.wdg.set_transparency(val)
+    def bind(self, evt, mth): return self.wdg.bind(evt, mth)
+
+    def attachNewNode(self, gui_itm, sort_order):
+        # it won't work if we name it attach_node. hopefully this will be
+        # possible when we'll use decorators in place of mixins
+        return self.wdg.attachNewNode(gui_itm, sort_order)
+
+    @property
+    def is_enabled(self): return self.wdg['state'] != DISABLED
+
+
+class P3dBtn(P3dAbs):
+
+    def __init__(
+            self, text='', parent=None, pos=(0, 0), scale=(1, 1),
+            cmd=None, frame_size=(-1, 1, -1, 1), click_snd=None,
+            text_fg=(1, 1, 1, 1), frame_col=(1, 1, 1, 1), text_font=None,
+            over_snd=None, extra_args=None, frame_texture=None, img=None,
+            tra_src=None, tra_tra=None, text_scale=1.0):
+        str2par = {'bottomcenter': base.a2dBottomCenter}
+        parent = str2par.get(parent, parent)
+        extra_args = extra_args or []
+        self.wdg = DirectButton(
+            text=text, parent=parent, pos=(pos[0], 1, pos[1]),
+            scale=(scale[0], 1, scale[1]), command=cmd,
+            frameSize=frame_size, clickSound=click_snd, text_fg=text_fg,
+            frameColor=frame_col, text_font=text_font, rolloverSound=over_snd,
+            extraArgs=extra_args, frameTexture=frame_texture, image=img,
+            text_scale=text_scale)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+        self['relief'] = FLAT
+        args = [(ENTER, self._on_enter), (EXIT, self._on_exit)]
+        list(map(lambda args: self.bind(*args), args))
+
+    def _on_enter(self, pos): pass  # pos comes from mouse
+
+    def _on_exit(self, pos): pass  # pos comes from mouse
+
+    # we add these with the mixins
+    # def enable(self): self['state'] = NORMAL
+
+    # def disable(self): self['state'] = DISABLED
+
+
+class P3dSlider(P3dAbs):
+
+    def __init__(
+            self, parent=None, pos=(0, 0), scale=1, val=0,
+            frame_col=(1, 1, 1, 1), thumb_frame_col=(1, 1, 1, 1),
+            cmd=None, range_=(0, 1), tra_src=None, tra_tra=None):
+        self.wdg = DirectSlider(
+            parent=parent, pos=(pos[0], 1, pos[1]), scale=scale, value=val,
+            frameColor=frame_col, thumb_frameColor=thumb_frame_col,
+            command=cmd, range=range_)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+
+
+class P3dCheckBtn(P3dAbs):
+
+    def __init__(
+            self, pos=(0, 0), text='', indicator_val=False,
+            indicator_frame_col=(1, 1, 1, 1), frame_col=(1, 1, 1, 1),
+            scale=(1, 1, 1), click_snd=None, over_snd=None,
+            text_fg=(1, 1, 1, 1), text_font=None, cmd=None, tra_src=None,
+            tra_tra=None):
+        self.wdg = DirectCheckButton(
+            pos=(pos[0], 1, pos[1]), text=text, indicatorValue=indicator_val,
+            indicator_frameColor=indicator_frame_col,
+            frameColor=frame_col, scale=scale, clickSound=click_snd,
+            rolloverSound=over_snd, text_fg=text_fg, text_font=text_font,
+            command=cmd)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+
+
+class P3dOptionMenu(P3dAbs):
+
+    def __init__(
+            self, text='', items=None, pos=(0, 0), scale=(1, 1, 1),
+            initialitem='', cmd=None, frame_size=(-1, 1, -1, 1),
+            click_snd=None, over_snd=None, text_may_change=False,
+            text_fg=(1, 1, 1, 1), item_frame_col=(1, 1, 1, 1),
+            frame_col=(1, 1, 1, 1), highlight_col=(1, 1, 1, 1),
+            text_scale=.05, popup_marker_col=(1, 1, 1, 1),
+            item_relief=None, item_text_font=None, text_font=None,
+            tra_src=None, tra_tra=None):
+        items = items or []
+        self.wdg = DirectOptionMenu(
+            text=text, items=items, pos=(pos[0], 1, pos[1]), scale=scale,
+            initialitem=initialitem, command=cmd, frameSize=frame_size,
+            clickSound=click_snd, rolloverSound=over_snd,
+            textMayChange=text_may_change, text_fg=text_fg,
+            item_frameColor=item_frame_col, frameColor=frame_col,
+            highlightColor=highlight_col, text_scale=text_scale,
+            popupMarker_frameColor=popup_marker_col,
+            item_relief=item_relief, item_text_font=item_text_font,
+            text_font=text_font)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+
+    def set(self, idx, f_cmd=1): return self.wdg.set(idx, f_cmd)
+
+    @property
+    def curr_val(self): return self.wdg.get()
+
+    @property
+    def curr_idx(self): return self.wdg.selectedIndex
+
+
+class P3dEntry(P3dAbs, DirectObject, Subject):
+
+    def __init__(
+            self, scale=.05, pos=(0, 0), entry_font=None, width=12,
+            frame_col=(1, 1, 1, 1), initial_text='', obscured=False,
+            cmd=None, focus_in_cmd=None, focus_in_args=None,
+            focus_out_cmd=None, focus_out_args=None, parent=None,
+            tra_src=None, tra_tra=None, text_fg=(1, 1, 1, 1), on_tab=None,
+            on_click=None):
+        self.__focused = False
+        self.__focus_in_cmd = focus_in_cmd
+        self.__focus_out_cmd = focus_out_cmd
+        DirectObject.__init__(self)
+        Subject.__init__(self)
+        focus_in_args = focus_in_args or []
+        focus_out_args = focus_out_args or []
+        self.wdg = DirectEntry(
+            scale=scale, pos=(pos[0], 1, pos[1]), entryFont=entry_font,
+            width=width, frameColor=frame_col, initialText=initial_text,
+            obscured=obscured, command=cmd, focusInCommand=self._focus_in_cmd,
+            focusInExtraArgs=focus_in_args,
+            focusOutCommand=self._focus_out_cmd,
+            focusOutExtraArgs=focus_out_args, parent=parent,
+            text_fg=text_fg)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+        if on_tab:
+            self.on_tab_cb = on_tab
+            self.accept('tab-up', self.on_tab)
+        if on_click: self.wdg.bind(B1PRESS, on_click)
+
+    def set(self, txt): return self.wdg.set(txt)
+
+    def _focus_in_cmd(self, *args):
+        self.__focused = True
+        if self.__focus_in_cmd: self.__focus_in_cmd(*args)
+        self.notify('on_entry_enter')
+
+    def _focus_out_cmd(self, *args):
+        self.__focused = False
+        if self.__focus_out_cmd: self.__focus_out_cmd(*args)
+        self.notify('on_entry_exit')
+
+    def on_tab(self):
+        if self.wdg['focus'] == ENTRY_FOCUS_STATE: self.on_tab_cb()
+
+    @property
+    def focused(self): return self.__focused
+
+    @property
+    def text(self): return self.wdg.get()
+
+    def enter_text(self, txt):
+        return self.wdg.enterText(txt)
+
+    def enable(self): self['state'] = NORMAL
+
+    def disable(self): self['state'] = DISABLED
+
+    def destroy(self):
+        self.ignore('tab-up')
+        self.on_tab_cb = None
+        Subject.destroy(self)
+        P3dAbs.destroy(self)
+
+
+class P3dLabel(P3dAbs):
+
+    def __init__(
+            self, text='', pos=(0, 0), parent=None, text_wordwrap=12,
+            text_align=None, text_fg=(1, 1, 1, 1), text_font=None, scale=.05,
+            frame_col=(1, 1, 1, 1), tra_src=None, tra_tra=None, hpr=(0, 0, 0)):
+        self.wdg = DirectLabel(
+            text=text, pos=(pos[0], 1, pos[1]), parent=parent,
+            text_wordwrap=text_wordwrap, text_align=text_align,
+            text_fg=text_fg, text_font=text_font, scale=scale,
+            frameColor=frame_col, hpr=hpr)
+        P3dAbs.__init__(self, tra_src, tra_tra)
+
+    def set_bin(self, bin_name, priority): return self.wdg.set_bin(bin_name, priority)
+
+    def set_x(self, x): return self.wdg.set_x(x)
+
+    def set_alpha_scale(self, alpha): return self.wdg.set_alpha_scale(alpha)
+
+
+class P3dTxt(P3dBase):
+
+    def __init__(
+            self, txt='', pos=(0, 0), scale=.05, wordwrap=12, parent=None,
+            fg=(1, 1, 1, 1), font=None, align=None, tra_src=None,
+            tra_tra=None):
+        str2par = {'bottomleft': base.a2dBottomLeft,
+                   'bottomright': base.a2dBottomRight,
+                   'leftcenter': base.a2dLeftCenter}
+        str2al = {'left': TextNode.A_left, 'right': TextNode.A_right,
+                  'center': TextNode.A_center}
+        if parent and parent in str2par: parent = str2par[parent]
+        if align: align = str2al[align]
+        self.wdg = OnscreenText(
+            text=txt, pos=pos, scale=scale, wordwrap=wordwrap,
+            parent=parent, fg=fg, font=font, align=align)
+        P3dBase.__init__(self, tra_src, tra_tra)
+
+    def set_r(self, r): return self.wdg.set_r(r)
+
+
+class P3dFrame(P3dAbs):
+
+    def __init__(self, frame_size=(-1, 1, -1, 1), frame_col=(1, 1, 1, 1),
+                 pos=(0, 0), parent=None, texture_coord=False):
+        P3dAbs.__init__(self)
+        self.wdg = DirectFrame(frameSize=frame_size, frameColor=frame_col,
+                               pos=(pos[0], 1, pos[1]), parent=parent)
+        if texture_coord: self.wdg['frameTexture'] = Texture()
+
+
+class P3dScrolledFrame(P3dAbs):
+
+    def __init__(
+            self, frame_sz=(-1, 1, -1, 1), canvas_sz=(0, 1, 0, 1),
+            scrollbar_width=.05, frame_col=(1, 1, 1, 1),
+            pos=(0, 0), parent='topleft'):
+        P3dAbs.__init__(self)
+        par2p3d = {'topleft': base.a2dTopLeft}
+        if parent and parent in par2p3d: parent = par2p3d[parent]
+        self.wdg = DirectScrolledFrame(
+            frameSize=frame_sz,
+            canvasSize=canvas_sz,
+            scrollBarWidth=scrollbar_width,
+            frameColor=frame_col,
+            pos=(pos[0], 1, pos[1]),
+            parent=parent)
+
+    @property
+    def canvas(self): return self.wdg.getCanvas()
diff --git a/lib/lib/p3d/ivals.py b/lib/lib/p3d/ivals.py
new file mode 100644 (file)
index 0000000..182c6e3
--- /dev/null
@@ -0,0 +1,33 @@
+from direct.interval.MetaInterval import Sequence
+from direct.interval.FunctionInterval import Func, Wait
+from direct.interval.LerpInterval import LerpPosInterval
+
+
+class P3dSeq:
+
+    def __init__(self, *ivals):
+        self.seq = Sequence(*[ival._ival for ival in ivals])
+        #TODO: don't access a protected member
+
+    def start(self): return self.seq.start()
+
+    def __add__(self, ival):
+        self.seq.append(ival._ival)  #TODO: don't access a protected member
+        return self.seq
+
+
+class P3dWait:
+
+    def __init__(self, time): self._ival = Wait(time)
+
+
+class P3dPosIval:
+
+    def __init__(self, node, time=1.0, pos=(0, 0, 0), blend_type='ease'):
+        btype = {'ease': 'easeInOut'}[blend_type]
+        self._ival = LerpPosInterval(node, time, pos=pos, blendType=btype)
+
+
+class P3dFunc:
+
+    def __init__(self, fun, *args): self._ival = Func(fun, *args)
diff --git a/lib/lib/p3d/joystick.py b/lib/lib/p3d/joystick.py
new file mode 100644 (file)
index 0000000..37ab8dd
--- /dev/null
@@ -0,0 +1,111 @@
+from panda3d.core import InputDevice
+
+
+class P3dJoystickMgr:
+
+    def __init__(self):
+        self.joysticks = []
+        self.curr_vibration = {}
+        self.__is_vibrating = [False, False, False, False]
+
+    def init_joystick(self):
+        for i, dev in enumerate(base.devices.getDevices(InputDevice.DeviceClass.gamepad)):
+            base.attachInputDevice(dev, prefix='joypad%s' % i)
+        taskMgr.add(self._update, 'update joysticks')
+        # pygame.init()
+        # joystick.init()
+        # self.joysticks = [
+        #     joystick.Joystick(idx) for idx in range(joystick.get_count())]
+        # list(map(lambda joystick: joystick.init(), self.joysticks))
+
+    @property
+    def num_joysticks(self):
+        return len(base.devices.getDevices(InputDevice.DeviceClass.gamepad))
+
+    @staticmethod
+    def get_joystick(player_idx):
+        devices = base.devices.getDevices(InputDevice.DeviceClass.gamepad)
+        if player_idx > len(devices) - 1:
+            return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+        gamepad = devices[player_idx]
+        face_a = gamepad.findButton('face_a')
+        face_b = gamepad.findButton('face_b')
+        face_x = gamepad.findButton('face_x')
+        face_y = gamepad.findButton('face_y')
+        dpad_l = gamepad.findButton('dpad_left')
+        dpad_r = gamepad.findButton('dpad_right')
+        dpad_u = gamepad.findButton('dpad_up')
+        dpad_d = gamepad.findButton('dpad_down')
+        trigger_l = gamepad.findButton('ltrigger')
+        trigger_r = gamepad.findButton('rtrigger')
+        shoulder_l = gamepad.findButton('lshoulder')
+        shoulder_r = gamepad.findButton('rshoulder')
+        stick_l = gamepad.findButton('lstick')
+        stick_r = gamepad.findButton('rstick')
+        left_x = gamepad.findAxis(InputDevice.Axis.left_x)
+        left_y = gamepad.findAxis(InputDevice.Axis.left_y)
+        trigger_l_axis = gamepad.findAxis(InputDevice.Axis.left_trigger)
+        trigger_r_axis = gamepad.findAxis(InputDevice.Axis.right_trigger)
+        trigger_l_known = trigger_l.known
+        trigger_r_known = trigger_r.known
+        return (left_x.value, -left_y.value, face_a.pressed, face_b.pressed,
+                face_x.pressed, face_y.pressed,
+                dpad_l.pressed, dpad_r.pressed, dpad_u.pressed, dpad_d.pressed,
+                trigger_l.pressed or trigger_l_axis.value > .5,
+                trigger_r.pressed or trigger_r_axis.value > .5,
+                shoulder_l.pressed, shoulder_r.pressed, stick_l.pressed,
+                stick_r.pressed, trigger_l_known, trigger_r_known)
+        # for _ in pygame.event.get(): pass
+        # if not self.joysticks: return 0, 0, 0, 0
+        # jstick = self.joysticks[0]
+        # axis, btn = jstick.get_axis, jstick.get_button
+        # return axis(0), axis(1), btn(0), btn(1)
+
+    def set_vibration(self, player_idx, code, time=-1):
+        devices = base.devices.getDevices(InputDevice.DeviceClass.gamepad)
+        if player_idx < 0 or player_idx > len(devices) - 1: return
+        if player_idx in self.curr_vibration and \
+               code in self.curr_vibration[player_idx]: return
+        if player_idx not in self.curr_vibration:
+            self.curr_vibration[player_idx] = {}
+        self.curr_vibration[player_idx][code] = time
+
+    def clear_vibration(self, player_idx, code=None):
+        devices = base.devices.getDevices(InputDevice.DeviceClass.gamepad)
+        if player_idx < 0 or player_idx > len(devices) - 1: return
+        if player_idx not in self.curr_vibration or \
+           code not in self.curr_vibration[player_idx]: return
+        if code is None: del self.curr_vibration[player_idx]
+        else: del self.curr_vibration[player_idx][code]
+
+    def _update(self, task):
+        devices = base.devices.getDevices(InputDevice.DeviceClass.gamepad)
+        for player_idx in self.curr_vibration:
+            for code in self.curr_vibration[player_idx]:
+                if self.curr_vibration[player_idx][code] != -1:
+                    dt = globalClock.getDt()
+                    self.curr_vibration[player_idx][code] -= dt
+        for player_idx in self.curr_vibration:
+            for code in list(self.curr_vibration[player_idx])[:]:
+                if self.curr_vibration[player_idx][code] != -1:
+                    if self.curr_vibration[player_idx][code] < 0:
+                        del self.curr_vibration[player_idx][code]
+        for player_idx in list(self.curr_vibration)[:]:
+            if not self.curr_vibration[player_idx]:
+                del self.curr_vibration[player_idx]
+        for player_idx, dev in enumerate(devices):
+            gamepad = devices[player_idx]
+            if player_idx in self.curr_vibration and \
+                    not self.__is_vibrating[player_idx]:
+                gamepad.set_vibration(.2, .4)
+                self.__is_vibrating[player_idx] = True
+            elif player_idx not in self.curr_vibration:
+                gamepad.set_vibration(0, 0)
+                self.__is_vibrating[player_idx] = False
+        return task.cont
+
+    def destroy(self):
+        pass
+        # joystick.quit()
+        # pygame.quit()
+        # self.joysticks = []
diff --git a/lib/lib/p3d/p3d.py b/lib/lib/p3d/p3d.py
new file mode 100755 (executable)
index 0000000..0721813
--- /dev/null
@@ -0,0 +1,352 @@
+import sys
+from logging import info
+from os.path import exists, dirname
+from os import getcwd, _exit
+from glob import glob
+from pathlib import Path
+from panda3d.core import loadPrcFileData, Texture, TextPropertiesManager, \
+    TextProperties, PandaSystem, Filename, WindowProperties, GraphicsWindow
+from panda3d.bullet import get_bullet_version
+from direct.showbase.ShowBase import ShowBase
+from direct.showbase.DirectObject import DirectObject
+from direct.task.Task import Task
+#from gltf import patch_loader
+
+
+class LibShowBase(ShowBase): pass
+
+
+class LibP3d(DirectObject):
+
+    task_cont = Task.cont
+
+    def __init__(self):
+        DirectObject.__init__(self)
+        self.__end_cb = self.__notify = None
+        self.__logged_keys = {}
+
+    @staticmethod
+    def runtime(): return not exists('main.py')
+
+    @staticmethod
+    def configure():
+        loadPrcFileData('', 'notify-level-ya2 info')
+        # loadPrcFileData('', 'gl-version 3 2')
+
+    @staticmethod
+    def fixpath(path):
+        home = '/home/flavio'
+        if sys.platform == 'win32' and not exists(exists(home + '/.wine/')):
+            if path.startswith('/'): path = path[1] + ':\\' + path[3:]
+            path = path.replace('/', '\\')
+        return path
+
+    @staticmethod
+    def p3dpath(path): return Filename.fromOsSpecific(path)
+
+    @property
+    def last_frame_dt(self): return globalClock.get_dt()
+
+    @property
+    def build_version(self):
+        appimg_mnt = glob('/tmp/.mount_Yocto*')
+        if appimg_mnt:
+            #with open(appimg_mnt[0] + '/usr/bin/appimage_version.txt') as fver:
+            with open(self.curr_path + '/assets/bld_version.txt') as fver:
+                return fver.read().strip()
+        try:
+            with open(self.curr_path + '/assets/bld_version.txt') as fver:
+                return fver.read().strip()
+        except FileNotFoundError:
+            info(self.curr_path + '/assets/bld_version.txt')
+            return 'notfound'
+
+    @property
+    def is_appimage(self):
+        par_path = str(Path(__file__).parent.absolute())
+        is_appimage = par_path.startswith('/tmp/.mount_Yocto')
+        return is_appimage and par_path.endswith('/usr/bin')
+
+    @property
+    def curr_path(self):
+        if sys.platform == 'darwin':
+            return dirname(__file__) + '/../Resources/'
+        # return dirname(__file__)
+        par_path = str(Path(__file__).parent.absolute())
+        if self.is_appimage:
+            return str(Path(par_path).absolute())
+        is_snap = par_path.startswith('/snap/')
+        is_snap = is_snap and par_path.endswith('/x1')
+        if is_snap:
+            return str(Path(par_path).absolute())
+        #return getcwd()
+        curr_path = dirname(__file__)
+        info('current path: %s' % curr_path)
+        return curr_path
+
+    @staticmethod
+    def send(msg): return messenger.send(msg)
+
+    @staticmethod
+    def do_later(time, meth, args=None):
+        args = args or []
+        return taskMgr.doMethodLater(
+            time, lambda meth, args: meth(*args), meth.__name__, [meth, args])
+
+    @staticmethod
+    def add_task(mth, priority=0):
+        return taskMgr.add(mth, mth.__name__, priority)
+
+    @staticmethod
+    def remove_task(tsk): taskMgr.remove(tsk)
+
+    def init(self, green=(.2, .8, .2, 1), red=(.8, .2, .2, 1), end_cb=None):
+        LibShowBase()
+        base.disableMouse()
+        #patch_loader(base.loader)
+        self.__end_cb = end_cb
+        self.__init_win()
+        self.__init_fonts(green, red)
+        self.__set_roots()
+        self.accept('aspectRatioChanged', self.on_aspect_ratio_changed)
+
+    @staticmethod
+    def __set_roots():
+        base.a2dTopQuarter = base.aspect2d.attachNewNode('a2dTopQuarter')
+        base.a2dTopQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dTop)
+        base.a2dTopThirdQuarter = \
+            base.aspect2d.attachNewNode('a2dTopThirdQuarter')
+        base.a2dTopThirdQuarter.set_pos(base.a2dRight / 2, 0, base.a2dTop)
+        base.a2dCenterQuarter = base.aspect2d.attachNewNode('a2dCenterQuarter')
+        base.a2dCenterQuarter.set_pos(base.a2dLeft / 2, 0, 0)
+        base.a2dCenterThirdQuarter = \
+            base.aspect2d.attachNewNode('a2dCenterThirdQuarter')
+        base.a2dCenterThirdQuarter.set_pos(base.a2dRight / 2, 0, 0)
+        base.a2dBottomQuarter = base.aspect2d.attachNewNode('a2dBottomQuarter')
+        base.a2dBottomQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dBottom)
+        base.a2dBottomThirdQuarter = \
+            base.aspect2d.attachNewNode('a2dBottomThirdQuarter')
+        base.a2dBottomThirdQuarter.set_pos(
+            base.a2dRight / 2, 0, base.a2dBottom)
+
+    @staticmethod
+    def on_aspect_ratio_changed():
+        base.a2dTopQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dTop)
+        base.a2dTopThirdQuarter.set_pos(base.a2dRight / 2, 0, base.a2dTop)
+        base.a2dBottomQuarter.set_pos(base.a2dLeft / 2, 0, base.a2dBottom)
+        base.a2dBottomThirdQuarter.set_pos(
+            base.a2dRight / 2, 0, base.a2dBottom)
+
+    @property
+    def has_window(self): return bool(base.win)
+
+    @property
+    def resolution(self):
+        if not isinstance(base.win, GraphicsWindow):
+            return 800, 600
+        win_prop = base.win.get_properties()
+        return win_prop.get_x_size(), win_prop.get_y_size()
+
+    @property
+    def resolutions(self):
+        d_i = base.pipe.get_display_information()
+
+        def res(idx):
+            return d_i.get_display_mode_width(idx), \
+                d_i.get_display_mode_height(idx)
+        ret = [res(idx) for idx in range(d_i.get_total_display_modes())]
+        return ret if ret else [self.resolution]
+
+    @staticmethod
+    def toggle_fullscreen():
+        props = WindowProperties()
+        props.set_fullscreen(not base.win.is_fullscreen())
+        base.win.request_properties(props)
+
+    @staticmethod
+    def set_resolution(res, fullscreen=None):
+        props = WindowProperties()
+        props.set_size(res)
+        if fullscreen: props.set_fullscreen(True)
+        if isinstance(base.win, GraphicsWindow):
+            base.win.request_properties(props)
+
+    def __init_win(self):
+        if base.win and isinstance(base.win, GraphicsWindow):
+            base.win.set_close_request_event('window-closed')
+        # not headless
+        self.accept('window-closed', self.__on_end)
+
+    @staticmethod
+    def __init_fonts(green=(.2, .8, .2, 1), red=(.8, .2, .2, 1)):
+        tp_mgr = TextPropertiesManager.get_global_ptr()
+        for namecol, col in zip(['green', 'red'], [green, red]):
+            props = TextProperties()
+            props.set_text_color(col)
+            tp_mgr.set_properties(namecol, props)
+        for namesize, col in zip(['small', 'smaller'], [.46, .72]):
+            props = TextProperties()
+            props.set_text_scale(.46)
+            tp_mgr.set_properties(namesize, props)
+        tp_italic = TextProperties()
+        tp_italic.set_slant(.2)
+        tp_mgr.set_properties('italic', tp_italic)
+
+    def __on_end(self):
+        base.closeWindow(base.win)
+        if self.__end_cb: self.__end_cb()
+        _exit(0)
+
+    @staticmethod
+    def load_font(filepath, outline=True):
+        font = base.loader.loadFont(filepath)
+        font.set_pixels_per_unit(60)
+        font.set_minfilter(Texture.FTLinearMipmapLinear)
+        if outline: font.set_outline((0, 0, 0, 1), .8, .2)
+        return font
+
+    @staticmethod
+    def log(msg): print(msg)
+
+    @property
+    def version(self): return PandaSystem.get_version_string()
+
+    @property
+    def lib_commit(self): return PandaSystem.get_git_commit()
+
+    @property
+    def phys_version(self): return get_bullet_version()
+
+    @property
+    def user_appdata_dir(self): return Filename.get_user_appdata_directory()
+
+    @property
+    def driver_vendor(self): return base.win.get_gsg().get_driver_vendor()
+
+    @property
+    def driver_renderer(self): return base.win.get_gsg().get_driver_renderer()
+
+    @property
+    def driver_shader_version_major(self):
+        return base.win.get_gsg().get_driver_shader_version_major()
+
+    @property
+    def driver_shader_version_minor(self):
+        return base.win.get_gsg().get_driver_shader_version_minor()
+
+    @property
+    def driver_version(self): return base.win.get_gsg().get_driver_version()
+
+    @property
+    def driver_version_major(self):
+        return base.win.get_gsg().get_driver_version_major()
+
+    @property
+    def driver_version_minor(self):
+        return base.win.get_gsg().get_driver_version_minor()
+
+    @property
+    def fullscreen(self):
+        if isinstance(base.win, GraphicsWindow):
+            return base.win.get_properties().get_fullscreen()
+
+    @property
+    def volume(self): return base.sfxManagerList[0].get_volume()
+
+    @volume.setter
+    def volume(self, vol): base.sfxManagerList[0].set_volume(vol)
+
+    @property
+    def mousepos(self):
+        mwn = base.mouseWatcherNode
+        if not mwn: return 0, 0
+        if not mwn.hasMouse(): return 0, 0
+        return mwn.get_mouse_x(), mwn.get_mouse_y()
+
+    @property
+    def aspect_ratio(self): return base.getAspectRatio()
+
+    @staticmethod
+    def set_icon(filename):
+        props = WindowProperties()
+        props.set_icon_filename(filename)
+        if isinstance(base.win, GraphicsWindow):
+            base.win.requestProperties(props)
+
+    @staticmethod
+    def __set_std_cursor(show):
+        props = WindowProperties()
+        props.set_cursor_hidden(not show)
+        if isinstance(base.win, GraphicsWindow):
+            base.win.requestProperties(props)
+
+    @staticmethod
+    def show_std_cursor(): LibP3d.__set_std_cursor(True)
+
+    @staticmethod
+    def hide_std_cursor(): LibP3d.__set_std_cursor(False)
+
+    @staticmethod
+    def find_geoms(model, name):  # no need to be cached
+        geoms = model.node.find_all_matches('**/+GeomNode')
+        is_nm = lambda geom: geom.get_name().startswith(name)
+        named_geoms = [geom for geom in geoms if is_nm(geom)]
+        return [ng for ng in named_geoms if name in ng.get_name()]
+
+    @staticmethod
+    def load_sfx(filepath, loop=False):
+        sfx = loader.loadSfx(filepath)
+        sfx.set_loop(loop)
+        return sfx
+
+    def remap_code(self, key):
+        kmap = base.win.get_keyboard_map()
+        for i in range(kmap.get_num_buttons()):
+            if key.lower() == kmap.get_mapped_button_label(i).lower():
+                self.__log_key(
+                    'code mapping %s to key %s' %
+                    (key, kmap.get_mapped_button(i)), key,
+                    kmap.get_mapped_button(i))
+                return kmap.get_mapped_button(i)
+        for i in range(kmap.get_num_buttons()):
+            if key.lower() == kmap.get_mapped_button(i).get_name().lower():
+                self.__log_key(
+                    'code mapping %s to key %s' %
+                    (key, kmap.get_mapped_button(i)), key,
+                    kmap.get_mapped_button(i))
+                return kmap.get_mapped_button(i)
+        self.__log_key('not found a code mapping for %s' %
+                       key, key, 'not_found')
+        return key
+
+    def remap_str(self, key):
+        if not base.win:  # when launched with --version
+            return key
+        #if isinstance(base.win, GraphicsBuffer):
+        #    return key
+        kmap = base.win.get_keyboard_map()
+        for i in range(kmap.get_num_buttons()):
+            if str(key).lower() == kmap.get_mapped_button_label(i).lower():
+                self.__log_key(
+                    'string mapping %s to key %s' %
+                    (key, kmap.get_mapped_button(i).get_name()), key,
+                    kmap.get_mapped_button(i).get_name())
+                return kmap.get_mapped_button(i).get_name()
+        for i in range(kmap.get_num_buttons()):
+            if key.lower() == kmap.get_mapped_button(i).get_name().lower():
+                self.__log_key(
+                    'string mapping %s to key %s' %
+                    (key, kmap.get_mapped_button(i).get_name()), key,
+                    kmap.get_mapped_button(i).get_name())
+                return kmap.get_mapped_button(i).get_name()
+        self.__log_key('not found a string mapping for %s' %
+                       key, key, kmap.get_mapped_button(i).get_name())
+        return key
+
+    def __log_key(self, msg, key1, key2):
+        if key1 in self.__logged_keys and self.__logged_keys[key1] == key2:
+            return
+        self.__logged_keys[key1] = key2
+        print(msg)
+
+    def destroy(self): pass
diff --git a/lib/lib/p3d/particle.py b/lib/lib/p3d/particle.py
new file mode 100755 (executable)
index 0000000..51daec3
--- /dev/null
@@ -0,0 +1,194 @@
+from math import pi, sin, cos
+from array import array
+from random import uniform
+from itertools import chain
+from logging import info
+from panda3d.core import Geom, GeomVertexFormat, GeomVertexData, GeomPoints, \
+    OmniBoundingVolume, GeomNode, Vec3, ShaderAttrib, TexGenAttrib, \
+    TextureStage, Texture, GeomEnums, NodePath
+from lib.lib.p3d.shader import load_shader
+from lib.lib.p3d.gfx import P3dNode
+from lib.gameobject import GameObject
+
+
+class P3dParticle(GameObject):
+
+    _vdata = {}  # don't regenerate input structures
+
+    def __init__(
+            self, emitter, texture, color=(1, 1, 1, 1), ampl=pi/6,
+            ray=.5, rate=.001, gravity=-9.81, vel=1.0, part_duration=1.0,
+            autodestroy=None, size=10):
+        GameObject.__init__(self)
+        self.__tex_pos = self.__tex_curr_pos = self.__tex_times = \
+            self.__tex_start_vel = self.__tex_curr_vel = self.__emitternode = \
+            None
+        self.__texture = texture
+        self.__color = color
+        self.__ampl = ampl
+        self.__ray = ray
+        self.__rate = rate
+        self.__gravity = gravity
+        self.__vel = vel
+        self.__part_duration = part_duration
+        self.__size = size
+        self.__npart = int(round(part_duration * 1 / rate))
+        if emitter.__class__ != P3dNode:  # emitter is a position
+            self.__emitternode = P3dNode(NodePath('tmp'))
+            self.__emitternode.set_pos(emitter)
+            self.__emitternode.reparent_to(self.eng.gfx.root)
+            emitter = self.__emitternode
+        self.__emitter = emitter
+        self.__old_pos = (0, 0, 0)
+        self._nodepath = render.attach_new_node(self.__node())
+        self._nodepath.set_transparency(True)
+        self._nodepath.set_bin('fixed', 0)
+        self.__set_shader()
+        self._nodepath.set_render_mode_thickness(10)
+        self._nodepath.set_tex_gen(TextureStage.getDefault(),
+                                   TexGenAttrib.MPointSprite)
+        self._nodepath.set_depth_write(False)
+        self.upd_tsk = taskMgr.add(self._update, 'update')
+        if autodestroy: self.eng.do_later(autodestroy, self.destroy)
+
+    def __node(self):
+        points = GeomPoints(GeomEnums.UH_static)
+        points.add_next_vertices(self.__npart)
+        geom = Geom(self.__vdata())
+        geom.add_primitive(points)
+        geom.set_bounds(OmniBoundingVolume())
+        node = GeomNode('node')
+        node.add_geom(geom)
+        return node
+
+    def __vdata(self):
+        entry = (self.__texture, self.__npart, self.__color, self.__ampl,
+                 self.__ray, self.__rate, self.__gravity)
+        if entry in P3dParticle._vdata:
+            vdata, pos, times, vels = P3dParticle._vdata[entry]
+            self.__set_textures(pos, times, vels)
+            return vdata
+        pos, times, vels = self.__init_textures()
+        self.__set_textures(pos, times, vels)
+        format_ = GeomVertexFormat.get_empty()
+        vdata = GeomVertexData('abc', format_, GeomEnums.UH_static)
+        P3dParticle._vdata[self.__texture, self.__npart, self.__color,
+                           self.__ampl, self.__ray, self.__rate,
+                           self.__gravity] = \
+            vdata, pos, times, vels
+        return P3dParticle._vdata[self.__texture, self.__npart, self.__color,
+                                  self.__ampl, self.__ray, self.__rate,
+                                  self.__gravity][0]
+
+    def __init_textures(self):
+        positions = [self.__rnd_pos() for i in range(self.__npart)]
+        pos_lst = [[pos.x, pos.y, pos.z, 1] for pos in positions]
+        pos_lst = list(chain.from_iterable(pos_lst))
+        emission_times = [
+            (self.__rate * i, 0, 0, 0) for i in range(self.__npart)]
+        times_lst = list(chain.from_iterable(emission_times))
+        velocities = self.__init_velocities()
+        vel_lst = [[v_vel[0], v_vel[1], v_vel[2], 1] for v_vel in velocities]
+        vel_lst = list(chain.from_iterable(vel_lst))
+        return pos_lst, times_lst, vel_lst
+
+    def __set_textures(self, pos_lst, times_lst, vel_lst):
+        self.__tex_pos = self.__buff_tex(pos_lst)
+        self.__tex_curr_pos = self.__buff_tex(pos_lst)
+        self.__tex_times = self.__buff_tex(times_lst)
+        self.__tex_start_vel = self.__buff_tex(vel_lst)
+        self.__tex_curr_vel = self.__buff_tex(vel_lst)
+
+    def __buff_tex(self, vals):
+        data = array('f', vals)
+        tex = Texture('tex')
+        tex.setup_buffer_texture(
+            self.__npart, Texture.T_float, Texture.F_rgba32,
+            GeomEnums.UH_static)
+        tex.set_ram_image(data)
+        return tex
+
+    def __rnd_pos(self):
+        ro = uniform(0, self.__ray)
+        alpha = uniform(0, 2 * pi)
+        return Vec3(ro * cos(alpha), ro * sin(alpha), 0)
+
+    def __init_velocities(self):
+        vels = []
+        for _ in range(self.__npart):
+            vec = self.__rnd_vel()
+            vels += [(vec.x, vec.y, vec.z)]
+        return vels
+
+    def __rnd_vel(self):
+        theta = uniform(0, self.__ampl)
+        phi = uniform(0, 2 * pi)
+        vec = Vec3(
+            sin(theta) * cos(phi),
+            sin(theta) * sin(phi),
+            cos(theta))
+        return vec * uniform(self.__vel * .8, self.__vel * 1.2)
+
+    def __set_shader(self):
+        path = 'assets/shaders/'
+        shader = load_shader(path + 'particle.vert', path + 'particle.frag')
+        if not shader: return
+        self._nodepath.set_shader(shader)
+        sha_attr = ShaderAttrib.make(shader)
+        sha_attr = sha_attr.set_flag(ShaderAttrib.F_shader_point_size, True)
+        self._nodepath.set_attrib(sha_attr)
+        img = loader.loadTexture('assets/images/game/%s.dds' % self.__texture)
+        self._nodepath.set_shader_inputs(
+            start_pos=self.__tex_pos,
+            positions=self.__tex_curr_pos,
+            emitter_old_pos=self.__old_pos,
+            emitter_pos=self.__emitter.get_pos(P3dNode(render)),
+            start_vel=self.__tex_start_vel,
+            velocities=self.__tex_curr_vel,
+            accel=(0, 0, self.__gravity),
+            start_time=globalClock.get_frame_time(),
+            emission_times=self.__tex_times,
+            part_duration=self.__part_duration,
+            emitting=1,
+            col=self.__color,
+            image=img,
+            size=self.__size)
+
+    def _update(self, task):
+        if self.__emitter and not self.__emitter.is_empty:
+            pos = self.__emitter.get_pos(P3dNode(render))
+        else: pos = (0, 0, 0)
+        try:
+            self._nodepath.set_shader_inputs(
+                emitter_old_pos=self.__old_pos,
+                emitter_pos=pos)
+            self.__old_pos = pos
+            return task.again
+        except AttributeError:
+            # _nodepath may be None on menu/pause
+            info('_nodepath: %s' % self._nodepath)
+
+    def destroy(self, now=False):
+        #TODO: the signature differs from the parent's one
+        try:
+            self._nodepath.set_shader_input('emitting', 0)
+        except AttributeError:
+            # _nodepath may be None on menu/pause
+            info('_nodepath: %s' % self._nodepath)
+        self.eng.do_later(0 if now else 1.2 * self.__part_duration,
+                          self.__destroy)
+
+    def __destroy(self):
+        try:
+            self.upd_tsk = taskMgr.remove(self.upd_tsk)
+        except TypeError:
+            info("can't remove %s" % self.upd_tsk)
+            # it may happen on pause/menu
+        try:
+            self._nodepath = self._nodepath.remove_node()
+        except AttributeError:
+            info("_nodepath %s" % self._nodepath)
+            # it may happen on pause/menu
+        if self.__emitternode:
+            self.__emitternode = self.__emitternode.destroy()
+        GameObject.destroy(self)
diff --git a/lib/lib/p3d/pause.py b/lib/lib/p3d/pause.py
new file mode 100644 (file)
index 0000000..b99066e
--- /dev/null
@@ -0,0 +1,114 @@
+from os import pardir  # pardir is .. (parent directory)
+from os.path import dirname, abspath, join
+from sys import modules
+from direct.task import Task
+from direct.interval.IntervalGlobal import ivalMgr
+from lib.gameobject import GameObject
+
+
+class TaskDec:
+
+    paused_taskchain = 'paused tasks'
+
+    def __init__(self, tsk):
+        self.tsk = tsk
+        path = dirname(modules[Task.__name__].__file__)
+        self.__direct_dir = abspath(join(path, pardir))  # path of direct.*
+
+    def process(self):
+        func = self.tsk.get_function()  # ordinary tasks
+        mod = func.__module__
+        modfile = ''
+        if "from '" in str(modules[mod]):
+            modfile = str(modules[mod]).split("from '")[1][:-2]
+        sys_mod = modfile.find(self.__direct_dir) < 0
+        actor_ival = False
+        if hasattr(func, 'im_class'):
+            actor_ival = func.im_class.__name__ == 'ActorInterval'
+        if mod.find('direct.interval') == 0 and not actor_ival:
+            self.tsk.interval.pause()  # python-based intervals
+            return self.tsk
+        if mod not in modules or sys_mod: return self.tsk
+        return None
+
+    def pause(self):
+        tsk = self.tsk
+        has_args = hasattr(tsk, 'getArgs')
+        tsk.stored_extraArgs = tsk.get_args() if has_args else None
+        if hasattr(tsk, 'getFunction'): tsk.stored_call = tsk.get_function()
+        has_p = hasattr(tsk, '_priority')
+        tsk.stored_priority = tsk._priority if has_p else tsk.get_sort()
+        if hasattr(tsk, 'remainingTime'): tsk.remove()  # do_later tasks
+        else:  # ordinary tasks
+            tsk.lastactivetime = -tsk.time if hasattr(tsk, 'time') else 0
+            tsk.setTaskChain(TaskDec.paused_taskchain)
+
+    def __resume_do_later(self):
+        tsk = self.tsk
+        d_t = globalClock.get_real_time() - globalClock.get_frame_time()
+        tmp_delay = tsk.remainingTime - d_t
+        upon_death = tsk.uponDeath if hasattr(tsk, 'uponDeath') else None
+        new_task = taskMgr.doMethodLater(
+            tmp_delay, tsk.stored_call, tsk.name, uponDeath=upon_death,
+            priority=tsk.stored_priority, extraArgs=tsk.stored_extraArgs)
+        if hasattr(tsk, 'remainingTime'): new_task.delayTime = tsk.delayTime
+
+    def resume(self):
+        tsk = self.tsk
+        if hasattr(tsk, 'interval'):
+            tsk.interval.resume()
+            if hasattr(tsk, 'stored_call'): tsk.set_function(tsk.stored_call)
+            return
+        if hasattr(tsk, 'remainingTime'):
+            self.__resume_do_later()
+            return
+        tsk.set_delay(tsk.lastactivetime)  # ordinary tasks
+        tsk.set_task_chain('default')
+        tsk.clear_delay()  # to avoid assertion error on resume
+
+
+class P3dPause(GameObject):
+
+    def __init__(self):
+        GameObject.__init__(self)
+        taskMgr.setupTaskChain(TaskDec.paused_taskchain, frameBudget=0)
+        self.__paused_ivals = []
+        self.__paused_tasks = []
+
+    @property
+    def paused(self):
+        tsk = taskMgr.getTasksNamed('__on_frame')[0]
+        return tsk.getTaskChain() == TaskDec.paused_taskchain
+
+    def pause_tasks(self):
+        is_tsk = lambda tsk: tsk and hasattr(tsk, 'getFunction')
+        tasks = [TaskDec(tsk) for tsk in taskMgr.getTasks() if is_tsk(tsk)]
+        tasks = [tsk for tsk in tasks
+                 if tsk.tsk.get_task_chain() != 'unpausable']
+        namefilter = ['igLoop', 'dataLoop', 'ivalLoop', 'collisionLoop',
+                      'garbageCollectStates', 'audioLoop',
+                      'resetPrevTransform', 'eventManager']
+        tasks = [tsk for tsk in tasks
+                 if tsk.tsk.get_name_prefix() not in namefilter]
+        not_none = lambda tsk: tsk is not None
+        paused_tasks = list(filter(not_none, [tsk.process() for tsk in tasks]))
+        self.__paused_tasks = list(map(TaskDec, paused_tasks))
+        for tsk in list(filter(is_tsk, taskMgr.getDoLaters())):
+            self.__paused_tasks += [TaskDec(tsk)]
+            tsk.remainingTime = tsk.wakeTime - globalClock.get_frame_time()
+        list(map(lambda tsk: tsk.pause(), self.__paused_tasks))
+
+    def remove_task(self, tsk):
+        list(map(self.__paused_tasks.remove, [ptsk for ptsk in self.__paused_tasks if ptsk.tsk == tsk]))
+
+    def pause(self):
+        self.__paused_ivals = ivalMgr.getIntervalsMatching('*')
+        self.pause_tasks()
+        return self.paused
+
+    def resume(self):
+        list(map(lambda ival: ival.resume(), self.__paused_ivals))
+        list(map(lambda tsk: tsk.resume(), self.__paused_tasks))
+        return self.paused
+
+    def destroy(self): GameObject.destroy(self)
diff --git a/lib/lib/p3d/shader.py b/lib/lib/p3d/shader.py
new file mode 100644 (file)
index 0000000..a054682
--- /dev/null
@@ -0,0 +1,251 @@
+from os.path import isfile, dirname
+from panda3d.core import AmbientLight, DirectionalLight, PointLight, \
+    Spotlight, LVector4f, LVector3f, Vec3, Shader, TextureStage, \
+    TexMatrixAttrib
+from direct.filter.FilterManager import FilterManager
+from lib.lib.builder import LibP3d
+
+
+def load_shader(vert, frag):
+
+    def is_file(path):
+        joinchar = '/' if LibP3d.runtime() and not path.startswith('/') else ''
+        dpath = LibP3d.runtime() and dirname(__file__)
+        return isfile((dpath or '') + joinchar + path)
+    if is_file(vert) and is_file(frag):
+        shader = Shader.load(Shader.SLGLSL, vert, frag)
+    else: shader = Shader.make(Shader.SLGLSL, vert, frag)
+    return shader
+
+
+class P3dShaderMgr:
+
+    def __init__(self, shaders, gamma):
+        self.filter_mgr = None
+        self.gamma, self.buffer, self.lcam, self.lights = gamma, None, None, []
+        if shaders: self.setup_post_fx()
+
+    def __set_lgt(self, lgt, col):
+        if type(col) in [int, float]: lgt.set_color_temperature(col)
+        else: lgt.set_color(col)
+        self.lights += [render.attach_new_node(lgt)]
+        render.set_light(self.lights[-1])
+
+    def set_amb_lgt(self, col):
+        self.__set_lgt(AmbientLight('ambient light'), col)
+
+    def set_dir_lgt(self, col, direction):
+        self.__set_lgt(DirectionalLight('directional light'), col)
+        self.lights[-1].set_hpr(*direction)
+
+    def set_shadow_lgt(self, direction):
+        self.__set_lgt(DirectionalLight('directional light'), (1, 1, 1, 1))
+        self.lights[-1].node().set_shadow_caster(True, 8192, 8192)
+        self.lights[-1].node().get_lens().set_film_size(2048, 2048)
+        self.lights[-1].node().get_lens().set_near_far(1, 2048)
+        #self.lights[-1].node().show_frustum()
+        self.lights[-1].set_hpr(*direction)
+        return self.lights[-1]
+
+    def set_pnt_lgt(self, col, pos):
+        self.__set_lgt(PointLight('point light'), col)
+        self.lights[-1].set_pos(*pos)
+
+    def set_spotlight(self, col, exp, cutoff, pos, look_at):
+        self.__set_lgt(Spotlight('spotlight'), col)
+        self.lights[-1].set_exponent(exp)
+        self.lights[-1].get_lens().set_fov(cutoff, cutoff)
+        self.lights[-1].set_pos(*pos)
+        self.lights[-1].look_at(*look_at)
+
+    @staticmethod
+    def set_default_args(idx):
+        pref = 'lights[%s].' % idx
+        args = [(pref + 'pos', LVector4f(0, 0, 0, 1)),
+                (pref + 'amb', LVector3f(0, 0, 0)),
+                (pref + 'diff', LVector3f(0, 0, 0)),
+                (pref + 'spec', LVector3f(0, 0, 0)),
+                (pref + 'dir', LVector3f(0, 0, 0)),
+                (pref + 'exp', .0),
+                (pref + 'cutoff', .0)]
+        list(map(lambda _args: render.set_shader_input(*_args), args))
+
+    def set_lgt_args(self, idx, lgt):
+        self.set_default_args(idx)
+        ShaderSetter.build(lgt).set('lights[%s].' % idx, lgt)
+
+    def clear_lights(self):
+        for lgt in self.lights:
+            base.render.clear_light(lgt)
+            lgt.removeNode()
+        self.lights = []
+
+    def setup_post_fx(self):
+        self.filter_mgr = FilterManager(base.win, base.cam)
+        # rendered_scene = Texture()
+        # aa_scene = Texture()
+        # filtered_scene = Texture()
+        # filter_quad = self.filter_mgr.renderQuadInto(colortex=filtered_scene)
+        # aa_quad = self.filter_mgr.renderQuadInto(colortex=aa_scene)
+        # final_quad = self.filter_mgr.renderSceneInto(colortex=rendered_scene)
+        # filter_quad.set_shader(self.__load_shader('filter', 'sobel_filter'))
+        # filter_quad.set_shader_input('in_tex', rendered_scene)
+        # aa_quad.set_shader(self.__load_shader('fxaa', 'fxaa'))
+        # aa_quad.set_shader_input('in_tex', filtered_scene)
+        # final_quad.set_shader(self.__load_shader('filter', 'pass'))
+        # final_quad.set_shader_input('gamma', self.gamma)
+        # final_quad.set_shader_input('in_tex', aa_scene)
+
+    @staticmethod
+    def __load_shader(vshad, fshad):
+        with open('assets/shaders/%s.vert' % vshad) as vfile:
+            fvert = vfile.read()
+        with open('assets/shaders/%s.frag' % fshad) as ffile:
+            ffrag = ffile.read()
+        return load_shader(fvert, ffrag)
+
+    def apply(self):
+        # winprops = WindowProperties.size(2048, 2048)
+        # props = FrameBufferProperties()
+        # props.set_rgb_color(1)
+        # props.set_alpha_bits(1)
+        # props.set_depth_bits(1)
+        # lbuffer = base.graphicsEngine.make_output(
+        #     base.pipe, 'offscreen buffer', -2, props, winprops,
+        #     GraphicsPipe.BFRefuseWindow, base.win.getGsg(), base.win)
+        # self.buffer = lbuffer
+        # ldepthmap = Texture()
+        # lbuffer.addRenderTexture(ldepthmap, GraphicsOutput.RTMBindOrCopy,
+        #                          GraphicsOutput.RTPDepthStencil)
+        # ldepthmap.set_minfilter(Texture.FTShadow)
+        # ldepthmap.set_magfilter(Texture.FTShadow)
+
+        # base.camLens.set_near_far(1.0, 10000)
+        # base.camLens.set_fov(75)
+
+        # self.lcam = base.makeCamera(lbuffer)
+        # self.lcam.node().set_scene(render)
+        # self.lcam.node().get_lens().set_fov(45)
+        # self.lcam.node().get_lens().set_near_far(1, 100)
+
+        # render.set_shader_input('light', self.lcam)
+        # render.set_shader_input('depthmap', ldepthmap)
+        # render.set_shader_input('ambient', .15, .15, .15, 1.0)
+
+        # lci = NodePath(PandaNode('light camera initializer'))
+        # lci.set_shader(self.__load_shader('caster', 'caster'))
+        # self.lcam.node().set_initial_state(lci.get_state())
+
+        # mci = NodePath(PandaNode('main camera initializer'))
+        # # use PTALVecBaseX instead
+        # # setShaderInput('vec3argname', PTALVecBase3(((0, 0, 0), (1, 1, 1))))
+        render.set_shader(self.__main_shader())
+        # render.set_shader_input('num_lights', len(self.lights))
+        # self.set_shader_pars(render)
+        # list(map(
+        #     lambda lgt: self.set_lgt_args(*lgt), enumerate(self.lights)))
+        # mci.setShader(self.__main_shader())
+        # base.cam.node().set_initial_state(mci.getState())
+
+        # self.lcam.set_pos(15, 30, 45)
+        # self.lcam.look_at(0, 15, 0)
+        # self.lcam.node().get_lens().set_near_far(1, 100)
+
+    def __main_shader(self):
+        with open('assets/shaders/main.vert') as fvert:
+            vert = fvert.read()
+        with open('assets/shaders/main.frag') as ffrag:
+            frag = ffrag.read()
+        frag = frag.replace('<LIGHTS>', str(len(self.lights)))
+        return load_shader(vert, frag)
+
+    def toggle_shader(self):
+        if render.get_shader():
+            render.set_shader_off()
+            render.set_shader_auto()
+            return
+        self.apply()
+
+    def set_shader_pars(self, model):
+        texture_stages = model.find_all_texture_stages()
+        model.set_shader_input('gloss_slot', 0)
+        model.set_shader_input('detail_slot', 0)
+        model.set_shader_input('detail_scale', (1, 1))
+        for tstage in texture_stages:
+            if tstage.getSort() == 0: continue
+            self.__set_slots(tstage, model, 1 if tstage.getSort() == 10 else 2)
+
+    @staticmethod
+    def __set_slots(tstage, model, slot):
+        if tstage.getMode() == TextureStage.MGloss:
+            model.set_shader_input('gloss_slot', slot)
+        else:
+            model.set_shader_input('detail_slot', slot)
+            attrib_type = TexMatrixAttrib.get_class_type()
+            for geom_np in model.find_all_matches('**/+GeomNode'):
+                geom_node = geom_np.node()
+                for i in range(geom_node.get_num_geoms()):
+                    state = geom_node.get_geom_state(i)
+                    if state.has_attrib(attrib_type):
+                        attrib = state.get_attrib(attrib_type)
+                        for j in range(attrib.get_num_stages()):
+                            stage = attrib.get_stage(j)
+                            scale = attrib.get_transform(stage).get_scale()
+                            model.set_shader_input('detail_scale', scale)
+
+    def destroy(self): self.clear_lights()
+
+
+class ShaderSetter:
+
+    @staticmethod
+    def build(lgt):
+        cls2sett = {
+            AmbientLight: ShaderSetterAmbient,
+            PointLight: ShaderSetterPointLight,
+            DirectionalLight: ShaderSetterDirectionalLight,
+            Spotlight: ShaderSetterSpotlight}
+        return cls2sett[lgt.node().__class__]()
+
+    @staticmethod
+    def _set_pars(pref, lgt_pos, lgt):
+        render.set_shader_input(pref + 'pos', lgt_pos)
+        render.set_shader_input(pref + 'diff', lgt.node().get_color())
+        render.set_shader_input(pref + 'spec', lgt.node().get_color())
+
+
+class ShaderSetterAmbient(ShaderSetter):
+
+    @staticmethod
+    def set(pref, lgt):
+        render.set_shader_input(pref + 'amb', lgt.node().get_color())
+
+
+class ShaderSetterPointLight(ShaderSetter):
+
+    @staticmethod
+    def set(pref, lgt):
+        lgt_pos = lgt.get_mat(base.cam).xform(LVector4f(0, 0, 0, 1))
+        ShaderSetter._set_pars(pref, lgt_pos, lgt)
+
+
+class ShaderSetterDirectionalLight(ShaderSetter):
+
+    @staticmethod
+    def set(pref, lgt):
+        lgt_vec = -render.get_relative_vector(lgt, Vec3(0, 1, 0))
+        lgt_pos = LVector4f(lgt_vec[0], lgt_vec[1], lgt_vec[2], 0)
+        ShaderSetter._set_pars(pref, lgt_pos, lgt)
+
+
+class ShaderSetterSpotlight(ShaderSetter):
+
+    @staticmethod
+    def set(pref, lgt):
+        lgt_vec = base.cam.get_relative_vector(lgt, Vec3(0, 1, 0))
+        lgt_pos = lgt.get_mat(base.cam).xform(LVector4f(0, 0, 0, 1))
+        ShaderSetter._set_pars(pref, lgt_pos, lgt)
+        render.set_shader_input(pref + 'dir', lgt_vec)
+        render.set_shader_input(pref + 'exp', lgt.node().get_exponent())
+        cutoff = lgt.node().get_lens().get_fov()[0]
+        render.set_shader_input(pref + 'cutoff', cutoff)
diff --git a/lib/lib/p3d/vec.py b/lib/lib/p3d/vec.py
new file mode 100755 (executable)
index 0000000..f5f851e
--- /dev/null
@@ -0,0 +1,86 @@
+from panda3d.core import Vec2, Vec3, Mat4, LVector2f, LVector3f
+
+
+class P3dVec2:
+
+    attr_lst = ['x', 'y']
+    p3d_cls = Vec2
+
+    def __init__(self, *args):
+        self._vec = self.p3d_cls(*args)
+
+    @property
+    def x(self): return self._vec.x
+
+    @property
+    def y(self): return self._vec.y
+
+    @property
+    def xy(self): return P3dVec2(self._vec.x, self._vec.y)
+
+    def signed_angle_deg(self, vec):
+        return self._vec.signed_angle_deg(LVector2f(vec.x, vec.y))
+
+    def dot(self, other):
+        if isinstance(other, tuple): other = self.__class__(*other)
+        return self._vec.dot(other._vec)
+        #TODO: don't access a protected member
+
+    def __neg__(self):
+        nvec = - self._vec
+        return self.__class__(*[getattr(nvec, attr) for attr in self.attr_lst])
+
+    def __add__(self, vec):
+        if isinstance(vec, tuple): vec = self.__class__(*vec)
+        svec = self._vec + vec._vec  #TODO: don't access a protected member
+        return self.__class__(*[getattr(svec, attr) for attr in self.attr_lst])
+
+    def __sub__(self, vec):
+        if isinstance(vec, tuple): vec = self.__class__(*vec)
+        svec = self._vec - vec._vec  #TODO: don't access a protected member
+        return self.__class__(*[getattr(svec, attr) for attr in self.attr_lst])
+
+    def __mul__(self, val):
+        svec = self._vec * val
+        return self.__class__(*[getattr(svec, attr) for attr in self.attr_lst])
+
+    def normalize(self):
+        self._vec.normalize()
+        return self.__class__(*self.attrs)
+
+    @property
+    def attrs(self): return [getattr(self._vec, fld) for fld in self.attr_lst]
+
+    @property
+    def normalized(self):
+        vec = self.p3d_cls(*self.attrs)
+        vec.normalize()
+        return self.__class__(*[getattr(vec, fld) for fld in self.attr_lst])
+
+    def rotate(self, deg):
+        rot_mat = Mat4()
+        rot_mat.set_rotate_mat(deg, (0, 0, 1))
+        self._vec = rot_mat.xform_vec(self._vec)
+
+    def length(self): return self._vec.length()
+
+    def __repr__(self):
+        tmpl = '%s(' + \
+               ', '.join(['%s' for _ in range(len(self.attr_lst))]) + ')'
+        rnd = lambda x: round(x, 3)
+        vals = [rnd(getattr(self._vec, attr)) for attr in self.attr_lst]
+        pars = tuple([self.__class__.__name__] + vals)
+        return tmpl % pars
+
+
+class P3dVec3(P3dVec2):
+
+    attr_lst = ['x', 'y', 'z']
+    p3d_cls = Vec3
+
+    @property
+    def z(self): return self._vec.z
+
+    def signed_angle_deg(self, vec):
+        v_up = LVector3f(0, 0, 1)
+        return self._vec.signed_angle_deg(LVector3f(vec.x, vec.y, vec.z), v_up)
diff --git a/lib/lib/p3d/widget.py b/lib/lib/p3d/widget.py
new file mode 100755 (executable)
index 0000000..c7a247c
--- /dev/null
@@ -0,0 +1,206 @@
+from panda3d.core import LVecBase4f
+from direct.gui.DirectGuiGlobals import NORMAL, DISABLED
+from lib.engine.vec import Vec2
+
+
+class WidgetMixin:
+
+    highlight_color_offset = [
+        LVecBase4f(.4, .4, 0, .4),
+        LVecBase4f(0, 0, .4, .4),
+        LVecBase4f(0, .4, 0, .4),
+        LVecBase4f(.4, 0, 0, .4)]
+
+    def __init__(self):
+        self.start_txt_color = self.start_frame_color = None
+        self.was_visible = True
+        self.curr_offset = LVecBase4f(0, 0, 0, 0)
+
+    def get_np(self): return self.img
+
+    def enable(self): pass
+
+    def disable(self): pass
+
+    @property
+    def pos(self):
+        try: pos = self.get_pos(self.wdg.get_parent())
+        except AttributeError: pos = self.get_pos(self.img.get_parent())
+        return Vec2(pos[0], pos[2])
+
+    @property
+    def global_pos(self):
+        pos = self.get_pos(render2d)
+        return Vec2(pos[0], pos[2])
+
+
+class ImgMixin(WidgetMixin):
+
+    def init(self, wdg): pass
+
+
+class FrameMixin(WidgetMixin):
+
+    def init(self, wdg):
+        self.curr_offset = LVecBase4f(0, 0, 0, 0)
+        self.start_frame_color = wdg.get_np()['frameColor']
+
+    def enable(self):
+        self['state'] = NORMAL
+        if hasattr(self, 'set_alpha_scale'): self.set_alpha_scale(1)
+        self.get_np()['frameColor'] = self.start_frame_color
+
+    def disable(self):
+        self['state'] = DISABLED
+        if hasattr(self, 'set_alpha_scale'): self.set_alpha_scale(.25)
+        col = self.start_frame_color
+        self.get_np()['frameColor'] = (col[0], col[1], col[2], col[3] * .4)
+
+    def on_wdg_enter(self, pos=None, player=0): pass
+
+    def on_wdg_exit(self, pos=None, player=0): pass
+
+
+class ScrolledFrameMixin(WidgetMixin):
+
+    def init(self, wdg): pass
+
+    def enable(self): self['state'] = NORMAL
+
+    def disable(self): self['state'] = DISABLED
+
+    def on_wdg_enter(self, pos=None, player=0): pass
+
+    def on_wdg_exit(self, pos=None, player=0): pass
+
+
+class BtnMixin(FrameMixin):
+
+    def init(self, wdg):
+        FrameMixin.init(self, wdg)
+        wdg = wdg.get_np()
+        self.start_txt_color = wdg.component('text0').textNode.get_text_color()
+        self.start_txt_scale = wdg.component('text0').textNode.get_text_scale()
+
+    def on_arrow(self, direction): pass
+
+    def on_wdg_enter(self, pos=None, player=0):  # pos: mouse's position
+        self.curr_offset += WidgetMixin.highlight_color_offset[player]
+        col = LVecBase4f(self.start_frame_color)
+        self.get_np()['frameColor'] = col + self.curr_offset
+        self.get_np()['text_fg'] = self.start_txt_color + self.curr_offset
+        self.get_np()['text_scale'] = self.start_txt_scale * 1.04
+        self.get_np().set_shader_input('col_offset', self.curr_offset)
+        self.get_np().component('text0').textNode.set_shadow(.064, .064)
+        self.get_np().component('text0').textNode.set_shadow_color(.2, .2, 0, .8)
+
+    def on_wdg_exit(self, pos=None, player=0):  # pos: mouse's position
+        self.curr_offset -= WidgetMixin.highlight_color_offset[player]
+        col = LVecBase4f(self.start_frame_color)
+        self.get_np()['frameColor'] = col + self.curr_offset
+        self.get_np()['text_fg'] = self.start_txt_color
+        self.get_np()['text_scale'] = self.start_txt_scale
+        self.get_np()['frameColor'] = self.start_frame_color
+        self.get_np().set_shader_input('col_offset', self.curr_offset)
+        self.get_np().component('text0').textNode.set_shadow(0, 0)
+        self.get_np().component('text0').textNode.set_shadow_color(1, 1, 1, 1)
+
+    def on_enter(self, player):
+        if self['command'] and self['state'] == NORMAL:
+            lst_arg = [player] if player is not None else []
+            self['command'](*self['extraArgs'] + lst_arg)
+
+    def enable(self):
+        FrameMixin.enable(self)
+        t0n = self.get_np().component('text0').textNode
+        t0n.set_text_color(self.start_txt_color)
+        t0n.set_text_scale(self.start_txt_scale)
+
+    def disable(self):
+        FrameMixin.disable(self)
+        col = self.start_txt_color
+        self.get_np()['text_fg'] = (col[0], col[1], col[2], col[3] * .4)
+        t0n = self.get_np().component('text0').textNode
+        t0n.set_text_scale(self.start_txt_scale)
+
+
+class EntryMixin(FrameMixin):
+
+    def on_arrow(self, direction): pass
+
+    def on_wdg_enter(self, pos=None, player=0):  # pos: mouse's position
+        FrameMixin.on_wdg_enter(self, pos, player)
+        self.curr_offset += WidgetMixin.highlight_color_offset[player]
+        col = LVecBase4f(self.start_frame_color)
+        self.get_np()['frameColor'] = col + self.curr_offset
+        # self.get_np()['focus'] = 1  # it focuses it if mouse over
+        # self.get_np().setFocus()
+
+    def on_wdg_exit(self, pos=None, player=0):  # pos: mouse's position
+        FrameMixin.on_wdg_exit(self, pos, player)
+        self.curr_offset -= WidgetMixin.highlight_color_offset[player]
+        col = LVecBase4f(self.start_frame_color)
+        self.get_np()['frameColor'] = col + self.curr_offset
+        # self.get_np()['focus'] = 0
+        # self.get_np().setFocus()
+
+    def on_enter(self, player=0):
+        self['focus'] = 1
+        if self['command'] and self['state'] == NORMAL:
+            self['command'](*self['extraArgs'])
+
+
+class CheckBtnMixin(BtnMixin):
+
+    def on_enter(self, player=0):
+        self['indicatorValue'] = not self['indicatorValue']
+        BtnMixin.on_enter(self, player)
+
+
+class SliderMixin(FrameMixin):
+
+    def on_arrow(self, direction):
+        if direction in [(-1, 0), (1, 0)]:
+            n_p = self.get_np()
+            delta = (n_p['range'][1] - n_p['range'][0]) / 10.0
+            n_p['value'] += -delta if direction == (-1, 0) else delta
+        return direction in [(-1, 0), (1, 0)]
+
+    def on_enter(self, player=0): pass
+
+
+class OptionMenuMixin(BtnMixin):
+
+    def on_arrow(self, direction):
+        is_hor = direction in [(-1, 0), (1, 0)]
+        nodepath = self.get_np()
+        if is_hor or nodepath.popupMenu.is_hidden(): return False
+        old_idx = nodepath.highlightedIndex
+        dir2offset = {(0, -1): 1, (0, 1): -1}
+        idx = nodepath.highlightedIndex + dir2offset[direction]
+        idx = min(len(nodepath['items']) - 1, max(0, idx))
+        if old_idx == idx: return True
+        fcol = nodepath.component('item%s' % idx)['frameColor']
+        old_cmp = nodepath.component('item%s' % old_idx)
+        nodepath._unhighlightItem(old_cmp, fcol)
+        nodepath._highlightItem(nodepath.component('item%s' % idx), idx)
+        return True
+
+    def on_enter(self, player=0):
+        nodepath = self.get_np()
+        if nodepath.popupMenu.is_hidden():
+            nodepath.showPopupMenu()
+            nodepath._highlightItem(nodepath.component('item0'), 0)
+        else:
+            nodepath.selectHighlightedIndex()
+            idx = nodepath.selectedIndex
+            if nodepath['command']: nodepath['command'](nodepath['items'][idx])
+            nodepath.hidePopupMenu()
+            idx += -1 if idx else 1
+            try:
+                fcol = nodepath.component('item%s' % idx)['frameColor']
+                curr_name = 'item%s' % nodepath.selectedIndex
+                nodepath._unhighlightItem(nodepath.component(curr_name), fcol)
+            except KeyError:  # when there is only one element
+                pass
+        return not nodepath.popupMenu.is_hidden()
diff --git a/lib/observer.py b/lib/observer.py
new file mode 100644 (file)
index 0000000..7c49116
--- /dev/null
@@ -0,0 +1,54 @@
+class ObsInfo:
+
+    def __init__(self, mth, sort, args):
+        self.mth = mth
+        self.sort = sort
+        self.args = args
+
+    def __repr__(self): return str(self.mth)
+
+
+class Subject:
+
+    def __init__(self):
+        self.observers = {}
+
+    def attach(self, obs_meth, sort=10, rename='', args=None):
+        args = args or []
+        onm = rename or obs_meth.__name__
+        if onm not in self.observers: self.observers[onm] = []
+        self.observers[onm] += [ObsInfo(obs_meth, sort, args)]
+        sorted_obs = sorted(self.observers[onm], key=lambda obs: obs.sort)
+        self.observers[onm] = sorted_obs
+
+    def detach(self, obs_meth, lambda_call=None):
+        if isinstance(obs_meth, str):
+            onm = obs_meth
+            observers = [obs for obs in self.observers[onm]
+                         if obs.mth == lambda_call]
+        else:
+            onm = obs_meth.__name__
+            observers = [obs for obs in self.observers[onm]
+                         if obs.mth == obs_meth]
+        if not observers: raise Exception
+        list(map(self.observers[onm].remove, observers))
+
+    def notify(self, meth, *args, **kwargs):
+        if meth not in self.observers: return  # no obs for this notification
+        for obs in self.observers[meth][:]:
+            if obs in self.observers[meth]:  # if an obs removes another one
+                try:
+                    act_args = obs.args + list(args)
+                    obs.mth(*act_args, **kwargs)
+                except SystemError:
+                    print('Quit')
+                    import sys; sys.exit()
+
+    def observing(self, obs_meth):
+        if callable(obs_meth): obs_meth = obs_meth.__name__
+        return obs_meth in self.observers and self.observers[obs_meth]
+
+    def destroy(self): self.observers = None
+
+
+class Observer: pass
diff --git a/lib/tools/apply_gloss.py b/lib/tools/apply_gloss.py
new file mode 100644 (file)
index 0000000..46eed32
--- /dev/null
@@ -0,0 +1,40 @@
+import sys
+
+if len(sys.argv) != 2:
+    print('Usage: apply_gloss.py filename.egg')
+    sys.exit(0)
+
+
+def __process_line(idx, line, _out_lines):
+    if line.strip() != '<Scalar> envtype { GLOSS }':
+        mth = __process_nongloss_line
+    else: mth = __process_gloss_line
+    return mth(idx, line, _out_lines)
+
+
+def __process_nongloss_line(idx, line, _out_lines):
+    is_scal = line.strip().startswith('<Scalar> alpha-file { ')
+    if not idx or not (line.strip() == _out_lines[-1].strip() and is_scal):
+        _out_lines += [line.rstrip()]
+    return _out_lines
+
+
+def __process_gloss_line(idx, line, _out_lines):
+    _out_lines += [line.rstrip()]
+    outl = ''
+    for char in line:
+        if char == ' ': outl += ' '
+        else: break
+    outl += '<Scalar> alpha-file { %s }' % lines[idx - 1].strip()
+    return _out_lines + [outl]
+
+
+out_lines = []
+with open(sys.argv[1]) as fin:
+    lines = fin.readlines()
+    for _idx, _line in enumerate(lines):
+        out_lines = __process_line(_idx, _line, out_lines)
+
+
+with open(sys.argv[1], 'w') as fout:
+    list(map(lambda outl: fout.write(outl + '\n'), out_lines))
diff --git a/lib/tools/build_metal_texture.py b/lib/tools/build_metal_texture.py
new file mode 100644 (file)
index 0000000..f20ba4a
--- /dev/null
@@ -0,0 +1,16 @@
+from sys import argv
+from os.path import abspath
+from os import system
+
+ao = abspath(argv[1])
+roughness = abspath(argv[2])
+metal = abspath(argv[3])
+fout = abspath(argv[4])
+
+system('cp %s %s' % (ao, fout))
+cmd = 'convert %s %s -alpha off -compose copy_green -composite %s' % (fout, roughness, fout)
+print(cmd)
+system(cmd)
+cmd = 'convert %s %s -alpha off -compose copy_blue -composite %s' % (fout, metal, fout)
+print(cmd)
+system(cmd)
diff --git a/lib/tools/fix_mask_texture.py b/lib/tools/fix_mask_texture.py
new file mode 100644 (file)
index 0000000..236f879
--- /dev/null
@@ -0,0 +1,81 @@
+class BlockReplacer:
+
+    def __init__(self, out_lines, start_line, end_line, new_lines):
+        self.out_lines = out_lines
+        self.start_line = start_line
+        self.end_line = end_line
+        self.new_lines = new_lines
+        self.is_replacing = False
+
+    def process_line(self, line):
+        if self.is_replacing and line.rstrip() == self.end_line:
+            self.is_replacing = False
+            return True
+        if line.rstrip() == self.start_line:
+            self.out_lines += self.new_lines
+            self.is_replacing = True
+        return self.is_replacing
+
+
+class Fixer:
+
+    def __init__(self):
+        self.out_lines = []
+        with open('track.egg') as fin:
+            self.lines = fin.readlines()
+        self.replacers = []
+        new_lines = [
+            '<Texture> MASKOBJTrack {\n',
+            '  "./tex/MASKOBJTrack.jpg"\n',
+            '  <Scalar> combine-rgb { INTERPOLATE }\n',
+            '  <Scalar> combine-rgb-source0 { PREVIOUS }\n',
+            '  <Scalar> combine-rgb-operand0 { SRC-COLOR }\n',
+            '  <Scalar> combine-rgb-source1 { LAST_SAVED_RESULT }\n',
+            '  <Scalar> combine-rgb-operand1 { SRC-COLOR }\n',
+            '  <Scalar> combine-rgb-source2 { TEXTURE }\n',
+            '  <Scalar> combine-rgb-operand2 { SRC-COLOR }\n',
+            '  <Scalar> minfilter { LINEAR_MIPMAP_LINEAR }\n',
+            '  <Scalar> magfilter { LINEAR_MIPMAP_LINEAR }\n',
+            '  <Scalar> wrap { REPEAT }\n',
+            '}\n']
+        rep_str = '<Texture> MASKOBJTrack {'
+        rep = BlockReplacer(self.out_lines, rep_str, '}', new_lines)
+        self.replacers += [rep]
+
+        new_lines = [
+            '<Texture> TEXREPOBJTrack1 {\n',
+            '  "./tex/snowbackground.jpg"\n',
+            '  <Scalar> envtype { MODULATE }\n']
+        rep_str = '<Texture> TEXREPOBJTrack1 {'
+        repl_str = '  <Scalar> envtype { MODULATE }'
+        rep = BlockReplacer(self.out_lines, rep_str, repl_str, new_lines)
+        self.replacers += [rep]
+
+        new_lines = [
+            '<Texture> TEXREPOBJTrack2 {\n',
+            '  "./tex/Tileable ice ground texture.jpg"\n',
+            '  <Scalar> envtype { MODULATE }\n',
+            '  <Scalar> saved-result { 1 }\n']
+        rep_str = '<Texture> TEXREPOBJTrack2 {'
+        repl_str = '  <Scalar> envtype { MODULATE }'
+        rep = BlockReplacer(self.out_lines, rep_str, repl_str, new_lines)
+        self.replacers += [rep]
+
+        new_lines = [
+            '      <TRef> { TEXREPOBJTrack1 }\n',
+            '      <TRef> { MASKOBJTrack }\n']
+        rep = BlockReplacer(
+            self.out_lines,
+            '      <TRef> { MASKOBJTrack }',
+            '      <TRef> { TEXREPOBJTrack1 }',
+            new_lines)
+        self.replacers += [rep]
+
+        for line in self.lines:
+            if not any(rep.process_line(line) for rep in self.replacers):
+                self.out_lines += [line]
+        with open('track_fixed.egg', 'w') as fout:
+            list(map(fout.write, self.out_lines))
+
+
+Fixer()
diff --git a/lib/tools/kill_yorg.py b/lib/tools/kill_yorg.py
new file mode 100644 (file)
index 0000000..38ae56b
--- /dev/null
@@ -0,0 +1,16 @@
+# this kills yocto when it is being launched by geany
+from subprocess import Popen, PIPE
+from os import system
+
+
+def exec_cmd(cmd):
+    ret = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
+    return '\n'.join(ret)
+
+
+processes_str = exec_cmd('ps aux | grep "python main.py"')
+processes = list(enumerate(processes_str.split('\n')))
+sh_cmd = ' /bin/sh -c python main.py'
+geany_row = [proc for proc in processes if proc[1].endswith(sh_cmd)][0][0]
+geany_pid = list(processes)[geany_row + 1][1].split()[1]
+system('kill -9 ' + geany_pid)
diff --git a/lib/tools/kill_yorg_server.py b/lib/tools/kill_yorg_server.py
new file mode 100644 (file)
index 0000000..ad2d1a5
--- /dev/null
@@ -0,0 +1,14 @@
+# this kills yocto's server
+from subprocess import Popen, PIPE
+from os import system
+
+
+def exec_cmd(cmd):
+    ret = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True).communicate()
+    return '\n'.join(ret)
+
+
+grep = "grep -E 'python.*server.*yocto.*main.py|python.*yocto.*server.*main.py'"
+proc_line = exec_cmd('ps aux | ' + grep).split('\n')[0]
+srv_pid = proc_line.split()[1]
+system('kill -9 ' + srv_pid)
diff --git a/lib/tools/pdfsingle.py b/lib/tools/pdfsingle.py
new file mode 100755 (executable)
index 0000000..1ad7cf8
--- /dev/null
@@ -0,0 +1,35 @@
+# python lib/tools/pdfsingle.py path/to/file.py
+from os import chdir, getcwd, system
+from os.path import dirname, basename, exists
+from sys import argv
+
+
+class InsideDir:
+
+    def __init__(self, dir_):
+        self.dir = dir_
+        self.old_dir = getcwd()
+
+    def __enter__(self):
+        chdir(self.dir)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        chdir(self.old_dir)
+
+
+filename = argv[1]
+name = basename(filename)
+path = dirname(filename)
+noext = name.rsplit('.', 1)[0]
+test_tmpl = "tail -n +1 {found} " + \
+    "| sed 's/==> /# ==> /' > tmp.txt ; enscript --font=Courier10 " + \
+    "--continuous-page-numbers --no-header --pretty-print=python " + \
+    "-o - tmp.txt | psnup -2 -P letter -p a4 -m12 | ps2pdf - {name}.pdf ; rm tmp.txt"
+    #"-o - tmp.txt | ps2pdf - {name}.pdf ; rm tmp.txt"
+found = filename
+with InsideDir('tests/' + path):
+    if exists('test_' + name):
+        found += ' lib/tests/%s/test_%s' % (path, name)
+test_cmd = test_tmpl.format(name=noext, found=found)
+system(test_cmd)
+#system('pdfnup --nup 2x1 -o {noext}.pdf {noext}.pdf'.format(noext=noext))
diff --git a/lib/tools/process_models.py b/lib/tools/process_models.py
new file mode 100644 (file)
index 0000000..97e3c28
--- /dev/null
@@ -0,0 +1,10 @@
+# usage: python lib/tools/process_models.py
+from os import system, walk
+from sys import executable
+
+
+for root, _, fnames in walk('assets/models'):
+    for fname in [fname for fname in fnames if fname.endswith('.egg')]:
+        _fname = root + '/' + fname
+        system('egg-trans -nv 30 -o %s %s' % (_fname, _fname))
+        system(executable + ' lib/tools/apply_gloss.py ' + _fname)
diff --git a/lib/tools/set_diffuse.py b/lib/tools/set_diffuse.py
new file mode 100755 (executable)
index 0000000..838e2c5
--- /dev/null
@@ -0,0 +1,21 @@
+from os import listdir
+
+
+def __set_diffuse(fpath):
+    out_lines = []
+    with open(fpath) as fin:
+        for line in fin.readlines():
+            out_lines = __process_line(line, out_lines)
+    with open(fpath, 'w') as fin:
+        list(map(lambda outl: fin.write(outl + '\n'), out_lines))
+
+
+def __process_line(line, out_lines):
+    if not line.strip().startswith('<Scalar> diff'):
+        new_lines = [line.rstrip()]
+    else: new_lines = [line.split(' { ')[0] + ' { 1.000000 }']
+    return out_lines + new_lines
+
+
+list(map(__set_diffuse,
+         [fname for fname in listdir('.') if fname.endswith('.egg')]))
diff --git a/licenses/bsd.txt b/licenses/bsd.txt
new file mode 100644 (file)
index 0000000..1dc32a6
--- /dev/null
@@ -0,0 +1,10 @@
+Copyright (c) 2020, Ya2
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+* Neither the name of the Ya2 nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL YA2 BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/licenses/cc_by-sa.txt b/licenses/cc_by-sa.txt
new file mode 100644 (file)
index 0000000..c90487c
--- /dev/null
@@ -0,0 +1,359 @@
+Creative Commons Legal Code
+
+Attribution-ShareAlike 3.0 Unported
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
+    DAMAGES RESULTING FROM ITS USE.
+
+License
+
+THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
+COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
+COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
+AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
+
+BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
+TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
+BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
+CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
+CONDITIONS.
+
+1. Definitions
+
+ a. "Adaptation" means a work based upon the Work, or upon the Work and
+    other pre-existing works, such as a translation, adaptation,
+    derivative work, arrangement of music or other alterations of a
+    literary or artistic work, or phonogram or performance and includes
+    cinematographic adaptations or any other form in which the Work may be
+    recast, transformed, or adapted including in any form recognizably
+    derived from the original, except that a work that constitutes a
+    Collection will not be considered an Adaptation for the purpose of
+    this License. For the avoidance of doubt, where the Work is a musical
+    work, performance or phonogram, the synchronization of the Work in
+    timed-relation with a moving image ("synching") will be considered an
+    Adaptation for the purpose of this License.
+ b. "Collection" means a collection of literary or artistic works, such as
+    encyclopedias and anthologies, or performances, phonograms or
+    broadcasts, or other works or subject matter other than works listed
+    in Section 1(f) below, which, by reason of the selection and
+    arrangement of their contents, constitute intellectual creations, in
+    which the Work is included in its entirety in unmodified form along
+    with one or more other contributions, each constituting separate and
+    independent works in themselves, which together are assembled into a
+    collective whole. A work that constitutes a Collection will not be
+    considered an Adaptation (as defined below) for the purposes of this
+    License.
+ c. "Creative Commons Compatible License" means a license that is listed
+    at http://creativecommons.org/compatiblelicenses that has been
+    approved by Creative Commons as being essentially equivalent to this
+    License, including, at a minimum, because that license: (i) contains
+    terms that have the same purpose, meaning and effect as the License
+    Elements of this License; and, (ii) explicitly permits the relicensing
+    of adaptations of works made available under that license under this
+    License or a Creative Commons jurisdiction license with the same
+    License Elements as this License.
+ d. "Distribute" means to make available to the public the original and
+    copies of the Work or Adaptation, as appropriate, through sale or
+    other transfer of ownership.
+ e. "License Elements" means the following high-level license attributes
+    as selected by Licensor and indicated in the title of this License:
+    Attribution, ShareAlike.
+ f. "Licensor" means the individual, individuals, entity or entities that
+    offer(s) the Work under the terms of this License.
+ g. "Original Author" means, in the case of a literary or artistic work,
+    the individual, individuals, entity or entities who created the Work
+    or if no individual or entity can be identified, the publisher; and in
+    addition (i) in the case of a performance the actors, singers,
+    musicians, dancers, and other persons who act, sing, deliver, declaim,
+    play in, interpret or otherwise perform literary or artistic works or
+    expressions of folklore; (ii) in the case of a phonogram the producer
+    being the person or legal entity who first fixes the sounds of a
+    performance or other sounds; and, (iii) in the case of broadcasts, the
+    organization that transmits the broadcast.
+ h. "Work" means the literary and/or artistic work offered under the terms
+    of this License including without limitation any production in the
+    literary, scientific and artistic domain, whatever may be the mode or
+    form of its expression including digital form, such as a book,
+    pamphlet and other writing; a lecture, address, sermon or other work
+    of the same nature; a dramatic or dramatico-musical work; a
+    choreographic work or entertainment in dumb show; a musical
+    composition with or without words; a cinematographic work to which are
+    assimilated works expressed by a process analogous to cinematography;
+    a work of drawing, painting, architecture, sculpture, engraving or
+    lithography; a photographic work to which are assimilated works
+    expressed by a process analogous to photography; a work of applied
+    art; an illustration, map, plan, sketch or three-dimensional work
+    relative to geography, topography, architecture or science; a
+    performance; a broadcast; a phonogram; a compilation of data to the
+    extent it is protected as a copyrightable work; or a work performed by
+    a variety or circus performer to the extent it is not otherwise
+    considered a literary or artistic work.
+ i. "You" means an individual or entity exercising rights under this
+    License who has not previously violated the terms of this License with
+    respect to the Work, or who has received express permission from the
+    Licensor to exercise rights under this License despite a previous
+    violation.
+ j. "Publicly Perform" means to perform public recitations of the Work and
+    to communicate to the public those public recitations, by any means or
+    process, including by wire or wireless means or public digital
+    performances; to make available to the public Works in such a way that
+    members of the public may access these Works from a place and at a
+    place individually chosen by them; to perform the Work to the public
+    by any means or process and the communication to the public of the
+    performances of the Work, including by public digital performance; to
+    broadcast and rebroadcast the Work by any means including signs,
+    sounds or images.
+ k. "Reproduce" means to make copies of the Work by any means including
+    without limitation by sound or visual recordings and the right of
+    fixation and reproducing fixations of the Work, including storage of a
+    protected performance or phonogram in digital form or other electronic
+    medium.
+
+2. Fair Dealing Rights. Nothing in this License is intended to reduce,
+limit, or restrict any uses free from copyright or rights arising from
+limitations or exceptions that are provided for in connection with the
+copyright protection under copyright law or other applicable laws.
+
+3. License Grant. Subject to the terms and conditions of this License,
+Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
+perpetual (for the duration of the applicable copyright) license to
+exercise the rights in the Work as stated below:
+
+ a. to Reproduce the Work, to incorporate the Work into one or more
+    Collections, and to Reproduce the Work as incorporated in the
+    Collections;
+ b. to create and Reproduce Adaptations provided that any such Adaptation,
+    including any translation in any medium, takes reasonable steps to
+    clearly label, demarcate or otherwise identify that changes were made
+    to the original Work. For example, a translation could be marked "The
+    original work was translated from English to Spanish," or a
+    modification could indicate "The original work has been modified.";
+ c. to Distribute and Publicly Perform the Work including as incorporated
+    in Collections; and,
+ d. to Distribute and Publicly Perform Adaptations.
+ e. For the avoidance of doubt:
+
+     i. Non-waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme cannot be waived, the Licensor
+        reserves the exclusive right to collect such royalties for any
+        exercise by You of the rights granted under this License;
+    ii. Waivable Compulsory License Schemes. In those jurisdictions in
+        which the right to collect royalties through any statutory or
+        compulsory licensing scheme can be waived, the Licensor waives the
+        exclusive right to collect such royalties for any exercise by You
+        of the rights granted under this License; and,
+   iii. Voluntary License Schemes. The Licensor waives the right to
+        collect royalties, whether individually or, in the event that the
+        Licensor is a member of a collecting society that administers
+        voluntary licensing schemes, via that society, from any exercise
+        by You of the rights granted under this License.
+
+The above rights may be exercised in all media and formats whether now
+known or hereafter devised. The above rights include the right to make
+such modifications as are technically necessary to exercise the rights in
+other media and formats. Subject to Section 8(f), all rights not expressly
+granted by Licensor are hereby reserved.
+
+4. Restrictions. The license granted in Section 3 above is expressly made
+subject to and limited by the following restrictions:
+
+ a. You may Distribute or Publicly Perform the Work only under the terms
+    of this License. You must include a copy of, or the Uniform Resource
+    Identifier (URI) for, this License with every copy of the Work You
+    Distribute or Publicly Perform. You may not offer or impose any terms
+    on the Work that restrict the terms of this License or the ability of
+    the recipient of the Work to exercise the rights granted to that
+    recipient under the terms of the License. You may not sublicense the
+    Work. You must keep intact all notices that refer to this License and
+    to the disclaimer of warranties with every copy of the Work You
+    Distribute or Publicly Perform. When You Distribute or Publicly
+    Perform the Work, You may not impose any effective technological
+    measures on the Work that restrict the ability of a recipient of the
+    Work from You to exercise the rights granted to that recipient under
+    the terms of the License. This Section 4(a) applies to the Work as
+    incorporated in a Collection, but this does not require the Collection
+    apart from the Work itself to be made subject to the terms of this
+    License. If You create a Collection, upon notice from any Licensor You
+    must, to the extent practicable, remove from the Collection any credit
+    as required by Section 4(c), as requested. If You create an
+    Adaptation, upon notice from any Licensor You must, to the extent
+    practicable, remove from the Adaptation any credit as required by
+    Section 4(c), as requested.
+ b. You may Distribute or Publicly Perform an Adaptation only under the
+    terms of: (i) this License; (ii) a later version of this License with
+    the same License Elements as this License; (iii) a Creative Commons
+    jurisdiction license (either this or a later license version) that
+    contains the same License Elements as this License (e.g.,
+    Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible
+    License. If you license the Adaptation under one of the licenses
+    mentioned in (iv), you must comply with the terms of that license. If
+    you license the Adaptation under the terms of any of the licenses
+    mentioned in (i), (ii) or (iii) (the "Applicable License"), you must
+    comply with the terms of the Applicable License generally and the
+    following provisions: (I) You must include a copy of, or the URI for,
+    the Applicable License with every copy of each Adaptation You
+    Distribute or Publicly Perform; (II) You may not offer or impose any
+    terms on the Adaptation that restrict the terms of the Applicable
+    License or the ability of the recipient of the Adaptation to exercise
+    the rights granted to that recipient under the terms of the Applicable
+    License; (III) You must keep intact all notices that refer to the
+    Applicable License and to the disclaimer of warranties with every copy
+    of the Work as included in the Adaptation You Distribute or Publicly
+    Perform; (IV) when You Distribute or Publicly Perform the Adaptation,
+    You may not impose any effective technological measures on the
+    Adaptation that restrict the ability of a recipient of the Adaptation
+    from You to exercise the rights granted to that recipient under the
+    terms of the Applicable License. This Section 4(b) applies to the
+    Adaptation as incorporated in a Collection, but this does not require
+    the Collection apart from the Adaptation itself to be made subject to
+    the terms of the Applicable License.
+ c. If You Distribute, or Publicly Perform the Work or any Adaptations or
+    Collections, You must, unless a request has been made pursuant to
+    Section 4(a), keep intact all copyright notices for the Work and
+    provide, reasonable to the medium or means You are utilizing: (i) the
+    name of the Original Author (or pseudonym, if applicable) if supplied,
+    and/or if the Original Author and/or Licensor designate another party
+    or parties (e.g., a sponsor institute, publishing entity, journal) for
+    attribution ("Attribution Parties") in Licensor's copyright notice,
+    terms of service or by other reasonable means, the name of such party
+    or parties; (ii) the title of the Work if supplied; (iii) to the
+    extent reasonably practicable, the URI, if any, that Licensor
+    specifies to be associated with the Work, unless such URI does not
+    refer to the copyright notice or licensing information for the Work;
+    and (iv) , consistent with Ssection 3(b), in the case of an
+    Adaptation, a credit identifying the use of the Work in the Adaptation
+    (e.g., "French translation of the Work by Original Author," or
+    "Screenplay based on original Work by Original Author"). The credit
+    required by this Section 4(c) may be implemented in any reasonable
+    manner; provided, however, that in the case of a Adaptation or
+    Collection, at a minimum such credit will appear, if a credit for all
+    contributing authors of the Adaptation or Collection appears, then as
+    part of these credits and in a manner at least as prominent as the
+    credits for the other contributing authors. For the avoidance of
+    doubt, You may only use the credit required by this Section for the
+    purpose of attribution in the manner set out above and, by exercising
+    Your rights under this License, You may not implicitly or explicitly
+    assert or imply any connection with, sponsorship or endorsement by the
+    Original Author, Licensor and/or Attribution Parties, as appropriate,
+    of You or Your use of the Work, without the separate, express prior
+    written permission of the Original Author, Licensor and/or Attribution
+    Parties.
+ d. Except as otherwise agreed in writing by the Licensor or as may be
+    otherwise permitted by applicable law, if You Reproduce, Distribute or
+    Publicly Perform the Work either by itself or as part of any
+    Adaptations or Collections, You must not distort, mutilate, modify or
+    take other derogatory action in relation to the Work which would be
+    prejudicial to the Original Author's honor or reputation. Licensor
+    agrees that in those jurisdictions (e.g. Japan), in which any exercise
+    of the right granted in Section 3(b) of this License (the right to
+    make Adaptations) would be deemed to be a distortion, mutilation,
+    modification or other derogatory action prejudicial to the Original
+    Author's honor and reputation, the Licensor will waive or not assert,
+    as appropriate, this Section, to the fullest extent permitted by the
+    applicable national law, to enable You to reasonably exercise Your
+    right under Section 3(b) of this License (right to make Adaptations)
+    but not otherwise.
+
+5. Representations, Warranties and Disclaimer
+
+UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
+OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
+KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
+INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
+FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
+LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
+WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
+OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
+
+6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
+LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
+ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
+ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
+BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. Termination
+
+ a. This License and the rights granted hereunder will terminate
+    automatically upon any breach by You of the terms of this License.
+    Individuals or entities who have received Adaptations or Collections
+    from You under this License, however, will not have their licenses
+    terminated provided such individuals or entities remain in full
+    compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
+    survive any termination of this License.
+ b. Subject to the above terms and conditions, the license granted here is
+    perpetual (for the duration of the applicable copyright in the Work).
+    Notwithstanding the above, Licensor reserves the right to release the
+    Work under different license terms or to stop distributing the Work at
+    any time; provided, however that any such election will not serve to
+    withdraw this License (or any other license that has been, or is
+    required to be, granted under the terms of this License), and this
+    License will continue in full force and effect unless terminated as
+    stated above.
+
+8. Miscellaneous
+
+ a. Each time You Distribute or Publicly Perform the Work or a Collection,
+    the Licensor offers to the recipient a license to the Work on the same
+    terms and conditions as the license granted to You under this License.
+ b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
+    offers to the recipient a license to the original Work on the same
+    terms and conditions as the license granted to You under this License.
+ c. If any provision of this License is invalid or unenforceable under
+    applicable law, it shall not affect the validity or enforceability of
+    the remainder of the terms of this License, and without further action
+    by the parties to this agreement, such provision shall be reformed to
+    the minimum extent necessary to make such provision valid and
+    enforceable.
+ d. No term or provision of this License shall be deemed waived and no
+    breach consented to unless such waiver or consent shall be in writing
+    and signed by the party to be charged with such waiver or consent.
+ e. This License constitutes the entire agreement between the parties with
+    respect to the Work licensed here. There are no understandings,
+    agreements or representations with respect to the Work not specified
+    here. Licensor shall not be bound by any additional provisions that
+    may appear in any communication from You. This License may not be
+    modified without the mutual written agreement of the Licensor and You.
+ f. The rights granted under, and the subject matter referenced, in this
+    License were drafted utilizing the terminology of the Berne Convention
+    for the Protection of Literary and Artistic Works (as amended on
+    September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
+    Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
+    and the Universal Copyright Convention (as revised on July 24, 1971).
+    These rights and subject matter take effect in the relevant
+    jurisdiction in which the License terms are sought to be enforced
+    according to the corresponding provisions of the implementation of
+    those treaty provisions in the applicable national law. If the
+    standard suite of rights granted under applicable copyright law
+    includes additional rights not granted under this License, such
+    additional rights are deemed to be included in the License; this
+    License is not intended to restrict the license of any rights under
+    applicable law.
+
+
+Creative Commons Notice
+
+    Creative Commons is not a party to this License, and makes no warranty
+    whatsoever in connection with the Work. Creative Commons will not be
+    liable to You or any party on any legal theory for any damages
+    whatsoever, including without limitation any general, special,
+    incidental or consequential damages arising in connection to this
+    license. Notwithstanding the foregoing two (2) sentences, if Creative
+    Commons has expressly identified itself as the Licensor hereunder, it
+    shall have all rights and obligations of Licensor.
+
+    Except for the limited purpose of indicating to the public that the
+    Work is licensed under the CCPL, Creative Commons does not authorize
+    the use by either party of the trademark "Creative Commons" or any
+    related trademark or logo of Creative Commons without the prior
+    written consent of Creative Commons. Any permitted use will be in
+    compliance with Creative Commons' then-current trademark usage
+    guidelines, as may be published on its website or otherwise made
+    available upon request from time to time. For the avoidance of doubt,
+    this trademark restriction does not form part of the License.
+
+    Creative Commons may be contacted at http://creativecommons.org/.
diff --git a/licenses/ffmpeg/COPYING.LGPLv2.1.txt b/licenses/ffmpeg/COPYING.LGPLv2.1.txt
new file mode 100644 (file)
index 0000000..58af0d3
--- /dev/null
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+\f
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+\f
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/licenses/freetype/FTL.TXT b/licenses/freetype/FTL.TXT
new file mode 100644 (file)
index 0000000..e874ba5
--- /dev/null
@@ -0,0 +1,169 @@
+                    The FreeType Project LICENSE
+                    ----------------------------
+
+                            2006-Jan-27
+
+                    Copyright 1996-2002, 2006 by
+          David Turner, Robert Wilhelm, and Werner Lemberg
+
+
+
+Introduction
+============
+
+  The FreeType  Project is distributed in  several archive packages;
+  some of them may contain, in addition to the FreeType font engine,
+  various tools and  contributions which rely on, or  relate to, the
+  FreeType Project.
+
+  This  license applies  to all  files found  in such  packages, and
+  which do not  fall under their own explicit  license.  The license
+  affects  thus  the  FreeType   font  engine,  the  test  programs,
+  documentation and makefiles, at the very least.
+
+  This  license   was  inspired  by  the  BSD,   Artistic,  and  IJG
+  (Independent JPEG  Group) licenses, which  all encourage inclusion
+  and  use of  free  software in  commercial  and freeware  products
+  alike.  As a consequence, its main points are that:
+
+    o We don't promise that this software works. However, we will be
+      interested in any kind of bug reports. (`as is' distribution)
+
+    o You can  use this software for whatever you  want, in parts or
+      full form, without having to pay us. (`royalty-free' usage)
+
+    o You may not pretend that  you wrote this software.  If you use
+      it, or  only parts of it,  in a program,  you must acknowledge
+      somewhere  in  your  documentation  that  you  have  used  the
+      FreeType code. (`credits')
+
+  We  specifically  permit  and  encourage  the  inclusion  of  this
+  software, with  or without modifications,  in commercial products.
+  We  disclaim  all warranties  covering  The  FreeType Project  and
+  assume no liability related to The FreeType Project.
+
+
+  Finally,  many  people  asked  us  for  a  preferred  form  for  a
+  credit/disclaimer to use in compliance with this license.  We thus
+  encourage you to use the following text:
+
+   """  
+    Portions of this software are copyright © <year> The FreeType
+    Project (www.freetype.org).  All rights reserved.
+   """
+
+  Please replace <year> with the value from the FreeType version you
+  actually use.
+
+
+Legal Terms
+===========
+
+0. Definitions
+--------------
+
+  Throughout this license,  the terms `package', `FreeType Project',
+  and  `FreeType  archive' refer  to  the  set  of files  originally
+  distributed  by the  authors  (David Turner,  Robert Wilhelm,  and
+  Werner Lemberg) as the `FreeType Project', be they named as alpha,
+  beta or final release.
+
+  `You' refers to  the licensee, or person using  the project, where
+  `using' is a generic term including compiling the project's source
+  code as  well as linking it  to form a  `program' or `executable'.
+  This  program is  referred to  as  `a program  using the  FreeType
+  engine'.
+
+  This  license applies  to all  files distributed  in  the original
+  FreeType  Project,   including  all  source   code,  binaries  and
+  documentation,  unless  otherwise  stated   in  the  file  in  its
+  original, unmodified form as  distributed in the original archive.
+  If you are  unsure whether or not a particular  file is covered by
+  this license, you must contact us to verify this.
+
+  The FreeType  Project is copyright (C) 1996-2000  by David Turner,
+  Robert Wilhelm, and Werner Lemberg.  All rights reserved except as
+  specified below.
+
+1. No Warranty
+--------------
+
+  THE FREETYPE PROJECT  IS PROVIDED `AS IS' WITHOUT  WARRANTY OF ANY
+  KIND, EITHER  EXPRESS OR IMPLIED,  INCLUDING, BUT NOT  LIMITED TO,
+  WARRANTIES  OF  MERCHANTABILITY   AND  FITNESS  FOR  A  PARTICULAR
+  PURPOSE.  IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE  FOR ANY DAMAGES CAUSED  BY THE USE OR  THE INABILITY TO
+  USE, OF THE FREETYPE PROJECT.
+
+2. Redistribution
+-----------------
+
+  This  license  grants  a  worldwide, royalty-free,  perpetual  and
+  irrevocable right  and license to use,  execute, perform, compile,
+  display,  copy,   create  derivative  works   of,  distribute  and
+  sublicense the  FreeType Project (in  both source and  object code
+  forms)  and  derivative works  thereof  for  any  purpose; and  to
+  authorize others  to exercise  some or all  of the  rights granted
+  herein, subject to the following conditions:
+
+    o Redistribution of  source code  must retain this  license file
+      (`FTL.TXT') unaltered; any  additions, deletions or changes to
+      the original  files must be clearly  indicated in accompanying
+      documentation.   The  copyright   notices  of  the  unaltered,
+      original  files must  be  preserved in  all  copies of  source
+      files.
+
+    o Redistribution in binary form must provide a  disclaimer  that
+      states  that  the software is based in part of the work of the
+      FreeType Team,  in  the  distribution  documentation.  We also
+      encourage you to put an URL to the FreeType web page  in  your
+      documentation, though this isn't mandatory.
+
+  These conditions  apply to any  software derived from or  based on
+  the FreeType Project,  not just the unmodified files.   If you use
+  our work, you  must acknowledge us.  However, no  fee need be paid
+  to us.
+
+3. Advertising
+--------------
+
+  Neither the  FreeType authors and  contributors nor you  shall use
+  the name of the  other for commercial, advertising, or promotional
+  purposes without specific prior written permission.
+
+  We suggest,  but do not require, that  you use one or  more of the
+  following phrases to refer  to this software in your documentation
+  or advertising  materials: `FreeType Project',  `FreeType Engine',
+  `FreeType library', or `FreeType Distribution'.
+
+  As  you have  not signed  this license,  you are  not  required to
+  accept  it.   However,  as  the FreeType  Project  is  copyrighted
+  material, only  this license, or  another one contracted  with the
+  authors, grants you  the right to use, distribute,  and modify it.
+  Therefore,  by  using,  distributing,  or modifying  the  FreeType
+  Project, you indicate that you understand and accept all the terms
+  of this license.
+
+4. Contacts
+-----------
+
+  There are two mailing lists related to FreeType:
+
+    o freetype@nongnu.org
+
+      Discusses general use and applications of FreeType, as well as
+      future and  wanted additions to the  library and distribution.
+      If  you are looking  for support,  start in  this list  if you
+      haven't found anything to help you in the documentation.
+
+    o freetype-devel@nongnu.org
+
+      Discusses bugs,  as well  as engine internals,  design issues,
+      specific licenses, porting, etc.
+
+  Our home page can be found at
+
+    http://www.freetype.org
+
+
+--- end of FTL.TXT ---
diff --git a/licenses/libjpg/README.txt b/licenses/libjpg/README.txt
new file mode 100644 (file)
index 0000000..001b6a3
--- /dev/null
@@ -0,0 +1,60 @@
+Independent JPEG Group's free JPEG software\r
+-------------------------------------------\r
+\r
+This package contains C software to implement JPEG image encoding, decoding,\r
+and transcoding.  JPEG is a standardized compression method for full-color\r
+and gray-scale images.\r
+\r
+The distributed programs provide conversion between JPEG "JFIF" format and\r
+image files in PBMPLUS PPM/PGM, GIF, BMP, and Targa file formats.  The\r
+core compression and decompression library can easily be reused in other\r
+programs, such as image viewers.  The package is highly portable C code;\r
+we have tested it on many machines ranging from PCs to Crays.\r
+\r
+We are releasing this software for both noncommercial and commercial use.\r
+Companies are welcome to use it as the basis for JPEG-related products.\r
+We do not ask a royalty, although we do ask for an acknowledgement in\r
+product literature (see the README file in the distribution for details).\r
+We hope to make this software industrial-quality --- although, as with\r
+anything that's free, we offer no warranty and accept no liability.\r
+\r
+For more information, contact jpeg-info@jpegclub.org.\r
+\r
+\r
+Contents of this directory\r
+--------------------------\r
+\r
+jpegsrc.vN.tar.gz contains source code, documentation, and test files for\r
+release N in Unix format.\r
+\r
+jpegsrN.zip contains source code, documentation, and test files for\r
+release N in Windows format.\r
+\r
+jpegaltui.vN.tar.gz contains source code for an alternate user interface for\r
+cjpeg/djpeg in Unix format.\r
+\r
+jpegaltuiN.zip contains source code for an alternate user interface for\r
+cjpeg/djpeg in Windows format.\r
+\r
+wallace.ps.gz is a PostScript file of Greg Wallace's introductory article\r
+about JPEG.  This is an update of the article that appeared in the April\r
+1991 Communications of the ACM.\r
+\r
+jpeg.documents.gz tells where to obtain the JPEG standard and documents\r
+about JPEG-related file formats.\r
+\r
+jfif.ps.gz is a PostScript file of the JFIF (JPEG File Interchange Format)\r
+format specification.\r
+\r
+jfif.txt.gz is a plain text transcription of the JFIF specification; it's\r
+missing a figure, so use the PostScript version if you can.\r
+\r
+TIFFTechNote2.txt.gz is a draft of the proposed revisions to TIFF 6.0's\r
+JPEG support.\r
+\r
+pm.errata.gz is the errata list for the first printing of the textbook\r
+"JPEG Still Image Data Compression Standard" by Pennebaker and Mitchell.\r
+\r
+jdosaobj.zip contains pre-assembled object files for JMEMDOSA.ASM.\r
+If you want to compile the IJG code for MS-DOS, but don't have an assembler,\r
+these files may be helpful.\r
diff --git a/licenses/libtiff/libtiff.txt b/licenses/libtiff/libtiff.txt
new file mode 100644 (file)
index 0000000..66940c3
--- /dev/null
@@ -0,0 +1,21 @@
+Copyright (c) 1988-1997 Sam Leffler
+Copyright (c) 1991-1997 Silicon Graphics, Inc.
+
+Permission to use, copy, modify, distribute, and sell this software and 
+its documentation for any purpose is hereby granted without fee, provided
+that (i) the above copyright notices and this permission notice appear in
+all copies of the software and related documentation, and (ii) the names of
+Sam Leffler and Silicon Graphics may not be used in any advertising or
+publicity relating to the software without the specific, prior written
+permission of Sam Leffler and Silicon Graphics.
+
+THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
+EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
+WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
+
+IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
+ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY 
+KIND, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
+OR PROFITS, WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE,
+AND ON ANY THEORY OF LIABILITY, ARISING OUT OF OR IN CONNECTION
+WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/licenses/licenses.txt b/licenses/licenses.txt
new file mode 100644 (file)
index 0000000..b282c06
--- /dev/null
@@ -0,0 +1,3 @@
+Assets' attribution
+
+Font hanken https://fontlibrary.org/en/font/hanken
diff --git a/licenses/openal_soft/COPYING b/licenses/openal_soft/COPYING
new file mode 100644 (file)
index 0000000..d0c8978
--- /dev/null
@@ -0,0 +1,484 @@
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+                    675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+\f
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+\f
+                 GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+\f
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+\f
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+\f
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+\f
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+\f
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+\f
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                           NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+     Appendix: How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the Free
+    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
diff --git a/licenses/opencv/license.txt b/licenses/opencv/license.txt
new file mode 100644 (file)
index 0000000..af03307
--- /dev/null
@@ -0,0 +1,37 @@
+IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. 
+
+ By downloading, copying, installing or using the software you agree to this license.
+ If you do not agree to this license, do not download, install,
+ copy or use the software.
+
+
+                          License Agreement
+               For Open Source Computer Vision Library
+
+Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
+Copyright (C) 2008-2011, Willow Garage Inc., all rights reserved.
+Third party copyrights are property of their respective owners.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice,
+    this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+
+  * The name of the copyright holders may not be used to endorse or promote products
+    derived from this software without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as is" and
+any express or implied warranties, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose are disclaimed.
+In no event shall the Intel Corporation or contributors be liable for any direct,
+indirect, incidental, special, exemplary, or consequential damages
+(including, but not limited to, procurement of substitute goods or services;
+loss of use, data, or profits; or business interruption) however caused
+and on any theory of liability, whether in contract, strict liability,
+or tort (including negligence or otherwise) arising in any way out of
+the use of this software, even if advised of the possibility of such damage.
diff --git a/licenses/openssl/license.txt b/licenses/openssl/license.txt
new file mode 100644 (file)
index 0000000..b941090
--- /dev/null
@@ -0,0 +1 @@
+This product includes software written by Tim Hudson (tjh@cryptsoft.com)
\ No newline at end of file
diff --git a/licenses/panda3d/license.txt b/licenses/panda3d/license.txt
new file mode 100644 (file)
index 0000000..49bd2f5