Skip to content

Commit

Permalink
Merge pull request #121 from fa0311/develop-v5
Browse files Browse the repository at this point in the history
v5.5.0
  • Loading branch information
fa0311 authored Feb 21, 2024
2 parents 637c512 + ae06387 commit 2e7b46b
Show file tree
Hide file tree
Showing 23 changed files with 399 additions and 52 deletions.
11 changes: 8 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"python.analysis.typeCheckingMode": "basic",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit",
"source.fixAll": "explicit"
},
"files.exclude": {
"**/__pycache__": true,
Expand All @@ -14,5 +14,10 @@
// "**/data": true,
"**/*.spec": true,
"**/.venv": true
}
}
},
"python.testing.unittestArgs": ["-v", "-p", "test_*.py", "-s", "test"],
"python.testing.cwd": "${workspaceRoot}",
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"discord.enabled": false
}
14 changes: 13 additions & 1 deletion DMMGamePlayerFastLauncher/DMMGamePlayerFastLauncher.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from app import App
from coloredlogs import ColoredFormatter
from component.logger import LoggingHandlerMask, StyleScheme, TkinkerLogger
from launch import GameLauncher, LanchLauncher
from launch import GameLauncher, GameLauncherUac, LanchLauncher
from lib.DGPSessionV2 import DgpSessionV2
from models.setting_data import AppConfig
from static.config import AssetsPathConfig, DataPathConfig, SchtasksConfig, UrlConfig
Expand Down Expand Up @@ -115,5 +115,17 @@ def loder(master: LanchLauncher):
lanch = GameLauncher(loder).create()
lanch.thread(id)
lanch.mainloop()

elif type == "force-user-game":
GameLauncherUac.wait([id, "--type", "kill-game"])
lanch = GameLauncher(loder).create()
lanch.thread(id, force_non_uac=True)
lanch.mainloop()

elif type == "kill-game":
lanch = GameLauncher(loder).create()
lanch.thread(id, kill=True)
lanch.mainloop()

else:
raise Exception("type error")
46 changes: 45 additions & 1 deletion DMMGamePlayerFastLauncher/component/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def create(self):

def enter_event(self, event):
assert self.tooltip is not None
self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 25)
self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 28)

def leave_event(self, event):
assert self.tooltip is not None
Expand Down Expand Up @@ -118,6 +118,50 @@ def call(self, cmd):
return lambda: cmd(self.variable)


class ButtonComponent(CTkFrame):
frame: CTkFrame
text: str
tooltip: Optional[str]
command: Callable[[], None]
state: str

def __init__(
self,
master: Frame,
text: str,
command: Callable[[], None],
tooltip: Optional[str] = None,
) -> None:
super().__init__(master, fg_color="transparent")
self.frame = CTkFrame(self.winfo_toplevel(), fg_color=CTkm.theme["LabelComponent"]["fg_color"])
self.pack(fill=ctk.X, expand=True)
self.text = text
self.tooltip = tooltip
self.command = command

def create(self):
button = CTkButton(self, text=self.text, command=self.command)
button.pack(fill=ctk.X, pady=5)
if self.tooltip is not None:
button.bind("<Enter>", self.enter_event)
button.bind("<Leave>", self.leave_event)
CTkLabel(self.frame, text=self.tooltip, fg_color=CTkm.theme["LabelComponent"]["fg_color"], justify=ctk.LEFT).pack(padx=5, pady=0)

return self

def enter_event(self, event):
assert self.tooltip is not None
self.frame.place(x=self.winfo_rootx() - self.frame.master.winfo_rootx(), y=self.winfo_rooty() - self.frame.master.winfo_rooty() + 35)

def leave_event(self, event):
assert self.tooltip is not None
self.frame.place_forget()

def destroy(self):
self.frame.destroy()
return super().destroy()


class PathComponentBase(EntryComponent):
variable: PathVar

Expand Down
60 changes: 46 additions & 14 deletions DMMGamePlayerFastLauncher/launch.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import json
import logging
import subprocess
import sys
import time
import traceback
from base64 import b64encode
from pathlib import Path
from typing import Callable

import customtkinter as ctk
import i18n
import psutil
from component.component import CTkProgressWindow
from customtkinter import CTk
from lib.DGPSessionWrap import DgpSessionWrap
from lib.process_manager import ProcessManager
from lib.discord import start_rich_presence
from lib.process_manager import ProcessIdManager, ProcessManager
from lib.thread import threading_wrapper
from lib.toast import ErrorWindow
from models.setting_data import AppConfig
Expand All @@ -36,17 +41,17 @@ def create(self):
return self

@threading_wrapper
def thread(self, id: str):
def thread(self, id: str, kill: bool = False, force_non_uac: bool = False):
try:
self.launch(id)
self.launch(id, kill, force_non_uac)
self.quit()
except Exception as e:
if not Env.DEVELOP:
self.iconify()
ErrorWindow(self, str(e), traceback.format_exc(), quit=True).create()
raise

def launch(self, id: str):
def launch(self, id: str, kill: bool = False, force_non_uac: bool = False):
path = DataPathConfig.SHORTCUT.joinpath(id).with_suffix(".json")
with open(path, "r", encoding="utf-8") as f:
data = ShortcutData.from_dict(json.load(f))
Expand All @@ -69,10 +74,6 @@ def launch(self, id: str):
with open(drm_path.absolute(), "w+") as f:
f.write(response["data"]["drm_auth_token"])

if not Env.DEVELOP:
if response["data"]["is_administrator"] and not ProcessManager.admin_check():
raise Exception(i18n.t("app.launch.admin_error"))

game_file = Path(game["detail"]["path"])
game_path = game_file.joinpath(response["data"]["exec_file_name"])

Expand All @@ -87,12 +88,31 @@ def launch(self, id: str):
session.set_config(dgp_config)

dmm_args = response["data"]["execute_args"].split(" ") + data.game_args.get().split(" ")

process = ProcessManager.run([str(game_path.relative_to(game_file))] + dmm_args, cwd=str(game_file))
assert process.stdout is not None

for line in process.stdout:
logging.debug(decode(line))
game_path = str(game_path.relative_to(game_file))
game_full_path = str(game_file.joinpath(game_path))
is_admin = ProcessManager.admin_check()
if response["data"]["is_administrator"] and (not is_admin) and (not force_non_uac):
pid_manager = ProcessIdManager()
process = ProcessManager.admin_run([game_path] + dmm_args, cwd=str(game_file))
game_pid = pid_manager.new_process().search(game_full_path)
if data.rich_presence.get():
start_rich_presence(game_pid, id, response["data"]["title"])
while psutil.pid_exists(game_pid):
time.sleep(1)
else:
process = ProcessManager.run([game_path] + dmm_args, cwd=str(game_file))
if kill:
try:
process.wait(2)
except subprocess.TimeoutExpired:
for child in psutil.Process(process.pid).children(recursive=True):
child.kill()
else:
if data.rich_presence.get():
start_rich_presence(process.pid, id, response["data"]["title"])
assert process.stdout is not None
for line in process.stdout:
logging.debug(decode(line))


class LanchLauncher(CTk):
Expand Down Expand Up @@ -153,6 +173,18 @@ def launch(self, id: str):
session.write()


class GameLauncherUac(CTk):
@staticmethod
def wait(args: list[str]):
if not ProcessManager.admin_check():
pid_manager = ProcessIdManager()
ProcessManager.admin_run([sys.executable, *args])
print(sys.executable)
game_pid = pid_manager.new_process().search(sys.executable)
while psutil.pid_exists(game_pid):
time.sleep(1)


def decode(s: bytes) -> str:
try:
return s.decode("utf-8").strip()
Expand Down
16 changes: 16 additions & 0 deletions DMMGamePlayerFastLauncher/lib/discord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import time

import i18n
from pypresence import Presence
from static.config import DiscordConfig


def start_rich_presence(pid: int, id: str, title: str):
RPC = Presence(DiscordConfig.CLIENT_ID)
RPC.connect()
RPC.update(
state=i18n.t("app.discord.state", name=title),
pid=pid,
start=int(time.time()),
large_image=f"https://media.games.dmm.com/freegame/client/{id}/200.gif",
)
52 changes: 48 additions & 4 deletions DMMGamePlayerFastLauncher/lib/process_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@
from pathlib import Path
from typing import Optional

import psutil
import win32security
from static.config import AssetsPathConfig, DataPathConfig, SchtasksConfig
from static.env import Env


class ProcessManager:
@staticmethod
def admin_run(args: list[str]) -> int:
args = [f'"{arg}"' for arg in args]
logging.info(args)
return ctypes.windll.shell32.ShellExecuteW(None, "runas", args[0], " ".join(args[1:]), None, 1)
def admin_run(args: list[str], cwd: Optional[str] = None) -> int:
file, *args = args
logging.info({"cwd": cwd, "args": args, "file": file})
return ctypes.windll.shell32.ShellExecuteW(None, "runas", file, " ".join([f"{arg}" for arg in args]), cwd, 1)

@staticmethod
def admin_check() -> bool:
Expand All @@ -37,6 +38,49 @@ def run_ps(args: str) -> int:
text = f'powershell -Command "{args}"'
return subprocess.call(text, shell=True)

@staticmethod
def search_process(name: str) -> psutil.Process:
for process in psutil.process_iter():
if process.name() == name:
return process
raise Exception(f"Process not found: {name}")


class ProcessIdManager:
process: list[tuple[int, Optional[str]]]

def __init__(self, _process: Optional[list[tuple[int, Optional[str]]]] = None) -> None:
def wrapper(x: psutil.Process) -> Optional[str]:
try:
return x.exe()
except Exception:
return None

if _process is None:
self.process = [(x.pid, wrapper(x)) for x in psutil.process_iter()]
else:
self.process = _process

def __sub__(self, other: "ProcessIdManager") -> "ProcessIdManager":
process = [x for x in self.process if x not in other.process]
return ProcessIdManager(process)

def __add__(self, other: "ProcessIdManager") -> "ProcessIdManager":
process = list(set(self.process + other.process))
return ProcessIdManager(process)

def __repr__(self) -> str:
return "\n".join([f"{x[0]}: {x[1]}" for x in self.process]) + "\n"

def new_process(self) -> "ProcessIdManager":
return ProcessIdManager() - self

def search(self, name: str) -> int:
process = [x[0] for x in self.process if x[1] == name]
if len(process) != 1:
raise Exception(f"Process not found: {name}")
return process[0]


def get_sid() -> str:
desc = win32security.GetFileSecurity(".", win32security.OWNER_SECURITY_INFORMATION)
Expand Down
2 changes: 1 addition & 1 deletion DMMGamePlayerFastLauncher/models/setting_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

@dataclass
class SettingData(VariableBase):
last_version: StringVar = field(default_factory=lambda: StringVar(value="v5.4.0")) # field(default_factory=lambda: StringVar(value=Env.VERSION))
last_version: StringVar = field(default_factory=lambda: StringVar(value="v0.0.0")) # field(default_factory=lambda: StringVar(value=Env.VERSION))
dmm_game_player_program_folder: PathVar = field(default_factory=lambda: PathVar(value=Env.DEFAULT_DMM_GAME_PLAYER_PROGURAM_FOLDER))
dmm_game_player_data_folder: PathVar = field(default_factory=lambda: PathVar(value=Env.DEFAULT_DMM_GAME_PLAYER_DATA_FOLDER))
proxy_http: StringVar = field(default_factory=StringVar)
Expand Down
1 change: 1 addition & 0 deletions DMMGamePlayerFastLauncher/models/shortcut_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ShortcutData(VariableBase):
game_args: StringVar = field(default_factory=StringVar)
auto_update: BooleanVar = field(default_factory=lambda: BooleanVar(value=True))
game_type: StringVar = field(default_factory=lambda: StringVar(value="GCL"))
rich_presence: BooleanVar = field(default_factory=lambda: BooleanVar(value=True))


@dataclass
Expand Down
4 changes: 4 additions & 0 deletions DMMGamePlayerFastLauncher/static/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ class UrlConfig(Dump):
class SchtasksConfig(Dump):
FILE = "schtasks_v1_{0}_{1}"
NAME = "\\Microsoft\\Windows\\DMMGamePlayerFastLauncher\\{0}"


class DiscordConfig(Dump):
CLIENT_ID = "1209708526889345075"
2 changes: 1 addition & 1 deletion DMMGamePlayerFastLauncher/static/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


class Env(Dump):
VERSION = "v5.4.1"
VERSION = "v5.5.0"
RELEASE_VERSION = requests.get(UrlConfig.RELEASE_API).json().get("tag_name", VERSION)

DEVELOP: bool = os.environ.get("ENV") == "DEVELOP"
Expand Down
Loading

0 comments on commit 2e7b46b

Please sign in to comment.