Панель пинга в углу экрана

Рисуется средствами самой игры: мост отдаёт команды её собственному батчу, с её
шрифтовым атласом и её масштабом интерфейса. Поэтому панель не выглядит
чужеродной и меняет размер вместе с настройкой интерфейса.

Гаснет вместе с HUD по той же клавише, что и компас, и мод об этой клавише не
знает: хук живёт в отрисовке компаса, а компас не рисуется, когда HUD скрыт.

Пинг считает мост: вендорский сетевой слой не хранит задержку, а единственный
сигнал, доступный на любом сервере, - отклонение интервала keep-alive от
собственного расписания сервера. Число помечено "~": это оценка, а не круговой
оборот.

Конфиг: enabled, opacity, position, offsetX/Y, lossAbove.
This commit is contained in:
SashegDev
2026-10-17 03:45:23 +03:00
commit b45e47fd47
5 changed files with 228 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# сборка
build/
dist/
*.class
# зависимости приходят из модлоадера и rsxm-lib
libs/
+47
View File
@@ -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 при старте игры.
+38
View File
@@ -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%
+4
View File
@@ -0,0 +1,4 @@
Manifest-Version: 1.0
Rsx-Mod-Entrypoint: com.rsxm.hud.HudMod
+132
View File
@@ -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};
}
}