forked from LeenkxTeam/LNXSDK
		
	
		
			
				
	
	
		
			1217 lines
		
	
	
		
			41 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1217 lines
		
	
	
		
			41 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from enum import Enum, unique
 | 
						|
import glob
 | 
						|
import itertools
 | 
						|
import json
 | 
						|
import locale
 | 
						|
import os
 | 
						|
import platform
 | 
						|
import random
 | 
						|
import re
 | 
						|
import shlex
 | 
						|
import shutil
 | 
						|
import subprocess
 | 
						|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 | 
						|
import webbrowser
 | 
						|
 | 
						|
import numpy as np
 | 
						|
 | 
						|
import bpy
 | 
						|
 | 
						|
import lnx.lib.lnxpack
 | 
						|
from lnx.lib.lz4 import LZ4
 | 
						|
import lnx.log as log
 | 
						|
import lnx.make_state as state
 | 
						|
import lnx.props_renderpath
 | 
						|
 | 
						|
if lnx.is_reload(__name__):
 | 
						|
    lnx.lib.lnxpack = lnx.reload_module(lnx.lib.lnxpack)
 | 
						|
    lnx.lib.lz4 = lnx.reload_module(lnx.lib.lz4)
 | 
						|
    from lnx.lib.lz4 import LZ4
 | 
						|
    log = lnx.reload_module(log)
 | 
						|
    state = lnx.reload_module(state)
 | 
						|
    lnx.props_renderpath = lnx.reload_module(lnx.props_renderpath)
 | 
						|
else:
 | 
						|
    lnx.enable_reload(__name__)
 | 
						|
 | 
						|
 | 
						|
class NumpyEncoder(json.JSONEncoder):
 | 
						|
    def default(self, obj):
 | 
						|
        if isinstance(obj, np.ndarray):
 | 
						|
            return obj.tolist()
 | 
						|
        return json.JSONEncoder.default(self, obj)
 | 
						|
 | 
						|
class WorkingDir:
 | 
						|
    """Context manager for safely changing the current working directory."""
 | 
						|
    def __init__(self, cwd: str):
 | 
						|
        self.cwd = cwd
 | 
						|
        self.prev_cwd = os.getcwd()
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        os.chdir(self.cwd)
 | 
						|
 | 
						|
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
						|
        os.chdir(self.prev_cwd)
 | 
						|
 | 
						|
def write_lnx(filepath, output):
 | 
						|
    if filepath.endswith('.lz4'):
 | 
						|
        with open(filepath, 'wb') as f:
 | 
						|
            packed = lnx.lib.lnxpack.packb(output)
 | 
						|
            # Prepend packed data size for decoding. Haxe can't unpack
 | 
						|
            # an unsigned int64 so we use a signed int64 here
 | 
						|
            f.write(np.int64(LZ4.encode_bound(len(packed))).tobytes())
 | 
						|
 | 
						|
            f.write(LZ4.encode(packed))
 | 
						|
    else:
 | 
						|
        if bpy.data.worlds['Lnx'].lnx_minimize:
 | 
						|
            with open(filepath, 'wb') as f:
 | 
						|
                f.write(lnx.lib.lnxpack.packb(output))
 | 
						|
        else:
 | 
						|
            filepath_json = filepath.split('.lnx')[0] + '.json'
 | 
						|
            with open(filepath_json, 'w') as f:
 | 
						|
                f.write(json.dumps(output, sort_keys=True, indent=4, cls=NumpyEncoder))
 | 
						|
 | 
						|
def unpack_image(image, path, file_format='JPEG'):
 | 
						|
    print('Leenkx Info: Unpacking to ' + path)
 | 
						|
    image.filepath_raw = path
 | 
						|
    image.file_format = file_format
 | 
						|
    image.save()
 | 
						|
 | 
						|
def convert_image(image, path, file_format='JPEG'):
 | 
						|
    # Convert image to compatible format
 | 
						|
    print('Leenkx Info: Converting to ' + path)
 | 
						|
    ren = bpy.context.scene.render
 | 
						|
    orig_quality = ren.image_settings.quality
 | 
						|
    orig_file_format = ren.image_settings.file_format
 | 
						|
    orig_color_mode = ren.image_settings.color_mode
 | 
						|
    ren.image_settings.quality = get_texture_quality_percentage()
 | 
						|
    ren.image_settings.file_format = file_format
 | 
						|
    if file_format == 'PNG':
 | 
						|
        ren.image_settings.color_mode = 'RGBA'
 | 
						|
    orig_image_colorspace = image.colorspace_settings.name
 | 
						|
    image.colorspace_settings.name = 'Non-Color'
 | 
						|
    image.save_render(path, scene=bpy.context.scene)
 | 
						|
    image.colorspace_settings.name = orig_image_colorspace
 | 
						|
    ren.image_settings.quality = orig_quality
 | 
						|
    ren.image_settings.file_format = orig_file_format
 | 
						|
    ren.image_settings.color_mode = orig_color_mode
 | 
						|
 | 
						|
 | 
						|
def get_random_color_rgb() -> list[float]:
 | 
						|
    """Return a random RGB color with values in range [0, 1]."""
 | 
						|
    return [random.random(), random.random(), random.random()]
 | 
						|
 | 
						|
 | 
						|
def is_livepatch_enabled():
 | 
						|
    """Returns whether live patch is enabled and can be used."""
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    # If the game is published, the target is krom-[OS] and not krom,
 | 
						|
    # so there is no live patch when publishing
 | 
						|
    return wrd.lnx_live_patch and state.target == 'krom'
 | 
						|
 | 
						|
 | 
						|
def blend_name():
 | 
						|
    return bpy.path.basename(bpy.context.blend_data.filepath).rsplit('.', 1)[0]
 | 
						|
 | 
						|
def build_dir():
 | 
						|
    return 'build_' + safestr(blend_name())
 | 
						|
 | 
						|
 | 
						|
def get_fp() -> str:
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    if wrd.lnx_project_root != '':
 | 
						|
        return bpy.path.abspath(wrd.lnx_project_root)
 | 
						|
    else:
 | 
						|
        s = None
 | 
						|
        if use_local_sdk and bpy.data.filepath == '':
 | 
						|
            s = os.getcwd()
 | 
						|
        else:
 | 
						|
            s = bpy.data.filepath.split(os.path.sep)
 | 
						|
            s.pop()
 | 
						|
            s = os.path.sep.join(s)
 | 
						|
        if get_os_is_windows() and len(s) == 2 and s[1] == ':':
 | 
						|
            # If the project is located at a drive root (C:/ for example),
 | 
						|
            # then s = "C:". If joined later with another path, no path
 | 
						|
            # separator is added by default because C:some_path is valid
 | 
						|
            # Windows path syntax (some_path is then relative to the CWD on the
 | 
						|
            # C drive). We prevent this by manually adding the path separator
 | 
						|
            # in these cases. Please refer to the Python doc of os.path.join()
 | 
						|
            # for more details.
 | 
						|
            s += os.path.sep
 | 
						|
        return s
 | 
						|
 | 
						|
 | 
						|
def get_fp_build():
 | 
						|
    return os.path.join(get_fp(), build_dir())
 | 
						|
 | 
						|
 | 
						|
def to_absolute_path(path: str, from_library: Optional[bpy.types.Library] = None) -> str:
 | 
						|
    """Convert the given absolute or relative path into an absolute path.
 | 
						|
 | 
						|
    - If `from_library` is not set (default), a given relative path will be
 | 
						|
      interpreted as relative to the project directory.
 | 
						|
    - If `from_library` is set, a given relative path will be interpreted as
 | 
						|
      relative to the filepath of the specified library.
 | 
						|
    """
 | 
						|
    return os.path.normpath(bpy.path.abspath(path, start=get_fp(), library=from_library))
 | 
						|
 | 
						|
 | 
						|
def get_os() -> str:
 | 
						|
    s = platform.system()
 | 
						|
    if s == 'Windows':
 | 
						|
        return 'win'
 | 
						|
    elif s == 'Darwin':
 | 
						|
        return 'mac'
 | 
						|
    else:
 | 
						|
        return 'linux'
 | 
						|
 | 
						|
 | 
						|
def get_os_is_windows() -> bool:
 | 
						|
    return get_os() == 'win'
 | 
						|
 | 
						|
 | 
						|
def get_os_is_windows_64() -> bool:
 | 
						|
    if platform.machine().endswith('64'):
 | 
						|
        return True
 | 
						|
    # Checks if Python (32 bit) is running on Windows (64 bit)
 | 
						|
    if 'PROCESSOR_ARCHITEW6432' in os.environ:
 | 
						|
        return True
 | 
						|
    if os.environ['PROCESSOR_ARCHITECTURE'].endswith('64'):
 | 
						|
        return True
 | 
						|
    if 'PROGRAMFILES(X86)' in os.environ:
 | 
						|
        if os.environ['PROGRAMW6432'] is not None:
 | 
						|
            return True
 | 
						|
    else:
 | 
						|
        return False
 | 
						|
 | 
						|
 | 
						|
def get_gapi():
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    if state.is_export:
 | 
						|
        item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index]
 | 
						|
        return getattr(item, target_to_gapi(item.lnx_project_target))
 | 
						|
    if wrd.lnx_runtime == 'Browser':
 | 
						|
        return 'webgl'
 | 
						|
    if get_os() == 'win' and lnx.utils.get_rp().rp_voxels == 'Off':
 | 
						|
        return 'direct3d11'
 | 
						|
    else:
 | 
						|
        return 'opengl'
 | 
						|
 | 
						|
 | 
						|
def is_gapi_gl_es() -> bool:
 | 
						|
    """Return whether the currently targeted graphics API is using OpenGL ES."""
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
 | 
						|
    if state.is_export:
 | 
						|
        item_exporter = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index]
 | 
						|
 | 
						|
        # See Khamake's ShaderCompiler.findType() and krafix::Target.es in krafix.cpp ("target.es")
 | 
						|
        if state.target == 'android-hl':
 | 
						|
            return item_exporter.lnx_gapi_android == 'opengl'
 | 
						|
        if state.target == 'ios-hl':
 | 
						|
            return item_exporter.lnx_gapi_ios == 'opengl'
 | 
						|
        elif state.target == 'html5':
 | 
						|
            return True
 | 
						|
        return False
 | 
						|
 | 
						|
    else:
 | 
						|
        return wrd.lnx_runtime == 'Browser'
 | 
						|
 | 
						|
 | 
						|
def get_rp() -> lnx.props_renderpath.LnxRPListItem:
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    if not state.is_export and wrd.lnx_play_renderpath != '' and lnx.props_renderpath.LnxRPListItem.get_by_name(wrd.lnx_play_renderpath) is not None:
 | 
						|
        return lnx.props_renderpath.LnxRPListItem.get_by_name(wrd.lnx_play_renderpath)
 | 
						|
    else:
 | 
						|
        return wrd.lnx_rplist[wrd.lnx_rplist_index]
 | 
						|
 | 
						|
 | 
						|
# Passed by load_post handler when lnxsdk is found in project folder
 | 
						|
use_local_sdk = False
 | 
						|
def get_sdk_path():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    if use_local_sdk:
 | 
						|
        return os.path.normpath(get_fp() + '/lnxsdk/')
 | 
						|
    else:
 | 
						|
        return os.path.normpath(addon_prefs.sdk_path)
 | 
						|
 | 
						|
def get_last_commit():
 | 
						|
    p = get_sdk_path() + 'leenkx/.git/refs/heads/main'
 | 
						|
 | 
						|
    try:
 | 
						|
        file = open(p, 'r')
 | 
						|
        commit = file.readline()
 | 
						|
    except:
 | 
						|
        commit = ''
 | 
						|
    return commit
 | 
						|
 | 
						|
 | 
						|
def get_lnx_preferences() -> bpy.types.AddonPreferences:
 | 
						|
    preferences = bpy.context.preferences
 | 
						|
    return preferences.addons["leenkx"].preferences
 | 
						|
 | 
						|
 | 
						|
def get_ide_bin():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return '' if not hasattr(addon_prefs, 'ide_bin') else addon_prefs.ide_bin
 | 
						|
 | 
						|
def get_ffmpeg_path():
 | 
						|
    path = get_lnx_preferences().ffmpeg_path
 | 
						|
    if path == "": path = shutil.which("ffmpeg")
 | 
						|
    return path
 | 
						|
 | 
						|
def get_renderdoc_path():
 | 
						|
    p = get_lnx_preferences().renderdoc_path
 | 
						|
    if p == '' and get_os() == 'win':
 | 
						|
        pdefault = 'C:\\Program Files\\RenderDoc\\qrenderdoc.exe'
 | 
						|
        if os.path.exists(pdefault):
 | 
						|
            p = pdefault
 | 
						|
    return p
 | 
						|
 | 
						|
def get_code_editor():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 'kodestudio' if not hasattr(addon_prefs, 'code_editor') else addon_prefs.code_editor
 | 
						|
 | 
						|
def get_ui_scale():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 1.0 if not hasattr(addon_prefs, 'ui_scale') else addon_prefs.ui_scale
 | 
						|
 | 
						|
def get_khamake_threads() -> int:
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    if hasattr(addon_prefs, 'khamake_threads_use_auto') and addon_prefs.khamake_threads_use_auto:
 | 
						|
        return -1
 | 
						|
    return 1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads
 | 
						|
 | 
						|
def get_compilation_server():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'compilation_server') else addon_prefs.compilation_server
 | 
						|
 | 
						|
def get_save_on_build():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'save_on_build') else addon_prefs.save_on_build
 | 
						|
 | 
						|
def get_debug_console_auto():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'debug_console_auto') else addon_prefs.debug_console_auto
 | 
						|
 | 
						|
def get_debug_console_visible_sc():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 192 if not hasattr(addon_prefs, 'debug_console_visible_sc') else addon_prefs.debug_console_visible_sc
 | 
						|
 | 
						|
def get_debug_console_scale_in_sc():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 219 if not hasattr(addon_prefs, 'debug_console_scale_in_sc') else addon_prefs.debug_console_scale_in_sc
 | 
						|
 | 
						|
def get_debug_console_scale_out_sc():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 221 if not hasattr(addon_prefs, 'debug_console_scale_out_sc') else addon_prefs.debug_console_scale_out_sc
 | 
						|
 | 
						|
def get_viewport_controls():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return 'qwerty' if not hasattr(addon_prefs, 'viewport_controls') else addon_prefs.viewport_controls
 | 
						|
 | 
						|
def get_legacy_shaders():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'legacy_shaders') else addon_prefs.legacy_shaders
 | 
						|
 | 
						|
def get_relative_paths():
 | 
						|
    """Whether to convert absolute paths to relative"""
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'relative_paths') else addon_prefs.relative_paths
 | 
						|
 | 
						|
def get_pref_or_default(prop_name: str, default: Any) -> Any:
 | 
						|
    """Return the preference setting for prop_name, or the value given as default if the property does not exist."""
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return getattr(addon_prefs, prop_name, default)
 | 
						|
 | 
						|
def get_node_path():
 | 
						|
    if get_os() == 'win':
 | 
						|
        return get_sdk_path() + '/nodejs/node.exe'
 | 
						|
    elif get_os() == 'mac':
 | 
						|
        return get_sdk_path() + '/nodejs/node-osx'
 | 
						|
    else:
 | 
						|
        return get_sdk_path() + '/nodejs/node-linux64'
 | 
						|
 | 
						|
def get_kha_path():
 | 
						|
    if os.path.exists('Kha'):
 | 
						|
        return 'Kha'
 | 
						|
    return get_sdk_path() + '/Kha'
 | 
						|
 | 
						|
def get_haxe_path():
 | 
						|
    if get_os() == 'win':
 | 
						|
        return get_kha_path() + '/Tools/windows_x64/haxe.exe'
 | 
						|
    elif get_os() == 'mac':
 | 
						|
        return get_kha_path() + '/Tools/macos/haxe'
 | 
						|
    else:
 | 
						|
        return get_kha_path() + '/Tools/linux_x64/haxe'
 | 
						|
 | 
						|
def get_khamake_path():
 | 
						|
    return get_kha_path() + '/make'
 | 
						|
 | 
						|
def krom_paths():
 | 
						|
    sdk_path = get_sdk_path()
 | 
						|
    if lnx.utils.get_os() == 'win':
 | 
						|
        krom_location = sdk_path + '/Krom'
 | 
						|
        if lnx.utils.get_rp().rp_voxels == 'Off':
 | 
						|
            krom_path = krom_location + '/Krom.exe'
 | 
						|
        else:
 | 
						|
            krom_path = krom_location + '/Krom_opengl.exe'
 | 
						|
    elif lnx.utils.get_os() == 'mac':
 | 
						|
        krom_location = sdk_path + '/Krom/Krom.app/Contents/MacOS'
 | 
						|
        krom_path = krom_location + '/Krom'
 | 
						|
    else:
 | 
						|
        krom_location = sdk_path + '/Krom'
 | 
						|
        krom_path = krom_location + '/Krom'
 | 
						|
    return krom_location, krom_path
 | 
						|
 | 
						|
def fetch_bundled_script_names():
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    wrd.lnx_bundled_scripts_list.clear()
 | 
						|
 | 
						|
    with WorkingDir(get_sdk_path() + '/leenkx/Sources/leenkx/trait'):
 | 
						|
        for file in glob.glob('*.hx'):
 | 
						|
            wrd.lnx_bundled_scripts_list.add().name = file.rsplit('.', 1)[0]
 | 
						|
 | 
						|
 | 
						|
script_props = {}
 | 
						|
script_props_defaults = {}
 | 
						|
script_warnings: Dict[str, List[Tuple[str, str]]] = {}  # Script name -> List of (identifier, warning message)
 | 
						|
 | 
						|
# See https://regex101.com/r/bbrCzN/8
 | 
						|
RX_MODIFIERS = r'(?P<modifiers>(?:public\s+|private\s+|static\s+|inline\s+|final\s+)*)?'  # Optional modifiers
 | 
						|
RX_IDENTIFIER = r'(?P<identifier>[_$a-z]+[_a-z0-9]*)'  # Variable name, follow Haxe rules
 | 
						|
RX_TYPE = r'(?:\s*:\s*(?P<type>[_a-z]+[\._a-z0-9]*))?'  # Optional type annotation
 | 
						|
RX_VALUE = r'(?:\s*=\s*(?P<value>(?:\".*\")|(?:[^;]+)|))?'  # Optional default value
 | 
						|
 | 
						|
PROP_REGEX_RAW = fr'@prop\s+{RX_MODIFIERS}(?P<attr_type>var|final)\s+{RX_IDENTIFIER}{RX_TYPE}{RX_VALUE};'
 | 
						|
PROP_REGEX = re.compile(PROP_REGEX_RAW, re.IGNORECASE)
 | 
						|
def fetch_script_props(filename: str):
 | 
						|
    """Parses @prop declarations from the given Haxe script."""
 | 
						|
    with open(filename, 'r', encoding='utf-8') as sourcefile:
 | 
						|
        source = sourcefile.read()
 | 
						|
 | 
						|
    if source == '':
 | 
						|
        return
 | 
						|
 | 
						|
    name = filename.rsplit('.', 1)[0]
 | 
						|
 | 
						|
    # Convert the name into a package path relative to the "Sources" dir
 | 
						|
    if 'Sources' in name:
 | 
						|
        name = name[name.index('Sources') + 8:]
 | 
						|
    if '/' in name:
 | 
						|
        name = name.replace('/', '.')
 | 
						|
    if '\\' in filename:
 | 
						|
        name = name.replace('\\', '.')
 | 
						|
 | 
						|
    script_props[name] = []
 | 
						|
    script_props_defaults[name] = []
 | 
						|
    script_warnings[name] = []
 | 
						|
 | 
						|
    for match in re.finditer(PROP_REGEX, source):
 | 
						|
 | 
						|
        p_modifiers: Optional[str] = match.group('modifiers')
 | 
						|
        p_identifier: str = match.group('identifier')
 | 
						|
        p_type: Optional[str] = match.group('type')
 | 
						|
        p_default_val: Optional[str] = match.group('value')
 | 
						|
 | 
						|
        if p_modifiers is not None:
 | 
						|
            if 'static' in p_modifiers:
 | 
						|
                script_warnings[name].append((p_identifier, '`static` modifier might cause unwanted behaviour!'))
 | 
						|
            if 'inline' in p_modifiers:
 | 
						|
                script_warnings[name].append((p_identifier, '`inline` modifier is not supported!'))
 | 
						|
                continue
 | 
						|
            if 'final' in p_modifiers or match.group('attr_type') == 'final':
 | 
						|
                script_warnings[name].append((p_identifier, '`final` properties are not supported!'))
 | 
						|
                continue
 | 
						|
 | 
						|
        # Property type is annotated
 | 
						|
        if p_type is not None:
 | 
						|
            if p_type.startswith("iron.object."):
 | 
						|
                p_type = p_type[12:]
 | 
						|
            elif p_type.startswith("iron.math."):
 | 
						|
                p_type = p_type[10:]
 | 
						|
 | 
						|
            type_default_val = get_type_default_value(p_type)
 | 
						|
            if type_default_val is None:
 | 
						|
                script_warnings[name].append((p_identifier, f'unsupported type `{p_type}`!'))
 | 
						|
                continue
 | 
						|
 | 
						|
            # Default value exists
 | 
						|
            if p_default_val is not None:
 | 
						|
                # Remove string quotes
 | 
						|
                p_default_val = p_default_val.replace('\'', '').replace('"', '')
 | 
						|
            else:
 | 
						|
                p_default_val = type_default_val
 | 
						|
 | 
						|
        # Default value is given instead, try to infer the properties type from it
 | 
						|
        elif p_default_val is not None:
 | 
						|
            p_type = get_prop_type_from_value(p_default_val)
 | 
						|
 | 
						|
            # Type is not recognized
 | 
						|
            if p_type is None:
 | 
						|
                script_warnings[name].append((p_identifier, 'could not infer property type from given value!'))
 | 
						|
                continue
 | 
						|
            if p_type == "String":
 | 
						|
                p_default_val = p_default_val.replace('\'', '').replace('"', '')
 | 
						|
 | 
						|
        else:
 | 
						|
            script_warnings[name].append((p_identifier, 'missing type or default value!'))
 | 
						|
            continue
 | 
						|
 | 
						|
        # Register prop
 | 
						|
        prop = (p_identifier, p_type)
 | 
						|
        script_props[name].append(prop)
 | 
						|
        script_props_defaults[name].append(p_default_val)
 | 
						|
 | 
						|
 | 
						|
def get_prop_type_from_value(value: str):
 | 
						|
    """
 | 
						|
    Returns the property type based on its representation in the code.
 | 
						|
 | 
						|
    If the type is not supported, `None` is returned.
 | 
						|
    """
 | 
						|
    # Maybe ast.literal_eval() is better here?
 | 
						|
    try:
 | 
						|
        int(value)
 | 
						|
        return "Int"
 | 
						|
    except ValueError:
 | 
						|
        try:
 | 
						|
            float(value)
 | 
						|
            return "Float"
 | 
						|
        except ValueError:
 | 
						|
            # "" is required, " alone will not work
 | 
						|
            if len(value) > 1 and value.startswith(("\"", "'")) and value.endswith(("\"", "'")):
 | 
						|
                return "String"
 | 
						|
            if value in ("true", "false"):
 | 
						|
                return "Bool"
 | 
						|
            if value.startswith("new "):
 | 
						|
                value = value.split()[1].split("(")[0]
 | 
						|
                if value.startswith("Vec"):
 | 
						|
                    return value
 | 
						|
                if value.startswith("iron.math.Vec"):
 | 
						|
                    return value[10:]
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
def get_type_default_value(prop_type: str):
 | 
						|
    """
 | 
						|
    Returns the default value of the given Haxe type.
 | 
						|
 | 
						|
    If the type is not supported, `None` is returned:
 | 
						|
    """
 | 
						|
    if prop_type == "Int":
 | 
						|
        return 0
 | 
						|
    if prop_type == "Float":
 | 
						|
        return 0.0
 | 
						|
    if prop_type == "String" or prop_type in (
 | 
						|
            "Object", "CameraObject", "LightObject", "MeshObject", "SpeakerObject"):
 | 
						|
        return ""
 | 
						|
    if prop_type == "Bool":
 | 
						|
        return False
 | 
						|
    if prop_type == "Vec2":
 | 
						|
        return [0.0, 0.0]
 | 
						|
    if prop_type == "Vec3":
 | 
						|
        return [0.0, 0.0, 0.0]
 | 
						|
    if prop_type == "Vec4":
 | 
						|
        return [0.0, 0.0, 0.0, 0.0]
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
def fetch_script_names():
 | 
						|
    if bpy.data.filepath == "":
 | 
						|
        return
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    # Sources
 | 
						|
    wrd.lnx_scripts_list.clear()
 | 
						|
    sources_path = os.path.join(get_fp(), 'Sources', safestr(wrd.lnx_project_package))
 | 
						|
    if os.path.isdir(sources_path):
 | 
						|
        with WorkingDir(sources_path):
 | 
						|
            # Glob supports recursive search since python 3.5 so it should cover both blender 2.79 and 2.8 integrated python
 | 
						|
            for file in glob.glob('**/*.hx', recursive=True):
 | 
						|
                mod = file.rsplit('.', 1)[0]
 | 
						|
                mod = mod.replace('\\', '/')
 | 
						|
                mod_parts = mod.rsplit('/')
 | 
						|
                if re.match('^[A-Z][A-Za-z0-9_]*$', mod_parts[-1]):
 | 
						|
                    wrd.lnx_scripts_list.add().name = mod.replace('/', '.')
 | 
						|
                    fetch_script_props(file)
 | 
						|
 | 
						|
    # Canvas
 | 
						|
    wrd.lnx_canvas_list.clear()
 | 
						|
    canvas_path = get_fp() + '/Bundled/canvas'
 | 
						|
    if os.path.isdir(canvas_path):
 | 
						|
        with WorkingDir(canvas_path):
 | 
						|
            for file in glob.glob('*.json'):
 | 
						|
                if file == "_themes.json":
 | 
						|
                    continue
 | 
						|
                wrd.lnx_canvas_list.add().name = file.rsplit('.', 1)[0]
 | 
						|
 | 
						|
def fetch_wasm_names():
 | 
						|
    if bpy.data.filepath == "":
 | 
						|
        return
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    # WASM modules
 | 
						|
    wrd.lnx_wasm_list.clear()
 | 
						|
    sources_path = get_fp() + '/Bundled'
 | 
						|
    if os.path.isdir(sources_path):
 | 
						|
        with WorkingDir(sources_path):
 | 
						|
            for file in glob.glob('*.wasm'):
 | 
						|
                name = file.rsplit('.', 1)[0]
 | 
						|
                wrd.lnx_wasm_list.add().name = name
 | 
						|
 | 
						|
 | 
						|
def fetch_trait_props():
 | 
						|
    for o in bpy.data.objects:
 | 
						|
        if o.override_library is None:
 | 
						|
            # We can't update the list of trait properties for linked
 | 
						|
            # objects because Blender doesn't allow to remove items from
 | 
						|
            # overridden lists
 | 
						|
            fetch_prop(o)
 | 
						|
 | 
						|
    for s in bpy.data.scenes:
 | 
						|
        fetch_prop(s)
 | 
						|
 | 
						|
 | 
						|
def fetch_prop(o: Union[bpy.types.Object, bpy.types.Scene]):
 | 
						|
    for item in o.lnx_traitlist:
 | 
						|
        if item.type_prop == 'Bundled Script':
 | 
						|
            name = 'leenkx.trait.' + item.name
 | 
						|
        else:
 | 
						|
            name = item.name
 | 
						|
        if name not in script_props:
 | 
						|
            continue
 | 
						|
        props = script_props[name]
 | 
						|
        defaults = script_props_defaults[name]
 | 
						|
        warnings = script_warnings[name]
 | 
						|
 | 
						|
        # Remove old props
 | 
						|
        for i in range(len(item.lnx_traitpropslist) - 1, -1, -1):
 | 
						|
            ip = item.lnx_traitpropslist[i]
 | 
						|
            if ip.name not in [p[0] for p in props]:
 | 
						|
                item.lnx_traitpropslist.remove(i)
 | 
						|
 | 
						|
        # Add new props
 | 
						|
        for index, p in enumerate(props):
 | 
						|
            found_prop = False
 | 
						|
            for i_prop in item.lnx_traitpropslist:
 | 
						|
                if i_prop.name == p[0]:
 | 
						|
                    if i_prop.type == p[1]:
 | 
						|
                        found_prop = i_prop
 | 
						|
                    else:
 | 
						|
                        item.lnx_traitpropslist.remove(item.lnx_traitpropslist.find(i_prop.name))
 | 
						|
                    break
 | 
						|
 | 
						|
            # Not in list
 | 
						|
            if not found_prop:
 | 
						|
                prop = item.lnx_traitpropslist.add()
 | 
						|
                prop.name = p[0]
 | 
						|
                prop.type = p[1]
 | 
						|
                prop.set_value(defaults[index])
 | 
						|
 | 
						|
            if found_prop:
 | 
						|
                prop = item.lnx_traitpropslist[found_prop.name]
 | 
						|
 | 
						|
                # Default value added and current value is blank (no override)
 | 
						|
                if (found_prop.get_value() is None
 | 
						|
                        or found_prop.get_value() == "") and defaults[index]:
 | 
						|
                    prop.set_value(defaults[index])
 | 
						|
                # Type has changed, update displayed name
 | 
						|
                if len(found_prop.name) == 1 or (len(found_prop.name) > 1 and found_prop.name[1] != p[1]):
 | 
						|
                    prop.name = p[0]
 | 
						|
                    prop.type = p[1]
 | 
						|
 | 
						|
            item.lnx_traitpropswarnings.clear()
 | 
						|
            for warning in warnings:
 | 
						|
                entry = item.lnx_traitpropswarnings.add()
 | 
						|
                entry.propName = warning[0]
 | 
						|
                entry.warning = warning[1]
 | 
						|
 | 
						|
 | 
						|
def fetch_bundled_trait_props():
 | 
						|
    # Bundled script props
 | 
						|
    for o in bpy.data.objects:
 | 
						|
        for t in o.lnx_traitlist:
 | 
						|
            if t.type_prop == 'Bundled Script':
 | 
						|
                file_path = get_sdk_path() + '/leenkx/Sources/leenkx/trait/' + t.name + '.hx'
 | 
						|
                if os.path.exists(file_path):
 | 
						|
                    fetch_script_props(file_path)
 | 
						|
                    fetch_prop(o)
 | 
						|
 | 
						|
def update_trait_collections():
 | 
						|
    for col in bpy.data.collections:
 | 
						|
        if col.name.startswith('Trait|'):
 | 
						|
            bpy.data.collections.remove(col)
 | 
						|
    for o in bpy.data.objects:
 | 
						|
        for t in o.lnx_traitlist:
 | 
						|
            if 'Trait|' + t.name not in bpy.data.collections:
 | 
						|
                col = bpy.data.collections.new('Trait|' + t.name)
 | 
						|
            else:
 | 
						|
                col = bpy.data.collections['Trait|' + t.name]
 | 
						|
            col.objects.link(o)
 | 
						|
 | 
						|
 | 
						|
def to_hex(val):
 | 
						|
    return '#%02x%02x%02x%02x' % (int(val[3] * 255), int(val[0] * 255), int(val[1] * 255), int(val[2] * 255))
 | 
						|
 | 
						|
 | 
						|
def color_to_int(val) -> int:
 | 
						|
    # Clamp values, otherwise the return value might not fit in 32 bit
 | 
						|
    # (and later cause problems, e.g. in the .arm file reader)
 | 
						|
    val = [max(0.0, min(v, 1.0)) for v in val]
 | 
						|
    return (int(val[3] * 255) << 24) + (int(val[0] * 255) << 16) + (int(val[1] * 255) << 8) + int(val[2] * 255)
 | 
						|
 | 
						|
 | 
						|
def unique_name_in_lists(item_lists: Iterable[list], name_attr: str, wanted_name: str, ignore_item: Optional[Any] = None) -> str:
 | 
						|
    """Creates a unique name that no item in the given lists already has.
 | 
						|
    The format follows Blender's behaviour when handling duplicate
 | 
						|
    object names.
 | 
						|
 | 
						|
    @param item_lists An iterable of item lists (any type).
 | 
						|
    @param name_attr The attribute of the items that holds the name.
 | 
						|
    @param wanted_name The name that should be preferably returned, if
 | 
						|
        no name collision occurs.
 | 
						|
    @param ignore_item (Optional) Ignore this item in the list when
 | 
						|
        comparing names.
 | 
						|
    """
 | 
						|
    def _has_collision(name: str) -> bool:
 | 
						|
        for item in itertools.chain(*item_lists):
 | 
						|
            if item == ignore_item:
 | 
						|
                continue
 | 
						|
            if getattr(item, name_attr) == name:
 | 
						|
                return True
 | 
						|
        return False
 | 
						|
 | 
						|
    # Check this once at the beginning to make sure the user can use
 | 
						|
    # a wanted name like "XY.001" if they want, even if "XY" alone does
 | 
						|
    # not collide
 | 
						|
    if not _has_collision(wanted_name):
 | 
						|
        return wanted_name
 | 
						|
 | 
						|
    # Get base name without numeric suffix
 | 
						|
    base_name = wanted_name
 | 
						|
    dot_pos = base_name.rfind('.')
 | 
						|
    if dot_pos != -1:
 | 
						|
        if base_name[dot_pos + 1:].isdecimal():
 | 
						|
            base_name = base_name[:dot_pos]
 | 
						|
 | 
						|
    num_collisions = 0
 | 
						|
    out_name = base_name
 | 
						|
    while _has_collision(out_name):
 | 
						|
        num_collisions += 1
 | 
						|
        out_name = f'{base_name}.{num_collisions:03d}'
 | 
						|
 | 
						|
    return out_name
 | 
						|
 | 
						|
 | 
						|
def merge_into_collection(col_src, col_dst, clear_dst=True):
 | 
						|
    """Merges the items of the `col_src` collection property into the
 | 
						|
    `col_dst` collection property.
 | 
						|
 | 
						|
    If `clear_dst` is true, the destination collection is cleared before
 | 
						|
    merging. Otherwise, new items are added on top of the existing items
 | 
						|
    in `col_dst`. There is no check for duplicates.
 | 
						|
    """
 | 
						|
    if clear_dst:
 | 
						|
        col_dst.clear()
 | 
						|
 | 
						|
    for item_src in col_src:
 | 
						|
        item_dst = col_dst.add()
 | 
						|
 | 
						|
        # collect names of writable properties
 | 
						|
        prop_names = [p.identifier for p in item_src.bl_rna.properties
 | 
						|
                      if not p.is_readonly]
 | 
						|
 | 
						|
        # copy those properties
 | 
						|
        for prop_name in prop_names:
 | 
						|
            setattr(item_dst, prop_name, getattr(item_src, prop_name))
 | 
						|
 | 
						|
 | 
						|
def safesrc(s):
 | 
						|
    s = safestr(s).replace('.', '_').replace('-', '_').replace(' ', '')
 | 
						|
    if s[0].isdigit():
 | 
						|
        s = '_' + s
 | 
						|
    return s
 | 
						|
 | 
						|
def safestr(s: str) -> str:
 | 
						|
    """Outputs a string where special characters have been replaced with
 | 
						|
    '_', which can be safely used in file and path names."""
 | 
						|
    for c in r'''[]/\;,><&*:§$%=+@!#^()|?^'"''':
 | 
						|
        s = s.replace(c, '_')
 | 
						|
    return ''.join([i if ord(i) < 128 else '_' for i in s])
 | 
						|
 | 
						|
def get_haxe_json_string(d: dict) -> str:
 | 
						|
    s = str(d)
 | 
						|
    s = s.replace('True', 'true')
 | 
						|
    s = s.replace('False', 'false')
 | 
						|
    s = s.replace("'", '"')
 | 
						|
    return s
 | 
						|
 | 
						|
def asset_name(bdata):
 | 
						|
    if bdata == None:
 | 
						|
        return None
 | 
						|
    s = bdata.name
 | 
						|
    # Append library name if linked
 | 
						|
    if bdata.library is not None:
 | 
						|
        s += '_' + bdata.library.name
 | 
						|
    return s
 | 
						|
 | 
						|
def asset_path(s):
 | 
						|
    """Remove leading '//'"""
 | 
						|
    return s[2:] if s[:2] == '//' else s
 | 
						|
 | 
						|
def extract_filename(s):
 | 
						|
    return os.path.basename(asset_path(s))
 | 
						|
 | 
						|
def get_render_resolution(scene):
 | 
						|
    render = scene.render
 | 
						|
    scale = render.resolution_percentage / 100
 | 
						|
    return int(render.resolution_x * scale), int(render.resolution_y * scale)
 | 
						|
 | 
						|
def get_texture_quality_percentage() -> int:
 | 
						|
    return int(bpy.data.worlds['Lnx'].lnx_texture_quality * 100)
 | 
						|
 | 
						|
def get_project_scene_name():
 | 
						|
    return get_active_scene().name
 | 
						|
 | 
						|
def get_active_scene() -> bpy.types.Scene:
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    if not state.is_export:
 | 
						|
        if wrd.lnx_play_scene is None:
 | 
						|
            return bpy.context.scene
 | 
						|
        return wrd.lnx_play_scene
 | 
						|
    else:
 | 
						|
        item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index]
 | 
						|
        return item.lnx_project_scene
 | 
						|
 | 
						|
def logic_editor_space(context_screen=None):
 | 
						|
    if context_screen == None:
 | 
						|
        context_screen = bpy.context.screen
 | 
						|
    if context_screen != None:
 | 
						|
        areas = context_screen.areas
 | 
						|
        for area in areas:
 | 
						|
            for space in area.spaces:
 | 
						|
                if space.type == 'NODE_EDITOR':
 | 
						|
                    if space.node_tree != None and space.node_tree.bl_idname == 'LnxLogicTreeType':
 | 
						|
                        return space
 | 
						|
    return None
 | 
						|
 | 
						|
def voxel_support():
 | 
						|
    # macos does not support opengl 4.5, needs metal
 | 
						|
    return state.target != 'html5' and get_os() != 'mac'
 | 
						|
 | 
						|
def get_cascade_size(rpdat):
 | 
						|
    cascade_size = int(rpdat.rp_shadowmap_cascade)
 | 
						|
    # Clamp to 4096 per cascade
 | 
						|
    if int(rpdat.rp_shadowmap_cascades) > 1 and cascade_size > 4096:
 | 
						|
        cascade_size = 4096
 | 
						|
    return cascade_size
 | 
						|
 | 
						|
 | 
						|
def check_blender_version(op: bpy.types.Operator):
 | 
						|
    """Check whether the Blender version is supported by Leenkx,
 | 
						|
    if not, report in UI.
 | 
						|
    """
 | 
						|
    if bpy.app.version[0] != 4 or bpy.app.version[1] != 2:
 | 
						|
        op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version such as 4.2 | 3.6 | 3.3')
 | 
						|
 | 
						|
 | 
						|
def check_saved(self):
 | 
						|
    if bpy.data.filepath == "":
 | 
						|
        msg = "Save blend file first"
 | 
						|
        self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
 | 
						|
        return False
 | 
						|
    return True
 | 
						|
 | 
						|
def check_path(s):
 | 
						|
    for c in r'[];><&*%=+@!#^()|?^':
 | 
						|
        if c in s:
 | 
						|
            return False
 | 
						|
    for c in s:
 | 
						|
        if ord(c) > 127:
 | 
						|
            return False
 | 
						|
    return True
 | 
						|
 | 
						|
def check_sdkpath(self):
 | 
						|
    s = get_sdk_path()
 | 
						|
    if not check_path(s):
 | 
						|
        msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now."
 | 
						|
        self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
 | 
						|
        return False
 | 
						|
    else:
 | 
						|
        return True
 | 
						|
 | 
						|
def check_projectpath(self):
 | 
						|
    s = get_fp()
 | 
						|
    if not check_path(s):
 | 
						|
        msg = f"Project path '{s}' contains special characters, build process may fail."
 | 
						|
        self.report({"ERROR"}, msg) if self is not None else log.warn(msg)
 | 
						|
        return False
 | 
						|
    else:
 | 
						|
        return True
 | 
						|
 | 
						|
def disp_enabled(target):
 | 
						|
    rpdat = get_rp()
 | 
						|
    if rpdat.lnx_rp_displacement == 'Tessellation':
 | 
						|
        return target == 'krom' or target == 'native'
 | 
						|
    return rpdat.lnx_rp_displacement != 'Off'
 | 
						|
 | 
						|
def is_object_animation_enabled(bobject):
 | 
						|
    # Checks if animation is present and enabled
 | 
						|
    if bobject.lnx_animation_enabled == False or bobject.type == 'BONE' or bobject.type == 'ARMATURE':
 | 
						|
        return False
 | 
						|
    if bobject.animation_data and bobject.animation_data.action:
 | 
						|
        return True
 | 
						|
    return False
 | 
						|
 | 
						|
def is_bone_animation_enabled(bobject):
 | 
						|
    # Checks if animation is present and enabled for parented armature
 | 
						|
    if bobject.parent and bobject.parent.type == 'ARMATURE':
 | 
						|
        if bobject.parent.lnx_animation_enabled == False:
 | 
						|
            return False
 | 
						|
        # Check for present actions
 | 
						|
        adata = bobject.parent.animation_data
 | 
						|
        has_actions = adata != None and adata.action != None
 | 
						|
        if not has_actions and adata != None:
 | 
						|
            if hasattr(adata, 'nla_tracks') and adata.nla_tracks != None:
 | 
						|
                for track in adata.nla_tracks:
 | 
						|
                    if track.strips == None:
 | 
						|
                        continue
 | 
						|
                    for strip in track.strips:
 | 
						|
                        if strip.action == None:
 | 
						|
                            continue
 | 
						|
                        has_actions = True
 | 
						|
                        break
 | 
						|
                    if has_actions:
 | 
						|
                        break
 | 
						|
        if adata != None and has_actions:
 | 
						|
            return True
 | 
						|
    return False
 | 
						|
 | 
						|
 | 
						|
def export_bone_data(bobject: bpy.types.Object) -> bool:
 | 
						|
    """Returns whether the bone data of the given object should be exported."""
 | 
						|
    return bobject.find_armature() and is_bone_animation_enabled(bobject) and get_rp().lnx_skin == 'On'
 | 
						|
 | 
						|
def export_morph_targets(bobject: bpy.types.Object) -> bool:
 | 
						|
    if get_rp().lnx_morph_target != 'On':
 | 
						|
        return False
 | 
						|
 | 
						|
    if not hasattr(bobject.data, 'shape_keys'):
 | 
						|
        return False
 | 
						|
 | 
						|
    shape_keys = bobject.data.shape_keys
 | 
						|
    if not shape_keys:
 | 
						|
        return False
 | 
						|
    if len(shape_keys.key_blocks) < 2:
 | 
						|
        return False
 | 
						|
    for shape_key in shape_keys.key_blocks[1:]:
 | 
						|
            if(not shape_key.mute):
 | 
						|
                return True
 | 
						|
    return False
 | 
						|
 | 
						|
def export_vcols(bobject: bpy.types.Object) -> bool:
 | 
						|
    for material in bobject.data.materials:
 | 
						|
        if material is not None and material.export_vcols:
 | 
						|
            return True
 | 
						|
    return False
 | 
						|
 | 
						|
def open_editor(hx_path=None):
 | 
						|
    ide_bin = get_ide_bin()
 | 
						|
 | 
						|
    if hx_path is None:
 | 
						|
        hx_path = lnx.utils.get_fp()
 | 
						|
 | 
						|
    if get_code_editor() == 'default':
 | 
						|
        # Get editor environment variables
 | 
						|
        # https://unix.stackexchange.com/q/4859
 | 
						|
        env_v_editor = os.environ.get('VISUAL')
 | 
						|
        env_editor = os.environ.get('EDITOR')
 | 
						|
 | 
						|
        if env_v_editor is not None:
 | 
						|
            ide_bin = env_v_editor
 | 
						|
        elif env_editor is not None:
 | 
						|
            ide_bin = env_editor
 | 
						|
 | 
						|
        # No environment variables set -> Let the system decide how to
 | 
						|
        # open the file
 | 
						|
        else:
 | 
						|
            webbrowser.open('file://' + hx_path)
 | 
						|
            return
 | 
						|
 | 
						|
    if os.path.exists(ide_bin):
 | 
						|
        args = [ide_bin, lnx.utils.get_fp()]
 | 
						|
 | 
						|
        # Sublime Text
 | 
						|
        if get_code_editor() == 'sublime':
 | 
						|
            project_name = lnx.utils.safestr(bpy.data.worlds['Lnx'].lnx_project_name)
 | 
						|
            subl_project_path = lnx.utils.get_fp() + f'/{project_name}.sublime-project'
 | 
						|
 | 
						|
            if not os.path.exists(subl_project_path):
 | 
						|
                generate_sublime_project(subl_project_path)
 | 
						|
 | 
						|
            args += ['--project', subl_project_path]
 | 
						|
 | 
						|
            args.append('--add')
 | 
						|
 | 
						|
        args.append(hx_path)
 | 
						|
 | 
						|
        if lnx.utils.get_os() == 'mac':
 | 
						|
            argstr = ""
 | 
						|
 | 
						|
            for arg in args:
 | 
						|
                if not (arg.startswith('-') or arg.startswith('--')):
 | 
						|
                    argstr += '"' + arg + '"'
 | 
						|
                argstr += ' '
 | 
						|
 | 
						|
            subprocess.Popen(argstr[:-1], shell=True)
 | 
						|
        else:
 | 
						|
            subprocess.Popen(args)
 | 
						|
 | 
						|
    else:
 | 
						|
        raise FileNotFoundError(f'Code editor executable not found: {ide_bin}. You can change the path in the Leenkx preferences.')
 | 
						|
 | 
						|
def open_folder(folder_path: str):
 | 
						|
    if lnx.utils.get_os() == 'win':
 | 
						|
        subprocess.run(['explorer', folder_path])
 | 
						|
    elif lnx.utils.get_os() == 'mac':
 | 
						|
        subprocess.run(['open', folder_path])
 | 
						|
    elif lnx.utils.get_os() == 'linux':
 | 
						|
        subprocess.run(['xdg-open', folder_path])
 | 
						|
    else:
 | 
						|
        webbrowser.open('file://' + folder_path)
 | 
						|
 | 
						|
def generate_sublime_project(subl_project_path):
 | 
						|
    """Generates a [project_name].sublime-project file."""
 | 
						|
    print('Generating Sublime Text project file')
 | 
						|
 | 
						|
    project_data = {
 | 
						|
        "folders": [
 | 
						|
            {
 | 
						|
                "path": ".",
 | 
						|
                "file_exclude_patterns": ["*.blend*", "*.lnx"]
 | 
						|
            },
 | 
						|
        ],
 | 
						|
    }
 | 
						|
 | 
						|
    with open(subl_project_path, 'w', encoding='utf-8') as project_file:
 | 
						|
        json.dump(project_data, project_file, ensure_ascii=False, indent=4)
 | 
						|
 | 
						|
def def_strings_to_array(strdefs):
 | 
						|
    defs = strdefs.split('_')
 | 
						|
    defs = defs[1:]
 | 
						|
    defs = ['_' + d for d in defs] # Restore _
 | 
						|
    return defs
 | 
						|
 | 
						|
def get_kha_target(target_name): # TODO: remove
 | 
						|
    if target_name == 'macos-hl':
 | 
						|
        return 'osx-hl'
 | 
						|
    elif target_name.startswith('krom'): # krom-windows
 | 
						|
        return 'krom'
 | 
						|
    elif target_name == 'custom':
 | 
						|
        return ''
 | 
						|
    return target_name
 | 
						|
 | 
						|
 | 
						|
def target_to_gapi(lnx_project_target: str) -> str:
 | 
						|
    # TODO: align target names
 | 
						|
    if lnx_project_target == 'krom':
 | 
						|
        return 'lnx_gapi_' + lnx.utils.get_os()
 | 
						|
    elif lnx_project_target == 'krom-windows':
 | 
						|
        return 'lnx_gapi_win'
 | 
						|
    elif lnx_project_target == 'windows-hl':
 | 
						|
        return 'lnx_gapi_win'
 | 
						|
    elif lnx_project_target == 'krom-linux':
 | 
						|
        return 'lnx_gapi_linux'
 | 
						|
    elif lnx_project_target == 'linux-hl':
 | 
						|
        return 'lnx_gapi_linux'
 | 
						|
    elif lnx_project_target == 'krom-macos':
 | 
						|
        return 'lnx_gapi_mac'
 | 
						|
    elif lnx_project_target == 'macos-hl':
 | 
						|
        return 'lnx_gapi_mac'
 | 
						|
    elif lnx_project_target == 'android-hl':
 | 
						|
        return 'lnx_gapi_android'
 | 
						|
    elif lnx_project_target == 'ios-hl':
 | 
						|
        return 'lnx_gapi_ios'
 | 
						|
    elif lnx_project_target == 'node':
 | 
						|
        return 'lnx_gapi_html5'
 | 
						|
    else: # html5, custom
 | 
						|
        return 'lnx_gapi_' + lnx_project_target
 | 
						|
 | 
						|
 | 
						|
def check_default_props():
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    if len(wrd.lnx_rplist) == 0:
 | 
						|
        wrd.lnx_rplist.add()
 | 
						|
        wrd.lnx_rplist_index = 0
 | 
						|
 | 
						|
    if wrd.lnx_project_name == '':
 | 
						|
        # Take blend file name
 | 
						|
        wrd.lnx_project_name = lnx.utils.blend_name()
 | 
						|
 | 
						|
# Enum Permissions Name
 | 
						|
class PermissionName(Enum):
 | 
						|
    ACCESS_COARSE_LOCATION = 'ACCESS_COARSE_LOCATION'
 | 
						|
    ACCESS_NETWORK_STATE = 'ACCESS_NETWORK_STATE'
 | 
						|
    ACCESS_FINE_LOCATION = 'ACCESS_FINE_LOCATION'
 | 
						|
    ACCESS_WIFI_STATE = 'ACCESS_WIFI_STATE'
 | 
						|
    BLUETOOTH = 'BLUETOOTH'
 | 
						|
    BLUETOOTH_ADMIN = 'BLUETOOTH_ADMIN'
 | 
						|
    CAMERA = 'CAMERA'
 | 
						|
    EXPAND_STATUS_BAR = 'EXPAND_STATUS_BAR'
 | 
						|
    FOREGROUND_SERVICE = 'FOREGROUND_SERVICE'
 | 
						|
    GET_ACCOUNTS = 'GET_ACCOUNTS'
 | 
						|
    INTERNET = 'INTERNET'
 | 
						|
    READ_EXTERNAL_STORAGE = 'READ_EXTERNAL_STORAGE'
 | 
						|
    VIBRATE = 'VIBRATE'
 | 
						|
    WRITE_EXTERNAL_STORAGE = 'WRITE_EXTERNAL_STORAGE'
 | 
						|
 | 
						|
# Add permission for target android
 | 
						|
def add_permission_target_android(permission_name_enum):
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    check = False
 | 
						|
    for item in wrd.lnx_exporter_android_permission_list:
 | 
						|
        if (item.lnx_android_permissions.upper() == str(permission_name_enum.value).upper()):
 | 
						|
            check = True
 | 
						|
            break
 | 
						|
    if not check:
 | 
						|
        wrd.lnx_exporter_android_permission_list.add()
 | 
						|
        wrd.lnx_exporter_android_permission_list[len(wrd.lnx_exporter_android_permission_list) - 1].lnx_android_permissions = str(permission_name_enum.value).upper()
 | 
						|
 | 
						|
def get_project_android_build_apk():
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    return wrd.lnx_project_android_build_apk
 | 
						|
 | 
						|
def get_android_sdk_root_path():
 | 
						|
    if os.getenv('ANDROID_SDK_ROOT') == None:
 | 
						|
        addon_prefs = get_lnx_preferences()
 | 
						|
        return '' if not hasattr(addon_prefs, 'android_sdk_root_path') else addon_prefs.android_sdk_root_path
 | 
						|
    else:
 | 
						|
        return os.getenv('ANDROID_SDK_ROOT')
 | 
						|
 | 
						|
def get_android_apk_copy_path():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return '' if not hasattr(addon_prefs, 'android_apk_copy_path') else addon_prefs.android_apk_copy_path
 | 
						|
 | 
						|
def get_android_apk_copy_open_directory():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'android_apk_copy_open_directory') else addon_prefs.android_apk_copy_open_directory
 | 
						|
 | 
						|
def get_android_emulators_list():
 | 
						|
    err = ''
 | 
						|
    items = []
 | 
						|
    path_file = get_android_emulator_file()
 | 
						|
    if len(path_file) > 0:
 | 
						|
        cmd = path_file + " -list-avds"
 | 
						|
        if get_os_is_windows():
 | 
						|
            process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
 | 
						|
        else:
 | 
						|
            process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE)
 | 
						|
        while True:
 | 
						|
            output = process.stdout.readline().decode("utf-8")
 | 
						|
            if len(output.strip()) == 0 and process.poll() is not None:
 | 
						|
                break
 | 
						|
            if output:
 | 
						|
                items.append(output.strip())
 | 
						|
    else:
 | 
						|
        err = 'File "'+ path_file +'" not found.'
 | 
						|
    return items, err
 | 
						|
 | 
						|
def get_android_emulator_path():
 | 
						|
    return os.path.join(get_android_sdk_root_path(), "emulator")
 | 
						|
 | 
						|
def get_android_emulator_file():
 | 
						|
    path_file = ''
 | 
						|
    if get_os_is_windows():
 | 
						|
        path_file = os.path.join(get_android_emulator_path(), "emulator.exe")
 | 
						|
    else:
 | 
						|
        path_file = os.path.join(get_android_emulator_path(), "emulator")
 | 
						|
    # File Exists
 | 
						|
    return '' if not os.path.isfile(path_file) else path_file
 | 
						|
 | 
						|
def get_android_emulator_name():
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    return '' if not len(wrd.lnx_project_android_list_avd.strip()) > 0 else wrd.lnx_project_android_list_avd.strip()
 | 
						|
 | 
						|
def get_android_open_build_apk_directory():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return False if not hasattr(addon_prefs, 'android_open_build_apk_directory') else addon_prefs.android_open_build_apk_directory
 | 
						|
 | 
						|
def get_html5_copy_path():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return '' if not hasattr(addon_prefs, 'html5_copy_path') else addon_prefs.html5_copy_path
 | 
						|
 | 
						|
def get_link_web_server():
 | 
						|
    addon_prefs = get_lnx_preferences()
 | 
						|
    return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server
 | 
						|
 | 
						|
 | 
						|
def get_file_lnx_version_tuple() -> tuple[int]:
 | 
						|
    wrd = bpy.data.worlds['Lnx']
 | 
						|
    return tuple(map(int, wrd.lnx_version.split('.')))
 | 
						|
 | 
						|
def type_name_to_type(name: str) -> bpy.types.bpy_struct:
 | 
						|
    """Return the Blender type given by its name, if registered."""
 | 
						|
    return bpy.types.bpy_struct.bl_rna_get_subclass_py(name)
 | 
						|
 | 
						|
def change_version_project(version: str) -> str:
 | 
						|
    ver = version.strip().replace(' ', '').split('.')
 | 
						|
    v_i = int(ver[len(ver) - 1]) + 1
 | 
						|
    ver[len(ver) - 1] = str(v_i)
 | 
						|
    version = ''
 | 
						|
    for i in ver:
 | 
						|
        if len(version) > 0:
 | 
						|
            version += '.'
 | 
						|
        version += i
 | 
						|
    return version
 | 
						|
 | 
						|
 | 
						|
def cpu_count(*, physical_only=False) -> Optional[int]:
 | 
						|
    """Returns the number of logical (default) or physical CPUs.
 | 
						|
    The result can be `None` if `os.cpu_count()` was not able to get the
 | 
						|
    correct count of logical CPUs.
 | 
						|
    """
 | 
						|
    if not physical_only:
 | 
						|
        return os.cpu_count()
 | 
						|
 | 
						|
    err_reason = ''
 | 
						|
    command = []
 | 
						|
 | 
						|
    _os = get_os()
 | 
						|
    try:
 | 
						|
        if _os == 'win':
 | 
						|
            sysroot = os.environ.get("SYSTEMROOT", default="C:\\WINDOWS")
 | 
						|
            command = [f'{sysroot}\\System32\\wbem\\wmic.exe', 'cpu', 'get', 'NumberOfCores']
 | 
						|
            result = subprocess.check_output(command)
 | 
						|
            result = result.decode('utf-8').splitlines()
 | 
						|
            result = int(result[2])
 | 
						|
            if result > 0:
 | 
						|
                return result
 | 
						|
 | 
						|
        elif _os == 'linux':
 | 
						|
            command = ["grep -P '^core id' /proc/cpuinfo | sort -u | wc -l"]
 | 
						|
            result = subprocess.check_output(command[0], shell=True)
 | 
						|
            result = result.decode('utf-8').splitlines()
 | 
						|
            result = int(result[0])
 | 
						|
            if result > 0:
 | 
						|
                return result
 | 
						|
 | 
						|
        # macOS
 | 
						|
        else:
 | 
						|
            command = ['sysctl', '-n', 'hw.physicalcpu']
 | 
						|
            return int(subprocess.check_output(command))
 | 
						|
 | 
						|
    except subprocess.CalledProcessError as e:
 | 
						|
        err_reason = f'Reason: command {command} exited with code {e.returncode}.'
 | 
						|
    except FileNotFoundError as e:
 | 
						|
        err_reason = f'Reason: couldn\'t open file from command {command} ({e.errno=}).'
 | 
						|
 | 
						|
    # Last resort even though it can be wrong
 | 
						|
    log.warn("Could not retrieve count of physical CPUs, using logical CPU count instead.\n\t" + err_reason)
 | 
						|
    return os.cpu_count()
 | 
						|
 | 
						|
 | 
						|
def register(local_sdk=False):
 | 
						|
    global use_local_sdk
 | 
						|
    use_local_sdk = local_sdk
 | 
						|
 | 
						|
def unregister():
 | 
						|
    pass
 |