Четыре готовых скрипта. Кидай в папку scripts, и они работают: от простого HUD до своего меню поверх клиентского. Разбирай, ломай, переделывай под себя.
Панелька в стиле клиента: ник, время, FPS, координаты. Что показывать и в каком порядке, игрок настраивает прямо в HUD-редакторе: правый клик по панели открывает список, где элементы можно перетаскивать и включать.
# Информационный бар: ник, время, FPS, координаты.
# Состав и порядок секций игрок меняет в HUD-редакторе (ПКМ по бару).
import time
FG = Color(207, 215, 251, 255) # текст
BG = Color(9, 9, 11, 204) # фон бара
DIV = Color(255, 255, 255, 20) # разделитель
def _username():
try:
return str(mc.getSession().getUsername())
except Exception:
return "player"
def _fps():
try:
return "%d fps" % int(mc.getCurrentFps())
except Exception:
return "0 fps"
def _coords():
p = world.self()
if p is None:
return "0, 0, 0"
return "%d, %d, %d" % (int(p.getX()), int(p.getY()), int(p.getZ()))
# подпись (она же пункт в списке) -> функция, которая отдаёт текст
ELEMENTS = [
("Brand", lambda: "Rockstar"),
("Username", _username),
("Time", lambda: time.strftime("%H:%M")),
("FPS", _fps),
("Coords", _coords),
]
PROVIDERS = {label: fn for label, fn in ELEMENTS}
DEFAULT_ON = ["Brand", "FPS", "Coords"]
bar = Hud("InfoBar", width=116, height=17, x=8, y=8)
# настройка живёт на самом HUD-элементе: список с галочками и перетаскиванием
sel = bar.select("Elements").draggable().min(0)
for label, _fn in ELEMENTS:
sel.add(label)
for label in DEFAULT_ON:
sel.select(label)
def build(ui):
with ui.row(align="center", gap=0, radius=6, bg=BG):
first = True
for label in list(sel.getValues()): # порядок берём из списка
if not sel.isSelected(label):
continue
if not first:
ui.column(width=1, height=7, bg=DIV)
with ui.row(align="center", gap=4, pad=(5, 7)):
ui.text(PROVIDERS[label], size=7, weight="medium", color=FG)
first = False
def signature():
# пока эта строка не изменилась, бар не пересобирается
return "|".join(list(sel.getValues())) + "#" + ",".join(sorted(sel.getSelected()))
bar.layout(build)
bar.signature(signature)Главная хитрость в том, что в ui.text передаётся функция, а не строка. Тогда текст становится живым: клиент дёргает функцию каждый кадр, и FPS с координатами обновляются сами. Никакого @events.tick не нужно. Само дерево виджетов при этом строится редко.
Когда его перестраивать, решает signature: функция возвращает строку из порядка и состава секций. Игрок перетащил элемент или снял галочку, строка изменилась, панель собралась заново. А сама настройка bar.select(...) создана на HUD-элементе, поэтому клиент сохраняет её сам: порядок секций переживёт перезаход.
Классический ESP: боксы вокруг игроков в радиусе, три стиля отрисовки, свой цвет и трассеры, всё через настройки модуля.
# ESP: боксы вокруг игроков с фильтром по дистанции.
mod = Module("PyESP", "Visuals")
mod.setDesc("Боксы и трассеры на игроков")
style = Mode(mod, "Стиль").add("Box").add("Filled").add("Ring").select("Box")
radius = Slider(mod, "Радиус").min(8).max(128).step(1).set(48).suffix(" м")
tint = ColorSetting(mod, "Цвет").color(255, 60, 90).alpha(True)
tracers = Checkbox(mod, "Трассеры")
def point_of(entity):
return [float(entity.getX()), float(entity.getY()), float(entity.getZ())]
@events.render_3d
def draw(event):
if not mod.isEnabled() or not world.ingame():
return
me = world.self()
eye = [float(me.getX()), float(me.getEyeY()), float(me.getZ())]
origin = point_of(me)
color = tint.get()
limit = radius.get()
kind = style.get()
for player in world.players():
target = point_of(player)
if vec.distance(origin, target) > limit:
continue
if kind == "Filled":
render3d.filled_box(event, player, color.withAlpha(60))
render3d.box(event, player, color)
elif kind == "Ring":
render3d.ring(event, player, radius=0.8, color=color)
else:
render3d.box(event, player, color)
if tracers.get():
render3d.line(event, eye, target, color)
@events.render_2d
def counter(event):
if not mod.isEnabled() or not world.ingame():
return
origin = point_of(world.self())
near = [p for p in world.players() if vec.distance(origin, point_of(p)) <= radius.get()]
if near:
event.getContext().drawText(
font("medium", 8), "Рядом: %d" % len(near), 10, 10, tint.get())3D рисуется только внутри @events.render_3d: событие приносит матрицы и камеру, поэтому его и передают первым аргументом в любой метод render3d. Боксу и кольцу можно скормить саму сущность, клиент сам возьмёт её хитбокс. А вот линия и маркер понимают только точки [x, y, z]: отсюда и point_of(), и vec для дистанции.
Настройки читаются внутри обработчика, а не запоминаются при загрузке. Тогда цвет и радиус меняются на лету, прямо во время игры. У режима спрашиваем style.get(), то есть строку. Метод is() есть, но вызвать его из скрипта нельзя: is ключевое слово Python.
Показывает, кого ты ударил и сколько у цели осталось здоровья, считает удары за сессию и хранит общий счётчик между заходами. Заодно умеет не бить своих: событие атаки можно отменить.
# Уведомление об ударе, счётчик в storage, защита друзей.
mod = Module("HitNotify", "Combat")
mod.setDesc("Уведомление о каждом ударе")
show_hp = Checkbox(mod, "Показывать HP").set(True)
to_chat = Checkbox(mod, "Дублировать в чат")
protect = Checkbox(mod, "Не бить друзей").set(True)
FRIENDS = ["Notch", "Steve"]
db = Storage("hitnotify")
total = int(db.get("total", 0))
session = 0
save_timer = Timer()
def name_of(entity):
try:
return str(entity.getNameForScoreboard())
except Exception:
return "цель"
def health_of(entity):
try:
return float(entity.getHealth())
except Exception:
return -1.0 # не живая сущность: лодка, стойка, вагонетка
@events.attack
def on_attack(event):
global total, session
if not mod.isEnabled():
return
victim = event.getEntity()
if victim is None:
return
nick = name_of(victim)
if protect.get() and nick in FRIENDS:
event.cancel() # удар не уйдёт на сервер
client.overlay("Свой: " + nick)
return
total += 1
session += 1
text = "Удар по " + nick
hp = health_of(victim)
if show_hp.get() and hp >= 0:
text += ", осталось %.1f HP" % hp
client.overlay(text)
if to_chat.get():
client.msg(text + " (за сессию: %d, всего: %d)" % (session, total))
if save_timer.finished(5000): # на диск не чаще раза в 5 секунд
db.set("total", total).save()
save_timer.reset()
@events.module_toggled
def on_toggle(event):
if event.getModule().getName() != "HitNotify":
return
if event.isState():
client.msg("HitNotify: всего ударов за всё время %d" % total)
else:
db.set("total", total).save()Событие attack можно отменить: event.cancel() не даёт пакету атаки уйти на сервер, вот тебе френдлист за пять строк. Имя цели берём через getNameForScoreboard(): он сразу отдаёт обычную строку, без возни с Text. А try/except спасает, когда цель вообще не живая сущность и здоровья у неё нет.
Счётчик живёт в storage, но на диск пишется не чаще раза в пять секунд (за это отвечает Timer) и обязательно при выключении модуля, через module_toggled. Функций on_enable и on_disable у модулей нет, это событие их заменяет.
Полноценное альтернативное меню. Берёт настоящие модули клиента, раскладывает их по категориям и рисует чужие настройки своими виджетами, через settings().
# Своё меню на реальных модулях клиента.
# Открыть: биндом модуля MiniMenu или кнопкой в его настройках.
CATS = ["COMBAT", "MOVEMENT", "VISUALS", "PLAYER", "OTHER"]
state = {"cat": "COMBAT"}
BG = Color(16, 16, 20, 245)
CARD = Color(26, 26, 32, 255)
TEXT = Color(235, 235, 245, 255)
GRAY = Color(140, 140, 155, 255)
LINE = Color(255, 255, 255, 18)
@screen("Mini Menu", 640, 440)
def menu(ui):
mods = client.modules.all()
present = [c for c in CATS if any(m.getCategory() == c for m in mods)]
if state["cat"] not in present and present:
state["cat"] = present[0]
accent = ui.color("accent")
def pick(cat):
return lambda: state.__setitem__("cat", cat)
def setting(ps):
kind = ps.type()
if kind == "boolean":
with ui.row(justify="between", align="center", fill_width=True, height=22):
ui.text(ps.name(), size=8, color=GRAY)
# у switch вторая функция записывает значение: клиент передаёт ей новое
ui.switch(lambda ps=ps: ps.boolGet(), lambda v, ps=ps: ps.boolSet(v))
elif kind == "slider":
with ui.column(gap=3, fill_width=True):
with ui.row(justify="between", fill_width=True):
ui.text(ps.name(), size=8, color=GRAY)
ui.text(lambda ps=ps: "%.1f" % ps.numGet(), size=8, color=TEXT)
ui.slider_bar(lambda ps=ps: ps.numGet(), lambda v, ps=ps: ps.numSet(v),
ps.numMin(), ps.numMax(), ps.numStep(), fill_width=True)
elif kind == "mode":
with ui.row(justify="between", align="center", fill_width=True, height=22):
ui.text(ps.name(), size=8, color=GRAY)
with ui.row(gap=4):
for i, label in enumerate(list(ps.optionLabels())):
# клик вешаем на строку: сам ui.text мышь не ловит
with ui.row(cursor="hand", radius=4, pad=(2, 5),
on_click=lambda i=i, ps=ps: ps.modeSelect(i)):
ui.text(label, size=8,
color=lambda i=i, ps=ps: accent if ps.modeIndex() == i else GRAY)
else:
ui.setting(ps) # редкий тип, пусть рисует клиент
def card(m):
with ui.column(gap=6, radius=10, pad=(10, 12), bg=CARD, width=280):
with ui.row(align="center", fill_width=True, height=20):
ui.text(m.getName(), size=9, weight="bold", color=TEXT)
ui.column(fill_width=True) # распорка: тумблер уедет вправо
ui.switch(lambda m=m: m.isEnabled(), lambda v, m=m: m.setEnabled(v))
visible = [ps for ps in list(m.settings()) if ps.visible()]
if visible:
ui.column(fill_width=True, height=1, bg=LINE)
for ps in visible:
setting(ps)
with ui.column(gap=0, fill=True, bg=BG):
with ui.row(align="center", gap=10, fill_width=True, height=44, pad=(0, 14)):
ui.text("Rockstar", size=11, weight="bold", color=accent)
for cat in present:
with ui.row(cursor="hand", radius=6, pad=(4, 7), on_click=pick(cat)):
ui.text(cat.capitalize(), size=9, weight="semibold",
color=lambda cat=cat: TEXT if state["cat"] == cat else GRAY)
ui.divider()
for cat in present:
with ui.column(gap=10, scroll=True, fill=True, pad=(12, 14),
visible_when=lambda cat=cat: state["cat"] == cat):
for m in [x for x in mods if x.getCategory() == cat]:
card(m)
opener = Module("MiniMenu", "Other")
opener.setDesc("Своё меню, забинди на клавишу")
Button(opener, "Открыть").action(lambda: menu.open())
@events.module_toggled
def on_toggle(event):
# модуль работает как кнопка: включили биндом, открыли меню, выключились
if event.getModule().getName() == "MiniMenu" and event.isState():
menu.open()
opener.setEnabled(False)Меню не хранит своих данных. client.modules.all() отдаёт живые модули клиента, mod.settings() их настройки. У каждой спрашиваем type() и рисуем подходящий виджет. Всё, что не опознали, отдаём клиенту через ui.setting(ps): меню не сломается, даже если в клиенте появится новый тип настройки.
Три вещи, на которых спотыкаются все. Первая: lambda ps=ps:, значение по умолчанию прибивает переменную цикла к лямбде, без этого все виджеты будут ссылаться на последнюю настройку. Вторая: у switch и slider_bar вторая функция записывает значение, она получает его аргументом, поэтому обязана его принимать (lambda v, ps=ps: ps.boolSet(v)). Третья: ui.text мышь не ловит, поэтому клик вешаем на ui.row вокруг него.
Лямбды-читалки делают интерфейс живым: тумблер сам подсветится, если модуль включили с бинда. А module_toggled превращает модуль-открывашку в горячую клавишу: бинд включает модуль, обработчик открывает экран и тут же гасит модуль обратно.