Панель пинга в углу экрана
Рисуется средствами самой игры: мост отдаёт команды её собственному батчу, с её шрифтовым атласом и её масштабом интерфейса. Поэтому панель не выглядит чужеродной и меняет размер вместе с настройкой интерфейса. Гаснет вместе с HUD по той же клавише, что и компас, и мод об этой клавише не знает: хук живёт в отрисовке компаса, а компас не рисуется, когда HUD скрыт. Пинг считает мост: вендорский сетевой слой не хранит задержку, а единственный сигнал, доступный на любом сервере, - отклонение интервала keep-alive от собственного расписания сервера. Число помечено "~": это оценка, а не круговой оборот. Конфиг: enabled, opacity, position, offsetX/Y, lossAbove.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# сборка
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# зависимости приходят из модлоадера и rsxm-lib
|
||||||
|
libs/
|
||||||
@@ -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 при старте игры.
|
||||||
@@ -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%
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
Manifest-Version: 1.0
|
||||||
|
Rsx-Mod-Entrypoint: com.rsxm.hud.HudMod
|
||||||
|
|
||||||
|
|
||||||
@@ -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};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user