__init__.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import copy
  2. import datetime
  3. import json
  4. import os
  5. import pathlib
  6. import ssl
  7. import subprocess
  8. import sys
  9. import traceback
  10. import typing
  11. import urllib.parse
  12. import urllib.request
  13. import tooncher.controls
  14. # official api documentation:
  15. # https://github.com/ToontownRewritten/api-doc/blob/master/login.md
  16. # https://github.com/ToontownRewritten/api-doc/blob/master/invasions.md
  17. _LOGIN_API_URL = "https://www.toontownrewritten.com/api/login?format=json"
  18. def start_engine(
  19. engine_path: pathlib.Path, gameserver: str, playcookie: str, **popen_kwargs
  20. ) -> subprocess.Popen:
  21. # without XAUTHORITY:
  22. # > :display:x11display(error): Could not open display ":0.0".
  23. # > :ToonBase: Default graphics pipe is glxGraphicsPipe (OpenGL).
  24. # > :ToonBase(warning): Unable to open 'onscreen' window.
  25. # > Traceback (most recent call last):
  26. # > File "<compiled '__voltorbmain__'>", line 0, in <module>
  27. # > [...]
  28. # > File "<compiled 'direct.vlt8f63e471.ShowBase'>", line 0, in vltf05fd21b
  29. # > Exception: Could not open window.
  30. # optirun sets plenty of env vars
  31. env = copy.copy(os.environ)
  32. env["TTR_GAMESERVER"] = gameserver
  33. env["TTR_PLAYCOOKIE"] = playcookie
  34. engine_path = engine_path.resolve()
  35. if sys.platform == "darwin":
  36. env["DYLD_LIBRARY_PATH"] = str(engine_path.parent.joinpath("Libraries.bundle"))
  37. env["DYLD_FRAMEWORK_PATH"] = str(engine_path.parent.joinpath("Frameworks"))
  38. return subprocess.Popen(
  39. args=[str(engine_path)], cwd=engine_path.parent, env=env, **popen_kwargs,
  40. )
  41. def _api_request(
  42. url: str, params: typing.Optional[dict] = None, validate_ssl_cert: bool = True
  43. ):
  44. resp = urllib.request.urlopen(
  45. url=url,
  46. data=urllib.parse.urlencode(params).encode("ascii") if params else None,
  47. context=None if validate_ssl_cert else ssl._create_unverified_context(),
  48. )
  49. return json.loads(resp.read().decode("ascii"))
  50. class _LoginSuccessful:
  51. def __init__(self, playcookie: str, gameserver: str):
  52. self.playcookie = playcookie
  53. self.gameserver = gameserver
  54. class _LoginDelayed:
  55. def __init__(self, queue_token: str):
  56. self.queue_token = queue_token
  57. def _login(
  58. username: typing.Optional[str] = None,
  59. password: typing.Optional[str] = None,
  60. queue_token: typing.Optional[str] = None,
  61. validate_ssl_cert: bool = True,
  62. ) -> typing.Union[_LoginSuccessful, _LoginDelayed]:
  63. if username is not None and queue_token is None:
  64. assert password is not None
  65. req_params = {
  66. "username": username,
  67. "password": password,
  68. }
  69. elif username is None and queue_token is not None:
  70. req_params = {
  71. "queueToken": queue_token,
  72. }
  73. else:
  74. raise Exception("either specify username or queue token")
  75. resp_data = _api_request(
  76. url=_LOGIN_API_URL, params=req_params, validate_ssl_cert=validate_ssl_cert,
  77. )
  78. if resp_data["success"] == "true":
  79. return _LoginSuccessful(
  80. playcookie=resp_data["cookie"], gameserver=resp_data["gameserver"],
  81. )
  82. if resp_data["success"] == "delayed":
  83. return _LoginDelayed(queue_token=resp_data["queueToken"],)
  84. raise Exception(repr(resp_data))
  85. def launch(
  86. engine_path: pathlib.Path,
  87. username: str,
  88. password: str,
  89. validate_ssl_certs: bool = True,
  90. cpu_limit_percent: typing.Optional[int] = None,
  91. enable_extended_keyboard_controls=False,
  92. extended_keyboard_control_toggle_keysym_name=None,
  93. engine_window_name=None,
  94. ) -> None:
  95. if engine_window_name and not enable_extended_keyboard_controls:
  96. raise Exception("Enable Extended Controls to change engine's window name",)
  97. result = _login(
  98. username=username, password=password, validate_ssl_cert=validate_ssl_certs,
  99. )
  100. if isinstance(result, _LoginDelayed):
  101. result = _login(
  102. queue_token=result.queue_token, validate_ssl_cert=validate_ssl_certs,
  103. )
  104. if not isinstance(result, _LoginSuccessful):
  105. raise Exception("unexpected response: {!r}".format(result))
  106. process = start_engine(
  107. engine_path=engine_path,
  108. gameserver=result.gameserver,
  109. playcookie=result.playcookie,
  110. )
  111. if cpu_limit_percent is not None:
  112. subprocess.Popen(
  113. args=[
  114. "cpulimit",
  115. "--pid",
  116. str(process.pid),
  117. "--limit",
  118. str(cpu_limit_percent),
  119. # '--verbose',
  120. ]
  121. )
  122. if enable_extended_keyboard_controls:
  123. try:
  124. tooncher.controls.ExtendedControls(
  125. primary_engine_pid=process.pid,
  126. primary_engine_window_name=engine_window_name,
  127. toggle_keysym_name=extended_keyboard_control_toggle_keysym_name,
  128. ).run()
  129. except Exception as e:
  130. if isinstance(e, KeyboardInterrupt):
  131. raise e
  132. else:
  133. traceback.print_exc()
  134. if process.poll() is None:
  135. process.wait()