mirror of https://github.com/H-uru/korman.git
29 changed files with 1808 additions and 278 deletions
@ -0,0 +1,148 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
from bpy.props import * |
||||
from . import korlib |
||||
|
||||
game_versions = [("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game"), |
||||
("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack"), |
||||
("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game")] |
||||
|
||||
class PlasmaGame(bpy.types.PropertyGroup): |
||||
name = StringProperty(name="Name", |
||||
description="Name of the Plasma Game", |
||||
options=set()) |
||||
path = StringProperty(name="Path", |
||||
description="Path to this Plasma Game", |
||||
options=set()) |
||||
version = EnumProperty(name="Version", |
||||
description="Plasma version of this game", |
||||
items=game_versions, |
||||
options=set()) |
||||
|
||||
|
||||
class KormanAddonPreferences(bpy.types.AddonPreferences): |
||||
bl_idname = __package__ |
||||
|
||||
games = CollectionProperty(type=PlasmaGame) |
||||
active_game_index = IntProperty(options={"SKIP_SAVE"}) |
||||
|
||||
def _check_py22_exe(self, context): |
||||
if self._ensure_abspath((2, 2)): |
||||
self._check_python((2, 2)) |
||||
def _check_py23_exe(self, context): |
||||
if self._ensure_abspath((2, 3)): |
||||
self._check_python((2, 3)) |
||||
def _check_py27_exe(self, context): |
||||
if self._ensure_abspath((2, 7)): |
||||
self._check_python((2, 7)) |
||||
|
||||
python22_executable = StringProperty(name="Python 2.2", |
||||
description="Path to the Python 2.2 executable", |
||||
options=set(), |
||||
subtype="FILE_PATH", |
||||
update=_check_py22_exe) |
||||
python23_executable = StringProperty(name="Python 2.3", |
||||
description="Path to the Python 2.3 executable", |
||||
options=set(), |
||||
subtype="FILE_PATH", |
||||
update=_check_py23_exe) |
||||
python27_executable = StringProperty(name="Python 2.7", |
||||
description="Path to the Python 2.7 executable", |
||||
options=set(), |
||||
subtype="FILE_PATH", |
||||
update=_check_py27_exe) |
||||
|
||||
def _validate_py_exes(self): |
||||
if not self.is_property_set("python22_valid"): |
||||
self._check_python((2, 2)) |
||||
if not self.is_property_set("python23_valid"): |
||||
self._check_python((2, 3)) |
||||
if not self.is_property_set("python27_valid"): |
||||
self._check_python((2, 7)) |
||||
return True |
||||
|
||||
# Internal error states |
||||
python22_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) |
||||
python23_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) |
||||
python27_valid = BoolProperty(options={"HIDDEN", "SKIP_SAVE"}) |
||||
python_validated = BoolProperty(get=_validate_py_exes, options={"HIDDEN", "SKIP_SAVE"}) |
||||
|
||||
def _check_python(self, py_version): |
||||
py_exe = getattr(self, "python{}{}_executable".format(*py_version)) |
||||
if py_exe: |
||||
valid = korlib.verify_python(py_version, py_exe) |
||||
else: |
||||
valid = True |
||||
setattr(self, "python{}{}_valid".format(*py_version), valid) |
||||
|
||||
def _ensure_abspath(self, py_version): |
||||
attr = "python{}{}_executable".format(*py_version) |
||||
path = getattr(self, attr) |
||||
if path.startswith("//"): |
||||
setattr(self, attr, bpy.path.abspath(path)) |
||||
return False |
||||
return True |
||||
|
||||
def draw(self, context): |
||||
layout = self.layout |
||||
split = layout.split() |
||||
main_col = split.column() |
||||
|
||||
main_col.label("Plasma Games:") |
||||
row = main_col.row() |
||||
row.template_list("PlasmaGameListRW", "games", self, "games", self, |
||||
"active_game_index", rows=3) |
||||
col = row.column(align=True) |
||||
col.operator("world.plasma_game_add", icon="ZOOMIN", text="") |
||||
col.operator("world.plasma_game_remove", icon="ZOOMOUT", text="") |
||||
col.operator("world.plasma_game_convert", icon="IMPORT", text="") |
||||
|
||||
# Game Properties |
||||
active_game_index = self.active_game_index |
||||
if bool(self.games) and active_game_index < len(self.games): |
||||
active_game = self.games[active_game_index] |
||||
|
||||
col = split.column() |
||||
col.label("Game Configuration:") |
||||
box = col.box().column() |
||||
|
||||
box.prop(active_game, "path", emboss=False) |
||||
box.prop(active_game, "version") |
||||
box.separator() |
||||
|
||||
row = box.row(align=True) |
||||
op = row.operator("world.plasma_game_add", icon="FILE_FOLDER", text="Change Path") |
||||
op.filepath = active_game.path |
||||
op.game_index = active_game_index |
||||
|
||||
# Python Installs |
||||
assert self.python_validated |
||||
col = layout.column() |
||||
col.label("Python Executables:") |
||||
col.alert = not self.python22_valid |
||||
col.prop(self, "python22_executable") |
||||
col.alert = not self.python23_valid |
||||
col.prop(self, "python23_executable") |
||||
col.alert = not self.python27_valid |
||||
col.prop(self, "python27_executable") |
||||
|
||||
@classmethod |
||||
def register(cls): |
||||
# Register the old-timey per-world Plasma Games for use in the conversion |
||||
# operator. What fun. I guess.... |
||||
from .properties.prop_world import PlasmaGames |
||||
PlasmaGames.games = CollectionProperty(type=PlasmaGame) |
@ -0,0 +1,429 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
from contextlib import contextmanager |
||||
import enum |
||||
from hashlib import md5 |
||||
from .. import korlib |
||||
import locale |
||||
import os |
||||
from pathlib import Path |
||||
from ..plasma_magic import plasma_python_glue |
||||
from PyHSPlasma import * |
||||
import shutil |
||||
import time |
||||
import weakref |
||||
import zipfile |
||||
|
||||
_CHUNK_SIZE = 0xA00000 |
||||
_encoding = locale.getpreferredencoding(False) |
||||
|
||||
def _hashfile(filename, hasher, block=0xFFFF): |
||||
with open(str(filename), "rb") as handle: |
||||
h = hasher() |
||||
data = handle.read(block) |
||||
while data: |
||||
h.update(data) |
||||
data = handle.read(block) |
||||
return h.digest() |
||||
|
||||
@enum.unique |
||||
class _FileType(enum.Enum): |
||||
generated_dat = 0 |
||||
sfx = 1 |
||||
sdl = 2 |
||||
python_code = 3 |
||||
generated_ancillary = 4 |
||||
|
||||
|
||||
class _OutputFile: |
||||
def __init__(self, **kwargs): |
||||
self.file_type = kwargs.get("file_type") |
||||
self.dirname = kwargs.get("dirname") |
||||
self.filename = kwargs.get("filename") |
||||
self.skip_hash = kwargs.get("skip_hash", False) |
||||
self.internal = kwargs.get("internal", False) |
||||
self.file_path = None |
||||
self.mod_time = None |
||||
|
||||
if self.file_type in (_FileType.generated_dat, _FileType.generated_ancillary): |
||||
self.file_data = kwargs.get("file_data", None) |
||||
self.file_path = kwargs.get("file_path", None) |
||||
self.mod_time = Path(self.file_path).stat().st_mtime if self.file_path else None |
||||
|
||||
# need either a data buffer OR a file path |
||||
assert bool(self.file_data) ^ bool(self.file_path) |
||||
|
||||
if self.file_type == _FileType.sfx: |
||||
self.id_data = kwargs.get("id_data") |
||||
path = Path(self.id_data.filepath) |
||||
try: |
||||
if path.exists(): |
||||
self.file_path = str(path.resolve()) |
||||
self.mod_time = path.stat().st_mtime |
||||
except OSError: |
||||
pass |
||||
|
||||
if self.id_data.packed_file is not None: |
||||
self.file_data = self.id_data.packed_file.data |
||||
else: |
||||
self.file_data = None |
||||
|
||||
if self.file_type in (_FileType.sdl, _FileType.python_code): |
||||
self.id_data = kwargs.get("id_data") |
||||
self.file_data = kwargs.get("file_data") |
||||
self.needs_glue = kwargs.get("needs_glue", True) |
||||
assert bool(self.id_data) or bool(self.file_data) |
||||
|
||||
self.mod_time = None |
||||
self.file_path = None |
||||
if self.id_data is not None: |
||||
path = Path(self.id_data.filepath) |
||||
try: |
||||
if path.exists(): |
||||
self.file_path = str(path.resolve()) |
||||
self.mod_time = path.stat().st_mtime |
||||
except OSError: |
||||
pass |
||||
|
||||
if self.file_data is None: |
||||
self.file_data = self.id_data.as_string() |
||||
|
||||
# Last chance for encryption... |
||||
enc = kwargs.get("enc", None) |
||||
if enc is not None: |
||||
self._encrypt(enc) |
||||
|
||||
def _encrypt(self, enc): |
||||
backing_stream = hsRAMStream() |
||||
with plEncryptedStream().open(backing_stream, fmCreate, enc) as enc_stream: |
||||
if self.file_path: |
||||
if plEncryptedStream.IsFileEncrypted(self.file_path): |
||||
with plEncryptedStream().open(self.file_path, fmRead, plEncryptedStream.kEncAuto) as dec_stream: |
||||
self._enc_spin_wash(enc_stream, dec_stream) |
||||
else: |
||||
with hsFileStream().open(self.file_path, fmRead) as dec_stream: |
||||
self._enc_spin_wash(enc_stream, dec_stream) |
||||
elif self.file_data: |
||||
if isinstance(self.file_data, str): |
||||
enc_stream.write(self.file_data.encode(_encoding)) |
||||
else: |
||||
enc_stream.write(self.file_data) |
||||
else: |
||||
raise RuntimeError() |
||||
|
||||
self.file_data = backing_stream.buffer |
||||
# do NOT copy over an unencrypted file... |
||||
self.file_path = None |
||||
|
||||
def _enc_spin_wash(self, enc_stream, dec_stream): |
||||
while True: |
||||
size_rem = dec_stream.size - dec_stream.pos |
||||
readsz = min(size_rem, _CHUNK_SIZE) |
||||
if readsz == 0: |
||||
break |
||||
data = dec_stream.read(readsz) |
||||
enc_stream.write(data) |
||||
|
||||
def __eq__(self, rhs): |
||||
return str(self) == str(rhs) |
||||
|
||||
def __hash__(self): |
||||
return hash(str(self)) |
||||
|
||||
def hash_md5(self): |
||||
if self.file_path: |
||||
with open(self.file_path, "rb") as handle: |
||||
h = md5() |
||||
data = handle.read(_CHUNK_SIZE) |
||||
while data: |
||||
h.update(data) |
||||
data = handle.read(_CHUNK_SIZE) |
||||
return h.digest() |
||||
elif self.file_data is not None: |
||||
if isinstance(self.file_data, str): |
||||
return md5(self.file_data.encode(_encoding)).digest() |
||||
else: |
||||
return md5(self.file_data).digest() |
||||
else: |
||||
raise RuntimeError() |
||||
|
||||
def __str__(self): |
||||
return "{}/{}".format(self.dirname, self.filename) |
||||
|
||||
|
||||
class OutputFiles: |
||||
def __init__(self, exporter, path): |
||||
self._exporter = weakref.ref(exporter) |
||||
self._export_file = Path(path).resolve() |
||||
if exporter.dat_only: |
||||
self._export_path = self._export_file.parent |
||||
else: |
||||
self._export_path = self._export_file.parent.parent |
||||
self._files = set() |
||||
self._is_zip = self._export_file.suffix.lower() == ".zip" |
||||
self._py_files = set() |
||||
self._time = time.time() |
||||
|
||||
def add_python_code(self, filename, text_id=None, str_data=None): |
||||
assert filename not in self._py_files |
||||
of = _OutputFile(file_type=_FileType.python_code, |
||||
dirname="Python", filename=filename, |
||||
id_data=text_id, file_data=str_data, |
||||
skip_hash=True, |
||||
internal=(self._version != pvMoul), |
||||
needs_glue=False) |
||||
self._files.add(of) |
||||
self._py_files.add(filename) |
||||
|
||||
def add_python_mod(self, filename, text_id=None, str_data=None): |
||||
assert filename not in self._py_files |
||||
of = _OutputFile(file_type=_FileType.python_code, |
||||
dirname="Python", filename=filename, |
||||
id_data=text_id, file_data=str_data, |
||||
skip_hash=True, |
||||
internal=(self._version != pvMoul), |
||||
needs_glue=True) |
||||
self._files.add(of) |
||||
self._py_files.add(filename) |
||||
|
||||
def add_sdl(self, filename, text_id=None, str_data=None): |
||||
of = _OutputFile(file_type=_FileType.sdl, |
||||
dirname="SDL", filename=filename, |
||||
id_data=text_id, file_data=str_data, |
||||
enc=self.super_secure_encryption) |
||||
self._files.add(of) |
||||
|
||||
|
||||
def add_sfx(self, sound_id): |
||||
of = _OutputFile(file_type=_FileType.sfx, |
||||
dirname="sfx", filename=sound_id.name, |
||||
id_data=sound_id) |
||||
self._files.add(of) |
||||
|
||||
@contextmanager |
||||
def generate_dat_file(self, filename, **kwargs): |
||||
dat_only = self._exporter().dat_only |
||||
dirname = kwargs.get("dirname", "dat") |
||||
bogus = dat_only and dirname != "dat" |
||||
|
||||
if self._is_zip or bogus: |
||||
stream = hsRAMStream(self._version) |
||||
else: |
||||
if dat_only: |
||||
file_path = str(self._export_file.parent / filename) |
||||
else: |
||||
file_path = str(self._export_path / dirname / filename) |
||||
stream = hsFileStream(self._version) |
||||
stream.open(file_path, fmCreate) |
||||
backing_stream = stream |
||||
|
||||
# No sense in wasting time encrypting data that isn't going to be used in the export |
||||
if not bogus: |
||||
enc = kwargs.get("enc", None) |
||||
if enc is not None: |
||||
stream = plEncryptedStream(self._version) |
||||
stream.open(backing_stream, fmCreate, enc) |
||||
|
||||
# The actual export code is run at the "yield" statement. If an error occurs, we |
||||
# do not want to track this file. Note that the except block is required for the |
||||
# else block to be legal. ^_^ |
||||
try: |
||||
yield stream |
||||
except: |
||||
raise |
||||
else: |
||||
# Must call the EncryptedStream close to actually encrypt the data |
||||
stream.close() |
||||
if not stream is backing_stream: |
||||
backing_stream.close() |
||||
|
||||
# Not passing enc as a keyword argument to the output file definition. It makes more |
||||
# sense to yield an encrypted stream from this context manager and encrypt as we go |
||||
# instead of doing lots of buffer copying to encrypt as a post step. |
||||
if not bogus: |
||||
kwargs = { |
||||
"file_type": _FileType.generated_dat if dirname == "dat" else |
||||
_FileType.generated_ancillary, |
||||
"dirname": dirname, |
||||
"filename": filename, |
||||
"skip_hash": kwargs.get("skip_hash", False), |
||||
"internal": kwargs.get("internal", False), |
||||
} |
||||
if isinstance(backing_stream, hsRAMStream): |
||||
kwargs["file_data"] = backing_stream.buffer |
||||
else: |
||||
kwargs["file_path"] = file_path |
||||
self._files.add(_OutputFile(**kwargs)) |
||||
|
||||
def _generate_files(self, func=None): |
||||
dat_only = self._exporter().dat_only |
||||
for i in self._files: |
||||
if dat_only and i.dirname != "dat": |
||||
continue |
||||
if func is not None: |
||||
if func(i): |
||||
yield i |
||||
else: |
||||
yield i |
||||
|
||||
def _package_compyled_python(self): |
||||
func = lambda x: x.file_type == _FileType.python_code |
||||
report = self._exporter().report |
||||
version = self._version |
||||
|
||||
# There can be some debate about what the correct Python version for pvMoul is. |
||||
# I, quite frankly, don't give a rat's ass at the moment because CWE will only |
||||
# load Python.pak and no ancillary packages. Maybe someone should fix that, mm? |
||||
if version <= pvPots: |
||||
py_version = (2, 2) |
||||
else: |
||||
py_version = (2, 3) |
||||
|
||||
try: |
||||
pyc_objects = [] |
||||
for i in self._generate_files(func): |
||||
if i.needs_glue: |
||||
py_code = "{}\n\n{}\n".format(i.file_data, plasma_python_glue) |
||||
else: |
||||
py_code = i.file_data |
||||
result, pyc = korlib.compyle(i.filename, py_code, py_version, report, indent=1) |
||||
if result: |
||||
pyc_objects.append((i.filename, pyc)) |
||||
except korlib.PythonNotAvailableError as error: |
||||
report.warn("Python {} is not available. Your Age scripts were not packaged.", error, indent=1) |
||||
else: |
||||
if pyc_objects: |
||||
with self.generate_dat_file("{}.pak".format(self._exporter().age_name), |
||||
dirname="Python", enc=self.super_secure_encryption) as stream: |
||||
korlib.package_python(stream, pyc_objects) |
||||
|
||||
def save(self): |
||||
# At this stage, all Plasma data has been generated from whatever crap is in |
||||
# Blender. The only remaining part is to make sure any external dependencies are |
||||
# copied or packed into the appropriate format OR the asset hashes are generated. |
||||
version = self._version |
||||
|
||||
# Step 1: Handle Python |
||||
if self._exporter().python_method != "none" and version != pvMoul: |
||||
self._package_compyled_python() |
||||
|
||||
# Step 2: Generate sumfile |
||||
if self._version != pvMoul: |
||||
self._write_sumfile() |
||||
|
||||
# Step 3: Ensure errbody is gut |
||||
if self._is_zip: |
||||
self._write_zipfile() |
||||
else: |
||||
self._write_deps() |
||||
|
||||
@property |
||||
def super_secure_encryption(self): |
||||
version = self._version |
||||
if version == pvEoa: |
||||
return plEncryptedStream.kEncAes |
||||
elif version == pvMoul: |
||||
# trollface.jpg |
||||
return None |
||||
else: |
||||
return plEncryptedStream.kEncXtea |
||||
|
||||
def want_py_text(self, text_id): |
||||
if text_id is None: |
||||
return False |
||||
method = self._exporter().python_method |
||||
if method == "none": |
||||
return False |
||||
elif method == "all": |
||||
return text_id.name not in self._py_files |
||||
else: |
||||
return text_id.plasma_text.package and text_id.name not in self._py_files |
||||
|
||||
def _write_deps(self): |
||||
times = (self._time, self._time) |
||||
func = lambda x: not x.internal and x.file_type not in (_FileType.generated_ancillary, _FileType.generated_dat) |
||||
report = self._exporter().report |
||||
|
||||
for i in self._generate_files(func): |
||||
# Will only ever run for non-"dat" directories. |
||||
dst_path = str(self._export_path / i.dirname / i.filename) |
||||
if i.file_data: |
||||
mode = "w" if isinstance(i.file_data, str) else "wb" |
||||
with open(dst_path, mode) as handle: |
||||
handle.write(i.file_data) |
||||
os.utime(dst_path, times) |
||||
elif i.file_path: |
||||
shutil.copy2(i.file_path, dst_path) |
||||
else: |
||||
report.warn("No data found for dependency file '{}'. It will not be copied into the export directory.", |
||||
str(i.dirname / i.filename), indent=1) |
||||
|
||||
def _write_sumfile(self): |
||||
version = self._version |
||||
dat_only = self._exporter().dat_only |
||||
enc = plEncryptedStream.kEncAes if version >= pvEoa else plEncryptedStream.kEncXtea |
||||
filename = "{}.sum".format(self._exporter().age_name) |
||||
if dat_only: |
||||
func = lambda x: (not x.skip_hash and not x.internal) and x.dirname == "dat" |
||||
else: |
||||
func = lambda x: not x.skip_hash and not x.internal |
||||
|
||||
with self.generate_dat_file(filename, enc=enc, skip_hash=True) as stream: |
||||
files = list(self._generate_files(func)) |
||||
stream.writeInt(len(files)) |
||||
stream.writeInt(0) |
||||
for i in files: |
||||
# ABM and UU don't want the directory for PRPs... Bug? |
||||
extension = Path(i.filename).suffix.lower() |
||||
if extension == ".prp" and version < pvPots: |
||||
filename = i.filename |
||||
else: |
||||
filename = "{}\\{}".format(i.dirname, i.filename) |
||||
mod_time = i.mod_time if i.mod_time else self._time |
||||
hash_md5 = i.hash_md5() |
||||
|
||||
stream.writeSafeStr(filename) |
||||
stream.write(hash_md5) |
||||
stream.writeInt(int(mod_time)) |
||||
stream.writeInt(0) |
||||
|
||||
def _write_zipfile(self): |
||||
dat_only = self._exporter().dat_only |
||||
export_time = time.localtime(self._time)[:6] |
||||
if dat_only: |
||||
func = lambda x: x.dirname == "dat" and not x.internal |
||||
else: |
||||
func = lambda x: not x.internal |
||||
report = self._exporter().report |
||||
|
||||
with zipfile.ZipFile(str(self._export_file), 'w', zipfile.ZIP_DEFLATED) as zf: |
||||
for i in self._generate_files(func): |
||||
arcpath = i.filename if dat_only else "{}/{}".format(i.dirname, i.filename) |
||||
if i.file_data: |
||||
if isinstance(i.file_data, str): |
||||
data = i.file_data.encode(_encoding) |
||||
else: |
||||
data = i.file_data |
||||
zi = zipfile.ZipInfo(arcpath, export_time) |
||||
zf.writestr(zi, data) |
||||
elif i.file_path: |
||||
zf.write(i.file_path, arcpath) |
||||
else: |
||||
report.warn("No data found for dependency file '{}'. It will not be archived.", arcpath, indent=1) |
||||
|
||||
@property |
||||
def _version(self): |
||||
return self._exporter().mgr.getVer() |
@ -0,0 +1,177 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
from pathlib import Path |
||||
from PyHSPlasma import * |
||||
|
||||
from .explosions import ExportError |
||||
from . import logger |
||||
from .. import korlib |
||||
from ..plasma_magic import plasma_python_glue, very_very_special_python |
||||
|
||||
class PythonPackageExporter: |
||||
def __init__(self, filepath, version): |
||||
self._filepath = filepath |
||||
self._modules = {} |
||||
self._pfms = {} |
||||
self._version = version |
||||
|
||||
def _compyle(self, report): |
||||
report.progress_advance() |
||||
report.progress_range = len(self._modules) + len(self._pfms) |
||||
inc_progress = report.progress_increment |
||||
|
||||
age = bpy.context.scene.world.plasma_age |
||||
Text = bpy.types.Text |
||||
if self._version <= pvPots: |
||||
py_version = (2, 2) |
||||
else: |
||||
py_version = (2, 3) |
||||
py_code = [] |
||||
|
||||
for filename, source in self._pfms.items(): |
||||
if isinstance(source, Text): |
||||
if not source.plasma_text.package and age.python_method != "all": |
||||
inc_progress() |
||||
continue |
||||
code = source.as_string() |
||||
else: |
||||
code = source |
||||
|
||||
code = "{}\n\n{}\n".format(code, plasma_python_glue) |
||||
success, result = korlib.compyle(filename, code, py_version, report, indent=1) |
||||
if not success: |
||||
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) |
||||
py_code.append((filename, result)) |
||||
inc_progress() |
||||
|
||||
for filename, source in self._modules.items(): |
||||
if isinstance(source, Text): |
||||
if not source.plasma_text.package and age.python_method != "all": |
||||
inc_progress() |
||||
continue |
||||
code = source.as_string() |
||||
else: |
||||
code = source |
||||
|
||||
# no glue needed here, ma! |
||||
success, result = korlib.compyle(filename, code, py_version, report, indent=1) |
||||
if not success: |
||||
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) |
||||
py_code.append((filename, result)) |
||||
inc_progress() |
||||
|
||||
# man that was ugly... |
||||
return py_code |
||||
|
||||
def _ensure_age_sdl_hook(self, report): |
||||
age_props = bpy.context.scene.world.plasma_age |
||||
if age_props.age_sdl: |
||||
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] |
||||
if age_py.plasma_text.package or age.python_method == "all": |
||||
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=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=fixed_agename) |
||||
|
||||
def _harvest_pfms(self, report): |
||||
objects = bpy.context.scene.objects |
||||
report.progress_advance() |
||||
report.progress_range = len(objects) |
||||
inc_progress = report.progress_increment |
||||
|
||||
for i in objects: |
||||
logic = i.plasma_modifiers.advanced_logic |
||||
if i.plasma_object.enabled and logic.enabled: |
||||
for j in logic.logic_groups: |
||||
tree_versions = (globals()[version] for version in j.version) |
||||
if self._version in tree_versions: |
||||
self._harvest_tree(j.node_tree) |
||||
inc_progress() |
||||
|
||||
def _harvest_modules(self, report): |
||||
texts = bpy.data.texts |
||||
report.progress_advance() |
||||
report.progress_range = len(texts) |
||||
inc_progress = report.progress_increment |
||||
|
||||
for i in texts: |
||||
if i.name.endswith(".py") and i.name not in self._pfms: |
||||
self._modules.setdefault(i.name, i) |
||||
inc_progress() |
||||
|
||||
def _harvest_tree(self, tree): |
||||
# Search the node tree for any python file nodes. Any that we find are PFMs |
||||
for i in tree.nodes: |
||||
if i.bl_idname == "PlasmaPythonFileNode": |
||||
if i.filename and i.text_id: |
||||
self._pfms.setdefault(i.filename, i.text_id) |
||||
|
||||
def run(self): |
||||
"""Runs a stripped-down version of the Exporter that only handles Python files""" |
||||
age_props = bpy.context.scene.world.plasma_age |
||||
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger |
||||
with korlib.ConsoleToggler(age_props.show_console), log(self._filepath) as report: |
||||
report.progress_add_step("Harvesting Plasma PythonFileMods") |
||||
report.progress_add_step("Harvesting Helper Python Modules") |
||||
report.progress_add_step("Compyling Python Code") |
||||
report.progress_add_step("Packing Compyled Code") |
||||
report.progress_start("PACKING PYTHON") |
||||
|
||||
# Harvest the Python code |
||||
self._harvest_pfms(report) |
||||
self._harvest_modules(report) |
||||
self._ensure_age_sdl_hook(report) |
||||
|
||||
# Compyle and package the Python |
||||
self._package_python(report) |
||||
|
||||
# DONE |
||||
report.progress_end() |
||||
report.raise_errors() |
||||
|
||||
def _package_python(self, report): |
||||
py_code = self._compyle(report) |
||||
if not py_code: |
||||
report.error("No Python files were packaged.") |
||||
self._write_python_pak(py_code, report) |
||||
|
||||
def _write_python_pak(self, py_code, report): |
||||
report.progress_advance() |
||||
|
||||
if self._version == pvEoa: |
||||
enc = plEncryptedStream.kEncAes |
||||
elif self._version == pvMoul: |
||||
enc = None |
||||
else: |
||||
enc = plEncryptedStream.kEncXtea |
||||
|
||||
if enc is None: |
||||
stream = hsFileStream(self._version).open(self._filepath, fmCreate) |
||||
else: |
||||
stream = plEncryptedStream(self._version).open(self._filepath, fmCreate, enc) |
||||
try: |
||||
korlib.package_python(stream, py_code) |
||||
finally: |
||||
stream.close() |
@ -1,73 +0,0 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import hashlib |
||||
from pathlib import Path |
||||
from PyHSPlasma import * |
||||
|
||||
def _hashfile(filename, hasher, block=0xFFFF): |
||||
with open(str(filename), "rb") as handle: |
||||
h = hasher() |
||||
data = handle.read(block) |
||||
while data: |
||||
h.update(data) |
||||
data = handle.read(block) |
||||
return h.digest() |
||||
|
||||
class SumFile: |
||||
def __init__(self): |
||||
self._files = set() |
||||
|
||||
def append(self, filename): |
||||
self._files.add(filename) |
||||
|
||||
def _collect_files(self, version): |
||||
files = [] |
||||
for file in self._files: |
||||
filename, extension = file.name, file.suffix.lower() |
||||
if extension in {".age", ".csv", ".fni", ".loc", ".node", ".p2f", ".pfp", ".sub"}: |
||||
filename = Path("dat") / filename |
||||
elif extension == ".prp" and version > pvPrime: |
||||
# ABM and UU don't want the directory for PRPs... Bug? |
||||
filename = Path("dat") / filename |
||||
elif extension in {".pak", ".py"}: |
||||
filename = Path("Python") / filename |
||||
elif extension in {".avi", ".bik", ".oggv", ".webm"}: |
||||
filename = Path("avi") / filename |
||||
elif extension in {".ogg", ".opus", ".wav"}: |
||||
filename = Path("sfx") / filename |
||||
elif extension == ".sdl": |
||||
filename = Path("SDL") / filename |
||||
# else the filename has no directory prefix... oh well |
||||
|
||||
md5 = _hashfile(file, hashlib.md5) |
||||
timestamp = file.stat().st_mtime |
||||
files.append((str(filename), md5, int(timestamp))) |
||||
return files |
||||
|
||||
|
||||
def write(self, sumpath, version): |
||||
"""Writes a .sum file for Uru ABM, PotS, Myst 5, etc.""" |
||||
files = self._collect_files(version) |
||||
enc = plEncryptedStream.kEncAes if version >= pvEoa else plEncryptedStream.kEncXtea |
||||
|
||||
with plEncryptedStream(version).open(str(sumpath), fmWrite, enc) as stream: |
||||
stream.writeInt(len(files)) |
||||
stream.writeInt(0) |
||||
for file in files: |
||||
stream.writeSafeStr(str(file[0])) |
||||
stream.write(file[1]) |
||||
stream.writeInt(file[2]) |
||||
stream.writeInt(0) |
@ -0,0 +1,202 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
from __future__ import generators # Python 2.2 |
||||
import marshal |
||||
import os.path |
||||
import sys |
||||
|
||||
_python_executables = {} |
||||
|
||||
class PythonNotAvailableError(Exception): |
||||
pass |
||||
|
||||
|
||||
def compyle(file_name, py_code, py_version, report=None, indent=0): |
||||
# NOTE: Should never run under Python 2.x |
||||
my_version = sys.version_info[:2] |
||||
assert my_version == (2, 7) or my_version[0] > 2 |
||||
|
||||
# Remember: Python 2.2 file, so no single line if statements... |
||||
idx = file_name.find('.') |
||||
if idx == -1: |
||||
module_name = file_name |
||||
else: |
||||
module_name = file_name[:idx] |
||||
|
||||
if report is not None: |
||||
report.msg("Compyling {}", file_name, indent=indent) |
||||
|
||||
if my_version != py_version: |
||||
import subprocess |
||||
|
||||
py_executable = _find_python(py_version) |
||||
args = (py_executable, __file__, module_name) |
||||
try: |
||||
py_code = py_code.encode("utf-8") |
||||
except UnicodeError: |
||||
if report is not None: |
||||
report.error("Could not encode '{}'", file_name, indent=indent+1) |
||||
return (False, "Could not encode file") |
||||
result = subprocess.run(args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
||||
if result.returncode != 0: |
||||
try: |
||||
error = result.stdout.decode("utf-8").replace('\r\n', '\n') |
||||
except UnicodeError: |
||||
error = result.stdout |
||||
if report is not None: |
||||
report.error("Compylation Error in '{}'\n{}", file_name, error, indent=indent+1) |
||||
return (result.returncode == 0, result.stdout) |
||||
else: |
||||
raise NotImplementedError() |
||||
|
||||
def _compyle(module_name, py_code): |
||||
# Old python versions have major issues with Windows style newlines. |
||||
# Also, bad things happen if there is no newline at the end. |
||||
py_code += '\n' # sigh, this is slow on old Python... |
||||
py_code = py_code.replace('\r\n', '\n') |
||||
py_code = py_code.replace('\r', '\n') |
||||
code_object = compile(py_code, module_name, "exec") |
||||
|
||||
# The difference between us and the py_compile module is twofold: |
||||
# 1) py_compile compyles to a file. We might be exporting to memory, so that's |
||||
# not what we want. |
||||
# 2) py_compile saves a *pyc format file containing information such as compyle |
||||
# time and marshal format version. These items are not included in Cyan's |
||||
# Python.pak format. |
||||
# Therefore, we simply return the marshalled data as a string. |
||||
return marshal.dumps(code_object) |
||||
|
||||
def _find_python(py_version): |
||||
def find_executable(py_version): |
||||
# First, try to use Blender to find the Python executable |
||||
try: |
||||
import bpy |
||||
except ImportError: |
||||
pass |
||||
else: |
||||
userprefs = bpy.context.user_preferences.addons["korman"].preferences |
||||
py_executable = getattr(userprefs, "python{}{}_executable".format(*py_version), None) |
||||
if verify_python(py_version, py_executable): |
||||
return py_executable |
||||
|
||||
# Second, try looking Python up in the registry. |
||||
try: |
||||
import winreg |
||||
except ImportError: |
||||
pass |
||||
else: |
||||
py_executable = _find_python_reg(winreg.HKEY_CURRENT_USER, py_version) |
||||
if verify_python(py_version, py_executable): |
||||
return py_executable |
||||
py_executable = _find_python_reg(winreg.HKEY_LOCAL_MACHINE, py_version) |
||||
if verify_python(py_version, py_executable): |
||||
return py_executable |
||||
|
||||
# I give up, you win. |
||||
return None |
||||
|
||||
py_executable = _python_executables.setdefault(py_version, find_executable(py_version)) |
||||
if py_executable: |
||||
return py_executable |
||||
else: |
||||
raise PythonNotAvailableError("{}.{}".format(*py_version)) |
||||
|
||||
def _find_python_reg(reg_key, py_version): |
||||
import winreg |
||||
subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version) |
||||
try: |
||||
python_dir = winreg.QueryValue(reg_key, subkey_name) |
||||
except FileNotFoundError: |
||||
return None |
||||
else: |
||||
return os.path.join(python_dir, "python.exe") |
||||
|
||||
def package_python(stream, pyc_objects): |
||||
# Python.pak format: |
||||
# uint32_t numFiles |
||||
# - safeStr filename |
||||
# - uint32_t offset |
||||
# ~~~~~ |
||||
# uint32_t filesz |
||||
# uint8_t data[filesz] |
||||
if not pyc_objects: |
||||
stream.writeInt(0) |
||||
return |
||||
|
||||
# `stream` might be a plEncryptedStream, which doesn't seek very well at all. |
||||
# Therefore, we will go ahead and calculate the size of the index block so |
||||
# there is no need to seek around to write offset values |
||||
base_offset = 4 # uint32_t numFiles |
||||
data_offset = 0 |
||||
pyc_info = [] # sad, but makes life easier... |
||||
for module_name, compyled_code in pyc_objects: |
||||
pyc_info.append((module_name, data_offset, compyled_code)) |
||||
|
||||
# index offset overall |
||||
base_offset += 2 # writeSafeStr length |
||||
# NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts |
||||
# the Python unicode/string object to UTF-8. Currently, this is true. |
||||
base_offset += len(module_name.encode("utf-8")) # writeSafeStr |
||||
base_offset += 4 |
||||
|
||||
# current file data offset |
||||
data_offset += 4 # uint32_t filesz |
||||
data_offset += len(compyled_code) |
||||
|
||||
stream.writeInt(len(pyc_info)) |
||||
for module_name, data_offset, compyled_code in pyc_info: |
||||
stream.writeSafeStr(module_name) |
||||
# offset of data == index size (base_offset) + offset to data blob (data_offset) |
||||
stream.writeInt(base_offset + data_offset) |
||||
for module_name, data_offset, compyled_code in pyc_info: |
||||
stream.writeInt(len(compyled_code)) |
||||
stream.write(compyled_code) |
||||
|
||||
def verify_python(py_version, py_exe): |
||||
if not py_exe: |
||||
return False |
||||
|
||||
import subprocess |
||||
try: |
||||
args = (py_exe, "-V") |
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5) |
||||
except OSError: |
||||
return False |
||||
else: |
||||
output = result.stdout.decode() |
||||
try: |
||||
py_str, py_check = output[:6], output[7:10] |
||||
except IndexError: |
||||
return False |
||||
else: |
||||
if py_str != "Python": |
||||
return False |
||||
return "{}.{}".format(*py_version) == py_check |
||||
|
||||
if __name__ == "__main__": |
||||
# Python tries to be "helpful" on Windows by converting \n to \r\n. |
||||
# Therefore we must change the mode of stdout. |
||||
if sys.platform == "win32": |
||||
import os, msvcrt |
||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
||||
|
||||
try: |
||||
module_name = sys.argv[1] |
||||
except IndexError: |
||||
module_name = "<string>" |
||||
py_code_source = sys.stdin.read() |
||||
py_code_object = _compyle(module_name, py_code_source) |
||||
sys.stdout.write(py_code_object) |
@ -0,0 +1,220 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
very_very_special_python = """ |
||||
from Plasma import * |
||||
from PlasmaTypes import * |
||||
|
||||
class {age_name}(ptResponder): |
||||
pass |
||||
""" |
||||
|
||||
very_very_special_sdl = """ |
||||
#============================================================== |
||||
# This VeryVerySpecial SDL File was automatically generated |
||||
# by Korman. Have a nice day! |
||||
# |
||||
# READ: When modifying an SDL record, do *not* modify the |
||||
# existing record. You must copy and paste a new version |
||||
# below the current one and make your changes there. |
||||
#============================================================== |
||||
|
||||
STATEDESC {age_name} |
||||
{{ |
||||
VERSION 0 |
||||
}} |
||||
|
||||
""" |
||||
|
||||
# Copypasta (with small fixes for str.format) of glue.py from CWE's moul-scripts |
||||
plasma_python_glue = """ |
||||
glue_cl = None |
||||
glue_inst = None |
||||
glue_params = None |
||||
glue_paramKeys = None |
||||
try: |
||||
x = glue_verbose |
||||
except NameError: |
||||
glue_verbose = 0 |
||||
def glue_getClass(): |
||||
global glue_cl |
||||
if glue_cl == None: |
||||
try: |
||||
cl = globals()[glue_name] |
||||
if issubclass(cl,ptModifier): |
||||
glue_cl = cl |
||||
else: |
||||
if glue_verbose: |
||||
print "Class %s is not derived from modifier" % (cl.__name__) |
||||
except: |
||||
if glue_verbose: |
||||
try: |
||||
print "Could not find class %s" % (glue_name) |
||||
except NameError: |
||||
print "Filename/classname not set!" |
||||
return glue_cl |
||||
def glue_getInst(): |
||||
global glue_inst |
||||
if type(glue_inst) == type(None): |
||||
cl = glue_getClass() |
||||
if cl != None: |
||||
glue_inst = cl() |
||||
return glue_inst |
||||
def glue_delInst(): |
||||
global glue_inst |
||||
global glue_cl |
||||
global glue_params |
||||
global glue_paramKeys |
||||
if type(glue_inst) != type(None): |
||||
del glue_inst |
||||
glue_cl = None |
||||
glue_params = None |
||||
glue_paramKeys = None |
||||
def glue_getVersion(): |
||||
inst = glue_getInst() |
||||
ver = inst.version |
||||
glue_delInst() |
||||
return ver |
||||
def glue_findAndAddAttribs(obj, glue_params): |
||||
if isinstance(obj,ptAttribute): |
||||
if glue_params.has_key(obj.id): |
||||
if glue_verbose: |
||||
print "WARNING: Duplicate attribute ids!" |
||||
print "%s has id %d which is already defined in %s" % (obj.name, obj.id, glue_params[obj.id].name) |
||||
else: |
||||
glue_params[obj.id] = obj |
||||
elif type(obj) == type([]): |
||||
for o in obj: |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
elif type(obj) == type(dict()): |
||||
for o in obj.values(): |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
elif type(obj) == type( () ): |
||||
for o in obj: |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
|
||||
def glue_getParamDict(): |
||||
global glue_params |
||||
global glue_paramKeys |
||||
if type(glue_params) == type(None): |
||||
glue_params = dict() |
||||
gd = globals() |
||||
for obj in gd.values(): |
||||
glue_findAndAddAttribs(obj, glue_params) |
||||
# rebuild the parameter sorted key list |
||||
glue_paramKeys = glue_params.keys() |
||||
glue_paramKeys.sort() |
||||
glue_paramKeys.reverse() |
||||
return glue_params |
||||
def glue_getClassName(): |
||||
cl = glue_getClass() |
||||
if cl != None: |
||||
return cl.__name__ |
||||
if glue_verbose: |
||||
print "Class not found in %s.py" % (glue_name) |
||||
return None |
||||
def glue_getBlockID(): |
||||
inst = glue_getInst() |
||||
if inst != None: |
||||
return inst.id |
||||
if glue_verbose: |
||||
print "Instance could not be created in %s.py" % (glue_name) |
||||
return None |
||||
def glue_getNumParams(): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
return len(pd) |
||||
if glue_verbose: |
||||
print "No attributes found in %s.py" % (glue_name) |
||||
return 0 |
||||
def glue_getParam(number): |
||||
global glue_paramKeys |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
# see if there is a paramKey list |
||||
if type(glue_paramKeys) == type([]): |
||||
if number >= 0 and number < len(glue_paramKeys): |
||||
return pd[glue_paramKeys[number]].getdef() |
||||
else: |
||||
print "glue_getParam: Error! %d out of range of attribute list" % (number) |
||||
else: |
||||
pl = pd.values() |
||||
if number >= 0 and number < len(pl): |
||||
return pl[number].getdef() |
||||
else: |
||||
if glue_verbose: |
||||
print "glue_getParam: Error! %d out of range of attribute list" % (number) |
||||
if glue_verbose: |
||||
print "GLUE: Attribute list error" |
||||
return None |
||||
def glue_setParam(id,value): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
if pd.has_key(id): |
||||
try: |
||||
pd[id].__setvalue__(value) |
||||
except AttributeError: |
||||
if isinstance(pd[id],ptAttributeList): |
||||
try: |
||||
if type(pd[id].value) != type([]): |
||||
pd[id].value = [] |
||||
except AttributeError: |
||||
pd[id].value = [] |
||||
pd[id].value.append(value) |
||||
else: |
||||
pd[id].value = value |
||||
else: |
||||
if glue_verbose: |
||||
print "setParam: can't find id=",id |
||||
else: |
||||
print "setParma: Something terribly has gone wrong. Head for the cover." |
||||
def glue_isNamedAttribute(id): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
try: |
||||
if isinstance(pd[id],ptAttribNamedActivator): |
||||
return 1 |
||||
if isinstance(pd[id],ptAttribNamedResponder): |
||||
return 2 |
||||
except KeyError: |
||||
if glue_verbose: |
||||
print "Could not find id=%d attribute" % (id) |
||||
return 0 |
||||
def glue_isMultiModifier(): |
||||
inst = glue_getInst() |
||||
if isinstance(inst,ptMultiModifier): |
||||
return 1 |
||||
return 0 |
||||
def glue_getVisInfo(number): |
||||
global glue_paramKeys |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
# see if there is a paramKey list |
||||
if type(glue_paramKeys) == type([]): |
||||
if number >= 0 and number < len(glue_paramKeys): |
||||
return pd[glue_paramKeys[number]].getVisInfo() |
||||
else: |
||||
print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) |
||||
else: |
||||
pl = pd.values() |
||||
if number >= 0 and number < len(pl): |
||||
return pl[number].getVisInfo() |
||||
else: |
||||
if glue_verbose: |
||||
print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) |
||||
if glue_verbose: |
||||
print "GLUE: Attribute list error" |
||||
return None |
||||
""" |
@ -0,0 +1,22 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
from bpy.props import * |
||||
|
||||
class PlasmaText(bpy.types.PropertyGroup): |
||||
package = BoolProperty(name="Export", |
||||
description="Package this file in the age export", |
||||
options=set()) |
@ -0,0 +1,28 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman is free software: you can redistribute it and/or modify |
||||
# it under the terms of the GNU General Public License as published by |
||||
# the Free Software Foundation, either version 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
|
||||
class PlasmaTextEditorHeader(bpy.types.Header): |
||||
bl_space_type = "TEXT_EDITOR" |
||||
|
||||
def draw(self, context): |
||||
layout, text = self.layout, context.space_data.text |
||||
|
||||
if text is not None: |
||||
is_py = text.name.endswith(".py") |
||||
row = layout.row(align=True) |
||||
row.enabled = is_py |
||||
row.prop(text.plasma_text, "package") |
Loading…
Reference in new issue