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