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.
 
 
 
 
 
 

222 lines
8.4 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 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())