mirror of https://github.com/H-uru/korman.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
452 lines
20 KiB
452 lines
20 KiB
# 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 * |
|
import cProfile |
|
from pathlib import Path |
|
from PyHSPlasma import * |
|
import pstats |
|
import subprocess |
|
|
|
from ..addon_prefs import game_versions |
|
from .. import exporter |
|
from ..helpers import UiHelper |
|
from .. import korlib, plasma_launcher |
|
from ..properties.prop_world import PlasmaAge |
|
|
|
class ExportOperator: |
|
def _get_default_path(self, context): |
|
blend_filepath = context.blend_data.filepath |
|
if not blend_filepath: |
|
blend_filepath = context.scene.world.plasma_age.age_name |
|
if not blend_filepath: |
|
blend_filepath = "Korman" |
|
return blend_filepath |
|
|
|
@property |
|
def has_reports(self): |
|
return hasattr(self.report) |
|
|
|
@classmethod |
|
def poll(cls, context): |
|
return context.scene.render.engine == "PLASMA_GAME" |
|
|
|
|
|
class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator): |
|
"""Exports ages for Cyan Worlds' Plasma Engine""" |
|
|
|
bl_idname = "export.plasma_age" |
|
bl_label = "Export Age" |
|
|
|
# Here's the rub: we define the properties in this dict. We're going to register them as a seekrit |
|
# over on the PlasmaAge world properties. We've got a helper so we can access them like they're actually on us... |
|
# If you want a volatile property, register it directly on this operator! |
|
_properties = { |
|
"verbose": (BoolProperty, {"name": "Display Verbose Log", |
|
"description": "Shows the verbose export log in the console", |
|
"default": False}), |
|
|
|
"show_console": (BoolProperty, {"name": "Display Log Console", |
|
"description": "Forces the Blender System Console open during the export", |
|
"default": True}), |
|
|
|
"texcache_path": (StringProperty, {"name": "Texture Cache Path", |
|
"description": "Texture Cache Filepath"}), |
|
|
|
"texcache_method": (EnumProperty, {"name": "Texture Cache", |
|
"description": "Texture Cache Settings", |
|
"items": [("skip", "Don't Use Texture Cache", "The texture cache is neither used nor updated."), |
|
("use", "Use Texture Cache", "Use (and update, if needed) cached textures."), |
|
("rebuild", "Rebuild Texture Cache", "Rebuilds the texture cache from scratch.")], |
|
"default": "use"}), |
|
|
|
"lighting_method": (EnumProperty, {"name": "Static Lighting", |
|
"description": "Static Lighting Settings", |
|
"items": [("skip", "Don't Bake Lighting", "Static lighting is not baked during this export (fastest export)"), |
|
("bake", "Bake Lighting", "Static lighting is baked according to your specifications"), |
|
("force_vcol", "Force Vertex Color Bake", "All static lighting is baked as vertex colors (faster export)"), |
|
("force_lightmap", "Force Lightmap Bake", "All static lighting is baked as lightmaps (slower export)")], |
|
"default": "bake"}), |
|
|
|
"envmap_method": (EnumProperty, {"name": "Environment Maps", |
|
"description": "Environment Map Settings", |
|
"items": [("skip", "Don't Export EnvMaps", "Environment Maps are not exported"), |
|
("dcm2dem", "Downgrade Planar EnvMaps", "When the engine doesn't support them, Planar Environment Maps are downgraded to Cube Maps"), |
|
("perengine", "Export Supported EnvMaps", "Only environment maps supported by the selected game engine are exported")], |
|
"default": "dcm2dem"}), |
|
|
|
"python_method": (EnumProperty, {"name": "Python", |
|
"description": "Specifies how Python should be packed", |
|
"items": [("none", "Pack Nothing", "Don't pack any Python files."), |
|
("as_requested", "Pack Requested Scripts", "Packs any script both linked as a Text file and requested for packaging."), |
|
("all", "Pack All Scripts", "Packs all Python files linked as a Text file.")], |
|
"default": "as_requested", |
|
"options": set()}), |
|
|
|
"localization_method": (EnumProperty, {"name": "Localization", |
|
"description": "Specifies how localization data should be exported", |
|
"items": [("database", "Localization Database", "A per-language database compatible with pfLocalizationEditor"), |
|
("database_back_compat", "Localization Database (Compat Mode)", "A per-language database compatible with pfLocalizationEditor and Korman <=0.11"), |
|
("single_file", "Single File", "A single file database, as in Korman <=0.11")], |
|
"default": "database", |
|
"options": set()}), |
|
|
|
"export_active": (BoolProperty, {"name": "INTERNAL: Export currently running", |
|
"default": False, |
|
"options": {"SKIP_SAVE"}}), |
|
} |
|
|
|
# This wigs out and very bad things happen if it's not directly on the operator... |
|
filepath = StringProperty(subtype="FILE_PATH") |
|
filter_glob = StringProperty(default="*.age;*.zip", options={'HIDDEN'}) |
|
|
|
version = EnumProperty(name="Version", |
|
description="Plasma version to export this age for", |
|
items=game_versions, |
|
default="pvPots", |
|
options=set()) |
|
|
|
dat_only = BoolProperty(name="Export Only PRPs", |
|
description="Only the Age PRPs should be exported", |
|
default=True, |
|
options={"HIDDEN"}) |
|
|
|
actions = EnumProperty(name="Actions", |
|
description="Actions for the exporter to perform", |
|
default={"EXPORT"}, |
|
items=[("EXPORT", "Export", "Export the age data"), |
|
("PROFILE", "Profile", "Profile the exporter"), |
|
("LAUNCH", "Launch Age", "Launch the age in Plasma")], |
|
options={"ENUM_FLAG"}) |
|
|
|
ki = IntProperty(name="KI", |
|
description="KI Number of the player to use when launching the game", |
|
options=set()) |
|
|
|
player = StringProperty(name="Player", |
|
description="Name of the player to use when launching the game", |
|
options=set()) |
|
|
|
serverini = StringProperty(name="Server INI", |
|
description="Name of the server configuation to use when launching the game", |
|
options=set()) |
|
|
|
def draw(self, context): |
|
layout = self.layout |
|
age = context.scene.world.plasma_age |
|
|
|
# The crazy mess we're doing with props on the fly means we have to explicitly draw them :( |
|
layout.prop(self, "actions") |
|
layout.prop(self, "version") |
|
if "LAUNCH" in self.actions: |
|
if self.version == "pvMoul": |
|
layout.alert = not self.ki |
|
layout.prop(self, "ki") |
|
layout.alert = False |
|
layout.prop(self, "serverini") |
|
else: |
|
layout.alert = not self.player.strip() |
|
layout.prop(self, "player") |
|
layout.alert = False |
|
layout.prop(age, "texcache_method", text="") |
|
layout.prop(age, "lighting_method") |
|
row = layout.row() |
|
row.enabled = korlib.ConsoleToggler.is_platform_supported() |
|
row.prop(age, "show_console") |
|
layout.prop(age, "verbose") |
|
|
|
def __getattr__(self, attr): |
|
if attr in self._properties: |
|
return getattr(bpy.context.scene.world.plasma_age, attr) |
|
raise AttributeError(attr) |
|
|
|
def __setattr__(self, attr, value): |
|
if attr in self._properties: |
|
setattr(bpy.context.scene.world.plasma_age, attr, value) |
|
else: |
|
super().__setattr__(attr, value) |
|
|
|
def execute(self, context): |
|
# Before we begin, do some basic sanity checking... |
|
if not self.actions: |
|
self.report({"ERROR"}, "Nothing to do?") |
|
return {"CANCELLED"} |
|
|
|
path = Path(self.filepath) |
|
if not self.filepath: |
|
self.error = "No file specified" |
|
return {"CANCELLED"} |
|
else: |
|
if not path.exists: |
|
try: |
|
path.mkdir(parents=True) |
|
except: |
|
self.report({"ERROR"}, "Failed to create export directory") |
|
return {"CANCELLED"} |
|
path.touch() |
|
|
|
# We need to back out of edit mode--this ensures that all changes are committed |
|
if context.mode != "OBJECT": |
|
bpy.ops.object.mode_set(mode="OBJECT") |
|
|
|
ageName = path.stem |
|
if korlib.is_python_keyword(ageName): |
|
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(ageName)) |
|
return {"CANCELLED"} |
|
|
|
# This prevents us from finding out at the very end that very, very bad things happened... |
|
if "LAUNCH" in self.actions: |
|
try: |
|
self._sanity_check_run_plasma() |
|
except exporter.ExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
return {"CANCELLED"} |
|
|
|
# Separate blender operator and actual export logic for my sanity |
|
if "EXPORT" in self.actions: |
|
with UiHelper(context) as _ui: |
|
e = exporter.Exporter(self) |
|
try: |
|
self.export_active = True |
|
if "PROFILE" in self.actions: |
|
profile_path = str(path.with_name("{}_cProfile".format(ageName))) |
|
profile = cProfile.runctx("e.run()", globals(), locals(), profile_path) |
|
else: |
|
e.run() |
|
except exporter.ExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
return {"CANCELLED"} |
|
except exporter.NonfatalExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
else: |
|
if "PROFILE" in self.actions: |
|
stats_out = path.with_name("{}_profile.log".format(ageName)) |
|
with open(str(stats_out), "w") as out: |
|
stats = pstats.Stats(profile_path, stream=out) |
|
stats = stats.sort_stats("time", "calls") |
|
stats.print_stats() |
|
finally: |
|
self.export_active = False |
|
|
|
if "LAUNCH" in self.actions: |
|
try: |
|
self._run_plasma(context) |
|
except exporter.ExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
return {"CANCELLED"} |
|
|
|
return {"FINISHED"} |
|
|
|
def invoke(self, context, event): |
|
# Called when a user hits "export" from the menu |
|
# We will prompt them for the export info, then call execute() |
|
if not self.filepath: |
|
bfp = self._get_default_path(context) |
|
self.filepath = str(Path(bfp).with_suffix(".age")) |
|
context.window_manager.fileselect_add(self) |
|
return {"RUNNING_MODAL"} |
|
|
|
@classmethod |
|
def register(cls): |
|
# BEGIN MAJICK |
|
# Register the exporter properties such that they will persist |
|
for name, (prop, options) in cls._properties.items(): |
|
# Hide these settings from being seen on the age properties |
|
age_options = dict(options) |
|
bl_options = age_options.setdefault("options", set()) |
|
bl_options.add("HIDDEN") |
|
|
|
# Now do the majick |
|
setattr(PlasmaAge, name, prop(**age_options)) |
|
|
|
def _sanity_check_run_plasma(self): |
|
if not bpy.app.binary_path_python: |
|
raise exporter.PlasmaLaunchError("Can't Launch Plasma: No Python executable available") |
|
if self.version == "pvMoul": |
|
if not self.ki: |
|
raise exporter.PlasmaLaunchError("Can't Launch Plasma: Player KI not set") |
|
else: |
|
if not self.player: |
|
raise exporter.PlasmaLaunchError("Can't Launch Plasma: Player Name not set") |
|
|
|
def _run_plasma(self, context): |
|
path = Path(self.filepath) |
|
client_dir = path.parent.parent |
|
|
|
# It would be nice to launch URU right here. Unfortunately, for single player URUs, we will |
|
# need to actually wait for the whole rigamaroll to finish. Therefore, we need to kick |
|
# open a separate python exe to launch URU and wait. |
|
args = [bpy.app.binary_path_python, plasma_launcher.__file__, |
|
str(client_dir), path.stem, self.version] |
|
if self.version == "pvMoul": |
|
if self.serverini: |
|
args.append("--serverini") |
|
args.append(self.serverini) |
|
args.append(str(self.ki)) |
|
else: |
|
args.append(self.player) |
|
|
|
with exporter.ExportVerboseLogger() as log: |
|
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
|
cwd=str(client_dir), universal_newlines=True) |
|
while True: |
|
line = proc.stdout.readline().strip() |
|
if line == "DIE": |
|
raise exporter.PlasmaLaunchError(proc.stderr.read().strip()) |
|
elif line in {"PLASMA_RUNNING", "DONE"}: |
|
break |
|
elif proc.returncode is not None: |
|
break |
|
elif line: |
|
log.msg(line) |
|
|
|
|
|
class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator): |
|
bl_idname = "export.plasma_loc" |
|
bl_label = "Export Localization" |
|
bl_description = "Export Age localization data" |
|
|
|
filepath = StringProperty(subtype="DIR_PATH") |
|
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) |
|
|
|
version = EnumProperty(name="Version", |
|
description="Plasma version to export this age for", |
|
items=game_versions, |
|
default="pvPots", |
|
options=set()) |
|
|
|
def execute(self, context): |
|
path = Path(self.filepath) |
|
if not self.filepath: |
|
self.report({"ERROR"}, "No file specified") |
|
return {"CANCELLED"} |
|
else: |
|
if not path.exists: |
|
try: |
|
path.mkdir(parents=True) |
|
except OSError: |
|
self.report({"ERROR"}, "Failed to create export directory") |
|
return {"CANCELLED"} |
|
|
|
# Age names cannot be python keywords |
|
age_name = context.scene.world.plasma_age.age_name |
|
if korlib.is_python_keyword(age_name): |
|
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) |
|
return {"CANCELLED"} |
|
|
|
# Bonus Fun: Implement Profile-mode here (later...) |
|
e = exporter.LocalizationConverter(age_name=age_name, path=self.filepath, |
|
version=globals()[self.version]) |
|
try: |
|
e.run() |
|
except exporter.ExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
return {"CANCELLED"} |
|
except exporter.NonfatalExportError as error: |
|
self.report({"WARNING"}, str(error)) |
|
return {"FINISHED"} |
|
else: |
|
return {"FINISHED"} |
|
|
|
|
|
class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator): |
|
bl_idname = "export.plasma_pak" |
|
bl_label = "Export Python Scripts" |
|
bl_description = "Export Age python script package" |
|
|
|
filepath = StringProperty(subtype="FILE_PATH") |
|
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) |
|
|
|
version = EnumProperty(name="Version", |
|
description="Plasma version to export this age for", |
|
items=game_versions, |
|
default="pvPots", |
|
options=set()) |
|
|
|
def draw(self, context): |
|
layout = self.layout |
|
age = context.scene.world.plasma_age |
|
|
|
# The crazy mess we're doing with props on the fly means we have to explicitly draw them :( |
|
row = layout.row() |
|
row.alert = age.python_method == "none" |
|
row.prop(age, "python_method") |
|
layout.prop(self, "version") |
|
row = layout.row() |
|
row.enabled = korlib.ConsoleToggler.is_platform_supported() |
|
row.prop(age, "show_console") |
|
layout.prop(age, "verbose") |
|
|
|
def execute(self, context): |
|
path = Path(self.filepath) |
|
if not self.filepath: |
|
self.report({"ERROR"}, "No file specified") |
|
return {"CANCELLED"} |
|
else: |
|
if not path.exists: |
|
try: |
|
path.mkdir(parents=True) |
|
except OSError: |
|
self.report({"ERROR"}, "Failed to create export directory") |
|
return {"CANCELLED"} |
|
path.touch() |
|
|
|
# Age names cannot be python keywords |
|
age_name = context.scene.world.plasma_age.age_name |
|
if korlib.is_python_keyword(age_name): |
|
self.report({"ERROR"}, "The Age name conflicts with the Python keyword '{}'".format(age_name)) |
|
return {"CANCELLED"} |
|
|
|
# Bonus Fun: Implement Profile-mode here (later...) |
|
e = exporter.PythonPackageExporter(filepath=self.filepath, |
|
version=globals()[self.version]) |
|
try: |
|
e.run() |
|
except exporter.ExportError as error: |
|
self.report({"ERROR"}, str(error)) |
|
return {"CANCELLED"} |
|
except korlib.PythonNotAvailableError as error: |
|
self.report({"ERROR"}, "Python Version {} not found".format(error)) |
|
return {"CANCELLED"} |
|
except exporter.NonfatalExportError as error: |
|
self.report({"WARNING"}, str(error)) |
|
return {"FINISHED"} |
|
else: |
|
return {"FINISHED"} |
|
|
|
def invoke(self, context, event): |
|
if not self.filepath: |
|
bfp = self._get_default_path(context) |
|
self.filepath = str(Path(bfp).with_suffix(".pak")) |
|
context.window_manager.fileselect_add(self) |
|
return {"RUNNING_MODAL"} |
|
|
|
|
|
# Add the export operator to the Export menu :) |
|
def menu_cb(self, context): |
|
if context.scene.render.engine == "PLASMA_GAME": |
|
self.layout.operator_context = "INVOKE_DEFAULT" |
|
op = self.layout.operator(PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)") |
|
op.actions = {"EXPORT"} |
|
op.dat_only = True |
|
self.layout.operator(PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)") |
|
|
|
|
|
def register(): |
|
bpy.types.INFO_MT_file_export.append(menu_cb) |
|
|
|
def unregister(): |
|
bpy.types.INFO_MT_file_export.remove(menu_cb)
|
|
|