Compare commits

...

4 Commits

Author SHA1 Message Date
51aabe8b53 Updated to new oauth format. Removed pypasser dependency. 2025-05-11 10:40:33 +02:00
c1b7056838 config update 2025-05-11 07:49:14 +02:00
8f2c1e081b Changed proxy url 2025-03-14 09:16:25 +01:00
92e46a56f5 added game type parameter 2025-03-10 23:37:34 +01:00
3 changed files with 103 additions and 73 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

@@ -4,46 +4,55 @@ import json
import hashlib import hashlib
import pathlib 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 from datetime import datetime, timedelta
# Set to False to allow packet sniffing/capture.
# Set separately for dmmUpdater
SSL_VERIFY = True
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): def load_config(game_id):
with open(config_name, "rt", encoding="utf-8") as f: config_name = game_id + ".cfg"
return json.load(f) if not os.path.exists(config_name):
return None
else:
with open(config_name, "rt", encoding="utf-8") as f:
return json.load(f)
def save_config(config_name, config): def save_config(game_id, config):
with open(config_name, 'wt', encoding="utf-8") as f: with open(game_id + '.cfg', 'wt', encoding="utf-8") as f:
json.dump(config, f, indent=1, ensure_ascii=False) json.dump(config, f, indent=1, ensure_ascii=False)
config_name = args.game + '.cfg' config = load_config(args.game)
if not os.path.exists(config_name): if config is None:
subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "beautifulsoup4", "PyPasser"]) subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "beautifulsoup4"])
config = { config = {
"game_id" : args.game, "game_id" : args.game,
"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"
} }
save_config(config_name, config)
save_config(args.game, config)
else: else:
config = load_config(config_name) print(f'Loaded settings from {args.game}.cfg')
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
def get_hash(data): def get_hash(data):
sha_obj = hashlib.sha256() sha_obj = hashlib.sha256()
@@ -69,44 +78,54 @@ def get_game_root_path(user_path, game_dir, exe_name):
else: else:
raise Exception(f"Path to game files is incorrect! Ensure it points to {exe_name} or contains {game_dir}!") 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_oauth_code(login, password, session : requests.Session):
try: try:
print("Retrieving login form") print("Retrieving login url")
url = "https://accounts.dmm.com/service/login/password" url = "https://apidgp-gameplayer.games.dmm.com/v5/auth/login/url"
result = session.get(url) data = {"prompt":"choose"}
result = session.post(url, json=data, verify=SSL_VERIFY)
result.raise_for_status()
url = result.json()["data"]["url"]
encoded_url = urllib.parse.quote_plus(url.replace("https://accounts.dmm.com/service/oauth/select/=/path=", "").replace("oauth/select/", "oauth/").replace("accounts?prompt=choose", "accounts"))
print("Retrieving login form token")
result = session.get(url, verify=SSL_VERIFY)
result.raise_for_status() result.raise_for_status()
page = BeautifulSoup(result.content, 'html.parser') page = BeautifulSoup(result.content, 'html.parser')
token = page.find('input', attrs={"name":"token"}).get("value") token = page.find('input', attrs={"name":"token"}).get("value")
return token
print("Retrieving oauth link")
url = "https://accounts.dmm.com/service/oauth/authenticate"
headers = {"Content-Type": "application/x-www-form-urlencoded",
"check_done_login":"true"}
data = f"token={token}&login_id={login}&password={password}&use_auto_login=1&path={encoded_url}&recaptchaToken="
result = session.post(url, data, headers=headers, verify=SSL_VERIFY)
result.raise_for_status()
page = BeautifulSoup(result.content, 'html.parser')
final_url = page.find('input', attrs={"id":"ga-param-service-url"}).get("value")
print("Retrieving oauth token")
result = session.get(final_url, allow_redirects=False, verify=SSL_VERIFY)
redirect_url = result.next.url
oauth_token = redirect_url.split("=")[-1]
return oauth_token
except Exception as e: except Exception as e:
print("Failed to retrieve login form:", e) print("Failed to retrieve login form:", e)
def retrieve_captcha_token(): def retrieve_access_token(oauth_token, session: requests.Session):
try: url = "https://apidgp-gameplayer.games.dmm.com/v5/auth/accesstoken/issue"
captcha = reCaptchaV3("https://www.google.com/recaptcha/enterprise/anchor?ar=1&k=6LfZLQEVAAAAAC-8pKwFNuzVoJW4tfUCghBX_7ZE&co=aHR0cHM6Ly9hY2NvdW50cy5kbW0uY29tOjQ0Mw..&hl=ja&v=1Bq_oiMBd4XPUhKDwr0YL1Js&size=invisible") result = session.post(url, json={"code":oauth_token}, verify=SSL_VERIFY)
return captcha result.raise_for_status()
except Exception as e: data = result.json()["data"]
print("Failed to solve captcha:", e) return data["access_token"], data["expires_in_seconds"]
def retrieve_auth_keys(login, password, token, captcha, session : requests.Session):
try:
print("Logging in")
url = "https://accounts.dmm.com/service/login/password/authenticate"
data = f"token={token}&login_id={login}&password={password}&prompt=&device=games-player&recaptchaToken={captcha}"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
result = session.post(url, data, headers=headers)
result.raise_for_status()
cookies = result.cookies.get_dict()
return cookies["login_secure_id"], cookies["login_session_id"]
except Exception as e:
print("Failed to log in:", e)
return None, None
def agree_to_game_terms(game_id, use_proxy): def agree_to_game_terms(game_id, use_proxy):
try: try:
print("Accepting updated game terms of use") print("Accepting updated game terms of use")
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"}
url = "" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/agreement/confirm/client" url = "https://katworks.sytes.net/proxy/agreement" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/agreement/confirm/client"
data = {"product_id":game_id,"is_notification":False,"is_myapp":False} data = {"product_id":game_id,"is_notification":False,"is_myapp":False}
result = requests.post(url, headers=headers, json=data) result = requests.post(url, headers=headers, json=data)
result.raise_for_status() result.raise_for_status()
@@ -118,22 +137,25 @@ def agree_to_game_terms(game_id, use_proxy):
print("Failed to accept terms of use:", e) print("Failed to accept terms of use:", e)
return False return False
def retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy, update_game): def retrieve_launch_params(game_id, game_type, mac_addr, hdd_serial, motherboard, access_token, session: requests.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",
cookies = {"login_secure_id":login_secure, "login_session_id":login_session} "Client-App": "DMMGamePlayer5",
"Client-version": "5.3.12",
"Content-Type": "application/json",
"actauth":access_token}
if update_game: if update_game:
url = "https://katworks.sytes.net/KF/Api/DMM/update" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/launch/cl" url = "https://katworks.sytes.net/proxy/launchAndUpdate" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/launch/cl"
else: else:
url = "https://katworks.sytes.net/KF/Api/DMM/launch" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/launch/cl" 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 = session.post(url, headers=headers, json=data, verify=SSL_VERIFY)
result.raise_for_status() result.raise_for_status()
result_json = result.json() result_json = result.json()
if result_json["result_code"] == 308: if result_json["result_code"] == 308:
if agree_to_game_terms(game_id, use_proxy): if agree_to_game_terms(game_id, use_proxy):
result = requests.post(url, cookies=cookies, headers=headers, json=data) result = session.post(url, headers=headers, json=data, verify=SSL_VERIFY)
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:
@@ -145,11 +167,18 @@ def retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_sec
data = result_json["data"] data = result_json["data"]
return data return data
except Exception as e: except Exception as e:
print("Failed to retrieve launch arguments:", e) if str(e).startswith("203:"):
config = load_config(game_id)
config["saved_login"] = None
save_config(game_id, config)
print("DMM session has expired. Saved login data cleared. Please run the program again.")
else:
print("Failed to retrieve launch arguments:", e)
def main(config): def main(config):
#required arguments #required arguments
game_id = config["game_id"] 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"])
@@ -172,30 +201,30 @@ def main(config):
saved_login = None saved_login = None
if saved_login is not None: if saved_login is not None:
login_secure, login_session = saved_login["login_secure"], saved_login["login_session"] access_token = saved_login["access_token"]
print("Loaded login data from previous session") print("Loaded token from previous session")
else: else:
token = retrieve_login_token(session) oauth_code = retrieve_oauth_code(login, password, session)
captcha = retrieve_captcha_token() if oauth_code == None:
if token == None or captcha == None:
return return
login_secure, login_session = retrieve_auth_keys(login, password, token, captcha, session) access_token, expiration_seconds = retrieve_access_token(oauth_code, 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 token_expiration = int((current_time + timedelta(seconds=expiration_seconds)).timestamp())
config["saved_login"] = saved_login = {"login_secure" : login_secure, "login_session" : login_session, "expiration": login_expiration} config["saved_login"] = saved_login = {"access_token" : access_token, "expiration": token_expiration}
del(config["update_game"]) del(config["update_game"])
save_config(config_name, config) save_config(game_id, config)
if not use_proxy: input("Enable VPN now and press Enter") if not use_proxy: input("Enable VPN now and press Enter")
#execute_args, file_list_url, file_access_params #execute_args, file_list_url, file_access_params
launch_data : dict = retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy, update_game) launch_data : dict = retrieve_launch_params(game_id, game_type, mac_addr, hdd_serial, motherboard, access_token, session, use_proxy, update_game)
if launch_data == None:
return
execute_args : str = launch_data.get("execute_args", None) execute_args : str = launch_data.get("execute_args", None)
if execute_args == None: if execute_args == None:
return return
@@ -219,8 +248,7 @@ def main(config):
print("Starting game") print("Starting game")
args = [os.path.join(game_root_path, exec_name)] + 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

@@ -6,10 +6,14 @@ import urllib.parse
from urllib.request import urlretrieve from urllib.request import urlretrieve
from pathlib import Path from pathlib import Path
# Set to False to allow packet sniffing/capture.
# Set separately for dmmBypass
SSL_VERIFY = True
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
print("Retrieving file list from " + url) print("Retrieving file list from " + url)
result = requests.get(url) result = requests.get(url, verify=SSL_VERIFY)
result.raise_for_status() result.raise_for_status()
data = result.json()["data"] data = result.json()["data"]
return data["domain"], data["file_list"] return data["domain"], data["file_list"]
@@ -71,7 +75,7 @@ def update_game(game_path, files_url, files_param):
file = files_to_download[index] file = files_to_download[index]
url, path = file["url"], file["path"] url, path = file["url"], file["path"]
response = requests.get(url, timeout=10, stream=True) response = requests.get(url, timeout=10, stream=True, verify=SSL_VERIFY)
total_size = int(response.headers.get("content-length", 0)) total_size = int(response.headers.get("content-length", 0))
block_size = 1024 * 1024 block_size = 1024 * 1024
downloaded = 0 downloaded = 0