diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py index c3906c7..380caa6 100644 --- a/korman/exporter/manager.py +++ b/korman/exporter/manager.py @@ -19,6 +19,7 @@ from PyHSPlasma import * import weakref from . import explosions +from .. import korlib from ..plasma_magic import * # These objects have to be in the plSceneNode pool in order to be loaded... @@ -243,21 +244,22 @@ class ExportManager: output = self._exporter().output # AgeSDL Hook Python - py_filename = "{}.py".format(age) + fixed_agename = korlib.replace_python2_identifier(age) + py_filename = "{}.py".format(fixed_agename) age_py = get_text(py_filename, None) if output.want_py_text(age_py): py_code = age_py.as_string() else: - py_code = very_very_special_python.format(age_name=age).lstrip() + py_code = very_very_special_python.format(age_name=fixed_agename).lstrip() output.add_python_mod(py_filename, text_id=age_py, str_data=py_code) # AgeSDL - sdl_filename = "{}.sdl".format(age) + sdl_filename = "{}.sdl".format(fixed_agename) age_sdl = get_text(sdl_filename) if age_sdl is not None: sdl_code = None else: - sdl_code = very_very_special_sdl.format(age_name=age).lstrip() + sdl_code = very_very_special_sdl.format(age_name=fixed_agename).lstrip() output.add_sdl(sdl_filename, text_id=age_sdl, str_data=sdl_code) def save_age(self): diff --git a/korman/exporter/python.py b/korman/exporter/python.py index e263b9f..ef87718 100644 --- a/korman/exporter/python.py +++ b/korman/exporter/python.py @@ -80,7 +80,8 @@ class PythonPackageExporter: def _ensure_age_sdl_hook(self, report): age_props = bpy.context.scene.world.plasma_age if age_props.age_sdl: - py_filename = "{}.py".format(age_props.age_name) + fixed_agename = korlib.replace_python2_identifier(age_props.age_name) + py_filename = "{}.py".format(fixed_agename) age_py = self._modules.get(py_filename) if age_py is not None: del self._modules[py_filename] @@ -88,11 +89,11 @@ class PythonPackageExporter: self._pfms[py_filename] = age_py else: report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) - self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) else: report.msg("Packing default AgeSDL Python", indent=1) very_very_special_python.format(age_name=age_props.age_name) - self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + self._pfms[py_filename] = very_very_special_python.format(age_name=fixed_agename) def _harvest_pfms(self, report): objects = bpy.context.scene.objects diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 3e03574..78a0fe9 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -76,6 +76,13 @@ finally: from .python import * from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY + _IDENTIFIER_RANGES = ((ord('0'), ord('9')), (ord('A'), ord('Z')), (ord('a'), ord('z'))) + from keyword import kwlist as _kwlist + _KEYWORDS = set(_kwlist) + # Python 2.x keywords + _KEYWORDS.add("exec") + _KEYWORDS.add("print") + def _wave_chunks(stream): while not stream.eof(): chunk_name = stream.read(4) @@ -101,3 +108,31 @@ finally: header.read(stream) return chunks[b"data"]["size"] + + def is_legal_python2_identifier(identifier): + if not identifier: + return False + + # FIXME: str.isascii in Python 3.7 + if any(ord(i) > 0x7F for i in identifier): + return False + if is_python_keyword(identifier): + return False + return identifier.isidentifier() + + is_python_keyword = _KEYWORDS.__contains__ + + def replace_python2_identifier(identifier): + """Replaces illegal characters in a Python identifier with a replacement character""" + + def process(identifier): + # No leading digits in identifiers, so skip the first range element (0...9) + yield next((identifier[0] for low, high in _IDENTIFIER_RANGES[1:] + if low <= ord(identifier[0]) <= high), '_') + for i in identifier[1:]: + yield next((i for low, high in _IDENTIFIER_RANGES if low <= ord(i) <= high), '_') + + if identifier: + return "".join(process(identifier)) + else: + return "" diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index fa8040b..b83ea5c 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -163,8 +163,12 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator): if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") - # Separate blender operator and actual export logic for my sanity ageName = path.stem + if korlib.is_python_keyword(ageName): + self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(ageName)) + return {"CANCELLED"} + + # Separate blender operator and actual export logic for my sanity with UiHelper(context) as _ui: e = exporter.Exporter(self) try: @@ -256,6 +260,12 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator): return {"CANCELLED"} path.touch() + # Age names cannot be python keywords + age_name = context.scene.world.plasma_age.age_name + if korlib.is_python_keyword(age_name): + self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) + return {"CANCELLED"} + # Bonus Fun: Implement Profile-mode here (later...) e = exporter.PythonPackageExporter(filepath=self.filepath, version=globals()[self.version]) diff --git a/korman/plasma_magic.py b/korman/plasma_magic.py index b67d438..ed89987 100644 --- a/korman/plasma_magic.py +++ b/korman/plasma_magic.py @@ -17,7 +17,8 @@ very_very_special_python = """ from Plasma import * from PlasmaTypes import * -globals()["{age_name}"] = type("{age_name}", (ptResponder,), dict()) +class {age_name}(ptResponder): + pass """ very_very_special_sdl = """ diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index 938f0f3..6618bfb 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -16,7 +16,7 @@ import bpy from pathlib import Path -from ..korlib import ConsoleToggler +from .. import korlib class AgeButtonsPanel: @@ -128,6 +128,9 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col.prop(active_page, "seq_suffix") col.prop_menu_enum(active_page, "version") + # Age Names should really be legal Python 2.x identifiers for AgeSDLHooks + legal_identifier = korlib.is_legal_python2_identifier(age.age_name) + # Core settings layout.separator() split = layout.split() @@ -140,15 +143,23 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): col = split.column() col.label("Age Settings:") col.prop(age, "seq_prefix", text="ID") - col.alert = not age.age_name.strip() + col.alert = not legal_identifier or '_' in age.age_name col.prop(age, "age_name", text="") + # Display a hint if the identifier is illegal + if not legal_identifier: + if korlib.is_python_keyword(age.age_name): + layout.label(text="Ages should not be named the same as a Python keyword", icon="ERROR") + elif age.age_sdl: + fixed_identifier = korlib.replace_python2_identifier(age.age_name) + layout.label(text="Age's SDL will use the name '{}'".format(fixed_identifier), icon="ERROR") + layout.separator() split = layout.split() col = split.column() col.label("Export Settings:") - col.enabled = ConsoleToggler.is_platform_supported() + col.enabled = korlib.ConsoleToggler.is_platform_supported() col.prop(age, "verbose") col.prop(age, "show_console")