Страницы, которые раздаёт сервер
WebGUI умеет раздавать файлы страниц сам — по тому соединению, по которому игрок и так играет. Положите их в config/webgui/web/ и адресуйте схемой webgui::
{ "mainMenuUrl": "webgui:/index.html", "deathScreenUrl": "webgui:/death.html" }Ни хостинга, ни домена, ни открытого порта — и это работает за NAT, как всё остальное, что присылает сервер. Здесь подробно: как именно это происходит, каким должен быть ваш билд и что смотреть, когда файл не загрузился.
Если хостинг у вас уже есть, ничего из этого вас не касается: http:// и https:// URL не затронуты и ведут себя как раньше. Смешивать одно с другим можно свободно.
Как файл доезжает от сервера до страницы
- Сервер сканирует папку при старте и на
/webgui reload. Каждый файл превращается в запись: путь, SHA-256, размер, content type. - Этот список уходит каждому игроку при входе одним пакетом — манифест. Только список; сами файлы пока не отправляются.
- Клиент поднимает небольшой HTTP-сервер на loopback,
http://127.0.0.1:25580, и только в этот момент: клиент, который на такой сервер не заходил, не слушает ничего. - Страница открывается по адресу
http://127.0.0.1:25580/<токен-сессии>/index.html— мод сам подставляет его вместоwebgui:/index.html. - Когда браузер просит файл, клиент смотрит в кэш (
webgui-cache/в папке игры, ключ — хеш). Если файла нет, он запрашивает его у сервера, и тот присылает байты чанками по 32 КиБ по тому же игровому соединению. - Байты проверяются по SHA-256 из манифеста до попадания в кэш, и только потом уходят в браузер.
- В HTML вплетается мост
window.webguiпрямо перед</head>— чтобы инлайновый скрипт в начале документа мог сразу его вызвать.
Два следствия, которые стоит держать в голове:
- Файлы тянутся по требованию, а не заранее. Едет только то, что страница реально запросила.
- Адресация по хешу. Изменённый файл — это другой хеш, поэтому устаревшая страница невозможна; а неизменённый файл не передаётся дважды, в том числе между сессиями.
Origin, на котором работает страница
http://127.0.0.1:25580/<токен-сессии>/<путь>| Origin | http://127.0.0.1:25580 — именно это видит ваш бэкенд и это указывается в CORS. |
| Порт | 25580 по умолчанию. Если занят — первый свободный до 25599. |
| Токен сессии | 16 случайных байт в hex, выпускается один раз на сессию мира. Запрос без него — 404. |
| Secure context | Да. 127.0.0.1 считается доверенным origin, поэтому crypto.subtle, service workers и clipboard API работают. |
| Как прочитать со страницы | window.webgui.assetsBase — origin вместе с токеном, например http://127.0.0.1:25580/ab12…. |
Порт фиксированный намеренно, а не случайный: ваш бэкенд должен назвать этот origin в CORS, а «плавающий» порт настроить было бы нельзя. Токен нужен, чтобы другой процесс на машине не мог перечислить то, что вам прислал сервер, и чтобы страница от предыдущего сервера не прочитала файлы текущего — при выходе из мира токен обнуляется.
Второй клиент на той же машине получит другой порт
Первый держит 25580, второй получит 25581. Origin у них разный, поэтому правило CORS, где указан только 25580, второго не покроет. Если ваши игроки запускают больше одного клиента — разрешите диапазон 25580–25583, а страницы пусть читают window.webgui.assetsBase, а не хардкодят порт.
Правила, которым должен соответствовать билд
Относительные пути к ассетам
Вот на чём спотыкаются все. Документ живёт внутри /<токен-сессии>/, поэтому абсолютный путь выходит из этого префикса и даёт 404:
<!-- 404: превращается в http://127.0.0.1:25580/assets/app.js, без токена -->
<script src="/assets/app.js"></script>
<!-- работает: разрешается внутри токена -->
<script src="./assets/app.js"></script>Абсолютные пути по умолчанию делает любой сборщик, и любому можно сказать не делать:
| Инструмент | Настройка |
|---|---|
| Vite | base: './' в vite.config.js |
| Create React App | "homepage": "." в package.json |
| Next.js (статический экспорт) | assetPrefix: '.' |
| Nuxt (статика) | app.baseURL: './' |
| Webpack | output.publicPath: '' |
| Parcel | --public-url ./ |
Если страница открылась пустой — проверяйте это первым делом. Лог клиента говорит об этом прямым текстом, по одной строке на каждый уникальный путь:
[webgui-assets/WARN] webgui: page asset /assets/app.js was requested without this session's
prefix, so it cannot be served. A build that emits absolute paths like /assets/app.js does
this - rebuild it with a relative base (Vite base: './', CRA homepage: '.').
Requested by: http://127.0.0.1:25580/df55…/index.htmlRequested by — это документ, который сделал запрос: если открыто несколько страниц, сразу видно, какая виновата.
Роутинга на стороне сервера нет
Клиент раздаёт файлы и больше ничего. А именно:
- Нет SPA-fallback. Глубокая ссылка вида
webgui:/dashboardдаст 404, еслиdashboardне существует как файл. Используйте hash-роутинг (#/dashboard) либо генерируйте реальные.html. - Нет индекса для подкаталогов. Только пустой путь превращается в
index.html;sub/не станетsub/index.html. - Нет листинга каталогов, rewrite-ов, редиректов и SSR. Нужен SSR — хостите страницу у себя и укажите
https://ваш-домен/…; этот путь не изменился.
Пути регистрозависимы
Список файлов ключуется именем на диске — даже на Windows, где файловая система регистр не различает. Index.html не найдёт index.html.
Query и фрагмент при поиске файла игнорируются
app.js?v=3 и app.js#x оба разрешаются в app.js, так что cache-busting в query не вредит — но и не помогает: эту работу уже делает хеш.
Content type определяется расширением
| Расширение | Отдаётся как |
|---|---|
html, htm | text/html; charset=utf-8 |
js, mjs | text/javascript; charset=utf-8 |
css | text/css; charset=utf-8 |
json, map | application/json; charset=utf-8 |
svg, png, jpg/jpeg, gif, webp, avif, ico | соответствующий image/* |
woff2, woff, ttf, otf | соответствующий font/* |
wasm | application/wasm |
mp3, ogg/oga, wav, mp4, webm | соответствующий audio/* или video/* |
txt, xml, pdf | text/plain, application/xml, application/pdf |
| всё остальное | application/octet-stream |
Source maps работают — .map отдаётся как JSON. Файл без расширения или с расширением не из таблицы приедет как application/octet-stream, и браузер откажется исполнять его как скрипт или применять как стиль.
Ограничения и что происходит при их превышении
| Ограничение | Значение | Что будет |
|---|---|---|
| На файл | 8 МиБ | Файл пропускается, остальные раздаются. |
| Всего | 64 МиБ | Сканирование останавливается. |
| Количество файлов | 2000 | Сканирование останавливается. |
| Глубина каталогов | 12 | Более глубокие файлы не видны. |
| Сам список файлов | 512 КиБ | Не раздаётся ничего — один пакет столько не унесёт. |
| Симлинки | — | Не разыменовываются и не раздаются. |
Всё пропущенное перечисляется в логе сервера при старте и после каждого релоада. Если ожидаемого файла нет — этот лог скажет почему. Последняя строка самая суровая: набор из тысяч длинных путей отклоняется целиком, а не публикуется наполовину, и в логе будет сказано раздавать меньше файлов или хостить их самому.
Есть ещё бюджет передачи на игрока — assetBytesPerSecond (по умолчанию 4 МиБ/с, 0 отключает). Страница, которая его пробивает, получает ошибки на запросы, а не очередь.
Общение с вашим бэкендом
CORS
Назовите origin страницы в ответе своего API:
Access-Control-Allow-Origin: http://127.0.0.1:25580
Access-Control-Allow-Credentials: trueУ внутриигрового браузера web security по умолчанию выключена — но полагаться на это нельзя
Rinku поставляется с cef-disable-web-security=true в config/rinku/rinku.properties. То есть по умолчанию same-origin policy не применяется и кросдоменный fetch проходит вообще без CORS-заголовков. Но это клиентская настройка, которую игрок может выключить — и тогда CORS начнёт работать как в обычном браузере.
Поэтому настраивайте CORS всё равно. Иначе у вас страница работает, а у единственного игрока, который поправил этот файл, — нет. Худший вид баг-репорта.
Обратное направление — внешняя страница читает файл из встроенного набора — возможно, но с оговорками. Ответы мода содержат Access-Control-Allow-Origin: *, и обычному <img src> или простому GET больше ничего не нужно. Но локальный сервер отвечает только на GET и HEAD, поэтому любой запрос с preflight не пройдёт, а браузеры ограничивают обращения с публичной страницы к локальному адресу, и правила эти меняются. Считайте это тем, что нужно проверить под свой случай, а не тем, на чём можно строить.
WebSocket
На WebSocket CORS не распространяется вообще — ни preflight, ни Access-Control-Allow-Origin. Браузер отправляет origin страницы заголовком, а решает ваш сервер:
Origin: http://127.0.0.1:25580Так что new WebSocket('wss://ваш-домен/hud') со встроенной страницы подключится, если проверка origin на вашей стороне это значение принимает. Добавьте его в тот allowlist, который использует ваш фреймворк (cors.origin у Socket.IO, список origin-ов у Django, map у nginx и так далее).
Mixed content тоже не мешает: блокируется только открытие ws:// со страницы https, а эта страница — http, поэтому разрешены и ws://, и wss://.
Cookie и авторизация
Встроенная страница — другой origin относительно вашего домена, поэтому cookie, выставленные на https://ваш-домен, здесь сторонние: им нужны SameSite=None; Secure, запросу — credentials: 'include', а ответу — ваш точный origin (не *) плюс Access-Control-Allow-Credentials: true. И даже тогда блокировка сторонних cookie может их отобрать.
Лучше используйте токен, который мод и так вам даёт: при включённом enableTokens в каждый URL, который открывает WebGUI, подставляется подписанный webgui_token с именем игрока. Прочитайте его из location.search и отправьте заголовком — см. Проверку токена.
/webgui reload
Релоад пересканирует папку и рассылает новый список файлов всем, кто онлайн. Встроенная страница, уже открытая у игрока, перезагружается на месте — можно править файл и сразу видеть результат, никому не нужно перезаходить. Страницы с внешнего хостинга не трогаются; их перезагружают так же, как в браузере.
URL страницы при релоаде не меняется, включая токен сессии, поэтому всё, что держало ссылку, продолжает работать.
Что смотреть, когда не работает
| Симптом | Причина | Что делать |
|---|---|---|
Страница пустая, в логе клиента page asset /assets/app.js was requested without this session's prefix | Абсолютные пути к ассетам | Собрать с относительным base — см. таблицу выше |
Один файл даёт 404, в логе клиента <путь> is not in this server's pages | Пропущен по ограничению или не совпал регистр | Смотреть лог сервера после релоада; имя должно совпадать с диском точно |
webgui:/dashboard даёт 404, а index.html работает | Нет SPA-fallback | Hash-роутинг либо реальный dashboard.html |
| Всё стало 404 на странице, которая минуту назад работала | Страница от предыдущей сессии мира, её токен мёртв | Открыть заново; не сохранять assetsBase между сессиями |
504, на странице The server did not send <path> | Передача не уложилась в 20 с или упёрлась в assetBytesPerSecond | Поднять лимит или уменьшить файл |
| Скрипт или стиль скачался, но проигнорирован | Незнакомое расширение, отдан как application/octet-stream | Использовать известное расширение |
Ничего не раздаётся, в логе no free port in 25580..25599 | Заняты все 20 loopback-портов | Освободить порт; внешние http(s) страницы при этом работают |
| Ничего не раздаётся, и строки про порт тоже нет | serveBundledPages выключен или папка пуста | Проверить config/webgui/server.json и папку |
| У вас работает, у одного игрока ошибка CORS | У него cef-disable-web-security=false | Настроить CORS по-человечески |
| Второй клиент на машине не достаёт до вашего API | Он на порту 25581, которого нет в вашем CORS | Разрешить диапазон, читать window.webgui.assetsBase |
Проблемы со стороны страницы видны в логе клиента (запросы, отсутствующие файлы, вывод консоли браузера), проблемы сканирования — в логе сервера (пропущенные файлы, лимиты). При первой настройке стоит держать открытыми оба.
Что выбрать: раздачу с сервера или свой хостинг
| Раздача с сервера | Свой хостинг | |
|---|---|---|
| Настройка | Папка | Домен, TLS, деплой |
| Работает за NAT | Да | Нужна доступность извне |
| SSR | Нет | Да |
| WebSocket к вашему бэкенду | Да | Да |
| Origin | http://127.0.0.1:25580 | Ваш — про CORS думать не надо |
| Обновление | Правка файла и /webgui reload | Ваш деплой |
| Объём | 64 МиБ, 2000 файлов | Сколько есть |
Одно другого не исключает. Частая схема — встроенная оболочка, которая общается с вашим API по WebSocket, или внешняя страница, которая берёт пару тяжёлых картинок из встроенного набора.