Browse Source

Close #159.

pull/162/head
Adam Johnson 5 years ago
parent
commit
c3157fc256
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 36
      korman/addon_prefs.py
  2. 1
      korman/exporter/__init__.py
  3. 10
      korman/exporter/explosions.py
  4. 4
      korman/exporter/logger.py
  5. 114
      korman/operators/op_export.py
  6. 201
      korman/plasma_launcher.py
  7. 54
      korman/ui/ui_world.py

36
korman/addon_prefs.py

@ -33,6 +33,23 @@ class PlasmaGame(bpy.types.PropertyGroup):
items=game_versions,
options=set())
player = StringProperty(name="Player",
description="Name of the player to use when launching the game",
options=set())
ki = IntProperty(name="KI",
description="KI Number of the player to use when launching the game",
options=set(), min=0)
serverini = StringProperty(name="Server INI",
description="Name of the server configuation to use when launching the game",
options=set())
@property
def can_launch(self):
if self.version == "pvMoul":
return self.is_property_set("ki") and self.ki
else:
return self.is_property_set("player") and bool(self.player.strip())
class KormanAddonPreferences(bpy.types.AddonPreferences):
bl_idname = __package__
@ -120,14 +137,23 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
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")
row.prop(active_game, "path")
op = row.operator("world.plasma_game_add", icon="FILE_FOLDER", text="")
op.filepath = active_game.path
op.game_index = active_game_index
box.prop(active_game, "version")
box.separator()
box.separator()
if active_game.version == "pvMoul":
box.alert = not active_game.is_property_set("ki")
box.prop(active_game, "ki", slider=False)
box.alert = False
box.prop(active_game, "serverini")
else:
box.alert = not active_game.is_property_set("player")
box.prop(active_game, "player")
# Python Installs
assert self.python_validated

1
korman/exporter/__init__.py

@ -19,5 +19,6 @@ from PyHSPlasma import *
from .convert import *
from .explosions import *
from .locman import *
from .logger import *
from .python import *
from . import utils

10
korman/exporter/explosions.py

@ -42,6 +42,16 @@ class ExportAssertionError(ExportError):
super(ExportError, self).__init__("Assertion failed")
class PlasmaLaunchError(ExportError):
def __init__(self, *args, **kwargs):
if not args:
super(Exception, self).__init__("Failed to start Plasma")
elif len(args) > 1:
super(Exception, self).__init__(args[0].format(*args[1:], **kwargs))
else:
super(Exception, self).__init__(args[0])
class TooManyUVChannelsError(ExportError):
def __init__(self, obj, mat, numUVTexs, maxUVTexCount=8):
msg = "There are too many UV Textures on the material '{}' associated with object '{}'. You can have at most {} (there are {})".format(

4
korman/exporter/logger.py

@ -34,8 +34,7 @@ class _ExportLogger:
self._time_start_overall = 0
def __enter__(self):
assert self._age_path is not None
if self._age_path is not None:
# Make the log file name from the age file path -- this ensures we're not trying to write
# the log file to the same directory Blender.exe is in, which might be a permission error
my_path = self._age_path.with_name("{}_export".format(self._age_path.stem)).with_suffix(".log")
@ -45,6 +44,7 @@ class _ExportLogger:
def __exit__(self, type, value, traceback):
if value is not None:
ConsoleToggler().keep_console = not isinstance(value, NonfatalExportError)
if self._file is not None:
self._file.close()
return False

114
korman/operators/op_export.py

@ -19,11 +19,12 @@ 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
from .. import korlib, plasma_launcher
from ..properties.prop_world import PlasmaAge
class ExportOperator:
@ -54,10 +55,6 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
# 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 = {
"profile_export": (BoolProperty, {"name": "Profile",
"description": "Profiles the exporter using cProfile",
"default": False}),
"verbose": (BoolProperty, {"name": "Display Verbose Log",
"description": "Shows the verbose export log in the console",
"default": False}),
@ -119,19 +116,49 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
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")
layout.prop(age, "profile_export")
def __getattr__(self, attr):
if attr in self._properties:
@ -146,6 +173,10 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
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"
@ -168,12 +199,21 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
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 self.profile_export:
if "PROFILE" in self.actions:
profile_path = str(path.with_name("{}_cProfile".format(ageName)))
profile = cProfile.runctx("e.run()", globals(), locals(), profile_path)
else:
@ -183,18 +223,25 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
return {"CANCELLED"}
except exporter.NonfatalExportError as error:
self.report({"ERROR"}, str(error))
return {"FINISHED"}
else:
if self.profile_export:
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()
return {"FINISHED"}
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()
@ -217,11 +264,52 @@ class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator):
# 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"
bl_description = "Export Age localization data"
filepath = StringProperty(subtype="DIR_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'})
@ -268,8 +356,8 @@ class PlasmaLocalizationExportOperator(ExportOperator, bpy.types.Operator):
class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator):
bl_idname = "export.plasma_pak"
bl_label = "Package Scripts"
bl_description = "Package Age Python scripts"
bl_label = "Export Python Scripts"
bl_description = "Export Age python script package"
filepath = StringProperty(subtype="FILE_PATH")
filter_glob = StringProperty(default="*.pak", options={'HIDDEN'})

201
korman/plasma_launcher.py

@ -0,0 +1,201 @@
# 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 argparse
from pathlib import Path
from PyHSPlasma import *
import shutil
import subprocess
import sys
import time
import traceback
main_parser = argparse.ArgumentParser(description="Korman Plasma Launcher")
main_parser.add_argument("cwd", type=Path, help="Working directory of the client")
main_parser.add_argument("age", type=str, help="Name of the age to launch into")
sub_parsers = main_parser.add_subparsers(title="Plasma Version", dest="version",)
moul_parser = sub_parsers.add_parser("pvMoul")
moul_parser.add_argument("ki", type=int, help="KI Number of the desired player")
moul_parser.add_argument("--serverini", type=str, default="server.ini")
sp_parser = sub_parsers.add_parser("pvPots", aliases=["pvPrime"])
sp_parser.add_argument("player", type=str, help="Name of the desired player")
autolink_chron_name = "OfflineKIAutoLink"
if sys.platform == "win32":
client_executables = {
"pvMoul": "plClient.exe",
"pvPots": "UruExplorer.exe"
}
else:
client_executables = {
"pvMoul": "plClient",
"pvPots": "UruExplorer"
}
def die(*args, **kwargs):
assert args
if len(args) == 1 and not kwargs:
sys.stderr.write(args[0])
else:
sys.stderr.write(args[0].format(*args[1:], **kwargs))
sys.stdout.write("DIE\n")
sys.exit(1)
def write(*args, **kwargs):
assert args
if len(args) == 1 and not kwargs:
sys.stdout.write(args[0])
else:
sys.stdout.write(args[0].format(*args[1:], **kwargs))
sys.stdout.write("\n")
# And this is why we aren't using print()...
sys.stdout.flush()
def backup_vault_dat(path):
backup_path = path.with_suffix(".dat.korman_backup")
shutil.copy2(str(path), str(backup_path))
write("DBG: Copied vault backup: {}", backup_path)
def set_link_chronicle(store, new_value, cond_value=None):
chron_folder = next((i for i in store.getChildren(store.firstNodeID)
if getattr(i, "folderType", None) == plVault.kChronicleFolder), None)
if chron_folder is None:
die("Could not locate vault chronicle folder.")
autolink_chron = next((i for i in store.getChildren(chron_folder.nodeID)
if getattr(i, "entryName", None) == autolink_chron_name), None)
if autolink_chron is None:
write("DBG: Creating AutoLink chronicle...")
autolink_chron = plVaultChronicleNode()
autolink_chron.entryName = autolink_chron_name
previous_value = ""
store.addRef(chron_folder.nodeID, store.lastNodeID + 1)
else:
write("DBG: Found AutoLink chronicle...")
previous_value = autolink_chron.entryValue
# Have to submit the changed node to the store
if cond_value is None or previous_value == cond_value:
write("DBG: AutoLink = '{}' (previously: '{}')", new_value, previous_value)
autolink_chron.entryValue = new_value
store.addNode(autolink_chron)
else:
write("DBG: ***Not*** changing chronicle! AutoLink = '{}' (expected: '{}')", previous_value, cond_value)
return previous_value
def find_player_vault(cwd, name):
sav_dir = cwd.joinpath("sav")
if not sav_dir.is_dir():
die("Could not locate sav directory.")
for i in sav_dir.iterdir():
if not i.is_dir():
continue
current_dir = i.joinpath("current")
if not current_dir.is_dir():
continue
vault_dat = current_dir.joinpath("vault.dat")
if not vault_dat.is_file():
continue
store = plVaultStore()
store.Import(str(vault_dat))
# First node is the Player node...
playerNode = store[store.firstNodeID]
if playerNode.playerName == name:
write("DBG: Vault found: {}", vault_dat)
return vault_dat, store
die("Could not locate the requested player vault.")
def main():
print("DBG: alive")
args = main_parser.parse_args()
executable = args.cwd.joinpath(client_executables[args.version])
if not executable.is_file():
die("Failed to locate client executable.")
# Have to find and mod the single player vault...
if args.version == "pvPots":
vault_path, vault_store = find_player_vault(args.cwd, args.player)
backup_vault_dat(vault_path)
vault_prev_autolink = set_link_chronicle(vault_store, args.age)
write("DBG: Saving vault...")
vault_store.Export(str(vault_path))
# Update init file for this schtuff...
init_path = args.cwd.joinpath("init", "net_age.fni")
with plEncryptedStream().open(str(init_path), fmWrite, plEncryptedStream.kEncXtea) as ini:
ini.writeLine("# This file was automatically generated by Korman.")
ini.writeLine("Nav.PageInHoldList GlobalAnimations")
ini.writeLine("Net.SetPlayer {}".format(vault_store.firstNodeID))
ini.writeLine("Net.SetPlayerByName \"{}\"".format(args.player))
# BUT WHY??? You ask...
# Because, sayeth Hoikas, if this command is not executed, you will remain ensconsed
# in the black void of the Link... forever... Sadly, it accepts no arguments and determines
# whether to link to AvatarCustomization, Cleft, Demo (whee!), or Personal all by itself.
ini.writeLine("Net.JoinDefaultAge")
# When URU runs, the player may change the vault. Remove any temptation to play with
# the stale vault...
del vault_store
# Sigh...
time.sleep(1.0)
# EXE args
plasma_args = [str(executable), "-iinit", "To_Dni"]
else:
write("DBG: Using a superior client :) :) :)")
plasma_args = [str(executable), "-LocalData", "-SkipLoginDialog", "-ServerIni={}".format(args.serverini),
"-PlayerId={}".format(args.ki), "-Age={}".format(args.age)]
try:
proc = subprocess.Popen(plasma_args, cwd=str(args.cwd), shell=True)
# signal everything is a-ok -- causes blender to detach
write("PLASMA_RUNNING")
# Wait for things to finish
proc.wait()
finally:
# Restore sp vault, if needed.
if args.version == "pvPots":
# Path of the Shell seems to have some sort of weird racing with the vault.dat around
# shutdown. This delay helps somewhat in that regard.
time.sleep(1.0)
vault_store = plVaultStore()
vault_store.Import(str(vault_path))
new_prev_autolink = set_link_chronicle(vault_store, vault_prev_autolink, args.age)
if new_prev_autolink != args.age:
write("DBG: ***Not*** resaving the vault!")
else:
write("DBG: Resaving vault...")
vault_store.Export(str(vault_path))
# All good!
write("DONE")
sys.exit(0)
if __name__ == "__main__":
try:
main()
except Exception as e:
if isinstance(e, SystemExit):
raise
else:
die(traceback.format_exc())

54
korman/ui/ui_world.py

@ -45,7 +45,7 @@ class PlasmaGameHelper:
if active_game is None:
return ""
age_name = bpy.context.world.plasma_age.age_name
return str((Path(active_game.path) / dirname / age_name).with_suffix(ext))
return Path(active_game.path, dirname, age_name).with_suffix(ext)
@property
def legal_game(self):
@ -55,6 +55,7 @@ class PlasmaGameHelper:
class PlasmaGameExportMenu(PlasmaGameHelper, bpy.types.Menu):
bl_label = "Plasma Export Menu"
bl_description = "Additional export methods"
def draw(self, context):
layout = self.layout
@ -62,7 +63,7 @@ class PlasmaGameExportMenu(PlasmaGameHelper, bpy.types.Menu):
active_game = self.active_game
legal_game = self.legal_game
# Localization
# Export Localization
row = layout.row()
row.operator_context = "EXEC_DEFAULT"
row.enabled = legal_game
@ -71,15 +72,41 @@ class PlasmaGameExportMenu(PlasmaGameHelper, bpy.types.Menu):
op.filepath = active_game.path
op.version = active_game.version
# Python
# Export Python
row = layout.row()
row.operator_context = "EXEC_DEFAULT"
row.enabled = legal_game and active_game.version != "pvMoul"
op = row.operator("export.plasma_pak", icon="FILE_SCRIPT")
op.filepath = self.format_path("Python", ".pak")
op.filepath = str(self.format_path("Python", ".pak"))
if active_game is not None:
op.version = active_game.version
# Launch Age
row = layout.row()
row.operator_context = "EXEC_DEFAULT"
age_path = self.format_path()
row.active = legal_game and active_game.can_launch and age_path.exists()
op = row.operator("export.plasma_age", icon="RENDER_ANIMATION", text="Launch Age")
if active_game is not None:
op.actions = {"LAUNCH"}
op.dat_only = False
op.filepath = str(age_path)
op.version = active_game.version
op.player = active_game.player
op.ki = active_game.ki
op.serverini = active_game.serverini
# Package Age
row = layout.row()
row.operator_context = "INVOKE_DEFAULT"
row.enabled = legal_game
op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age")
if active_game is not None:
op.actions = {"EXPORT"}
op.dat_only = False
op.filepath = "{}.zip".format(age_name)
op.version = active_game.version
class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
bl_label = "Plasma Games"
@ -106,22 +133,29 @@ class PlasmaGamePanel(AgeButtonsPanel, PlasmaGameHelper, bpy.types.Panel):
row.enabled = legal_game
op = row.operator("export.plasma_age", icon="EXPORT")
if active_game is not None:
op.actions = {"EXPORT"}
op.dat_only = False
op.filepath = self.format_path()
op.filepath = str(self.format_path())
op.version = active_game.version
# Package Age
# Test Age (exports and tests the age)
row = row.row(align=True)
row.operator_context = "EXEC_DEFAULT"
# Sadly, if we nuke this row, the menu is nuked as well...
row.enabled = legal_game
row.operator_context = "INVOKE_DEFAULT"
op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age")
op.dat_only = False
op.filepath = "{}.zip".format(age.age_name)
op = row.operator("export.plasma_age", icon="RENDER_ANIMATION", text="Test Age")
if active_game is not None:
op.actions = {"EXPORT", "LAUNCH"}
op.dat_only = False
op.filepath = str(self.format_path())
op.version = active_game.version
op.player = active_game.player
op.ki = active_game.ki
op.serverini = active_game.serverini
# Special Menu
row = row.row(align=True)
row.enabled = True
row.menu("PlasmaGameExportMenu", icon='DOWNARROW_HLT', text="")

Loading…
Cancel
Save