commit b45e47fd476c1178ed5934aaaf0997759196406a Author: SashegDev Date: Sat Oct 17 03:45:23 2026 +0300 Панель пинга в углу экрана Рисуется средствами самой игры: мост отдаёт команды её собственному батчу, с её шрифтовым атласом и её масштабом интерфейса. Поэтому панель не выглядит чужеродной и меняет размер вместе с настройкой интерфейса. Гаснет вместе с HUD по той же клавише, что и компас, и мод об этой клавише не знает: хук живёт в отрисовке компаса, а компас не рисуется, когда HUD скрыт. Пинг считает мост: вендорский сетевой слой не хранит задержку, а единственный сигнал, доступный на любом сервере, - отклонение интервала keep-alive от собственного расписания сервера. Число помечено "~": это оценка, а не круговой оборот. Конфиг: enabled, opacity, position, offsetX/Y, lossAbove. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..398c07c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# сборка +build/ +dist/ +*.class + +# зависимости приходят из модлоадера и rsxm-lib +libs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..a25c7f1 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# rsxm-hud + +Панель пинга в углу экрана, нарисованная средствами самой игры. + +## Как это выглядит + +Тёмная полупрозрачная плашка в левом верхнем углу: `ping 42 ms`, а потери +добавляются справа, только когда они больше порога. Палитра, отступы и масштаб - +те же, что у компаса и полосок еды и здоровья, потому что рисует их тот же код +игры. + +## Как это работает + +Мод не рисует ничего сам. Он говорит мосту «нарисуй», а мост отдаёт команды +собственной системе отрисовки игры - тот же батч, тот же шрифтовый атлас, тот +же масштаб интерфейса. Поэтому панель не выглядит чужеродной и меняет размер +вместе с настройкой интерфейса. + +Панель гаснет вместе с HUD по той же клавише, что и компас: мод не знает про +эту клавишу вообще. Хук живёт в отрисовке компаса, а компас не рисуется, когда +HUD скрыт. + +## Пинг + +Вендор заменил сетевой слой на свой, поля задержки в нём нет, и игра пинг нигде +не считает. Считает мост, по одному доступному везде сигналу: сервер шлёт +keep-alive раз в 15 секунд по своему расписанию, и отклонение интервала прихода - +это сеть. Число помечено `~`, потому что это оценка, а не измеренный круговой +оборот; она сходится за несколько выборок. + +## Настройки + +`%LOCALAPPDATA%\rsx-mod\config\rsxm-hud.json`: + + enabled главный выключатель, независимый от клавиши HUD + opacity 0.05..1, прозрачность плашки и текста + position top-left | top-right | bottom-left | bottom-right + offsetX/Y отступ от края + lossAbove показывать потери, только если они выше этого процента + +## Сборка + + build.cmd + +Нужен только JDK. Компилируется против `rsx-api.jar` и `rsxm-lib.jar`, но не +содержит ни одного: библиотека лежит в `libs\` и подмешивается в системный +class path при старте игры. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..3c22cba --- /dev/null +++ b/build.cmd @@ -0,0 +1,38 @@ +@echo off +rem Builds the mod and drops the jar into the client's mods folder. +rem Only a JDK is needed: no Gradle, no network, nothing but javac and jar. +setlocal +cd /d "%~dp0" + +if not exist "build\classes" mkdir "build\classes" +if not exist "build\mods" mkdir "build\mods" + +set ENTRY=com.example.Diagnostics + +rem where the modloader looks for mods; override with set MODS_DIR=... +if "%MODS_DIR%"=="" set MODS_DIR=%LOCALAPPDATA%\rsx-mod\mods + +echo [1/3] compiling... +dir /s /b src\main\java\*.java > build\sources.txt +javac -encoding UTF-8 --release 8 -nowarn -cp "libs\rsx-api.jar;libs\rsxm-lib.jar" -d build\classes @build\sources.txt +if errorlevel 1 ( + echo. + echo ?????? ?? ???????. + exit /b 1 +) + +echo [2/3] packaging rsxm-hud.jar... +if exist "build\mods\rsxm-hud.jar" del "build\mods\rsxm-hud.jar" +jar cfm "build\mods\rsxm-hud.jar" manifest.txt -C build/classes . +if errorlevel 1 exit /b 1 + +echo [3/3] installing... +if not exist "%MODS_DIR%" ( + echo. + echo ????? ????? ?? ???????: %MODS_DIR% + echo ???????? build\mods\rsxm-hud.jar ? ??? ???. + exit /b 0 +) +copy /y "build\mods\rsxm-hud.jar" "%MODS_DIR%\" >nul +echo. +echo ??????: build\mods\rsxm-hud.jar ?????????? ? %MODS_DIR% diff --git a/manifest.txt b/manifest.txt new file mode 100644 index 0000000..cc0f746 --- /dev/null +++ b/manifest.txt @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Rsx-Mod-Entrypoint: com.rsxm.hud.HudMod + + diff --git a/src/main/java/com/rsxm/hud/HudMod.java b/src/main/java/com/rsxm/hud/HudMod.java new file mode 100644 index 0000000..7ec02ee --- /dev/null +++ b/src/main/java/com/rsxm/hud/HudMod.java @@ -0,0 +1,132 @@ +package com.rsxm.hud; + +import net.rustexremake.api.Rsx; +import net.rustexremake.api.RsxMod; +import net.rsxm.lib.config.Config; + +/** + * Панель пинга в углу экрана. + * + * Рисуется не своим кодом, а через API игры: та же система команд, тот же + * масштаб интерфейса, те же шрифтовые текстуры. Поэтому панель выглядит частью + * игры и исчезает вместе с ней - по той же клавише, что и HUD, без единой строки + * про эту клавишу здесь. + * + * Настройки живут в config\rsxm-hud.json: enabled, opacity, положение и + * порог, после которого потери вообще показываются. + */ +public class HudMod implements RsxMod { + + private static final String MOD = "rsxm-hud"; + + // палитра игры: очень тёмная подложка, почти белый текст, акцент + private static final int PANEL = 0x20201C; + private static final int TEXT = 0xECE3DB; + private static final int MUTED = 0x94918C; + private static final int GOOD = 0x6B8A54; + private static final int BAD = 0xC0704A; + + private boolean enabled = true; + private float opacity = 0.85f; + private String position = "top-left"; + private float offsetX = 8f; + private float offsetY = 8f; + private float lossAbove = 0.1f; + + @Override + public int apiVersion() { + return Rsx.API_VERSION; + } + + @Override + public void onLoad() { + Config cfg = Config.of(MOD) + .set("enabled", true) + .set("opacity", 0.85) + .set("position", "top-left") + .set("offsetX", 8) + .set("offsetY", 8) + .set("lossAbove", 0.1); + read(cfg); + + Rsx.info("панель HUD: " + (enabled ? "включена" : "выключена") + + ", прозрачность " + Math.round(opacity * 100) + "%"); + Rsx.info("пинг: " + Rsx.pingLine()); + + Rsx.onHud(new Rsx.HudDraw() { + @Override + public void draw(Rsx.Hud hud) { + if (enabled && hud.available()) { + paint(hud); + } + } + }); + } + + private void read(Config cfg) { + enabled = cfg.bool("enabled", true); + opacity = (float) cfg.decimal("opacity", 0.85); + if (opacity < 0.05) { + opacity = 0.05f; + } + if (opacity > 1.0) { + opacity = 1.0f; + } + position = cfg.text("position", "top-left"); + offsetX = cfg.integer("offsetX", 8); + offsetY = cfg.integer("offsetY", 8); + lossAbove = (float) cfg.decimal("lossAbove", 0.1); + } + + private void paint(Rsx.Hud hud) { + int ping = Rsx.ping(); + float loss = Rsx.packetLoss(); + boolean exact = Rsx.pingExact(); + + // пока нечем показать - не занимаем место на экране + if (ping < 0 && loss <= 0f) { + return; + } + + String pingText = ping < 0 ? "--" + : (exact ? "" : "~") + ping; + String lossText = loss > lossAbove + ? String.format(java.util.Locale.US, "%.1f%%", loss) + : null; + + String label = "ping " + pingText + " ms"; + int labelWidth = (int) hud.textWidth(label); + int lossWidth = lossText == null ? 0 : (int) hud.textWidth(lossText); + int pad = 4; + int width = labelWidth + lossWidth + pad * 2; + int height = 9 + pad; + + float[] at = place(hud, width, height); + float x = at[0]; + float y = at[1]; + + hud.rect(x, y, width, height, PANEL, opacity); + + int colour = lossText == null ? TEXT : BAD; + hud.text(x + pad, y + pad * 0.5f, label, colour, opacity); + if (lossText != null) { + hud.text(x + pad + labelWidth + pad, y + pad * 0.5f, lossText, + BAD, opacity); + } + } + + /** Where the panel goes, in the game's own HUD units. */ + private float[] place(Rsx.Hud hud, int width, int height) { + if ("top-right".equals(position)) { + return new float[] {hud.width() - width - offsetX, offsetY}; + } + if ("bottom-left".equals(position)) { + return new float[] {offsetX, hud.height() - height - offsetY}; + } + if ("bottom-right".equals(position)) { + return new float[] {hud.width() - width - offsetX, + hud.height() - height - offsetY}; + } + return new float[] {offsetX, offsetY}; + } +}