Исходник UltraLight for Moonloader

damirtakioma

Участник
Автор темы
57
33
UltraLight for Moonloader
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)

  1. ultralight_lua.asi (32 бит, без Ultralight внутри). Запускает хелпер и владеет всеми окнами и всем вводом: хранит список окон и их порядок (клик по окну поднимает его наверх), определяет, какое окно под курсором, двигает и растягивает окна, направляет клавиатуру в окно с активным полем ввода и рисует кадры поверх игры через хук EndScene. Ввод ловится подменой оконной процедуры игры, поэтому onWindowMessage в скриптах не нужен, а окна из разных скриптов не конфликтуют.
  2. ul_helper.exe (64 бит). Содержит Ultralight: на каждое окно создаётся отдельный View, он загружает страницу, исполняет JS и рендерит на CPU. Кадр каждого окна кладётся в отдельную область общей памяти (размер зависит от max_w × max_h окна). Хелпер добавляет в страницы служебный JS и блокирует копирование. Завершается вместе с игрой.
  3. lib/ultralight.lua. Lua-обёртка через FFI: объекты окон (ul.create_window, win:push, win:on и т.д.). Всё, что вы используете в своих скриптах, находится здесь.
  4. Общая память. Две очереди сообщений (в хелпер и из хелпера, у каждого сообщения номер окна) и по области кадров на каждое окно.

Путь данных 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

Установка
  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(...) сами, как раньше.
Флаг interactive решает, принимает ли окно мышь и клавиатуру. По умолчанию он равен cursor: окно с cursor = false прозрачно для мыши, клики проходят сквозь него (чисто информационные HUD не перехватывают стрельбу и клики). Если вы управляете курсором сами, поставьте interactive = true.

Копирование текста из окон запрещено полностью и по умолчанию:
  • выделение текста мышью отключено (в том числе перетаскивание и двойной клик), user-select отключается для всей страницы;
  • события copy, cut, dragstart и контекстное меню блокируются;
  • сочетания Ctrl+C, Ctrl+X, Ctrl+Insert не доходят до страницы: хелпер отбрасывает их ещё при отправке клавиш;
  • в полях ввода печатать и стирать можно, а выделить и скопировать введённое нельзя.
Защита добавляется хелпером в любую страницу после загрузки, ничего подключать не нужно. Включить копирование для конкретного окна: allow_copy = true. При смене флага на лету (win.opts.allow_copy = true) изменение применится при следующей загрузке страницы.

Текущие настройки окна лежат в win.opts (в режиме одного окна: ul.opts), их можно менять в любой момент:
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
Положение и размер после перетаскивания и изменения размера всегда актуальны (get_rect читает их из ASI), так что их можно сохранить в конфиг и подставить в create_window при следующем запуске.



Справочник по 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:push сбрасывается автоматически, значения придут заново.

  • 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 и в страницу уходят как есть.
  • Страница → игра (в чат, в 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
Ограничение: ul.u8 конвертирует только русские буквы. Другие не-ASCII символы (например, «—», «№», кавычки-ёлочки, эмодзи) в cp1251-строку правильно не превращаются. Для сообщений в игру используйте обычные символы.

  • Скриптам ничего делать не нужно: мышь, колесо и клавиатуру обрабатывает 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)
Клавиатура включается автоматически, пока курсор ввода в поле. Backspace, Enter, стрелки и русский ввод работают.

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>
Анимации, плавные переходы и таймеры делаются средствами CSS и JS на стороне страницы, Lua для них не нужен.

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)
Обработчики ul.on остаются, кэш push сбрасывается автоматически при каждой загрузке. Либо делайте одну страницу с несколькими блоками и переключайте их из JS.

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), так что перекрывающиеся окна из разных скриптов ведут себя правильно: клик попадает в верхнее окно и поднимает его.
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 }
В каждом скрипте вызывайте ul.process() и ul.shutdown() для своих окон.
Что ещё можно сделать:
  • Панели информации (статистика, задания, рейтинги)
  • Игровое меню, магазин, чекбоксы, переключатели, ползунки
  • Окна чата, уведомления, подсказки, мини-карта на CSS/SVG
  • Графики и диаграммы на SVG или Canvas
  • Любой интерфейс, который вы бы сделали на сайте



Готовый скрипт: /ui, информация о персонаже

В комплекте идёт ul_charinfo.lua, демонстрация всех возможностей. Команда /ui показывает и скрывает окно. Значения обновляются автоматически 10 раз в секунду (через ul.push). Окно можно двигать мышью, а курсор включает сама библиотека (флаги draggable = true, cursor = true).

Что показывает:
  • Игрок: ник, ID, цвет, деньги, score, розыск, пинг, скин, состояние (пешком, в машине, в воде, в воздухе), HP и броня
  • Позиция: координаты, угол, интерьер, район
  • Транспорт: модель, HP, скорость, двигатель, водитель или пассажир (пешком вместо этого показывается скорость движения)
  • Оружие: то, что в руках, и все слоты с патронами
  • Сервер: название, адрес, онлайн, FPS, игровое время и время ПК
Все игровые функции в скрипте обёрнуты в безопасный вызов: если в вашей сборке MoonLoader или SAMPFUNCS какой-то функции нет, соответствующее поле остаётся пустым, а окно продолжает работать. HTML окна лежит внутри самого .lua файла и создаётся скриптом.



Ограничения
  • Одновременно до 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. В готовой версии флаг лучше выключить: болтливая страница засоряет консоль.

  1. Выведите в консоль print(ul.status()): -1 хелпер не запустился (проверьте, что GTA/ultralight/ul_helper.exe и все DLL на месте и что DLL именно x64); 1 запускается; 2 работает.
  2. Лог Ultralight: GTA/ultralight/ultralight.log.
  3. Проверьте, что ul.create_window не вернул nil: local win, err = ul.create_window{...}; if not win then print(err) end. Возможные причины: хелпер не запущен или уже открыто 8 окон.
  4. Вызываете ли вы ul.process() в цикле и win:set_visible(true)?
  5. Не за пределами ли экрана окно (проверьте win:get_rect())? Не перекрыто ли оно другим окном?

Текст страницы и данные для неё должны быть в UTF-8. Сохраняйте скрипт в UTF-8, а строки из игры (ник, название сервера) передавайте в страницу через u8(...) из библиотеки encoding. Строки, пришедшие из страницы, переводите в cp1251 через ul.u8(...) перед выводом в чат. Не оборачивайте в u8(...) строки, написанные прямо в UTF-8-скрипте: получится двойная перекодировка. Подробности в спойлере «Кодировки».

  1. Окно должно быть интерактивным: по умолчанию это окна с cursor = true. Окно с cursor = false прозрачно для мыши, клики проходят сквозь него. Если курсором управляете сами, поставьте interactive = true.
  2. Курсор должен быть включён: флаг cursor = true либо ваш вызов showCursor(true, true) или showCursor(true, false).
  3. Не перекрыто ли окно другим видимым окном (своим или из другого скрипта)? Верхнее окно получает клики, клик по нижнему поднимет его наверх.
  4. Окно видимо (win:set_visible(true)) и ul.status() возвращает 2.
  5. Если после установки плагин другого мода перехватил оконную процедуру игры позже нас, клики могут не доходить. Сообщите в теме, какие плагины у вас стоят.

  1. Включены ли флаги draggable = true и resizable = true в ul.create_window (или ul.init)? Окно должно быть интерактивным (cursor = true или interactive = true).
  2. Если в странице есть data-ul-drag, двигать можно только за эти элементы. Нажатие на кнопки, ссылки и поля ввода окно не двигает.
  3. Для изменения размера берите окно ровно за край (в пределах resize_border). Форма курсора не меняется, это нормально.
  4. Размер нельзя сделать больше max_w × max_h, заданных при создании окна. Для растягиваемого окна по умолчанию это 1920×1080.
  5. Ввод и служебный JS перетаскивания обеспечивают ASI и хелпер, onWindowMessage в скрипте для этого не нужен. Если окно всё равно не двигается, убедитесь, что установлены новые ultralight_lua.asi, ul_helper.exe и lib/ultralight.lua (все три должны быть одной версии), и перезапустите игру целиком.

Так и задумано: копирование из окон отключено. Для конкретного окна его можно включить флагом allow_copy = true (при смене флага на лету применится при следующей загрузке страницы).

Хелпер должен завершаться вместе с игрой. Если остался процесс, завершите его вручную в диспетчере задач и сообщите об этом в теме.



Важно знать
  • Библиотека запускает дополнительный процесс (ul_helper.exe) и использует ASI-плагин с хуком D3D9. На серверах с античитом или особыми правилами уточняйте, разрешено ли такое.
  • Ultralight бесплатен для некоммерческого использования. Условия лицензии смотрите на сайте и в поставке SDK.

Скачать
UltraLight-moonloader
 
Последнее редактирование:

#Northn

Известный
Всефорумный модератор
2,662
2,615
хорошачя работа, я когда-то года три назад садился реализовывать точь-в-точь то, что ты сделал, и так же через ультралайт. но мне под конец уже было впадлу и я забил, а так, там уже многое работало, включая биндинг к муну
разве что небольшая ремарка: видео воспроизводятся ужасно (проприетарных кодеков нет), а на ультралайт, кажется, забили
@RTD слыыыш смотри что тут
 

damirtakioma

Участник
Автор темы
57
33
хорошачя работа, я когда-то года три назад садился реализовывать точь-в-точь то, что ты сделал, и так же через ультралайт. но мне под конец уже было впадлу и я забил, а так, там уже многое работало, включая биндинг к муну
разве что небольшая ремарка: видео воспроизводятся ужасно (проприетарных кодеков нет), а на ультралайт, кажется, забили
@RTD слыыыш смотри что тут
ну, увы, исключительная проблема движка.
для условных худов, интерфейсов простеньких и тд в самый раз
у меня набросок масштабного фикса готов, там еще плюсом пару функций добавится, дома буду — реализую. ну а так, в целом с этим играться можно как угодно, это очень гибко. можно даже попробовать инжектнуть ультралайт и адекватно воспроизводить видосы. идеи что добавить есть — пишите
 
  • Нравится
Реакции: #Northn

MSIshka

Активный
134
42
Рассчитано на GTA SA 1.0 US. Для другой версии игры нужно поменять адрес в asi/ultralight_lua.cpp (константа kGtaDevicePtr).
Библиотека неплохая, но в следующий раз читай что тебе пишет нейронка, либо залей исходники на гитхаб.
 
  • Нравится
Реакции: pathtohell

damirtakioma

Участник
Автор темы
57
33
ХОТФИКС. топикстарт изменен и архив на гугл диске также обновлен

Добавлено:

  • Несколько окон: до 8 одновременно, в одном или в разных скриптах. Окно создаётся через ul.create_window{...} и возвращает объект с методами load_url, load_html, eval, call, push, reset_cache, on, set_rect, get_rect, set_visible, raise, destroy. У каждого окна своя страница, свои события, свой кэш push и свои флаги.
  • Порядок окон: клик по окну поднимает его наверх, верхнее окно получает клики. Из кода окно можно поднять через win:raise().
  • Флаги окна: name, draggable (двигать мышью), resizable (менять размер за край и угол), cursor и lock_controls (курсор и блокировка управления через showCursor), interactive, allow_copy, min_w, min_h, max_w, max_h, resize_border, visible. Флаги можно менять на лету через win.opts.
  • Копирование из окон отключено: выделение текста, Ctrl+C, Ctrl+X, Ctrl+Insert, перетаскивание текста и контекстное меню заблокированы в любой странице. Включается флагом allow_copy = true.
  • Служебный JS добавляет хелпер в любую страницу (функция lua(), отслеживание фокуса полей ввода, «ручка» перетаскивания с атрибутами data-ul-drag и data-ul-nodrag). Вручную отправлять __ready, __kbd, __drag не нужно. Событие 'ready' приходит в Lua, когда страница загружена.
  • ul.u8(str): перевод строки из страницы (UTF-8) в cp1251 для чата и функций SAMP (русские буквы, включая «ё»).

Изменено:
  • Ввод обрабатывает ASI (подмена оконной процедуры игры): мышь, колесо, клавиатура, перетаскивание, изменение размера, порядок окон. Поэтому окна из разных скриптов не конфликтуют, а onWindowMessage и ul.window_message в скриптах больше не нужны (старые скрипты с ними не ломаются, функция ничего не делает).
  • У каждого окна своя область памяти для кадров. Её размер зависит от max_w × max_h (по умолчанию размер окна, для resizable это 1920×1080).
  • Курсор считается по всем окнам сразу: он включён, пока видно хотя бы одно окно с cursor = true. Управление блокируется, если оно нужно хотя бы одному из видимых окон.
  • Окно с тем же name в том же скрипте при перезагрузке скрипта (Ctrl+R) пересоздаётся, дубликаты не накапливаются. ul.shutdown() закрывает все окна этого скрипта.
  • win:get_rect() и win.opts.x/y/w/h всегда возвращают актуальное положение и размер, в том числе после перетаскивания и изменения размера мышью.
  • Режим одного окна сохранён: ul.init{...}, ul.load_url, ul.push, ul.on, ul.set_visible и другие работают как раньше.

Исправлено:
  • Окна без курсора (cursor = false) принимали мышь по всему прямоугольнику и «съедали» клики по игре. Добавлен флаг interactive (по умолчанию равен cursor): такие окна теперь прозрачны для мыши, клики проходят в игру. Порядок окон и попадание мыши считаются только по интерактивным окнам.
  • Если курсор включается вручную через showCursor(...), а у окна cursor = false, добавьте окну interactive = true, иначе клики по нему проходить не будут.





UPD: ХОТФИКС ОТ 06.10.2026
1. RefreshDisplay. Хелпер вызывает RefreshDisplay(0) перед каждым кадром, иначе CSS-анимации, transition и requestAnimationFrame не работали. Полосы HP в окне персонажа теперь будут плавно меняться. Если компилятор ругнётся на вызов, это место в ul_helper.cpp.
2. Кириллица в пути. ASI переводит путь из cp1251 в UTF-8, хелпер проверяет ресурсы через GetFileAttributesW. Ошибка с недостающим файлом теперь показывает путь правильно.
3. Потолок размера. Лимит поднят до 3840×2160, а запрос больше лимита обрезается с предупреждением в консоли.
4. Состояние хелпера проверяется раз в секунду. Если он упал, один раз пишется предупреждение и вызывается ul.on_error('helper_dead'). Ошибки создания окна теперь подсказывают про VC++ Redistributable.
5. Фокус клавиатуры. Клик мимо окна снимает фокус с поля ввода (blur), клик по другому окну тоже, а клик по окну с уже сфокусированным полем возвращает клавиатуру.
6. Ошибки JS и console.log. Новый флаг окна debug = true печатает их в консоль MoonLoader с префиксом [ul:имя]. По умолчанию выключен, чтобы страница не засоряла консоль.
7. Буфер событий в Lua 64 КБ вместо 16 КБ. Лимит одной команды поднят до 1 МБ. Страницы больше ~900 КБ load_html отправляет через файл сам.
8. Окна закрываются автоматически при выгрузке или перезагрузке скрипта. Хелпер при этом не завершается: он живёт до выхода из игры, чтобы окна открывались быстро. Это написано в справочнике.
9. Если хелпер успел выдать два кадра за время копирования, кадр перекопируется. Разрыв кадра теперь не показывается.
10. Ключ кэша push для таблиц считается с сортировкой ключей, лишних отправок нет.
 
Последнее редактирование:
  • Нравится
Реакции: B365

damirtakioma

Участник
Автор темы
57
33
UPD: нужен фидбэк. кому нужно пользоваться на этом аудио/видео плеером? в силу движка ультралайт я не могу, но могу перекинуть задачу на хелпер и через ффмпег переделывать и вставлять в оверлей. но: .. лишние 40 мегабайт библиотеки, муторно, сложно, тяжело. кому это реально надо будет?
 

Similar threads