Repe [T3DU] and Moises Jpelaez updates

This commit is contained in:
2026-05-12 23:54:06 -07:00
parent 6b404f9da6
commit 39091e8db3
147 changed files with 5539 additions and 1750 deletions

View File

@ -1,4 +1,5 @@
import glob
import importlib.util
import io
import json
import os
@ -10,6 +11,7 @@ from typing import List
import bpy
import lnx.assets as assets
import lnx.log as log
import lnx.make_renderpath as make_renderpath
import lnx.make_state as state
import lnx.utils
@ -17,6 +19,7 @@ import lnx.utils
if lnx.is_reload(__name__):
import lnx
assets = lnx.reload_module(assets)
log = lnx.reload_module(log)
make_renderpath = lnx.reload_module(make_renderpath)
state = lnx.reload_module(state)
lnx.utils = lnx.reload_module(lnx.utils)
@ -24,6 +27,96 @@ else:
lnx.enable_reload(__name__)
def get_library_hooks():
"""Discover and load `lnx_hooks.py` from Libraries and Subprojects.
Each hook module can define a write_main() function that returns a dict with:
- 'imports': Haxe import statements to add at the top
- 'main_pre': Code to add at the start of main() before Starter.main()
- 'main_post': Code to add at the end of main() after Starter.main()
- 'wrap_init': Tuple of (before, after) strings to wrap around the main() body
- 'priority': Integer for wrap_init nesting order (lower = outer, default = 0)
- 'defines': List of Haxe defines to add to khafile.js
- 'parameters': List of Haxe parameters to add to khafile.js
- 'assets': List of asset entries for khafile.js. Each entry can be:
- A string path: "Assets/icons.png"
- A tuple (path, options_dict): ("Assets/icons.png", {"noCompress": True})
Multiple wrap_init hooks are nested based on priority:
- Priority 0 (outer): LibA.init(function() {
- Priority 10 (inner): LibB.init(function() {
- Starter.main(); - Priority 10 (inner): });
- Priority 0 (outer): });
Returns a dict with combined strings for each injection point.
"""
hooks = {
'imports': '', # Haxe import statements
'main_pre': '', # Code to add at the start of main() before Starter.main()
'main_post': '', # Code to add at the end of main() after Starter.main()
'wrap_inits': [], # List of (priority, before, after) tuples
'defines': [], # List of Haxe defines for khafile.js
'parameters': [], # List of Haxe parameters for khafile.js
'assets': [] # List of assets for khafile.js
}
project_path = lnx.utils.get_fp()
hook_dirs = []
# Check Libraries folder
libraries_path = os.path.join(project_path, 'Libraries')
if os.path.exists(libraries_path):
for lib in os.listdir(libraries_path):
lib_path = os.path.join(libraries_path, lib)
if os.path.isdir(lib_path):
hook_dirs.append((lib, lib_path))
# Check Subprojects folder
subprojects_path = os.path.join(project_path, 'Subprojects')
if os.path.exists(subprojects_path):
for lib in os.listdir(subprojects_path):
lib_path = os.path.join(subprojects_path, lib)
if os.path.isdir(lib_path):
hook_dirs.append((lib, lib_path))
# Load each hook module
for lib_name, lib_path in hook_dirs:
hook_file = os.path.join(lib_path, 'lnx_hooks.py')
if os.path.isfile(hook_file):
try:
spec = importlib.util.spec_from_file_location(f"lnx_hooks_{lib_name}", hook_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, 'write_main'):
result = module.write_main()
if result:
if 'imports' in result and result['imports']:
hooks['imports'] += result['imports'] + '\n'
if 'main_pre' in result and result['main_pre']:
hooks['main_pre'] += result['main_pre'] + '\n'
if 'main_post' in result and result['main_post']:
hooks['main_post'] += result['main_post'] + '\n'
if 'wrap_init' in result and result['wrap_init']:
priority = result.get('priority', 0)
hooks['wrap_inits'].append((priority, result['wrap_init'][0], result['wrap_init'][1]))
if 'defines' in result and result['defines']:
hooks['defines'].extend(result['defines'])
if 'parameters' in result and result['parameters']:
hooks['parameters'].extend(result['parameters'])
if 'assets' in result and result['assets']:
hooks['assets'].extend(result['assets'])
log.info(f"Loaded library hook: {lib_name}")
except Exception as e:
log.warn(f"Failed to load hook from {lib_name}: {e}")
# Sort wrap_inits by priority (lower = outer wrapper)
hooks['wrap_inits'].sort(key=lambda x: x[0])
return hooks
def on_same_drive(path1: str, path2: str) -> bool:
drive_path1, _ = os.path.splitdrive(path1)
drive_path2, _ = os.path.splitdrive(path2)
@ -70,6 +163,9 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo
wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp()
# Get library hooks for defines and parameters
hooks = get_library_hooks()
sdk_path = lnx.utils.get_sdk_path()
rel_path = lnx.utils.get_relative_paths() # Convert absolute paths to relative
project_path = lnx.utils.get_fp()
@ -78,13 +174,47 @@ def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, expo
# Whether to use relative paths for paths inside the SDK
do_relpath_sdk = rel_path and on_same_drive(sdk_path, project_path)
# Determine if assets should go to data folder (used for hook assets and later)
use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl' or state.target == 'html5')
with open('khafile.js', 'w', encoding="utf-8") as khafile:
khafile.write(
"""// Auto-generated
let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' + wrd.lnx_project_version) + """');
project.addSources('Sources');
""")
# Add library hook assets
for asset in hooks['assets']:
dest_opt = ', destination: "data/{name}"' if use_data_dir else ''
if isinstance(asset, tuple):
path, options = asset
opts = []
for key, value in options.items():
if isinstance(value, bool):
opts.append(f"{key}: {'true' if value else 'false'}")
elif isinstance(value, str):
opts.append(f"{key}: '{value}'")
else:
opts.append(f"{key}: {value}")
if use_data_dir and 'destination' not in options:
opts.append('destination: "data/{name}"')
opts_str = ', '.join(opts)
khafile.write(f'project.addAssets("{path}", {{ {opts_str} }});\n')
else:
khafile.write(f'project.addAssets("{asset}", {{ notinlist: true{dest_opt} }});\n')
# Add library hook defines
for d in hooks['defines']:
khafile.write("project.addDefine('" + d + "');\n")
for p in assets.khafile_params:
khafile.write("project.addParameter('" + p + "');\n")
# Add library hook parameters
for p in hooks['parameters']:
khafile.write("project.addParameter('" + p + "');\n")
khafile.write("project.addSources('Sources');\n")
# Auto-add assets located in Bundled directory
if os.path.exists('Bundled'):
@ -247,8 +377,7 @@ project.addSources('Sources');
shaders_path = os.path.relpath(shaders_path, project_path).replace('\\', '/')
khafile.write('project.addShaders("' + shaders_path + '", { noprocessing: true, noembed: ' + str(noembed).lower() + ' });\n')
# Move assets for published game to /data folder
use_data_dir = is_publish and (state.target == 'krom-windows' or state.target == 'krom-linux' or state.target == 'windows-hl' or state.target == 'linux-hl' or state.target == 'html5')
# Add define for data directory usage
if use_data_dir:
assets.add_khafile_def('lnx_data_dir')
@ -353,6 +482,10 @@ project.addSources('Sources');
if rpdat.lnx_particles != 'Off':
assets.add_khafile_def('lnx_particles')
if rpdat.lnx_particles == 'GPU':
assets.add_khafile_def('lnx_gpu_particles')
elif rpdat.lnx_particles == 'CPU':
assets.add_khafile_def('lnx_cpu_particles')
if rpdat.rp_draw_order == 'Index':
assets.add_khafile_def('lnx_draworder_index')
@ -369,9 +502,6 @@ project.addSources('Sources');
if wrd.lnx_winresize or state.target == 'html5':
assets.add_khafile_def('lnx_resizable')
if get_winmode(wrd.lnx_winmode) == 1 and state.target.startswith('html5'):
assets.add_khafile_def('kha_html5_disable_automatic_size_adjust')
# if bpy.data.scenes[0].unit_settings.system_rotation == 'DEGREES':
# assets.add_khafile_def('lnx_degrees')
@ -381,9 +511,6 @@ project.addSources('Sources');
for d in assets.khafile_defs:
khafile.write("project.addDefine('" + d + "');\n")
for p in assets.khafile_params:
khafile.write("project.addParameter('" + p + "');\n")
if state.target.startswith('android'):
bundle = 'org.leenkx3d.' + wrd.lnx_project_package if wrd.lnx_project_bundle == '' else wrd.lnx_project_bundle
khafile.write("project.targetOptions.android_native.package = '{0}';\n".format(lnx.utils.safestr(bundle)))
@ -424,7 +551,10 @@ project.addSources('Sources');
khafile.write("project.targetOptions.ios.bundle = '{0}';\n".format(lnx.utils.safestr(bundle)))
if wrd.lnx_project_icon != '':
shutil.copy(bpy.path.abspath(wrd.lnx_project_icon), project_path + '/icon.png')
icon_src = os.path.normpath(bpy.path.abspath(wrd.lnx_project_icon))
icon_dst = os.path.normpath(os.path.join(project_path, 'icon.png'))
if not os.path.isfile(icon_dst) or not os.path.samefile(icon_src, icon_dst):
shutil.copy2(icon_src, icon_dst)
if wrd.lnx_khafile is not None:
khafile.write(wrd.lnx_khafile.as_string())
@ -494,10 +624,18 @@ def write_mainhx(scene_name, resx, resy, is_play, is_publish):
elif rpdat.rp_driver != 'Leenkx':
pathpack = rpdat.rp_driver.lower()
# Get library hooks
hooks = get_library_hooks()
with open('Sources/Main.hx', 'w', encoding="utf-8") as f:
f.write(
"""// Auto-generated
package;\n""")
package;
""")
# Write hook imports
if hooks['imports']:
f.write(hooks['imports'])
f.write("""
class Main {
@ -511,10 +649,9 @@ class Main {
public static inline var voxelgiVoxelSize = """ + str(round(rpdat.lnx_voxelgi_size * 100) / 100) + """;""")
if rpdat.rp_bloom:
if bpy.app.version <= (4, 2, 4):
f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if rpdat.lnx_bloom_follow_blender else rpdat.lnx_bloom_radius};")
else:
f.write(f"public static var bloomRadius = {rpdat.lnx_bloom_radius};")
follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False
f.write(f"public static var bloomRadius = {bpy.context.scene.eevee.bloom_radius if follow_blender else rpdat.lnx_bloom_radius};")
if rpdat.lnx_rp_resolution == 'Custom':
f.write("""
@ -523,49 +660,71 @@ class Main {
f.write("""\n
public static function main() {""")
# Calculate base indentation (2 tabs = 8 spaces)
base_indent = " "
wrap_count = len(hooks['wrap_inits'])
# Write wrap_init opening statements (outer to inner, sorted by priority)
for i, (priority, before, after) in enumerate(hooks['wrap_inits']):
indent = base_indent + (" " * i)
f.write("\n" + indent + before)
# Calculate content indentation based on wrap depth
content_indent = base_indent + (" " * wrap_count)
# Write main_pre hooks
if hooks['main_pre']:
f.write("\n" + content_indent + hooks['main_pre'].replace('\n', '\n' + content_indent))
if rpdat.lnx_skin != 'Off':
f.write("""
iron.object.BoneAnimation.skinMaxBones = """ + str(rpdat.lnx_skin_max_bones) + """;""")
f.write("\n" + content_indent + "iron.object.BoneAnimation.skinMaxBones = " + str(rpdat.lnx_skin_max_bones) + ";")
if rpdat.rp_shadows:
if rpdat.rp_shadowmap_cascades != '1':
f.write("""
iron.object.LightObject.cascadeCount = """ + str(rpdat.rp_shadowmap_cascades) + """;
iron.object.LightObject.cascadeSplitFactor = """ + str(rpdat.lnx_shadowmap_split) + """;""")
f.write("\n" + content_indent + "iron.object.LightObject.cascadeCount = " + str(rpdat.rp_shadowmap_cascades) + ";")
f.write("\n" + content_indent + "iron.object.LightObject.cascadeSplitFactor = " + str(rpdat.lnx_shadowmap_split) + ";")
if rpdat.lnx_shadowmap_bounds != 1.0:
f.write("""
iron.object.LightObject.cascadeBounds = """ + str(rpdat.lnx_shadowmap_bounds) + """;""")
f.write("\n" + content_indent + "iron.object.LightObject.cascadeBounds = " + str(rpdat.lnx_shadowmap_bounds) + ";")
if is_publish and wrd.lnx_loadscreen:
asset_references = list(set(assets.assets))
loadscreen_class = 'leenkx.trait.internal.LoadingScreen'
if os.path.isfile(lnx.utils.get_fp() + '/Sources/' + wrd.lnx_project_package + '/LoadingScreen.hx'):
loadscreen_class = wrd.lnx_project_package + '.LoadingScreen'
f.write("""
leenkx.system.Starter.numAssets = """ + str(len(asset_references)) + """;
leenkx.system.Starter.drawLoading = """ + loadscreen_class + """.render;""")
f.write("\n" + content_indent + "leenkx.system.Starter.numAssets = " + str(len(asset_references)) + ";")
f.write("\n" + content_indent + "leenkx.system.Starter.drawLoading = " + loadscreen_class + ".render;")
if wrd.lnx_ui == 'Enabled':
if wrd.lnx_canvas_img_scaling_quality == 'low':
f.write("""
leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;""")
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.Low;")
elif wrd.lnx_canvas_img_scaling_quality == 'high':
f.write("""
leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;""")
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;")
# Write Starter.main call
starter_indent = content_indent + " "
f.write("\n" + content_indent + "leenkx.system.Starter.main(")
f.write("\n" + starter_indent + "'" + lnx.utils.safestr(scene_name) + scene_ext + "',")
f.write("\n" + starter_indent + str(winmode) + ",")
f.write("\n" + starter_indent + ('true' if wrd.lnx_winresize else 'false') + ",")
f.write("\n" + starter_indent + ('true' if wrd.lnx_winminimize else 'false') + ",")
f.write("\n" + starter_indent + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + ",")
f.write("\n" + starter_indent + str(resx) + ",")
f.write("\n" + starter_indent + str(resy) + ",")
f.write("\n" + starter_indent + str(int(rpdat.lnx_samples_per_pixel)) + ",")
f.write("\n" + starter_indent + ('true' if wrd.lnx_vsync else 'false') + ",")
f.write("\n" + starter_indent + pathpack + ".renderpath.RenderPathCreator.get")
f.write("\n" + content_indent + ");")
# Write main_post hooks
if hooks['main_post']:
f.write("\n" + content_indent + hooks['main_post'].replace('\n', '\n' + content_indent))
# Write wrap_init closing statements (inner to outer, reverse order)
for i, (priority, before, after) in enumerate(reversed(hooks['wrap_inits'])):
indent = base_indent + (" " * (wrap_count - 1 - i))
f.write("\n" + indent + after)
f.write("""
leenkx.system.Starter.main(
'""" + lnx.utils.safestr(scene_name) + scene_ext + """',
""" + str(winmode) + """,
""" + ('true' if wrd.lnx_winresize else 'false') + """,
""" + ('true' if wrd.lnx_winminimize else 'false') + """,
""" + ('true' if (wrd.lnx_winresize and wrd.lnx_winmaximize) else 'false') + """,
""" + str(resx) + """,
""" + str(resy) + """,
""" + str(int(rpdat.lnx_samples_per_pixel)) + """,
""" + ('true' if wrd.lnx_vsync else 'false') + """,
""" + pathpack + """.renderpath.RenderPathCreator.get
);
}
}""")
@ -584,28 +743,27 @@ def write_indexhtml(w, h, is_publish):
"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>""")
<meta charset='utf-8'/>""")
if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen':
f.write("""
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
""")
<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>""")
f.write("""
<title>"""+html.escape( wrd.lnx_project_name)+"""</title>
</head>
<body style="margin: 0; padding: 0;">
<body style='margin: 0; padding: 0'>
""")
if rpdat.rp_stereo or wrd.lnx_winmode == 'Fullscreen':
f.write("""
<canvas style="object-fit: contain; min-width: 100%; max-width: 100%; max-height: 100vh; min-height: 100vh; display: block;" id='khanvas' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
<canvas style='object-fit: contain; min-width: 100%; max-width: 100%; max-height: 100vh; min-height: 100vh; display: block;' id='khanvas' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
""")
else:
if wrd.lnx_winmode != 'Headless':
f.write("""
<p align="center"><canvas align="center" style="outline: none;" id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas></p>
<p align='center'><canvas align='center' style='outline: none;' id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas></p>
""")
else:
f.write("""
<canvas align="center" style="display: none;" id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
<canvas align='center' style='display: none;' id='khanvas' width='""" + str(w) + """' height='""" + str(h) + """' tabindex='-1'""" + str(popupmenu_in_browser) + """></canvas>
<script>
// Quick solution for headless mode only for HTML target
window.onload = function() {
@ -614,7 +772,7 @@ def write_indexhtml(w, h, is_publish):
</script>
""")
f.write("""
<script src='kha.js'></script>
<script type='text/javascript' src='kha.js'></script>
</body>
</html>
""")
@ -707,16 +865,13 @@ const float ssgiRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + "
""")
if rpdat.rp_bloom:
follow_blender = rpdat.lnx_bloom_follow_blender
follow_blender = rpdat.lnx_bloom_follow_blender if bpy.app.version < (4, 3, 0) else False
eevee_settings = bpy.context.scene.eevee
if bpy.app.version <= (4, 2, 4):
threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.lnx_bloom_threshold
strength = eevee_settings.bloom_intensity if follow_blender else rpdat.lnx_bloom_strength
knee = eevee_settings.bloom_knee if follow_blender else rpdat.lnx_bloom_knee
else:
threshold = rpdat.lnx_bloom_threshold
strength = rpdat.lnx_bloom_strength
knee = rpdat.lnx_bloom_knee
threshold = eevee_settings.bloom_threshold if follow_blender else rpdat.lnx_bloom_threshold
strength = eevee_settings.bloom_intensity if follow_blender else rpdat.lnx_bloom_strength
knee = eevee_settings.bloom_knee if follow_blender else rpdat.lnx_bloom_knee
f.write(
"""const float bloomThreshold = """ + str(round(threshold * 100) / 100) + """;