Структура
- Файл cef.asi должен находится в корневой папке с игрой (собирается как loader.dll).
- Папка cef со всем содержимым так же там же.
- Так же создается папка CEF по пути Мои документы/GTA San Andreas User Files/CEF/, где хранится кэш, куки файлы и прочие вещи, необходимые для корректной работы Chromium.
- gta_sa.exe
- cef.asi
- cef/
- client.dll
- libcef.dll
- renderer.exe
- etc ...
Советы по использованию и некоторые ограничения
- В идеале иметь один браузер со всеми интерфейсами. Не создавать новые для разных действий, а использовать встроенную систему событий.
- Если имеются клиентские плагины, которые используют относительные пути, то, скорее всего, они поломаются и будут неверно работать. К сожалению, на данный момент во время инициализации меняется текущая директория в другом потоке. Как пример: CLEO библиотека может создать свой лог cleo.log, а так же папки cleo_text и cleo_saves в папке cef. Для корректной работы следует лучше узнавать путь до текущего исполняемого файла (gta_sa.exe).
Pawn API
Создает браузер для указанного игрока.
cef_create_browser(player_id, browser_id, const url[], hidden, focused)
Удаляет браузер.
cef_destroy_browser(player_id, browser_id)
Скрывает браузер.
cef_hide_browser(player_id, browser_id, hide)
Вызвать событие у клиента. Поддерживаемые типы аргументов:
string,
integer,
float.
cef_emit_event(player_id, const event_name[], args...)
Подписаться на событие от клиента. Сигнатура функции колбека:
Callback(player_id, const arguments[])
cef_subscribe(const event_name[], const callback[])
Проверка на наличие плагина у клиента.
cef_player_has_plugin(player_id)
Создает браузер как и в первом случае, но с пометкой, что он будет отображаться на объектах на определенной текстуре. Параметр
scale указывает во сколько раз нужно увеличить стандартную текстуру. Например, если стандартная текстура имеет размер
250x30 будет иметь размер
1250x150 при переданном параметре в 5 единиц.
cef_create_ext_browser(player_id, browser_id, const texture[], const url[], scale)
Заменяет текстуру переданного объекта на изображение браузера у клиента. Браузер должен быть создан с помощью
cef_create_ext_browser, а так же должна присутствовать текстура, указанная при создании, для корректного отображения.
cef_append_to_object(player_id, browser_id, object_id)
Возвращает оригинальную текстуру объекта.
cef_remove_from_object(player_id, browser_id, object_id)
Включает / выключает инструменты разработчика.
cef_toggle_dev_tools(player_id, browser_id, enabled)
Выставляет максимальную слышимую дистанцию для браузера на объекте.
reference_distance - расстояние, до которого будет максимальная громкость, а после пойдет на понижение от
max_distance до 0.
native cef_set_audio_settings(player_id, browser_id, Float:max_distance, Float:reference_distance)
Функция делает браузер сфокусированным. Выводится на первый план, получает все события с клавиатуры и мыши. То же самое, что и передача аргумента
focused = true при создании браузера.
cef_focus_browser(player_id, browser_id, focused)
Позволяет браузеру получать ввод с клавиатуры в фоне, даже если браузер не имеет фокуса или скрыт. Это позволяет использовать в JS коде функции для подписки на события от клавиатуры все время. Например, можно сделать открытие / закрытие интерфейса по нажатию клавиши (
window.addEventListener("keyup")).
cef_always_listen_keys(player_id, browser_id, listen)
Загружает указанный URL у заданного браузера. Быстрее, чем пересоздание браузера.
cef_load_url(player_id, browser_id, const url[])
Так же есть два события встроенных в плагин:
Вызывается, когда клиент создал у себя браузер по запросу от сервера / плагина. Значение
status_code либо 0 (при неудачном создании), либо HTTP код (200, 404 etc).
forward OnCefBrowserCreated(player_id, browser_id, status_code)
Вызывается после подключения клиента к CEF серверу, либо по истечению тайм-аута. Грубо говоря, замена ручной проверки
cef_player_has_plugin.
forward OnCefInitialize(player_id, success)
Browser API
Так же у браузеров есть свое API для управления ими.
Фокусируется на браузере, что позволяет ему быть поверх всех остальных окон, а так же иметь возможность вводить с клавиатуры и мыши в него.
cef.set_focus(focused)
Подписывается на событие от браузера / других плагинов.
cef.on(event_name, callback)
Отписывается от события. Чтобы использовать данную функцию, необходимо передавать переменную, которая содержит функцию на колбек события, указанный при подписке на событие.
Не работает на данный момент, сломано!
cef.off(event_name, callback)
Скрывает браузер и отключает звук от него.
cef.hide(hide)
Вызвать событие на сервере / в сторонних плагинах с указанными аргументами. Поддерживает все типы, кроме объектов с полями и функций. Замечание: в плагинах возможно использовать все типы по человечески, но на сервере все аргументы преобразуются в единую строку, разделенную пробелами.
cef.emit(event_name, args...)
C API
Устаревший пример, больше не работает [Показать]
#include <cstdint>
// Отменить продолжение выполнения колбеков события. А так же не отправлять его серверу.
static const int EXTERNAL_BREAK = 1;
// Продолжить выполнение. Если никто не отменил его, будет передано серверу.
static const int EXTERNAL_CONTINUE = 0;
using BrowserReadyCallback = void(*)(uint32_t);
using EventCallback = int(*)(const char*, cef_list_value_t*);
extern "C" {
// Проверка на существование браузера в игре.
bool cef_browser_exists(uint32_t browser);
// Создан ли браузер и загружен веб-сайт.
bool cef_browser_ready(uint32_t browser);
// Создать браузер с указанными параметрами. Эта функция асинхронная, браузер создается не сразу.
void cef_create_browser(uint32_t id, const char *url, bool hidden, bool focused);
// Создать CefListValue внутри клиента.
cef_list_value_t *cef_create_list();
// Удалить браузер у клиента.
void cef_destroy_browser(uint32_t id);
// Вызвать событие у браузера.
void cef_emit_event(const char *event, cef_list_value_t *list);
// Сфокусировать ввод на браузере, а так же вывести его поверх всех остальных.
void cef_focus_browser(uint32_t id, bool focus);
// Активно ли окно игры.
bool cef_gta_window_active();
// Скрыть браузер.
void cef_hide_browser(uint32_t id, bool hide);
// Проверить доступен ли ввод для конкретного браузера.
bool cef_input_available(uint32_t browser);
// Подписаться на событие полного создания браузера.
void cef_on_browser_ready(uint32_t browser, BrowserReadyCallback callback);
bool cef_ready();
// Подписаться на события от браузера.
void cef_subscribe(const char *event, EventCallback callback);
// Попытаться сфокусироваться на браузере. Аналогично паре cef_input_available + cef_focus_browser,
// но с одним значительным условием, между выполнением этих двух функции кто-то другой может захватить фокус.
// А данная функция атомарна, что позволяет проверить и сразу же захватить, гарантируя,
// что никто другой не сможет в это время получить фокус.
bool cef_try_focus_browser(uint32_t browser);
}
Инструкция к применению
Описание
Браузер можно создать из двух мест: из игрового мода и плагинов.
Браузер имеет два дополнительных состояния:
hidden и
focused. Первое состояние отвечает за отображение браузера на экране игрока. Второе состояние сложнее: если браузер сфокусирован (
focused = true), то у игрока замораживается камера, появляется курсор и весь его ввод (от клавиатуры и мыши) идет прямиком в браузер, минуя GTA и SA:MP (за исключением некоторых функций по типу скриншота на F8). Игрок самостоятельно никогда не сможет выйти из этого состояния, вы должны поспособствовать этому в коде интерфейса браузера. Например, можно слушать нажатие клавиши ESC и при ее нажатии вызывать
cef.set_focus(false).
Условно говоря, что открыв какой-нибудь youtube.com из него уже никогда не выйти, не закрыв игру или не поставив таймер на удаление браузера в моде.
Эти два состояния полностью независимы друг от друга, то есть браузер может быть
hidden = false, но в тот же момент
focused = false, в таком случае браузер будет показан, но доступа к вводу у него не будет, а игрок может спокойно совершать действия в игре.
Взаимодействие из игрового мода
В двух словах: игровой мод должен использовать только несколько нативных функций (создание / удаление браузеров, вызов событий в браузере, а так же подписка на них).
Ну и чуть кода:
Скрытый текст [Показать]
- browser.js
cef.on("lp_open", () => Interface.draw(LOGIN_PAGE)); // чтобы не усложнять код, будем подразумевать, что Interface.draw отображает куски интерфейса
cef.on("lp_success", () => {
Interface.draw(LOGIN_PAGE_SUCCESS);
// по истечению 5 секунд показа интерфейса с успешным логином
setTimeout(() => {
cef.set_focus(false); // убираем фокус с браузера, позволив игроку пользоваться игрой
cef.hide(true); // скрываем интерфейс
Interface.draw(NONE);
}, 5000);
});
LoginPage.on_password_enter((login, pwd) => {
cef.emit("lp_response", 1, login, pwd); // если игрок что-то ввел в поле ввода и нажал клавишу войти, то отправим событие на сервер
});
window.addEventListener("keydown", (event) => {
// если нажали ESC
if (is_esc_pressed(event)) {
// нажат ESC во время авторизации
if (LoginPage.is_active()) {
cef.emit("lp_response", 0); // пользователь отказался от ввода
}
// могут быть еще какие проверки на интерфейс, когда будет в нем больше логики
// в конце концов скрываем интерфейс
// но при условии, что ничего не нужно ПРОСТО рисовать
if (!Interface.should_draw_something_passive()) {
cef.hide(true);
}
cef.set_focused(false);
}
});
- gamemode.pwn
#define CEF_INTERFACE_BROWSER_ID 1
new CEF = ...;
public OnBrowserCreated(playerid, browserid, status) {
if (status != 200) {
// интернет провайдер может блокировать некоторые страницы
SendClientMessage(playerid, -1, "Невозможно открыть страницу. Возможно, есть проблемы с интернет соединением.");
return;
}
// браузер с интерфейсом успешно создан, откроем страницу с логином, если нужна
if (browserid == CEF_INTERFACE_BROWSER_ID) {
if (CEF[playerid][await_lp]) {
open_login_page(playerid);
}
}
}
public OnLoginPageResponse(playerid, const arguments[]) {
if (CEF[playerid][await_lp_response] == false) {
Kick(playerid); // ну это так ... просто никто не гарантирует, что страница не была подменена, а может и проблемы с соединением
return;
}
new resp, login[128], pwd[128];
if (!sscanf(..., resp, login, pwd) {
if (resp == 0) {
Kick(playerid); // отказался от ввода
return;
}
CEF[playerid][tries] += 1;
if (CEF[playerid][tries] > MAX_PWD_TRIES) {
Kick(playerid);
return;
}
new success = check_login_pwd(login, pwd);
if (success) {
cef_emit_event(playerid, "lp_success");
} else {
cef_emit_event(playerid, "lp_try_again", CEF[playerid][tries]);
}
}
}
main() {
cef_subscribe("lp_response", "OnLoginPageResponse");
}
public OnPlayerConnect(playerid) {
}
stock initialize_interface(playerid) {
cef_create_browser(playerid, CEF_INTERFACE_BROWSER_ID, "https://cef.project.com", true, false); // создается единожды браузер со страницей всего интерфейса
CEF[playerid][init] = true;
CEF[playerid][await_lp] = true;
}
stock open_login_page(playerid) {
cef_emit_event(playerid, "lp_open");
CEF[playerid][await_lp] = false;
CEF[playerid][await_lp_response] = true;
}
Так же есть небольшой пример псевдокода, показывающий взаимодействие игрового мода, плагина и браузера:
Скрытый текст [Показать]
- circle.rs
use std::os::raw::c_char;
use std::sync::mpsc::{Receiver, Sender};
use std::time::{Duration, Instant};
use cef_api::CefApi;
use cef_api::{cef_list_value_t, List};
const DLL_PROCESS_ATTACH: u32 = 1;
const DLL_PROCESS_DETACH: u32 = 0;
/// суть приложения заключается в следующем:
/// есть небольшая веб страница с простым интерфейсом. при нажатии на кнопку 0x72 игрок встает на месте,
/// появляется курсор и он должен кликнуть на какого-либо персонажа
/// далее этот плагин ищет игрока, на которого кликнули и отправляет событие обратно в браузер
/// браузер в свою очередь создает кольцо со списком действий над игроком (как пример, показать документы, пожать руку или еще что)
/// при клике на нужное действие отправляется событие уже на сервер, где происходит его обработка
/// браузер сам внутри себя скрывается и обратно разблокирует ввод игроком
/// к сожалению, исходный код той тестовой страницы не сохранился. подразумевалось, что сервер сам создавал браузер как часть своего интерфейса
/// грубо говоря, там был следующий код, который будет в файле ниже
struct App {
circle: bool,
cef: CefApi,
pressed: Instant,
event_tx: Sender<(i32, i32)>,
event_rx: Receiver<(i32, i32)>,
}
static mut APP: Option<App> = None;
const CEF_INTERFACE_BROWSER: u32 = 102;
#[no_mangle]
pub extern "C" fn cef_initialize() {
let cef = CefApi::wait_loading().expect("No client.dll");
let (event_tx, event_rx) = std::sync::mpsc::channel();
// подписка на события от браузера (так же можно и от сервера слушать)
cef.subscribe("circle_click", circle_click);
cef.subscribe("circle_closed", circle_closed);
let app = App {
circle: false,
pressed: Instant::now(),
cef,
event_tx,
event_rx,
};
unsafe {
APP = Some(app);
}
}
#[no_mangle]
pub extern "C" fn cef_samp_mainloop() {
if let Some(app) = unsafe { APP.as_mut() } {
if client_api::utils::is_key_pressed(0x72) {
if app.pressed.elapsed() >= Duration::from_millis(500) {
if !app.circle {
// если нажата кнопочка и можно в данный момент показать браузер, то показываем его и отсылаем событие
if app.cef.try_focus_browser(CEF_INTERFACE_BROWSER) {
let args = app.cef.create_list();
app.cef.hide_browser(CEF_INTERFACE_BROWSER, false);
app.cef.emit_event("show_actions", &args);
app.circle = true;
}
} else {
app.cef.focus_browser(CEF_INTERFACE_BROWSER, false);
app.cef.hide_browser(CEF_INTERFACE_BROWSER, true);
app.circle = false;
}
app.pressed = Instant::now();
}
}
// если есть новые события от цефа, то обрабатываем их
while let Ok((x, y)) = app.event_rx.try_recv() {
let x = x as f32;
let y = y as f32;
let mut min = 10000.0f32;
let mut min_id = u16::max_value();
// поиск по всем игрокам в зоне стрима нужных, а именно тех, на которых игрок кликнул в браузере
if let Some(mut players) = client_api::samp::players::players() {
for player in players.filter(|p| p.is_in_stream()) {
if let Some(remote) = player.remote_player() {
let pos = remote.position();
let (p_x, p_y) = client_api::gta::display::calc_screen_coords(&pos)
.unwrap_or((-1.0, -1.0));
let delta = ((p_x - x).powf(2.0) + (p_y - y).powf(2.0)).sqrt();
if delta < min {
min = delta;
min_id = remote.id();
}
}
}
}
if min <= 20.0 {
if let Some(player) = client_api::samp::players::find_player(min_id as i32)
.as_ref()
.and_then(|p| p.name())
{
let args = app.cef.create_list();
let name = cef::types::string::CefString::new(player);
args.set_string(0, &name);
args.set_integer(1, min_id as i32);
// если игрок найден, отправляет событие обратно в браузер с именем игрока и его ID.
app.cef.emit_event("circle_found_player", &args);
}
}
}
}
}
// событие приходит, когда игрок в браузере нажал ЛКМ в какую-либо точку
// событие содержит в себе координаты экрана X и Y. в браузере выглядит как cef.emit("circle_click", mouse.x, mouse.y);
pub extern "C" fn circle_click(event: *const c_char, args: *mut cef_list_value_t) -> i32 {
if let Some(args) = List::try_from_raw(args) {
if args.len() == 3 {
let x = args.integer(1);
let y = args.integer(2);
unsafe {
APP.as_mut().map(|app| app.event_tx.send((x, y)));
}
}
}
1
}
pub extern "C" fn circle_closed(_: *const c_char, _: *mut cef_list_value_t) -> i32 {
if let Some(app) = unsafe { APP.as_mut() } {
app.circle = false;
}
1
}
- gamemode.pwn
forward on_circle_action(player_id, const arguments[]);
public on_circle_action(player_id, const arguments[]) {
// к сожалению, из-за специфики павн, приходится отправлять аргументы как строку
new target_id, action_id;
if (sscanf("%d %d", arguments, player_id, action_id)) {
switch (action_id) {
case 0: {
show_docs(player_id, target_id);
break;
}
case 1: {
do_handshake(player_id, target_id);
break;
}
}
}
}
main() {
cef_subscribe("circle_do_action", "on_circle_action");
}
- interface.js
cef.on("circle_found_player", (name, id) => {
Circle.show_for_player(name, id); // тут рисовался круг с действиями
Circle.on_select_action((player_id, action_id) => {
cef.emit("circle_do_action", player_id, action_id); // отправляет выбранное действие уже на сервер, а не в плагин
cef.hide(true); // скрыть браузер
cef.set_focus(false); // разблокировать ввод игроку
cef.emit("circle_closed"); // уведомить плагин, что действие выбрано
});
});
cef.on("show_actions", () => {
Circle.wait_for_input((x, y) => {
cef.emit("circle_click", x, y); // если игрок кликнул куда-либо на экране, то отправляется событие в плагин
});
});
Этот пост почти полная копия оригинала в котором есть неточности и битые ссылки. Также существует вероятность что его могут удалить, по этому решил продублировать на форум.
Сообщение отредактировал Макс: 06 марта 2023 - 06:34