- 57
- 33
UltraLight for Moonloader
HTML / CSS / JS интерфейсы прямо в GTA SA и SA-MP, управляемые из Lua
HTML / CSS / JS интерфейсы прямо в GTA SA и SA-MP, управляемые из Lua
Что это
Библиотека для MoonLoader, которая позволяет делать окна интерфейса на обычном HTML, CSS и JavaScript и показывать их поверх игры. Вместо ручной отрисовки через render/imgui вы пишете страницу, а из Lua только отправляете в неё данные и получаете события от кнопок.
Под капотом работает движок Ultralight (лёгкий WebKit-рендерер для игр и приложений).
Как это устроено
GTA SA, 32-разрядная программа, а Ultralight 1.4 распространяется только в 64-разрядной сборке. Поэтому работа разделена на два процесса:
Код:
gta_sa.exe (x86) ul_helper.exe (x64)
├─ MoonLoader: Lua-скрипты (их может быть много) └─ Ultralight 1.4
│ └─ lib/ultralight.lua (FFI) по одному View на каждое окно
└─ ultralight_lua.asi ◄─── общая память ───►
• список окон, порядок, фокус команды: load_url, eval, мышь, клавиши
• мышь, перетаскивание, размер, клавиатура ◄── кадры каждого окна, сообщения JS → Lua
• рисует окна поверх игры (D3D9)
- ultralight_lua.asi (32 бит, без Ultralight внутри). Запускает хелпер и владеет всеми окнами и всем вводом: хранит список окон и их порядок (клик по окну поднимает его наверх), определяет, какое окно под курсором, двигает и растягивает окна, направляет клавиатуру в окно с активным полем ввода и рисует кадры поверх игры через хук EndScene. Ввод ловится подменой оконной процедуры игры, поэтому onWindowMessage в скриптах не нужен, а окна из разных скриптов не конфликтуют.
- ul_helper.exe (64 бит). Содержит Ultralight: на каждое окно создаётся отдельный View, он загружает страницу, исполняет JS и рендерит на CPU. Кадр каждого окна кладётся в отдельную область общей памяти (размер зависит от max_w × max_h окна). Хелпер добавляет в страницы служебный JS и блокирует копирование. Завершается вместе с игрой.
- lib/ultralight.lua. Lua-обёртка через FFI: объекты окон (ul.create_window, win:push, win:on и т.д.). Всё, что вы используете в своих скриптах, находится здесь.
- Общая память. Две очереди сообщений (в хелпер и из хелпера, у каждого сообщения номер окна) и по области кадров на каждое окно.
Путь данных Lua → страница:
ul.call / ul.push превращаются в JS-вызов, который выполняется в странице.Путь данных страница → Lua: страница вызывает
lua('имя', данные), это уходит в очередь, а Lua забирает событие в ul.process() и вызывает ваш обработчик, зарегистрированный через ul.on.Перезагрузка Lua-скрипта (Ctrl+R) безопасна: хелпер уже запущен и просто переиспользуется.
Требования
- MoonLoader, SAMPFUNCS, ASI Loader
- Microsoft Visual C++ Redistributable 2015-2022 x64 (нужен хелперу и DLL Ultralight; у многих установлен только x86). Без него хелпер не запускается,
ul.status()возвращает -1
Установка
- Скопируйте содержимое
архивав корень GTA:
Код:
GTA/
├─ ultralight_lua.asi
├─ ultralight/
│ ├─ ul_helper.exe
│ ├─ Ultralight.dll, UltralightCore.dll, WebCore.dll, AppCore.dll
│ └─ resources/ (cacert.pem, icudt67l.dat)
└─ moonloader/
└─ lib/ultralight.lua
HTML создаётся из Lua-скрипта
Отдельные .html файлы в архиве не нужны. Страница хранится обычной строкой внутри Lua-скрипта, а библиотека сама превращает её в файл:
Lua:
local HTML = [==[
<html><head><meta charset="utf-8"> ... </head><body> ... </body></html>
]==]
local url = ul.create_page('mypage', HTML) -- создаёт moonloader/resource/ul/mypage_<хеш>.html
win:load_url(url) -- win: окно из ul.create_window (или ul.load_url(url) в режиме одного окна)
- Файл записывается в moonloader/resource/ul/<имя>_<хеш>.html автоматически при старте скрипта. Хеш зависит от содержимого страницы: если страница не менялась, файл уже существует и диск не трогается; если изменили, создаётся новый файл, а старые версии удаляются. Существующие файлы библиотека никогда не перезаписывает (их может держать открытыми хелпер, и Windows запрещает запись).
- Если файл всё же не удалось записать (права на папку, антивирус), библиотека выведет предупреждение в консоль MoonLoader и загрузит страницу из памяти, win:load_url(url) при этом работает так же. Единственное отличие: относительные пути к картинкам и шрифтам в таком режиме не работают.
- Весь интерфейс лежит в одном .lua файле. Его удобно распространять, и ничего не нужно раскладывать по папкам.
- Служебный JS (функция lua(), отслеживание фокуса полей ввода, «ручка» перетаскивания, запрет копирования) в страницу добавляет сам хелпер после загрузки. Это работает для любой страницы, как бы она ни была загружена: через ul.create_page, load_html или из вашего собственного .html файла. Свою функцию lua() объявлять не нужно, а если объявите, она не будет заменена.
- Хотите вообще без файла? Используйте win:load_html(HTML). Но при загрузке из файла работают относительные пути к картинкам и шрифтам.
- Сохраняйте скрипт в UTF-8: текст страницы пишется на диск как есть. Если скрипт в cp1251, передавайте u8(HTML). Строки из страницы в игру (чат, SAMP-функции) переводите в cp1251 функцией ul.u8(...), см. спойлер «Кодировки».
- Если страница лежит в отдельном файле, загружайте её так: win:load_url('file:///moonloader/resource/мои_файлы/page.html'). Путь считается от папки GTA.
Быстрый старт
Минимальный скрипт: окно с кнопкой, по нажатию в чат приходит сообщение.
Lua:
script_name('ul_hello')
local ul = require 'ultralight'
local HTML = [==[
<html><head><meta charset="utf-8"><style>
body { margin:0; background:transparent; font-family:Arial; color:#fff; }
.card { margin:8px; padding:16px; border-radius:12px; background:rgba(20,24,32,.9); }
button { padding:8px 14px; border:0; border-radius:6px; background:#3d7bdc; color:#fff; cursor:pointer; }
</style></head><body>
<div class="card">
<h3 id="title">Привет из HTML</h3>
<button onclick="lua('hello', { text: 'Кнопку нажали' })">Нажми</button>
</div>
<script>function setTitle(s){ document.getElementById('title').textContent = s; }</script>
</body></html>
]==]
local shown = true
function main()
while not isSampAvailable() do wait(100) end
ul.init { -- режим одного окна
x = 100, y = 100, w = 400, h = 200,
draggable = true, -- окно можно двигать мышью
cursor = true, -- библиотека сама включает курсор
}
ul.load_url((ul.create_page('hello', HTML)))
ul.on('hello', function(d) -- событие из страницы
sampAddChatMessage('[UI] ' .. ul.u8(d.text), -1) -- UTF-8 из страницы -> cp1251 для игры
end)
sampRegisterChatCommand('hello', function()
shown = not shown
ul.set_visible(shown) -- курсор включится и выключится сам
end)
while true do
wait(0)
ul.process() -- ОБЯЗАТЕЛЬНО каждый кадр
end
end
function onScriptTerminate(s)
if s == thisScript() then ul.shutdown() end
end
Функцию lua() в странице объявлять не нужно, её добавляет хелпер. onWindowMessage в скрипте не нужен: мышь и клавиатуру обрабатывает ASI. Курсор включается флагом cursor = true, а окно двигается за любое место без кнопок.
Два окна в одном скрипте (то же самое можно делать и из разных скриптов):
Lua:
local a = ul.create_window { name = 'a', x = 80, y = 100, w = 360, h = 220, draggable = true, cursor = true }
local b = ul.create_window { name = 'b', x = 480, y = 140, w = 360, h = 260,
draggable = true, resizable = true, cursor = true }
a:load_url((ul.create_page('a', HTML_A)))
b:load_url((ul.create_page('b', HTML_B)))
a:on('ping', function(d) sampAddChatMessage('[a] ' .. tostring(d), -1) end) -- у каждого окна свои события
b:on('ping', function(d) sampAddChatMessage('[b] ' .. tostring(d), -1) end)
-- в main(): while true do wait(0); ul.process() end
-- в onScriptTerminate: ul.shutdown() (закроет окна этого скрипта)
Несколько окон и флаги окна
Окон может быть до 8 одновременно, в одном или в разных скриптах. Каждое окно создаётся функцией ul.create_window{...} и возвращает объект с методами load_url, push, call, on, set_visible, set_rect, get_rect, raise, destroy. Всё настраивается флагами при создании, дополнительного кода не нужно:
Lua:
local win = ul.create_window {
name = 'shop', -- имя окна внутри скрипта (по умолчанию 'main')
x = 100, y = 100, w = 520, h = 400,
draggable = true, -- окно можно двигать мышью (по умолчанию false)
resizable = true, -- размер меняется за край или угол (по умолчанию false)
cursor = true, -- курсор включается вместе с окном (по умолчанию false)
lock_controls = true, -- при cursor=true блокировать управление (по умолчанию true)
interactive = true, -- окно принимает мышь и клавиатуру (по умолчанию = cursor)
allow_copy = false, -- разрешить выделение и копирование текста (по умолчанию false)
visible = true, -- показать сразу (по умолчанию true)
debug = false, -- печатать в консоль MoonLoader console.log и ошибки JS этого окна (по умолчанию false)
min_w = 300, min_h = 200, -- границы размера при изменении мышью (по умолчанию 100 x 60)
max_w = 1280, max_h = 720, -- предел размера (определяет память под кадры; по умолчанию размер окна,
-- а для resizable: 1920 x 1080)
resize_border = 8, -- толщина зоны захвата по краям, px (по умолчанию 8)
}
if not win then ... end -- create_window возвращает nil и причину, если не получилось
Режим одного окна остаётся: ul.init{...} принимает те же флаги, а функции ul.load_url, ul.push, ul.on, ul.set_visible и другие работают с этим окном.
- У каждого окна своя страница, свои события (win:on), свой кэш win:push и свои флаги.
- Порядок окон определяет ASI: клик по окну поднимает его наверх, верхнее окно получает клики. Из кода окно можно поднять через win:raise().
- Окна из разных скриптов работают вместе и не конфликтуют: ввод обрабатывает ASI, а не скрипты.
- Окно с тем же name в том же скрипте при перезагрузке скрипта пересоздаётся, дубликаты не накапливаются. Если нужны два окна в одном скрипте, дайте им разные имена.
- Курсор: он включается, пока видно хотя бы одно окно с cursor = true, и выключается, когда не видно ни одного. Управление блокируется, если оно нужно хотя бы одному из видимых окон.
- Каждое показанное окно рендерится и копируется отдельно, нагрузка растёт с числом и размером окон. Скрытые окна не рисуются.
- Размер окна ограничен max_w × max_h, указанными при создании: под них выделяется память кадров (2 кадра по 4 байта на пиксель).
- Окно тащится левой кнопкой мыши за любое место, где нет кнопок и полей ввода. Нажатие на кнопку, ссылку, <input>, <textarea> или <select> окно не двигает, они работают как обычно.
- Чтобы тащить только за шапку, пометьте её атрибутом data-ul-drag. Как только в странице есть хотя бы один такой элемент, остальные места окна перестают его двигать.
- Чтобы исключить область (список с прокруткой, ползунок, карту), пометьте её атрибутом data-ul-nodrag. Он действует и на вложенные элементы.
- Окно не выходит за границы экрана.
- Служебный JS для перетаскивания добавляет хелпер в любую страницу сам, ничего вызывать не нужно.
HTML:
<div class="card">
<div class="head" data-ul-drag> <!-- тащим только за эту полосу -->
Заголовок окна
<button onclick="lua('close')">✕</button> <!-- кнопка в шапке работает как кнопка -->
</div>
<div class="body" data-ul-nodrag> ... </div> <!-- здесь окно не двигается -->
</div>
- Размер меняется, если взяться мышью за любой край или угол окна (зона захвата по краям шириной resize_border, 8 пикселей по умолчанию) и потянуть. Левый и верхний края тоже работают, окно при этом сдвигается.
- Размер ограничен значениями min_w, min_h, max_w, max_h, а также границами экрана. Максимум самой библиотеки: 3840×2160.
- Нажатия в зоне захвата идут библиотеке, а не странице. Не размещайте кнопки вплотную к краю окна (оставьте отступ больше resize_border).
- Форма курсора не меняется (из Lua её не поменять), поэтому нарисуйте в углу окна небольшую «ручку» (◢), чтобы пользователь понимал, что размер можно менять.
- Страница должна подстраиваться под размер: используйте проценты, flex, grid, vw/vh, а не фиксированные пиксели. Во время изменения размер применяется не чаще ~30 раз в секунду.
HTML:
<style>
html, body { height:100%; margin:0; }
.card { height:100%; display:flex; flex-direction:column; box-sizing:border-box; }
.body { flex:1; overflow:auto; } /* растёт вместе с окном */
.grip { position:absolute; right:10px; bottom:6px; opacity:.4; }
</style>
<div class="card"><div class="body">...</div><div class="grip">◢</div></div>
Флаг cursor = true поручает показ курсора библиотеке: он включается при показе окна и выключается при скрытии окна или ul.shutdown(). Под капотом это функция showCursor(show, lockControls):
- cursor = true, lock_controls = true → showCursor(true, true): курсор виден, управление персонажем и камерой заблокировано. Для меню, инвентарей, диалогов.
- cursor = true, lock_controls = false → showCursor(true, false): курсор виден, персонаж и камера управляются. Для кликабельных HUD, когда нужно ходить.
- Все видимые окна с cursor скрыты или скрипт завершён → showCursor(false, false): курсор спрятан, управление возвращено.
- cursor = false (по умолчанию): библиотека курсор не трогает, вы вызываете showCursor(...) сами, как раньше.
Копирование текста из окон запрещено полностью и по умолчанию:
- выделение текста мышью отключено (в том числе перетаскивание и двойной клик), user-select отключается для всей страницы;
- события copy, cut, dragstart и контекстное меню блокируются;
- сочетания Ctrl+C, Ctrl+X, Ctrl+Insert не доходят до страницы: хелпер отбрасывает их ещё при отправке клавиш;
- в полях ввода печатать и стирать можно, а выделить и скопировать введённое нельзя.
Текущие настройки окна лежат в win.opts (в режиме одного окна: ul.opts), их можно менять в любой момент:
Положение и размер после перетаскивания и изменения размера всегда актуальны (get_rect читает их из ASI), так что их можно сохранить в конфиг и подставить в create_window при следующем запуске.
Lua:
win.opts.draggable = false -- запретить двигать (например, пока открыт диалог)
win.opts.resizable = true
win.opts.lock_controls = false -- применится, пока окно открыто
win:set_rect(200, 100, 600, 400) -- переместить и изменить размер из кода
local x, y, w, h = win:get_rect() -- текущие положение и размер
print(win.opts.x, win.opts.y, win.opts.w, win.opts.h) -- то же самое через opts
Справочник по Lua-функциям
ul.create_window{ name, x, y, w, h, ...флаги }: создаёт окно и возвращает его объект (илиnil, причина). До 8 окон одновременно. Флаги описаны в разделе «Несколько окон и флаги окна».ul.init{ ... }: режим одного окна. То же, чтоcreate_windowс именемmain; после него работаютul.load_url, ul.push, ul.on, ul.set_visibleи т.д. без объекта окна.win:set_rect(x, y, w, h): переместить и/или изменить размер окна (размер не большеmax_w × max_h, заданных при создании).win:get_rect(): текущиеx, y, w, h(учитывает перетаскивание и изменение размера мышью).win:set_visible(true/false): показать или скрыть. Скрытое окно не рисуется и не нагружает процессор (страница и таймеры при этом продолжают работать). Курсор включается и выключается сам (приcursor = true).win:raise(): поднять окно над остальными.win:destroy(): закрыть окно и освободить память.ul.shutdown(): закрыть все окна этого скрипта и выключить курсор. При выгрузке или перезагрузке скрипта (Ctrl+R) библиотека вызывает его сама, но явный вызов вonScriptTerminateтоже безопасен. Хелпер (ul_helper.exe) при этом не завершается, а продолжает работать до выхода из игры, чтобы окна открывались быстро.ul.status(): состояние хелпера: -1 упал или не запустился, 0 не создан, 1 запускается, 2 готов.ul.on_error = function(reason) ... end: вызывается, если хелпер перестал работать (reason = 'helper_dead'). Кроме того, библиотека раз в секунду проверяет хелпер и один раз пишет предупреждение в консоль MoonLoader.
- ul.create_page(name, html): создать HTML-файл из Lua-строки в moonloader/resource/ul/<name>_<хеш>.html. Возвращает url, путь_на_диске (при невозможности записи вернёт специальный url для загрузки из памяти и nil вместо пути). Имя очищается от лишних символов (остаются буквы, цифры, _ и -).
- win:load_url(url): загрузить страницу по адресу. Локальные файлы указываются относительно папки GTA: file:///moonloader/resource/ul/mypage_xxxxxxxx.html (имя возвращает ul.create_page, вручную его писать не нужно).
- win:load_html(html): загрузить HTML прямо из строки, без файла.
- win:eval(js): выполнить произвольный JS-код в странице окна.
- win:call('имя', арг1, арг2, ...): вызвать JS-функцию в странице окна. Аргументы передаются как JSON: числа, строки, булевы значения, таблицы.
- win:push(таблица [, 'имя_js_функции']): отправить только изменившиеся значения. По умолчанию вызывается JS-функция update(diff). Таблицы-значения (списки, вложенные объекты) сравниваются по содержимому. Возвращает true, если что-то было отправлено. Вызывайте в цикле сколько угодно: лишний трафик не создаётся.
- win:reset_cache(): заставить следующий win:push отправить всё заново (например, при повторном открытии окна).
- win:on('событие', function(данные) ... end): зарегистрировать обработчик для этого окна. Данные приходят уже в виде Lua-значения (число, строка, таблица).
- ul.process(): забирает накопившиеся события всех окон этого скрипта и вызывает обработчики. Вызывайте каждый кадр в цикле main(). Ошибки в обработчиках перехватываются и печатаются в консоль, скрипт не падает.
- Служебное событие: 'ready' (страница загрузилась, можно отправить стартовые данные).
- Строки в данных события приходят в UTF-8. Чтобы вывести их в чат игры, переведите в cp1251: ul.u8(d.text).
Страницы работают в UTF-8, а чат и функции игры (SAMP) ожидают cp1251. Поэтому при передаче строк между игрой и страницей нужна конвертация. Рекомендуемая схема: скрипт сохранён в UTF-8, тогда русские строки в самом коде и в HTML уже в UTF-8 и в страницу уходят как есть.
Ограничение: ul.u8 конвертирует только русские буквы. Другие не-ASCII символы (например, «—», «№», кавычки-ёлочки, эмодзи) в cp1251-строку правильно не превращаются. Для сообщений в игру используйте обычные символы.
- Страница → игра (в чат, в SAMP-функции): ul.u8(строка) переводит UTF-8-строку (например, текст, пришедший из страницы) в cp1251. Конвертируются русские буквы, включая «ё» и «Ё»; цифры, латиница и знаки препинания ASCII остаются как есть. Принимает и числа (ul.u8(123) вернёт "123"), а для nil вернёт пустую строку.
- Игра → страница: строки, которые вернула игра (ник, название сервера), приходят в cp1251. Перед отправкой в страницу их нужно перевести в UTF-8 функцией u8(...) из библиотеки encoding (local u8 = require('encoding').UTF8, encoding.default = 'CP1251').
- Строки, которые вы пишете в самом UTF-8-скрипте (например, ul.call('notify', 'Вы получили награду')), конвертировать не нужно. Оборачивать их в u8(...) нельзя: получится двойная перекодировка.
Lua:
-- из страницы в чат игры:
win:on('send', function(d)
sampAddChatMessage('[UI] ' .. ul.u8(d.text), -1) -- UTF-8 -> cp1251
end)
-- из игры в страницу:
local encoding = require 'encoding'
encoding.default = 'CP1251'
local u8 = encoding.UTF8
win:push { nick = u8(sampGetPlayerNickname(id)) } -- cp1251 -> UTF-8
- Скриптам ничего делать не нужно: мышь, колесо и клавиатуру обрабатывает ASI. Функция ul.window_message и onWindowMessage больше не используются (старые скрипты с ними продолжают работать, функция просто ничего не делает).
- Мышь работает над окнами с включённым interactive (по умолчанию это окна с cursor = true): левая, средняя и правая кнопки, колесо прокрутки. Клик по окну поднимает его над остальными.
- Курсор включается флагом cursor = true (см. «Несколько окон и флаги окна») или вручную функцией showCursor(show, lockControls):
- showCursor(true, true): показывает курсор и блокирует управление персонажем и камерой. Подходит для открытых меню, инвентарей, диалоговых окон.
- showCursor(true, false): показывает курсор, но управление персонажем и камерой остаётся активным. Удобно для кликабельных элементов во время ходьбы и специальных HUD.
- showCursor(false, false) (или showCursor(false)): прячет курсор и возвращает стандартное управление игрой.
- Клавиатура уходит в окно только пока в нём сфокусировано поле ввода (<input> или <textarea>). Это определяется автоматически, а нажатия при этом не доходят до игры.
- Русский ввод: символы конвертируются в UTF-8 автоматически.
- win.id: номер окна (меняется на -1 после win:destroy())
- win.opts: текущие настройки окна (x, y, w, h и все флаги), их можно менять на лету. В режиме одного окна: ul.opts
- ul.windows: список окон этого скрипта
Что должно быть на стороне страницы (JS)
HTML:
<script>
// Страница → Lua. Эту функцию хелпер добавляет сам, если вы не определили свою.
function lua(name, data) {
console.log('@lua ' + JSON.stringify({ e: name, d: data === undefined ? null : data }));
}
// Lua → страница: сюда приходит win:push{...} (только изменившиеся поля)
function update(s) {
if (s.hp !== undefined) document.getElementById('hp').style.width = s.hp + '%';
}
</script>
Из служебного кода в страницу добавляется (хелпером, после загрузки DOM):
- функция lua(имя, данные), если вы не объявили свою;
- отслеживание фокуса: пока курсор ввода в <input> или <textarea>, клавиатура направляется в это окно;
- «ручка» перетаскивания для окон с draggable = true (см. атрибуты data-ul-drag и data-ul-nodrag);
- запрет выделения и копирования (если у окна не включён allow_copy).
Событие __ready, __kbd и __drag вручную отправлять не нужно: хелпер делает это сам. Событие 'ready' в Lua приходит, когда страница загрузилась (кэш win:push при этом сбрасывается).
Примеры применения
Lua:
-- в main(), в цикле:
local t = 0
while true do
wait(0)
ul.process()
if os.clock() - t > 0.1 then -- 10 раз в секунду достаточно
t = os.clock()
ul.push { -- уйдут только изменившиеся значения
hp = getCharHealth(PLAYER_PED),
armor = getCharArmour(PLAYER_PED),
}
end
end
HTML:
<div class="bar"><div id="hp"></div></div>
<div class="bar"><div id="armor"></div></div>
<script>
function update(s) {
if (s.hp !== undefined) document.getElementById('hp').style.width = Math.min(100, s.hp) + '%';
if (s.armor !== undefined) document.getElementById('armor').style.width = Math.min(100, s.armor) + '%';
}
</script>
HTML:
<button onclick="lua('menu', {id: 1})">Начать работу</button>
<button onclick="lua('menu', {id: 2})">Купить</button>
Lua:
ul.on('menu', function(d)
if d.id == 1 then sampSendChat('/work') end
if d.id == 2 then sampSendChat('/buy') end
end)
HTML:
<input id="msg" placeholder="Введите текст">
<button onclick="lua('send', { text: document.getElementById('msg').value })">Отправить</button>
Lua:
ul.on('send', function(d)
sampAddChatMessage('Вы ввели: ' .. ul.u8(d.text), -1) -- из UTF-8 в cp1251
end)
Lua:
ul.push { items = {
{ name = 'Аптечка', count = 3 },
{ name = 'Отмычка', count = 1 },
}}
HTML:
<div id="list"></div>
<script>
function update(s) {
if (!s.items) return;
var box = document.getElementById('list');
box.innerHTML = '';
s.items.forEach(function (it) {
var row = document.createElement('div');
row.textContent = it.name + ' × ' + it.count;
box.appendChild(row);
});
}
</script>
Lua:
ul.call('notify', 'Вы получили награду', 'ok') -- скрипт в UTF-8: строка уходит как есть
HTML:
<script>
function notify(text, kind) {
var el = document.createElement('div');
el.className = 'toast ' + kind;
el.textContent = text;
document.body.appendChild(el);
setTimeout(function () { el.remove(); }, 3000); // исчезнет через 3 сек
}
</script>
Lua:
local MENU, SHOP = [==[ ...html меню... ]==], [==[ ...html магазина... ]==]
ul.load_url((ul.create_page('menu', MENU)))
ul.on('goto_shop', function() ul.load_url((ul.create_page('shop', SHOP))) end)
Lua:
local car = isCharInAnyCar(PLAYER_PED) and storeCarCharIsInNoSave(PLAYER_PED)
ul.push {
inCar = car and true or false,
speed = car and math.floor(getCarSpeed(car) * 3.6) or 0, -- множитель подберите под свой сервер
}
Lua:
ul.init {
x = 200, y = 120, w = 480, h = 360,
draggable = true, resizable = true,
min_w = 320, min_h = 220,
cursor = true, lock_controls = false, -- курсор виден, но ходить можно
}
ul.load_url((ul.create_page('panel', HTML)))
HTML:
<style>
html, body { height:100%; margin:0; background:transparent; font-family:Arial; color:#fff; }
.card { height:100%; box-sizing:border-box; display:flex; flex-direction:column;
border-radius:12px; background:rgba(20,24,32,.92); overflow:hidden; }
.head { padding:10px 14px; background:rgba(255,255,255,.06); font-weight:bold; }
.body { flex:1; padding:14px; overflow:auto; }
.grip { position:absolute; right:10px; bottom:6px; opacity:.4; }
</style>
<div class="card">
<div class="head" data-ul-drag>Панель (тяните за заголовок)</div>
<div class="body">Содержимое растягивается вместе с окном.</div>
<div class="grip">◢</div>
</div>
Lua:
-- HUD: прозрачный для мыши (клики и стрельба проходят сквозь него), без курсора
local hud = ul.create_window { name = 'hud', x = 20, y = 20, w = 260, h = 90 }
-- Меню: интерактивное, двигается и растягивается, включает курсор и блокирует управление
local menu = ul.create_window { name = 'menu', x = 400, y = 150, w = 480, h = 360,
draggable = true, resizable = true, cursor = true, lock_controls = true,
visible = false }
hud:load_url((ul.create_page('hud', HUD_HTML)))
menu:load_url((ul.create_page('menu', MENU_HTML)))
menu:on('close', function() menu:set_visible(false) end)
sampRegisterChatCommand('menu', function() menu:set_visible(not menu.opts.visible) end)
-- в цикле main():
-- ul.process()
-- hud:push { hp = getCharHealth(PLAYER_PED) } -- у каждого окна свой push и свой кэш
Каждый скрипт подключает библиотеку и создаёт свои окна, они независимы. Порядок окон и ввод общие (их ведёт ASI), так что перекрывающиеся окна из разных скриптов ведут себя правильно: клик попадает в верхнее окно и поднимает его.
В каждом скрипте вызывайте ul.process() и ul.shutdown() для своих окон.
Lua:
-- скрипт A
local ul = require 'ultralight'
local w = ul.create_window { name = 'inventory', x = 100, y = 100, w = 400, h = 300, draggable = true, cursor = true }
-- скрипт B (другой файл)
local ul = require 'ultralight'
local w = ul.create_window { name = 'quests', x = 300, y = 150, w = 400, h = 300, draggable = true, cursor = true }
- Панели информации (статистика, задания, рейтинги)
- Игровое меню, магазин, чекбоксы, переключатели, ползунки
- Окна чата, уведомления, подсказки, мини-карта на CSS/SVG
- Графики и диаграммы на SVG или Canvas
- Любой интерфейс, который вы бы сделали на сайте
Готовый скрипт: /ui, информация о персонаже
В комплекте идёт ul_charinfo.lua, демонстрация всех возможностей. Команда /ui показывает и скрывает окно. Значения обновляются автоматически 10 раз в секунду (через ul.push). Окно можно двигать мышью, а курсор включает сама библиотека (флаги draggable = true, cursor = true).
Что показывает:
- Игрок: ник, ID, цвет, деньги, score, розыск, пинг, скин, состояние (пешком, в машине, в воде, в воздухе), HP и броня
- Позиция: координаты, угол, интерьер, район
- Транспорт: модель, HP, скорость, двигатель, водитель или пассажир (пешком вместо этого показывается скорость движения)
- Оружие: то, что в руках, и все слоты с патронами
- Сервер: название, адрес, онлайн, FPS, игровое время и время ПК
Ограничения
- Одновременно до 8 окон на всю игру (всех скриптов вместе). Размер каждого окна не больше 3840×2160 и не больше max_w × max_h, заданных при создании.
- Окно с interactive (по умолчанию это окна с cursor = true) ловит мышь по всему своему прямоугольнику, в том числе по прозрачным местам. Для чисто информационного HUD оставляйте cursor = false: такое окно прозрачно для мыши, клики проходят в игру.
- При resizable = true полоса по краям окна (resize_border, 8 px) принадлежит библиотеке: клики по ней в страницу не доходят. Форма курсора при наведении на край не меняется.
- Копирование текста из окон отключено (включается флагом allow_copy = true).
- Рендер программный (CPU). WebGL недоступен, HTML5 видео и аудио экспериментальные.
- Ultralight построен на WebKit, поэтому самые новые возможности JS и CSS могут не поддерживаться. Проверяйте на своей странице.
- Ввод ловится подменой оконной процедуры игры. Если другой плагин подменит её позже ASI, клики по окнам могут не доходить.
- Библиотека не заменяет и не перехватывает встроенные CEF-интерфейсы лаунчеров. Это отдельный способ рисовать собственный интерфейс.
Производительность
- Рендер идёт не чаще 60 раз в секунду и только для показанных окон; скрытые окна не рисуются.
- Новый кадр передаётся в игру только если картинка окна изменилась.
- Окно 520×640 передаёт около 1,3 МБ на кадр: на практике это незаметно. Нагрузка растёт с числом и размером окон: каждое показанное окно рендерится и копируется отдельно.
- Память под кадры выделяется на каждое окно по его max_w × max_h (2 кадра по 4 байта на пиксель). Не задавайте лишний запас, если окно не растягивается.
- Не вызывайте win:call каждый кадр с одними и теми же данными: используйте win:push, он отправляет только изменения.
- Для данных из игры достаточно опроса 5-10 раз в секунду.
Если что-то не работает
Включите флаг
debug = true в ul.create_window: все console.log, console.error и необработанные ошибки JS этой страницы (а также ошибки из win:eval и win:call) будут печататься в консоль MoonLoader с префиксом [ul:имя_окна] и попадут в moonloader.log. В готовой версии флаг лучше выключить: болтливая страница засоряет консоль.- Выведите в консоль print(ul.status()): -1 хелпер не запустился (проверьте, что GTA/ultralight/ul_helper.exe и все DLL на месте и что DLL именно x64); 1 запускается; 2 работает.
- Лог Ultralight: GTA/ultralight/ultralight.log.
- Проверьте, что ul.create_window не вернул nil: local win, err = ul.create_window{...}; if not win then print(err) end. Возможные причины: хелпер не запущен или уже открыто 8 окон.
- Вызываете ли вы ul.process() в цикле и win:set_visible(true)?
- Не за пределами ли экрана окно (проверьте win:get_rect())? Не перекрыто ли оно другим окном?
Текст страницы и данные для неё должны быть в UTF-8. Сохраняйте скрипт в UTF-8, а строки из игры (ник, название сервера) передавайте в страницу через u8(...) из библиотеки encoding. Строки, пришедшие из страницы, переводите в cp1251 через ul.u8(...) перед выводом в чат. Не оборачивайте в u8(...) строки, написанные прямо в UTF-8-скрипте: получится двойная перекодировка. Подробности в спойлере «Кодировки».
- Окно должно быть интерактивным: по умолчанию это окна с cursor = true. Окно с cursor = false прозрачно для мыши, клики проходят сквозь него. Если курсором управляете сами, поставьте interactive = true.
- Курсор должен быть включён: флаг cursor = true либо ваш вызов showCursor(true, true) или showCursor(true, false).
- Не перекрыто ли окно другим видимым окном (своим или из другого скрипта)? Верхнее окно получает клики, клик по нижнему поднимет его наверх.
- Окно видимо (win:set_visible(true)) и ul.status() возвращает 2.
- Если после установки плагин другого мода перехватил оконную процедуру игры позже нас, клики могут не доходить. Сообщите в теме, какие плагины у вас стоят.
- Включены ли флаги draggable = true и resizable = true в ul.create_window (или ul.init)? Окно должно быть интерактивным (cursor = true или interactive = true).
- Если в странице есть data-ul-drag, двигать можно только за эти элементы. Нажатие на кнопки, ссылки и поля ввода окно не двигает.
- Для изменения размера берите окно ровно за край (в пределах resize_border). Форма курсора не меняется, это нормально.
- Размер нельзя сделать больше max_w × max_h, заданных при создании окна. Для растягиваемого окна по умолчанию это 1920×1080.
- Ввод и служебный JS перетаскивания обеспечивают ASI и хелпер, onWindowMessage в скрипте для этого не нужен. Если окно всё равно не двигается, убедитесь, что установлены новые ultralight_lua.asi, ul_helper.exe и lib/ultralight.lua (все три должны быть одной версии), и перезапустите игру целиком.
Так и задумано: копирование из окон отключено. Для конкретного окна его можно включить флагом allow_copy = true (при смене флага на лету применится при следующей загрузке страницы).
Хелпер должен завершаться вместе с игрой. Если остался процесс, завершите его вручную в диспетчере задач и сообщите об этом в теме.
Важно знать
- Библиотека запускает дополнительный процесс (ul_helper.exe) и использует ASI-плагин с хуком D3D9. На серверах с античитом или особыми правилами уточняйте, разрешено ли такое.
- Ultralight бесплатен для некоммерческого использования. Условия лицензии смотрите на сайте и в поставке SDK.
Скачать
UltraLight-moonloader
Последнее редактирование: