1
0
Fork 0

Compare commits

...

5 Commits

3 changed files with 129 additions and 74 deletions

View File

@ -2,9 +2,10 @@
Efficient KF3/DMM game launcher. Efficient KF3/DMM game launcher.
## Requirements ## Requirements
Python 3 and pip. - Python 3 and pip.
- DMM account that owns KF3
KF3 installed from DMM. DMM itself does not need to be running. KF3 installed from DMM is recommended but not required.
## Instructions ## Instructions
Download the files from this repo. Unzip them to a safe place. Download the files from this repo. Unzip them to a safe place.
@ -18,6 +19,3 @@ Input your DMM email and then password when prompted, and `yes` if you want to u
If you make a mistake or you want to change login details, edit the `kfp2g.cfg` config file, or delete it to run the configuration again. If you make a mistake or you want to change login details, edit the `kfp2g.cfg` config file, or delete it to run the configuration again.
If KF3 needs to update, use `update_and_run_KF3.bat` and it will install any new updates. If KF3 needs to update, use `update_and_run_KF3.bat` and it will install any new updates.
## Known issues
Retrieving launch arguments - error 308: Every once in a while, DMM games may have updated terms of service that need to be accepted. This can only be done via the official DMM app.

View File

@ -2,17 +2,27 @@ import os
import sys import sys
import json import json
import hashlib import hashlib
import pathlib
import argparse import argparse
import requests
import subprocess import subprocess
import urllib.parse import urllib.parse
from uuid import getnode from uuid import getnode
from datetime import datetime, timedelta
parser = argparse.ArgumentParser(description='DMM bypass script') parser = argparse.ArgumentParser(description='DMM bypass script')
parser.add_argument('-g', '--game', type=str, help="DMM code name of the game", default="kfp2g") parser.add_argument('-g', '--game', type=str, help="DMM code name of the game", default="kfp2g")
parser.add_argument('-t', '--type', help="DMM game type (ACL/GCL)", default="GCL")
parser.add_argument('-u', '--update', help="Check for game update before launching", action='store_true') parser.add_argument('-u', '--update', help="Check for game update before launching", action='store_true')
args = parser.parse_args() args = parser.parse_args()
def load_config(config_name):
with open(config_name, "rt", encoding="utf-8") as f:
return json.load(f)
def save_config(config_name, config):
with open(config_name, 'wt', encoding="utf-8") as f:
json.dump(config, f, indent=1, ensure_ascii=False)
config_name = args.game + '.cfg' config_name = args.game + '.cfg'
if not os.path.exists(config_name): if not os.path.exists(config_name):
@ -22,17 +32,18 @@ if not os.path.exists(config_name):
"file_path" : input('Enter full file path to KF3 .exe: ').strip('\"'), "file_path" : input('Enter full file path to KF3 .exe: ').strip('\"'),
"dmm_login" : input('DMM Login (email): '), "dmm_login" : input('DMM Login (email): '),
"dmm_password" : input('DMM Password: '), "dmm_password" : input('DMM Password: '),
"use_proxy" : input('Your login tokens will be sent through my VPN machine.\nIs that okay? (yes/no): ').lower() == "yes" "use_proxy" : input('Your login data will be sent through my VPN.\nIs that okay? (yes/no): ').lower() == "yes"
} }
with open(config_name, 'wt', encoding="utf-8") as f:
json.dump(config, f, indent=1, ensure_ascii=False) save_config(config_name, config)
else: else:
with open(config_name, "rt", encoding="utf-8") as f: config = load_config(config_name)
config = json.load(f)
print(f'Loaded settings from {config_name}') print(f'Loaded settings from {config_name}')
config["update_game"] = args.update config["update_game"] = args.update
config["game_type"] = args.type
import requests
import dmmUpdater import dmmUpdater
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pypasser import reCaptchaV3 from pypasser import reCaptchaV3
@ -47,6 +58,20 @@ def get_mac():
mac = getnode() mac = getnode()
return ':'.join(("%012X" % mac)[i:i+2] for i in range(0, 12, 2)).lower() return ':'.join(("%012X" % mac)[i:i+2] for i in range(0, 12, 2)).lower()
def get_game_root_path(user_path, game_dir, exe_name):
"""Return valid game install directory"""
path = pathlib.Path(user_path)
parts = list(path.parts)
if path.name == exe_name:
return str(path.parent.resolve())
elif game_dir in parts:
#get index of last occurence of game_dir in user path (+1)
dir_idx = len(parts) - parts[::-1].index(game_dir)
parts = parts[:dir_idx]
return str(pathlib.Path(*parts).resolve())
else:
raise Exception(f"Path to game files is incorrect! Ensure it points to {exe_name} or contains {game_dir}!")
def retrieve_login_token(session : requests.Session): def retrieve_login_token(session : requests.Session):
try: try:
print("Retrieving login form") print("Retrieving login form")
@ -80,89 +105,125 @@ def retrieve_auth_keys(login, password, token, captcha, session : requests.Sessi
print("Failed to log in:", e) print("Failed to log in:", e)
return None, None return None, None
def retrieve_update_params(game_id, login_secure, login_session, use_proxy): def agree_to_game_terms(game_id, use_proxy):
try: try:
print("Retrieving update file list") print("Accepting updated game terms of use")
data = {"product_id":game_id,"game_type":"GCL","game_os":"win"}
headers = {"User-Agent": "DMMGamePlayer5-Win/5.3.12 Electron/32.1.0", "Client-App": "DMMGamePlayer5", "Client-version": "5.3.12", "Content-Type": "application/json"} headers = {"User-Agent": "DMMGamePlayer5-Win/5.3.12 Electron/32.1.0", "Client-App": "DMMGamePlayer5", "Client-version": "5.3.12", "Content-Type": "application/json"}
cookies = {"login_secure_id":login_secure, "login_session_id":login_session} url = "https://katworks.sytes.net/proxy/agreement" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/agreement/confirm/client"
url = "https://katworks.sytes.net/KF/Api/DMM/filelist" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/filelist/cl" data = {"product_id":game_id,"is_notification":False,"is_myapp":False}
result = requests.post(url, cookies=cookies, headers=headers, json=data) result = requests.post(url, headers=headers, json=data)
result.raise_for_status() result.raise_for_status()
result_json = result.json() result_json = result.json()
if result_json["result_code"] != 100: if result_json["result_code"] != 100:
raise Exception(f'{result_json["result_code"]}: {result_json["error"]}') raise Exception(f'{result_json["result_code"]}: {result_json["error"]}')
data = result_json["data"] return True
game_version = data["latest_version"]
print("Latest version:", game_version)
file_list_url = data["file_list_url"]
file_list_params = data["sign"]
file_list_params = "?" + file_list_params.replace(";", "&").replace("CloudFront-", "")
return file_list_url, file_list_params
except Exception as e: except Exception as e:
print("Failed to retrieve update file list:", e) print("Failed to accept terms of use:", e)
return None, None return False
def retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy): def retrieve_launch_params(game_id, game_type, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy = False, update_game = False):
try: try:
print("Retrieving launch arguments") print("Retrieving launch arguments")
data = {"product_id":game_id,"game_type":"GCL","game_os":"win","launch_type":"LIB","mac_address":mac_addr,"hdd_serial":hdd_serial,"motherboard":motherboard,"user_os":"win"} data = {"product_id":game_id,"game_type":game_type,"game_os":"win","launch_type":"LIB","mac_address":mac_addr,"hdd_serial":hdd_serial,"motherboard":motherboard,"user_os":"win"}
headers = {"User-Agent": "DMMGamePlayer5-Win/5.3.12 Electron/32.1.0", "Client-App": "DMMGamePlayer5", "Client-version": "5.3.12", "Content-Type": "application/json"} headers = {"User-Agent": "DMMGamePlayer5-Win/5.3.12 Electron/32.1.0", "Client-App": "DMMGamePlayer5", "Client-version": "5.3.12", "Content-Type": "application/json"}
cookies = {"login_secure_id":login_secure, "login_session_id":login_session} cookies = {"login_secure_id":login_secure, "login_session_id":login_session}
url = "https://katworks.sytes.net/KF/Api/DMM/launch" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/launch/cl" if update_game:
url = "https://katworks.sytes.net/proxy/launchAndUpdate" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/launch/cl"
else:
url = "https://katworks.sytes.net/proxy/launch" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/launch/cl"
result = requests.post(url, cookies=cookies, headers=headers, json=data) result = requests.post(url, cookies=cookies, headers=headers, json=data)
result.raise_for_status() result.raise_for_status()
result_json = result.json() result_json = result.json()
if result_json["result_code"] != 100: if result_json["result_code"] == 308:
if agree_to_game_terms(game_id, use_proxy):
result = requests.post(url, cookies=cookies, headers=headers, json=data)
result.raise_for_status()
result_json = result.json()
if result_json["result_code"] != 100:
raise Exception(f'{result_json["result_code"]}: {result_json["error"]}')
else:
raise Exception("Failed to agree to updated game terms of use. Use the DMM app to confirm.")
elif result_json["result_code"] != 100:
raise Exception(f'{result_json["result_code"]}: {result_json["error"]}') raise Exception(f'{result_json["result_code"]}: {result_json["error"]}')
data = result_json["data"] data = result_json["data"]
return data["execute_args"] return data
except Exception as e: except Exception as e:
print("Failed to retrieve launch arguments:", e) print("Failed to retrieve launch arguments:", e)
def main(config): def main(config):
game_id = config["game_id"] #required arguments
game_id = config["game_id"]
game_type = config["game_type"]
exe_location = config["file_path"] exe_location = config["file_path"]
login = urllib.parse.quote_plus(config["dmm_login"]) login = urllib.parse.quote_plus(config["dmm_login"])
password = urllib.parse.quote_plus(config["dmm_password"]) password = urllib.parse.quote_plus(config["dmm_password"])
update_game = config["update_game"] if "update_game" in config else False #optional arguments
use_proxy = config["use_proxy"] if "use_proxy" in config else False update_game = config.get("update_game", False)
use_proxy = config.get("use_proxy", False)
saved_login = config.get("saved_login", None)
#dmm requires these values
mac_addr = get_mac() mac_addr = get_mac()
hdd_serial = get_hash('') hdd_serial = get_hash('') #DMM sends an empty hash as well
#actual moterboard serial is unknown, but this works motherboard = get_hash(getnode()) #actual moterboard serial is unknown, but this works
motherboard = get_hash(getnode())
current_time = datetime.now()
with requests.Session() as session: with requests.Session() as session:
token = retrieve_login_token(session) if saved_login is not None:
captcha = retrieve_captcha_token() login_expiration = datetime.fromtimestamp(saved_login["expiration"])
if login_expiration < current_time:
print("Login data has expired")
saved_login = None
if token == None or captcha == None: if saved_login is not None:
return login_secure, login_session = saved_login["login_secure"], saved_login["login_session"]
print("Loaded login data from previous session")
else:
token = retrieve_login_token(session)
captcha = retrieve_captcha_token()
login_secure, login_session = retrieve_auth_keys(login, password, token, captcha, session) if token == None or captcha == None:
if login_secure == None or login_session == None: return
return
login_secure, login_session = retrieve_auth_keys(login, password, token, captcha, session)
if login_secure == None or login_session == None:
return
login_expiration = int((current_time + timedelta(days=364)).timestamp()) #expire in less than a year
config["saved_login"] = saved_login = {"login_secure" : login_secure, "login_session" : login_session, "expiration": login_expiration}
del(config["update_game"])
save_config(config_name, config)
if not use_proxy: input("Enable VPN now and press Enter") if not use_proxy: input("Enable VPN now and press Enter")
if update_game: #execute_args, file_list_url, file_access_params
file_list_url, file_access_params = retrieve_update_params(game_id, login_secure, login_session, use_proxy) launch_data : dict = retrieve_launch_params(game_id, game_type, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy, update_game)
if file_list_url == None or file_access_params == None:
return
execute_args = retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy) execute_args : str = launch_data.get("execute_args", None)
if execute_args == None: if execute_args == None:
return return
if update_game:
file_list_url = launch_data.get("file_list_url", None)
file_list_params : str = launch_data.get("sign", None)
if file_list_url == None or file_list_params == None:
return
print("Latest version:", launch_data["latest_version"])
file_list_params = "?" + file_list_params.replace(";", "&").replace("CloudFront-", "")
exec_name = launch_data["exec_file_name"]
install_dir = launch_data["install_dir"]
game_root_path = get_game_root_path(exe_location, install_dir, exec_name)
if not use_proxy: input("Disable VPN now and press Enter") if not use_proxy: input("Disable VPN now and press Enter")
if update_game: if update_game:
dmmUpdater.update_game(os.path.dirname(exe_location), file_list_url, file_access_params) dmmUpdater.update_game(game_root_path, file_list_url, file_list_params)
print("Starting game") print("Starting game")
args = [exe_location] + execute_args.split() args = [os.path.join(game_root_path, exec_name)] + execute_args.split()
print(args)
subprocess.Popen(args, start_new_session=True) subprocess.Popen(args, start_new_session=True)
input("Done. Press enter to exit (this will close the game)") input("Done. Press enter to exit")
main(config) main(config)

View File

@ -4,6 +4,7 @@ import hashlib
import requests import requests
import urllib.parse import urllib.parse
from urllib.request import urlretrieve from urllib.request import urlretrieve
from pathlib import Path
def get_file_list(url): def get_file_list(url):
url = "https://apidgp-gameplayer.games.dmm.com" + url url = "https://apidgp-gameplayer.games.dmm.com" + url
@ -27,33 +28,28 @@ def get_file_hash(file_path):
def update_game(game_path, files_url, files_param): def update_game(game_path, files_url, files_param):
print("Updating game") print("Updating game")
server_url, server_files = get_file_list(files_url) server_url, server_files = get_file_list(files_url)
server_file_dict = {file["local_path"]: file for file in server_files} server_file_dict = {str(Path(game_path, file["local_path"].lstrip('/')).resolve()): file for file in server_files}
local_file_dict = {str(Path(dp, f).resolve()): "" for dp, dn, filenames in os.walk(game_path) for f in filenames}
local_files = [os.path.join(dp, f).replace("\\", "/") for dp, dn, filenames in os.walk(game_path) for f in filenames]
local_file_dict = {"/" + os.path.relpath(r, game_path).replace("\\", "/"): {"abs_path":r, "hash":""} for r in local_files}
files_to_download = [] files_to_download = []
files_to_delete = [] files_to_delete = []
for server_file_key in server_file_dict.keys(): for abs_file_path in server_file_dict.keys():
server_file = server_file_dict[server_file_key] server_file = server_file_dict[abs_file_path]
if server_file_key in local_file_dict: if abs_file_path in local_file_dict:
local_file = local_file_dict[server_file_key]
if server_file["force_delete_flg"]: if server_file["force_delete_flg"]:
files_to_delete.append(local_file["abs_path"]) files_to_delete.append(abs_file_path)
else: else:
local_file["hash"] = get_file_hash(local_file["abs_path"]) local_file_hash = get_file_hash(abs_file_path)
if server_file["check_hash_flg"] and local_file["hash"] == server_file["hash"]: if server_file["check_hash_flg"] and local_file_hash == server_file["hash"]:
continue continue
download_url = urllib.parse.urljoin(server_url, server_file["path"]) + files_param download_url = urllib.parse.urljoin(server_url, server_file["path"]) + files_param
download_path = game_path.replace("\\", "/") + server_file_key files_to_download.append({"url":download_url, "path":abs_file_path})
files_to_download.append({"url":download_url, "path":download_path})
else: else:
download_url = urllib.parse.urljoin(server_url, server_file["path"]) + files_param download_url = urllib.parse.urljoin(server_url, server_file["path"]) + files_param
download_path = game_path.replace("\\", "/") + server_file_key files_to_download.append({"url":download_url, "path":abs_file_path})
files_to_download.append({"url":download_url, "path":download_path})
print("Files to download:", len(files_to_download)) print("Files to download:", len(files_to_download))