Browse Source

Replace fancy AgeSDL Python meta hacks

Path of the Shell did not like my fancy metaprogramming tricks for
defining an AgeSDL Python class that contained characters that are
illegal in Python identifiers. So, now, we revert to just using a
standard class declaration.

That means that we need to strip out any illegal identifiers from the
age name first. A legal Python 2.x identifier is constrained to the
ASCII alphanumeric characters and the underscore with the stipulation
that the first character cannot be a number. To illustrate this to the
artist, we alert the age name property field if an illegal character is
found in the age name. We also alert on the underscore, which is now
used as a very very special replacement character. In the case of an
illegal character, an error message is shown in the UI with the correct
AgeSDL name.

Of course, I hope no one really uses those illegal characters and this
is just more fulmination on my part...
pull/128/head
Adam Johnson 6 years ago
parent
commit
6c4aedb17a
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 10
      korman/exporter/manager.py
  2. 7
      korman/exporter/python.py
  3. 35
      korman/korlib/__init__.py
  4. 12
      korman/operators/op_export.py
  5. 3
      korman/plasma_magic.py
  6. 17
      korman/ui/ui_world.py

10
korman/exporter/manager.py

@ -19,6 +19,7 @@ from PyHSPlasma import *
import weakref import weakref
from . import explosions from . import explosions
from .. import korlib
from ..plasma_magic import * from ..plasma_magic import *
# These objects have to be in the plSceneNode pool in order to be loaded... # These objects have to be in the plSceneNode pool in order to be loaded...
@ -243,21 +244,22 @@ class ExportManager:
output = self._exporter().output output = self._exporter().output
# AgeSDL Hook Python # 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) age_py = get_text(py_filename, None)
if output.want_py_text(age_py): if output.want_py_text(age_py):
py_code = age_py.as_string() py_code = age_py.as_string()
else: 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) output.add_python_mod(py_filename, text_id=age_py, str_data=py_code)
# AgeSDL # AgeSDL
sdl_filename = "{}.sdl".format(age) sdl_filename = "{}.sdl".format(fixed_agename)
age_sdl = get_text(sdl_filename) age_sdl = get_text(sdl_filename)
if age_sdl is not None: if age_sdl is not None:
sdl_code = None sdl_code = None
else: 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) output.add_sdl(sdl_filename, text_id=age_sdl, str_data=sdl_code)
def save_age(self): def save_age(self):

7
korman/exporter/python.py

@ -80,7 +80,8 @@ class PythonPackageExporter:
def _ensure_age_sdl_hook(self, report): def _ensure_age_sdl_hook(self, report):
age_props = bpy.context.scene.world.plasma_age age_props = bpy.context.scene.world.plasma_age
if age_props.age_sdl: 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) age_py = self._modules.get(py_filename)
if age_py is not None: if age_py is not None:
del self._modules[py_filename] del self._modules[py_filename]
@ -88,11 +89,11 @@ class PythonPackageExporter:
self._pfms[py_filename] = age_py self._pfms[py_filename] = age_py
else: else:
report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) 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: else:
report.msg("Packing default AgeSDL Python", indent=1) report.msg("Packing default AgeSDL Python", indent=1)
very_very_special_python.format(age_name=age_props.age_name) 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): def _harvest_pfms(self, report):
objects = bpy.context.scene.objects objects = bpy.context.scene.objects

35
korman/korlib/__init__.py

@ -76,6 +76,13 @@ finally:
from .python import * from .python import *
from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY 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): def _wave_chunks(stream):
while not stream.eof(): while not stream.eof():
chunk_name = stream.read(4) chunk_name = stream.read(4)
@ -101,3 +108,31 @@ finally:
header.read(stream) header.read(stream)
return chunks[b"data"]["size"] 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 ""

12
korman/operators/op_export.py

@ -163,8 +163,12 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
if context.mode != "OBJECT": if context.mode != "OBJECT":
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
# Separate blender operator and actual export logic for my sanity
ageName = path.stem 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: with UiHelper(context) as _ui:
e = exporter.Exporter(self) e = exporter.Exporter(self)
try: try:
@ -256,6 +260,12 @@ class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
path.touch() 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...) # Bonus Fun: Implement Profile-mode here (later...)
e = exporter.PythonPackageExporter(filepath=self.filepath, e = exporter.PythonPackageExporter(filepath=self.filepath,
version=globals()[self.version]) version=globals()[self.version])

3
korman/plasma_magic.py

@ -17,7 +17,8 @@ very_very_special_python = """
from Plasma import * from Plasma import *
from PlasmaTypes import * from PlasmaTypes import *
globals()["{age_name}"] = type("{age_name}", (ptResponder,), dict()) class {age_name}(ptResponder):
pass
""" """
very_very_special_sdl = """ very_very_special_sdl = """

17
korman/ui/ui_world.py

@ -16,7 +16,7 @@
import bpy import bpy
from pathlib import Path from pathlib import Path
from ..korlib import ConsoleToggler from .. import korlib
class AgeButtonsPanel: class AgeButtonsPanel:
@ -128,6 +128,9 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col.prop(active_page, "seq_suffix") col.prop(active_page, "seq_suffix")
col.prop_menu_enum(active_page, "version") 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 # Core settings
layout.separator() layout.separator()
split = layout.split() split = layout.split()
@ -140,15 +143,23 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
col = split.column() col = split.column()
col.label("Age Settings:") col.label("Age Settings:")
col.prop(age, "seq_prefix", text="ID") 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="") 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() layout.separator()
split = layout.split() split = layout.split()
col = split.column() col = split.column()
col.label("Export Settings:") col.label("Export Settings:")
col.enabled = ConsoleToggler.is_platform_supported() col.enabled = korlib.ConsoleToggler.is_platform_supported()
col.prop(age, "verbose") col.prop(age, "verbose")
col.prop(age, "show_console") col.prop(age, "show_console")

Loading…
Cancel
Save