BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Интерактивный редактор тир-листа по Blox Fruits в стиле макета Nexus Tier List (сакура). Чистый HTML/CSS/JS, без сборки. Просто дважды кликните start.bat — он поднимет локальный сервер и откроет редактор в браузере (нужен установленный Python; адрес: http://127.0.0.1:8780/). Можно открыть и index.html напрямую двойным кликом — редактор работает, но кнопка «Скачать PNG» при открытии файлом (file://) не сработает: браузер блокирует экспорт картинок. Поэтому для скачивания используйте start.bat.README 전문 보기 ↗
Интерактивный редактор тир-листа по Blox Fruits в стиле макета Nexus Tier List (сакура). Чистый HTML/CSS/JS, без сборки.
Просто дважды кликните start.bat — он поднимет локальный сервер и откроет редактор в браузере
(нужен установленный Python; адрес: http://127.0.0.1:8780/).
Можно открыть и
index.htmlнапрямую двойным кликом — редактор работает, но кнопка «Скачать PNG» при открытии файлом (file://) не сработает: браузер блокирует экспорт картинок. Поэтому для скачивания используйтеstart.bat.
Альтернатива без .bat (в папке проекта):
python -m http.server 8780
и открыть http://127.0.0.1:8780/
+ Тир), переименовать (клик по названию), свой логотип тира (🖼),
переместить ▲▼, удалить ✕.+ в тире или + Предмет), перетаскивать между тирами (drag-and-drop).js/i18n.js, узлы помечены data-i18n* в разметке./api/promo.php, таблица promo) и настраиваются на странице
/admin/promo. Подробности — в разделе «Реклама» ниже.js/protect.js плюс класс
body.protected в css/base.css; админу защита снимается целиком, иначе
не работали бы ни перенос ячеек, ни редактор поста. Это заслон от обычного
посетителя, а не от исходников: разметка видна через «Просмотр кода», а
экран можно сфотографировать..json.Отдельная лента на /news (адрес чистый — редиректит на news.html через
.htaccess, ссылки на неё есть в тулбаре обеих страниц). Правится на
/admin/news — см. раздел «Админка».
Таблицу
newsнужно завести на боевой БД вручную (phpMyAdmin илиmysql < schema.sql). Автоматически это не делается:.cpanel.ymlкопирует в веб-рут толькоpublic_html/, аschema.sqlлежит уровнем выше.Порядок при этом свободный: без таблицы
api/news.phpотвечает пустой лентой, а не пятисоткой — тот же приём, что и вpromo_load(). Страница открывается и честно говорит, что новостей нет. Таблица нужна к моменту, когда захочется опубликовать первый пост:news_save.phpбез неё не запишет.
/newsнамеренно закомментирован вsitemap.xml, пока лента пустая: приглашать поисковик на страницу без содержимого незачем. Раскомментировать вместе с первым постом.
js/news.js, проверяется на сервере api/news_save.php),
поэтому они не могут разойтись в том, что считают допустимой категорией.api/news_save.php сам переводит
пустые EN-поля — заголовок, абзацы, цитаты, пункты списков, подписи к
картинкам — через бесплатный эндпоинт Google (api/lib/translate.php),
сохраняя жирный, ссылки и прочую разметку. Написанный вручную английский не
трогается; английский, оставшийся от прежней версии поправленного русского
абзаца, переводится заново. Код не переводится.pickLang (js/news.js) откатывается на русский
заголовок/текст там, где английского нет — лента не показывает пустые
карточки. Эндпоинт неофициальный, без ключа: Google вправе его ограничить,
и тогда вместо перевода остаётся этот откат.NEWS_FEED_LIMIT в api/news.php — единственное место, где
меняется это число.news (schema.sql), отдельно от tierlist. Публикация
поста не трогает JSON и rev тирлиста, поэтому не сбрасывает кеш и не
вызывает у посетителей тирлиста лишнюю перезагрузку данных.index.html разметка (тулбар, шапка, тиры, легенда, футер, команда, модалка)
news.html разметка ленты новостей (тулбар, сцена, карточки, редактор админа)
css/base.css общий хром обеих страниц: тулбар, кнопки, чипы, переключатель языка, сцена, лепестки, модалка
css/styles.css только тирлист: тема, верстка, дорожки, ячейки
css/news.css только лента: карточки, фильтры, редактор поста (префиксы nw-/ne-)
admin.php /admin — редактор тирлиста: отдаёт index.html с шапкой админки
admin-news.php /admin/news — лента новостей в режиме редактирования
admin-promo.php /admin/promo — панель рекламы
admin-logout.php /admin/logout — выход (POST из шапки)
api/lib/admin_page.php проверка роли, шапка админки, страница входа
css/admin-shell.css шапка админки и экран входа
css/styles.css тема, верстка, дорожки, ячейки
js/app.js вся логика редактора
js/news.js категории, выбор языка и дата поста — общее для ленты и её редактора (без DOM — тестируется в node)
js/news-page.js загрузка ленты, рендер карточек, фильтры и редактор админа на news.html
js/i18n.js строки интерфейса RU/EN (без DOM — тестируется в node)
js/tiers.js марки полос тиров и перевод старых сохранений (без DOM — тестируется в node)
js/protect.js запрет копирования текста и картинок для гостя — общее для тирлиста и ленты (тестируется в node)
js/html2canvas.min.js экспорт в PNG (локально, офлайн)
assets/ картинки макета + шрифты
poster/ всё, что вырезано из PSD «макнеми тир под сайт» (актуальный вид)
bg.webp / bg-m.webp / bg-export.jpg фон (сакура/храм/водопад), уже с цветокором
petals.webp / petals-m.webp / petals-export.png лепестки и декор
band.webp / band.png плашка тира
logo-bf.png логотип Blox Fruits
marks.png марки шапки (молния + GLH + MK)
dot-*.png / trend-*.png точки спроса и тренды легенды
trend-new.png / trend-wip.png они же на карточках предметов (NEW и «?»)
design/legend/ значки типов (FR, CS, CM, MS, PM, GP, CR, VH) и тренды из
редизайна — вектор, один набор на легенду и на карточки
bg.png / petals.png / bg-stage*.webp фон прошлого макета (не подключён)
dots-band.png полутоновые точки плашки тира прошлого макета (не подключён)
dots-tile.png полутоновая текстура дорожки
brand-logos.png логотип шапки прошлого макета (не подключён)
logo-bolt.png / logo-glh.png / logo-mk.png логотипы тиров, те же марки, что в шапке
logo-flame.png марка тира прошлого макета (не подключён — старые сохранения переводятся на logo-bolt.png)
bf-logo-trim.png логотип Blox Fruits прошлого макета (не подключён)
dot-*.png / trend-*.png точки спроса и тренды на карточках предметов и в модалке
dot-neon.png точка «оверпрайс» — единственная с переливом, кадр из неонового градиента
dot-{red,orange,yellow,green}.png остальные точки, 72x72 как неон; рисует tools/make-demand-dots.py
icon-sample.png стандартная иконка предмета
fonts/Bootshaus/Bootshaus-Regular.ttf шрифт легенды и подвала
fonts/ProtoSans56.otf шрифт заголовка и даты
Файлы прошлого макета оставлены намеренно: деплой копирует дерево поверх, ничего не удаляя, так что удаление из репозитория всё равно не убрало бы их с сервера — зато сломало бы откат, если новый вид не зайдёт.
Всё редактирование живёт на отдельных адресах, под общей шапкой
«Тирлист · Новости · Реклама · Обращения». Ссылку на панель видят только админы
и модераторы — пунктом в меню аватара, — а ни app.js, ни news-page.js не
спрашивают роль у сервера.
| Адрес | Что это |
|---|---|
/admin | редактор тирлиста — тот же постер, что на сайте, плюс тулбар |
/admin/news | лента новостей с кнопкой «+ Новость» и ✎/✕ на карточках |
/admin/promo | панель рекламы |
/admin/support | обращения из /support |
/admin/logout | выход, только POST из шапки |
Пароля у панели нет. Админы и модераторы входят через Roblox, как все, а
права дают два списка Roblox id в config.php:
| Ключ | Что открыто |
|---|---|
admin_ids | вся панель: тирлист, новости, реклама, обращения, баны |
moderator_ids | только /admin/support и /admin/bans (и уведомления в Telegram) |
Роль проверяет сервер, до отдачи разметки (admin_page_guard()). Всем, кого
нет в списках, — и гостю, и вошедшему игроку — панели как будто нет: ответ
байт в байт тот же 404, что LiteSpeed отдаёт на несуществующий адрес, без
кнопки входа и без «нет доступа»; в robots.txt её тоже нет. Админ и модератор
входят на сайт через Roblox из шапки, как все, и открывают панель пунктом меню
аватара. Модератор на странице админа уходит на /admin/support. Роль в
сессии не хранится и сверяется с конфигом на каждом запросе: вычеркнутый id
теряет доступ сразу. /api/login.php оставлен заглушкой с 410 — деплой не удаляет
файлы, и прежний на сервере продолжал бы принимать пароль.
Бан — третий список, banned_ids, в том же config.php. Roblox id
забаненного берётся из адреса его профиля (/profile?id=…). Со следующего
запроса он выходит из аккаунта на всех устройствах: сессия пустеет, ключи
«запомнить вход» гаснут. Войти заново не может: шапка показывает «Этот аккаунт
заблокирован». Его объявления пропадают из ленты и профиля, переписка у
собеседников остаётся. Бан сильнее роли. Чтобы снять бан, id убирают из
списка, и неистёкшие объявления возвращаются.
Бан из панели. Модераторы и админы банят и без доступа к серверу: кнопкой
в шапке переписки на /chat или на /admin/bans, где игрока ищут по нику,
логину Roblox или id. Срок — 1, 3, 7, 30 дней или навсегда, причина по
желанию. Модератор банит только обычных игроков, админ — ещё и модераторов,
админа из панели не забанит никто (иначе модератор мог бы закрыть владельцу
вход в панель). Такие баны лежат в таблице user_bans (миграция
docs/migrations/2026-10-10-user-bans.sql) и действуют так же, как список в
конфиге: проверка одна, site_banned(). Бан из config.php панель показывает,
но не снимает. Пока миграции нет, работает только список в конфиге.
Бан бывает временным: вместо id пишут id => срок по Москве,
'2222222222' => '2026-10-20' (до 00:00 этого дня) или
'2222222222' => '2026-10-20 18:30'. Когда срок выходит, бан снимается сам и
трогать конфиг не нужно, но войти придётся заново. Шапка называет дату конца
во времени того, кто смотрит. Если дата похожа на настоящую, но такой нет
(2026-02-30), бан считается бессрочным.
Разметка не продублирована: admin.php читает index.html, admin-news.php —
news.html, и оба дописывают шапку, window.NX_ADMIN_PAGE и свою таблицу
стилей (редактор тирлиста ещё и вырезает счётчик статистики). Копия разъехалась бы с
оригиналом на первой же правке вёрстки.
Разница между двумя редакторами есть, и она вынужденная. У тирлиста тулбар
перемешан с публичными кнопками — фильтры, переключатель языка, — поэтому он
остаётся в index.html, а посетителю просто скрыт. У новостей редактор
самостоятельный: модалка на восемь полей и кнопка «+ Новость» вставляются
только admin-news.php, и публичная лента не несёт админской разметки вовсе.
Адрес /admin без завершающего слэша (его снимает 301 в .htaccess) — это
не косметика. Относительные пути внутри index.html и app.js
(assets/dot-green.png, js/html2canvas.min.js) считаются от адреса документа:
на /admin они разрешаются от корня, на /admin/ уехали бы в /admin/assets/
и вернули бы 404.
Прежний адрес панели рекламы /promo-admin.html редиректит на /admin/promo.
Правило стоит в .htaccess до отдачи файлов, поэтому работает и на боевом
сервере, где старый файл ещё лежит: деплой копирует поверх и ничего не удаляет.
Посетитель входит на сайт своим аккаунтом Roblox — по официальному OAuth 2.0
/ OpenID Connect самого Roblox. Логин, пароль и 2FA он вводит на roblox.com,
обратно приходит одноразовый код, и сервер сам забирает по нему ник и аватар.
Своей регистрации с паролем на сайте нет: хранить чужие пароли ради тирлиста
незачем, а «войти через Roblox» здесь и честнее — аккаунт в игре и есть та
личность, которая нужна для трейдов.
| Адрес | Что делает |
|---|---|
/api/roblox_start.php | кладёт в сессию state + PKCE-verifier и уводит на страницу согласия Roblox |
/api/roblox_callback.php | сверяет state, меняет код на токен, забирает профиль, запоминает вход |
/api/session.php | {admin, moderator, user, roblox, roblox_public} — по нему шапка решает, что показать |
/api/logout.php | выход, POST (тот же, что у админки: сессия одна) |
Запрашиваются только openid profile — ник, отображаемое имя и аватар.
Инвентарь и трейды OAuth не отдаёт вовсе: какие у игрока фрукты, узнать
отсюда нельзя, и на это рассчитывать не стоит. Токены Roblox нигде не
сохраняются — access token живёт один запрос userinfo и выбрасывается вместе с
refresh-токеном: нечего хранить — нечего и утекать.
create.roblox.com/dashboard/credentials (на аккаунте
должна быть включена 2FA). Взять Client ID и Client Secret.https://maknemy.com/api/roblox_callback.php.
Roblox сверяет строку целиком: лишний слэш или www. — и кода не будет.
Разрешение: openid, profile.schema.sql (появилась таблица users).config.php (тот, что лежит выше public_html):'roblox_client_id' => '...',
'roblox_client_secret' => '...',
'roblox_redirect_uri' => 'https://maknemy.com/api/roblox_callback.php',
'roblox_login_public' => false,
Пока этих ключей нет, вход выключен целиком: /api/roblox_start.php
отвечает 503, а кнопка профиля в шапке остаётся прежней заглушкой «В активной
разработке». Ничего выкатывать заново для включения не надо — правки конфига
достаточно.
Пока приложение в Roblox не опубликовано, войти могут только 10 разных
аккаунтов — 11-й получит отказ от самого Roblox. Поэтому при
'roblox_login_public' => false кнопку входа видят не все, а только открывшие
сайт по ссылке с ?signin:
https://maknemy.com/?signin
Остальные видят прежнюю заглушку «В активной разработке», и места не
разбирают случайные посетители. js/topbar.js запоминает приглашение в
localStorage (nexus-signin-v1) и убирает signin из адреса: дальше по
сайту параметр не нужен, а скопированный адрес не разнесёт приглашение. Это
ограничение показа кнопки, а не доступа: /api/roblox_start.php приглашения
не проверяет.
После одобрения приложения в config.php ставится
'roblox_login_public' => true — и кнопку видят все. Выкатывать ничего не
нужно. Проверка в браузере: node tools/check-signin-invite-ui.mjs.
state — одноразовый, живёт в серверной сессии и сверяется через
hash_equals. Заявка гасится до всех проверок, поэтому один код нельзя
принести дважды.client_secret перехваченный из адресной
строки код бесполезен без verifier, а тот по сети не ходит.?return=https://зло.example
превращается в / — иначе ссылку «вход на maknemy» можно было бы разослать как
переадресацию на чужой сайт.picture из userinfo
отбрасывается, а не попадает в <img src>.sub — строка из одних цифр. (int)"12abc" дал бы юзера 12, то есть
чужой аккаунт.session_regenerate_id) — подсунутый заранее
PHPSESSID не станет идентификатором авторизованного посетителя./privacy и /terms — политика конфиденциальности и условия использования.
Обе собирает api/lib/legal_page.php: своей вёрстки у них нет, только текст.
Каждая страница несёт обе языковые версии сразу — русскую и следом
английскую с lang="en". Переключатель языка сюда не подключён намеренно:
юридический текст не должен зависеть от того, что лежит в localStorage, а
ревьюер Roblox англоязычный и обязан увидеть английскую версию, ничего не
нажимая.
Шапка на этих страницах урезана до логотипа и двух пилюль назад на сайт —
пятая и шестая копии полного хрома ничего бы не дали, кроме двух новых мест,
где он может разъехаться. Ссылки на обе страницы стоят в общем подвале сайта
(.mk-foot-legal), это требование Roblox: политику должно быть видно с сайта,
а не только по прямой ссылке из карточки приложения.
tests/legal_pages_test.php держит текст в согласии с кодом: если из политики
пропадёт упоминание Google Analytics и его cookie или фраза о том, что
паролей мы не храним, тест упадёт. Документ, разошедшийся с поведением сайта, — это не
устаревший текст, а неправда.
Свежее OAuth-приложение живёт в private mode с потолком 10 уникальных
пользователей. Чтобы потолок сняли, в карточке приложения заполняются
Description, Entry Link (https://maknemy.com/?signin — см. «Вход по
приглашению»), Privacy Policy URL (https://maknemy.com/privacy) и Terms of
Service URL (https://maknemy.com/terms), после чего Edit and Publish →
Review and Publish → Submit for Review.
Форма ревью требует ещё две вещи:
openid и
profile, то есть всё, что просит сайт.Пока ревью идёт, приложение редактировать нельзя, и оно остаётся приватным.
После одобрения вернуть его в private уже не получится, а в config.php
ставится 'roblox_login_public' => true.
Описание приложения проходит через модерацию Roblox, и она глухая: отвечает «contained inappropriate text», не называя слова. Подбирать приходится короткими правками — на формулировке про trading она уже срабатывала.
Разметка на всех четырёх страницах не менялась: кнопка профиля приезжает той же
заглушкой data-soon, а js/topbar.js по ответу /api/session.php делает из
неё аватар с меню («Профиль в Roblox», «Выйти») для вошедшего, ссылку «Войти
через Roblox» — если вход открыт всем или посетитель пришёл по приглашению, а
иначе оставляет заглушкой. Поэтому в PHP-страницах нет ни одного ветвления по
входу — и четвёртой копии условия тоже.
Меню лежит в <body> с position: fixed, а не внутри шапки: у .mk-top стоит
overflow-x: clip с откатом на hidden, и в движках без clip выпадающий
список обрезало бы по краю шапки.
Три места: карусель в середине постера (strip), боковые борта (rail,
только десктоп от 1460×760) и всплывающее окно через 12 секунд после захода
(popup). Статика и анимация (WebP/GIF) смешиваются свободно.
| Файл | Что делает |
|---|---|
js/promo.js | расписание, взвешенная ротация, частотный лимит окна. Без DOM — тестируется в node |
api/promo.php | хранение кампаний, отдельная таблица promo, свой rev |
admin-promo.php + js/promo-admin.js | панель /admin/promo: список кампаний, загрузка макетов, предпросмотр |
docs/mediakit/ | коммерческое предложение рекламодателю: места, пакеты, спецификация, собранный PDF. Вне public_html — на сайт не публикуется, файл отдаётся клиенту напрямую. Цены — заглушки, правятся в блоке PRICES в конце mediakit.html |
Кампании принципиально не лежат в state. Причины записаны в шапке
api/promo.php: всё, что попадает в блоб тирлиста, проходит через
save_image_bytes() и ужимается до 256 px; сохранение блоба работает по
принципу «последний записал — тот и прав»; а сам блоб отдаётся с годовым
immutable-кэшем, и смена баннера заставляла бы каждого посетителя качать
тирлист заново.
Файл называется promo.js, а не ads.js, классы — ptn-*, а не ad-*:
##.ad-block и /js/ads.js — стандартные правила блокировщиков рекламы.
Новая таблица заводится тем же schema.sql. Пока он не прогнан, сайт работает:
promo_load() и state.php читают отсутствие таблицы как «кампаний нет».
Локальный показ без PHP (файлы в .gitignore, на боевой сайт не уезжают):
__seed.local.html кладёт снимок боевой базы в localStorage
__promo.local.html кладёт демо-кампании и открывает сайт
Иконки предметов хранятся с длинной стороной не больше 256 px
(ICON_MAX_SIDE в api/lib/images.php). Порог не косметический: на экране
иконка занимает максимум ~130 device-px, а Safari держит в памяти
распакованный битмап целиком. Исходники по 800×800 давали 63 МБ битмапов на
полный тирлист — вкладка начинала выбрасывать декодированные картинки, и
иконки пропадали прямо во время прокрутки.
Сжатие применяется при загрузке автоматически. Картинки, загруженные раньше, переводятся разово:
php tools/downscale-images.php --dry-run # только отчёт
php tools/downscale-images.php # сжать и перепривязать
На боевом хостинге config.php лежит над веб-рутом, вне клона репозитория,
поэтому туда нужно указать путь явно:
php tools/downscale-images.php --config=/home/maknemyt/config.php --dry-run
Сайт работает на PHP 7.4 — писать код нужно под неё.
Локально почти наверняка стоит восьмёрка, и это ловушка: php -l проверяет
только синтаксис, а функции 8.0+ синтаксически безупречны на любой версии.
str_contains() спокойно линтуется, проходит весь прогон тестов на машине
разработчика — и падает Call to undefined function уже на бою.
Держит это tests/php74_compat_test.php: он разбирает все .php в
public_html/ токенайзером и роняет прогон, если встретит функцию или
синтаксис новее 7.4. Список запрещённого лежит в самом тесте — если однажды
на бою вылезет что-то мимо него, допишите туда вместе с починкой.
Из-под ограничения выведены tests/ и .claude/: по .cpanel.yml
публикуется только public_html/, на сервер они не уезжают. Тесты всё же
проверяются тем же стражем — прогон могут запустить и на 7.4.
.htaccess отдаёт на все ответы заголовок CSP. Главное в нём — script-src 'self' https://www.googletagmanager.com
без 'unsafe-inline':
если в чат, объявление или профиль пролезет разметка со скриптом или
onerror=, браузер её не исполнит, и чужой код не сможет действовать от имени
вошедшего админа.
Из этого правила для кода:
<script>…</script> и обработчиков on*= в
разметке — их браузер молча не исполнит. Данные для скриптов передаются
через <meta> (nx-rev в тирлисте, nx-post в новостях) или отдельным
файлом (js/admin-flag.js). <script type="application/ld+json"> —
данные, их можно.metrika_counter_html() печатает его
блок (js/ga.js, номер — GA_ID в api/lib/google_analytics.php; пустой
номер выключает GA). Цели по кликам на рекламу шлёт PROMO.goal() из
js/promo.js. Яндекс Метрику сняли 2026-10-20; имя функции осталось от неё.Всё это держит tests/csp_test.php. Если после деплоя что-то сломалось,
полный откат — закомментировать строку Header always set Content-Security-Policy в .htaccess.
bash tests/run_all.sh
PHP-тесты покрывают API, node-тесты (tests/*_test.mjs) — словарь интерфейса,
язык описаний, марки полос тиров, логику показа рекламы и защиту от
копирования. Если node не установлен, его часть пропускается с явным
предупреждением.
tests/images_test.php и tests/creatives_test.php требуют GD. В XAMPP
расширение выключено в php.ini, и без него эти тесты дают ложные падения.
run_all.sh берёт бинарник из $PHP одним словом, поэтому флаг в
переменную не запихнуть — нужна обёртка:
printf '#!/usr/bin/env bash\nexec /c/xampp/php/php.exe -d extension=gd "$@"\n' > /tmp/php-gd.sh
chmod +x /tmp/php-gd.sh
PHP=/tmp/php-gd.sh bash tests/run_all.sh