|
|
|
# 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 contextlib import contextmanager
|
|
|
|
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)
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def open_vault_stream(vault_path, fm):
|
|
|
|
stream_type = globals().get("hsWindowsStream", "hsFileStream")
|
|
|
|
write("DBG: Opened '{}' stream with provider '{}'", vault_path, stream_type.__name__)
|
|
|
|
|
|
|
|
encrypted = plEncryptedStream.IsFileEncrypted(vault_path)
|
|
|
|
encryption_type = plEncryptedStream.kEncAuto if fm in {fmRead, fmReadWrite} else plEncryptedStream.kEncXtea
|
|
|
|
|
|
|
|
backing_stream = stream_type().open(vault_path, fm)
|
|
|
|
if encrypted:
|
|
|
|
enc_stream = plEncryptedStream().open(backing_stream, fm, encryption_type)
|
|
|
|
output_stream = enc_stream
|
|
|
|
else:
|
|
|
|
output_stream = backing_stream
|
|
|
|
try:
|
|
|
|
yield output_stream
|
|
|
|
finally:
|
|
|
|
if encrypted:
|
|
|
|
enc_stream.close()
|
|
|
|
backing_stream.flush()
|
|
|
|
backing_stream.close()
|
|
|
|
|
|
|
|
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()
|
|
|
|
with open_vault_stream(vault_dat, fmRead) as stream:
|
|
|
|
store.Import(stream)
|
|
|
|
|
|
|
|
# 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...")
|
|
|
|
|
|
|
|
with open_vault_stream(vault_path, fmCreate) as stream:
|
|
|
|
vault_store.Export(stream)
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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":
|
|
|
|
vault_store = plVaultStore()
|
|
|
|
with open_vault_stream(vault_path, fmRead) as stream:
|
|
|
|
vault_store.Import(stream)
|
|
|
|
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...")
|
|
|
|
with open_vault_stream(vault_path, fmCreate) as stream:
|
|
|
|
vault_store.Export(stream)
|
|
|
|
|
|
|
|
# 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())
|