大乱斗换人别拼手速!一个小工具,看中谁,点一下就换!
兄弟们,大乱斗最痛苦的瞬间是什么?
不是被分到亚索,而是替补席明明躺着一个“救世主”,刚准备点击——没了。
只能默默感慨一句:卧槽,哥们手速真快!
所以我做了个小工具:ARAM Picker(大乱斗换人助手)。
1
2
功能介绍
- 自动检测英雄联盟客户端
- 自动接受对局(可关闭)
- 自动识别大乱斗选人阶段
- 显示当前英雄、替补席英雄和倒计时
- 点击替补英雄即可发起交换
- 遇到交换冷却时自动等待并重试
- 目标英雄被队友换走后自动取消等待
- 支持可选的自动接受对局
- 进入选人阶段自动置顶窗口
- 英雄名称优先读取客户端中文数据
使用方法
- 启动英雄联盟客户端和本工具
- 进入大乱斗选人阶段
- 在左侧列表点击想交换的英雄
- 等待交换完成,开始快乐游戏
注意事项
本代码没有明显的注入型外挂行为,只通过本机英雄联盟客户端的 LCU 接口读取选人状态,调用替补交换和自动接受对局接口。
理论上不会封号,但凡事没有绝对,所以大家使用前请自行评估封号风险(我个人测试了挺久,都是没问题的)
如果你也经历过“想换的英雄永远在队友手里”,欢迎试试。
愿大家每局都能换到心仪英雄,少玩折磨阵容,多玩快乐大乱斗
[Python] 纯文本查看 复制代码
import json
import os
import threading
from pathlib import Path
import psutil
import requests
from urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
class LCUConnector:
"""连接本机英雄联盟客户端接口。"""
def __init__(self):
self.port = None
self.auth_token = None
self.base_url = None
self.session = None
self._http_lock = threading.Lock()
@property
def connected(self):
return self.session is not None
def connect(self):
return self._try_lockfile() or self._try_process_args()
def _try_lockfile(self):
paths = []
for process in psutil.process_iter(["name", "exe"]):
try:
name = process.info.get("name") or ""
executable = process.info.get("exe")
if name.startswith("LeagueClient") and executable:
paths.append(Path(executable).parent / "lockfile")
except (psutil.Error, OSError):
continue
for env_name in ("LOCALAPPDATA", "PROGRAMFILES", "PROGRAMFILES(X86)"):
base = os.environ.get(env_name)
if base:
paths.append(Path(base) / "Riot Games" / "LeagueClient" / "lockfile")
for path in dict.fromkeys(paths):
try:
# 锁文件的端口和令牌分别在第三、第四段
parts = path.read_text(encoding="utf-8").strip().split(":")
if len(parts) < 5:
continue
self.port, self.auth_token = parts[2], parts[3]
self._setup_session()
return True
except (OSError, UnicodeError):
continue
return False
def _try_process_args(self):
for process in psutil.process_iter(["name", "cmdline"]):
try:
if process.info.get("name") != "LeagueClientUx.exe":
continue
port = self._argument_value(process.info.get("cmdline") or [], "--app-port")
token = self._argument_value(
process.info.get("cmdline") or [], "--remoting-auth-token"
)
if port and token:
self.port, self.auth_token = port, token
self._setup_session()
return True
except (psutil.Error, OSError):
continue
return False
@staticmethod
def _argument_value(arguments, option):
for index, argument in enumerate(arguments):
if argument == option and index + 1 < len(arguments):
return arguments[index + 1]
if argument.startswith(f"{option}="):
return argument.split("=", 1)[1]
return None
def _setup_session(self):
self.base_url = f"https://127.0.0.1:{self.port}"
self.session = requests.Session()
self.session.auth = ("riot", self.auth_token)
self.session.verify = False
self.session.headers.update(
{"Accept": "application/json", "Content-Type": "application/json"}
)
def reset(self):
with self._http_lock:
if self.session:
self.session.close()
self.session = None
self.base_url = None
self.port = None
self.auth_token = None
def get(self, endpoint):
try:
with self._http_lock:
if not self.session:
return None, 0
response = self.session.get(f"{self.base_url}{endpoint}", timeout=5)
if response.status_code != 200:
return None, response.status_code
try:
return response.json(), 200
except requests.JSONDecodeError:
return None, 200
except requests.RequestException:
return None, 0
def post(self, endpoint):
try:
with self._http_lock:
if not self.session:
return False, 0, {"error": "客户端未连接"}
response = self.session.post(f"{self.base_url}{endpoint}", timeout=5)
try:
body = response.json()
except requests.JSONDecodeError:
body = {}
return response.status_code in (200, 201, 204), response.status_code, body
except requests.RequestException as error:
return False, 0, {"error": str(error)}
class ChampionNameMapper:
"""加载并缓存英雄中文名。"""
def __init__(self, cache_file=None):
self.name_map = {}
self.cache_file = cache_file or self._default_cache_file()
@staticmethod
def _default_cache_file():
if local_app_data := os.environ.get("LOCALAPPDATA"):
return Path(local_app_data) / "ARAM Picker" / "champion_names.json"
return Path.home() / ".aram_picker" / "champion_names.json"
def load(self, lcu=None):
if lcu and lcu.connected and self._fetch_local(lcu):
self._save_cache()
return True
if self._load_cache():
return True
if self._fetch_datadragon():
self._save_cache()
return True
return False
def _load_cache(self):
try:
data = json.loads(self.cache_file.read_text(encoding="utf-8"))
self.name_map = {int(key): value for key, value in data.items()}
return bool(self.name_map)
except (OSError, ValueError, TypeError, AttributeError):
return False
def _save_cache(self):
try:
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
content = json.dumps(self.name_map, ensure_ascii=False, indent=2)
self.cache_file.write_text(content, encoding="utf-8")
except OSError:
pass
def _fetch_local(self, lcu):
data, _ = lcu.get("/lol-game-data/assets/v1/champion-summary.json")
if not isinstance(data, list):
return False
names = {}
for champion in data:
if not isinstance(champion, dict):
continue
champion_id = champion.get("id", -1)
name = champion.get("name") or champion.get("alias")
if isinstance(champion_id, int) and champion_id > 0 and name:
names[champion_id] = str(name)
if names:
self.name_map = names
return bool(names)
def _fetch_datadragon(self):
try:
versions_response = requests.get(
"https://ddragon.leagueoflegends.com/api/versions.json", timeout=10
)
versions_response.raise_for_status()
versions = versions_response.json()
if not versions:
return False
champions_response = requests.get(
"https://ddragon.leagueoflegends.com/cdn/"
f"{versions[0]}/data/zh_CN/champion.json",
timeout=10,
)
champions_response.raise_for_status()
champions = champions_response.json()["data"].values()
self.name_map = {
int(champion["key"]): champion["name"] for champion in champions
}
return bool(self.name_map)
except (requests.RequestException, KeyError, TypeError, ValueError):
return False
def get_name(self, champion_id):
return self.name_map.get(champion_id, f"英雄#{champion_id}")