Compare commits

..

10 Commits

6 changed files with 215 additions and 177 deletions

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# Japari Bypass
Efficient KF3/DMM game launcher.
## Requirements
Python 3 and pip.
KF3 installed from DMM. DMM itself does not need to be running.
## Instructions
Download the files from this repo. Unzip them to a safe place.
Run `run_KF3.bat`, it will ask you for some information.
It will ask for the location of the KF3 executable. Generally this will be `C:\users\[your name]\KFP2G\けもフレ3.exe`.
Input your DMM email and then password when prompted, and `yes` if you want to use the public VPN to connect to DMM's server. If you choose not to use the public VPN, then you will need to enable your own Japanese VPN while logging in to the game.
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.
## 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

@ -1,22 +1,50 @@
import os import os
import sys import sys
import wmi
import json import json
import hashlib import hashlib
import pathlib
import argparse
import requests import requests
import dmmUpdater
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.add_argument('-g', '--game', type=str, help="DMM code name of the game", default="kfp2g")
parser.add_argument('-u', '--update', help="Check for game update before launching", action='store_true')
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'
if not os.path.exists(config_name):
subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "beautifulsoup4", "PyPasser"])
config = {
"game_id" : args.game,
"file_path" : input('Enter full file path to KF3 .exe: ').strip('\"'),
"dmm_login" : input('DMM Login (email): '),
"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"
}
save_config(config_name, config)
else:
config = load_config(config_name)
print(f'Loaded settings from {config_name}')
config["update_game"] = args.update
import dmmUpdater
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from pypasser import reCaptchaV3 from pypasser import reCaptchaV3
def print_request(url, data : str, headers, cookies_dict):
print("Sending to", url)
vars = {"headers":headers, "cookies":cookies_dict, "data":data}
print(json.dumps(vars, ensure_ascii=False, indent=1))
pass
def get_hash(data): def get_hash(data):
sha_obj = hashlib.sha256() sha_obj = hashlib.sha256()
sha_obj.update(str(data).encode()) sha_obj.update(str(data).encode())
@ -27,16 +55,25 @@ 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_motherboard(): def get_game_root_path(user_path, game_dir, exe_name):
return wmi.WMI().Win32_BaseBoard()[0].SerialNumber """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")
url = "https://accounts.dmm.com/service/login/password" url = "https://accounts.dmm.com/service/login/password"
print_request(url, "", dict(session.headers), session.cookies.get_dict())
result = session.get(url) result = session.get(url)
print(result.status_code, result.text)
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")
@ -57,104 +94,133 @@ def retrieve_auth_keys(login, password, token, captcha, session : requests.Sessi
url = "https://accounts.dmm.com/service/login/password/authenticate" url = "https://accounts.dmm.com/service/login/password/authenticate"
data = f"token={token}&login_id={login}&password={password}&prompt=&device=games-player&recaptchaToken={captcha}" data = f"token={token}&login_id={login}&password={password}&prompt=&device=games-player&recaptchaToken={captcha}"
headers = {"Content-Type": "application/x-www-form-urlencoded"} headers = {"Content-Type": "application/x-www-form-urlencoded"}
print_request(url, data, headers, session.cookies.get_dict())
result = session.post(url, data, headers=headers) result = session.post(url, data, headers=headers)
print(result.status_code, result.text)
result.raise_for_status() result.raise_for_status()
cookies = result.cookies.get_dict() cookies = result.cookies.get_dict()
return cookies["login_secure_id"], cookies["login_session_id"] return cookies["login_secure_id"], cookies["login_session_id"]
except Exception as e: except Exception as e:
print("Failed to log in:", e) print("Failed to log in:", e)
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 = "" 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}
print_request(url, data, headers, cookies) result = requests.post(url, headers=headers, json=data)
result = requests.post(url, cookies=cookies, headers=headers, json=data)
print(result.status_code, result.text)
result.raise_for_status() result.raise_for_status()
data = result.json()["data"] result_json = result.json()
game_version = data["latest_version"] if result_json["result_code"] != 100:
print("Latest version:", game_version) raise Exception(f'{result_json["result_code"]}: {result_json["error"]}')
file_list_url = data["file_list_url"] return True
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 False
def retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy): def retrieve_launch_params(game_id, mac_addr, hdd_serial, motherboard, login_secure, login_session, use_proxy, update_game):
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":"GCL","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:
print_request(url, data, headers, cookies) url = "https://katworks.sytes.net/KF/Api/DMM/update" if use_proxy else "https://apidgp-gameplayer.games.dmm.com/v5/r2/launch/cl"
else:
url = "https://katworks.sytes.net/KF/Api/DMM/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)
print(result.status_code, result.text)
result.raise_for_status() result.raise_for_status()
data = result.json()["data"] result_json = result.json()
return data["execute_args"] 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"]}')
data = result_json["data"]
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(args): def main(config):
if len(args) != 7: #required arguments
print("Usage:", game_id = config["game_id"]
"\tpython dmmBypass.py game_id game_path email password update_game use_proxy", exe_location = config["file_path"]
"\t- game_id: DMM code name of the game", login = urllib.parse.quote_plus(config["dmm_login"])
"\t- game_path: full path to the game .exe. Wrap in \" if there are spaces in the path", password = urllib.parse.quote_plus(config["dmm_password"])
"\t- email, password: dmm credentials", #optional arguments
"\t- update_game: \"true\" to check for game update before launching", update_game = config.get("update_game", False)
"\t- use_proxy: \"true\" to send required request through Katboi VPN. Otherwise use your own VPN", use_proxy = config.get("use_proxy", False)
"\texample: python dmmBypass.py kfp2g \"D:\Games\KFP2G\けもフレ3.exe\" kat@email.com abc123 true", sep="\n") saved_login = config.get("saved_login", None)
return #dmm requires these values
game_id = args[1]
exe_location = args[2]
login = urllib.parse.quote_plus(args[3])
password = urllib.parse.quote_plus(args[4])
update_game = args[5].lower() == "true"
use_proxy = args[6].lower() == "true"
mac_addr = get_mac() mac_addr = get_mac()
hdd_serial = get_hash('') hdd_serial = get_hash('') #DMM sends an empty hash as well
motherboard = get_hash(get_motherboard()) motherboard = get_hash(getnode()) #actual moterboard serial is unknown, but this works
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:
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, 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) print(args)
subprocess.Popen(args, start_new_session=True) subprocess.Popen(args, start_new_session=True)
input("Done. Press enter to exit") input("Done. Press enter to exit (this will close the game)")
args = sys.argv main(config)
main(args)

View File

@ -1,23 +1,10 @@
import os import os
import sys
import hashlib 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
import sys
def progressbar(it, prefix="", size=60, out=sys.stdout): # Python3.3+
"""https://stackoverflow.com/questions/3160699/python-progress-bar"""
count = len(it)
if count == 0: return
def show(j):
x = int(size*j/count)
print("{}[{}{}] {}/{}".format(prefix, "#"*x, "."*(size-x), j, count),
end='\r', file=out, flush=True)
show(0)
for i, item in enumerate(it):
yield item
show(i+1)
print("\n", flush=True, file=out)
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
@ -41,40 +28,69 @@ 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))
for file in progressbar(files_to_download, "Downloading: ", 40): count = len(files_to_download)
url, path = file["url"], file["path"] if count > 0:
os.makedirs(os.path.dirname(path), exist_ok=True) index = 0
urlretrieve(url, path) max_len = 0
def show(j, downloaded, total_size):
nonlocal max_len
x = int(40*j/count)
string = "Downloading: [{}{}] {}/{} ({}/{})".format("#"*x, "."*(40-x), j, count, min(downloaded,total_size), total_size)
max_len = max(len(string), max_len)
print(string.ljust(max_len, ' '), end='\r', file=sys.stdout, flush=True)
retries = 3
show(0, 0, 0)
while index < len(files_to_download):
try:
file = files_to_download[index]
url, path = file["url"], file["path"]
response = requests.get(url, timeout=10, stream=True)
total_size = int(response.headers.get("content-length", 0))
block_size = 1024 * 1024
downloaded = 0
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as out_file:
for data in response.iter_content(block_size):
out_file.write(data)
downloaded += len(data)
show(index + 1, downloaded, total_size)
index += 1
retries = 3
except Exception as e:
print(e, "retrying")
retries -= 1
if retries == 0:
print(f'Retry for file {file["url"]} failed 3 times')
return False
print("\n", flush=True, file=sys.stdout)
# #files_to_delete is unused until fully tested # #files_to_delete is unused until fully tested
# for file in files_to_delete: # for file in files_to_delete:

View File

@ -1,3 +0,0 @@
beautifulsoup4
PyPasser
wmi

View File

@ -1,35 +1,3 @@
@echo off @echo off
chcp 65001 python dmmBypass.py -g kfp2g
setlocal enabledelayedexpansion
set file_name="kfp2g.cfg"
IF NOT EXIST %file_name% (
set /p null="Make sure python is installed. In next step, required packages will be installed. Press Enter to continue"
pip install -r requirements.txt
set /p file_path=Enter full file path to KF3 .exe:
set /p dmm_login=DMM Login:
set /p dmm_password=DMM Password:
set /p confirm="Your login tokens will be sent through Katboi's VPN machine, is that ok? Personal VPN is required if not (yes/no):"
if /i "!confirm!"=="yes" (
set use_proxy=true
) else (
set use_proxy=false
)
echo !file_path!> %file_name%
echo !dmm_login!>> %file_name%
echo !dmm_password!>> %file_name%
echo !use_proxy!>> %file_name%
) ELSE (
< %file_name% (
set /p file_path=
set /p dmm_login=
set /p dmm_password=
set /p use_proxy=
)
echo Loaded settings from %file_name%
)
python dmmBypass.py kfp2g %file_path% %dmm_login% %dmm_password% false %use_proxy%
pause pause

View File

@ -1,35 +1,3 @@
@echo off @echo off
chcp 65001 python dmmBypass.py -g kfp2g -u
setlocal enabledelayedexpansion
set file_name="kfp2g.cfg"
IF NOT EXIST %file_name% (
set /p null="Make sure python is installed. In next step, required packages will be installed. Press Enter to continue"
pip install -r requirements.txt
set /p file_path=Enter full file path to KF3 .exe:
set /p dmm_login=DMM Login:
set /p dmm_password=DMM Password:
set /p confirm="Your login tokens will be sent through Katboi's VPN machine, is that ok? Personal VPN is required if not (yes/no):"
if /i "!confirm!"=="yes" (
set use_proxy=true
) else (
set use_proxy=false
)
echo !file_path!> %file_name%
echo !dmm_login!>> %file_name%
echo !dmm_password!>> %file_name%
echo !use_proxy!>> %file_name%
) ELSE (
< %file_name% (
set /p file_path=
set /p dmm_login=
set /p dmm_password=
set /p use_proxy=
)
echo Loaded settings from %file_name%
)
python dmmBypass.py kfp2g %file_path% %dmm_login% %dmm_password% true %use_proxy%
pause pause