Files
sasheg dev d2a0a7a93b Вход без пароля, права админа и F3+B
Аутентификация переделана по схеме пользователя: лаунчер — единственный
источник токена, сервер проверяет его сам и лаунчера не спрашивает.

- rsx-auth/GameToken: токен это base64url(payload) + "." + base64url(HMAC-SHA256),
  payload {uuid, nick, aud, exp, jti, typ}, typ = session или reload.
- Сессия живёт час, reload-токен полтора часа и только внутри игры. Пакет
  SessionReload продлевает сессию и выдаёт новый reload-токен.
- Сервер проверяет подпись, аудиенс, срок и совпадение uuid. HTTP-запрос к
  лаунчеру из сервера убран.
- Клиент: окно пароля убрано совсем, ник берётся из токена, клиент сам
  продлевает сессию за 5 минут до конца.
- Для локальной разработки сервер с --dev-token пишет свой токен в
  world/dev-token.txt, клиент берёт токен оттуда.
- Права админа: свой список world/admins.txt, перечитывается раз в 5 секунд,
  поэтому право снимается на лету. Пакет AdminState.
- Ванильные блоки мира ломает только админ, режим игры прав не даёт.
- F3+B только для админов: без прав показываем отказ, с правом включаются и
  ванильные хитбоксы, и наш отладочный рендер. Права сняли посреди просмотра —
  гаснут сразу.
- run-game.ps1 умеет -Token.

Проверено на живом сервере: own session ok, player in, rights of Player given.
Тесты: протокол 34, сервер 88, форк собирается чисто.
2026-10-22 08:51:27 +03:00

277 lines
9.8 KiB
Java

package net.rustnotmc.client;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.client.renderer.debug.DebugRenderer;
import net.minecraft.entity.Entity;
import net.rustnotmc.protocol.Hitbox;
import net.rustnotmc.protocol.Packets;
/**
* Draws the boxes of the models.
*
* The boxes are the editor: the server reads them from a file and sends them
* here, and they are drawn around the things they belong to. A model that
* does not fit is changed in the file, the file is read again, and the new
* numbers are on the screen.
*
* The command is:
*
* <pre>
* /rsx hitboxes on show the boxes
* /rsx hitboxes off hide them
* /rsx hitboxes reload ask the server to read its file again
* /rsx hitboxes list what the server knows
* /rsx hitboxes show NAME put one model in front of the player
* </pre>
*/
public final class RsxHitboxDebug {
/** The model whose boxes describe a player. */
private static final String PLAYER_MODEL = "player";
/** The height the player model is written for. */
private static final float PLAYER_MODEL_HEIGHT = 1.8F;
private static final Map<String, Packets.HitboxModel> MODELS =
new LinkedHashMap<String, Packets.HitboxModel>();
private static final List<Packets.HitboxInstance> INSTANCES =
new ArrayList<Packets.HitboxInstance>();
private static boolean visible;
private static String preview;
private RsxHitboxDebug() {
}
/** Takes the boxes of the models from the server. */
public static void accept(Packets.Hitboxes boxes) {
MODELS.clear();
for (int i = 0; i < boxes.models.length; i++) {
MODELS.put(boxes.models[i].name, boxes.models[i]);
}
}
/** Takes the places where the models stand. */
public static void accept(Packets.HitboxInstances instances) {
INSTANCES.clear();
for (int i = 0; i < instances.instances.length; i++) {
INSTANCES.add(instances.instances[i]);
}
}
/** True when the boxes are being drawn. */
public static boolean visible() {
return visible;
}
/**
* Shows or hides the boxes.
*
* The right to look at them belongs to the administrators, so when the
* right is taken away the boxes go at once.
*/
public static void setVisible(boolean value) {
visible = value;
if (!value) {
preview = null;
}
}
/** How many models the server has sent. */
public static int modelCount() {
return MODELS.size();
}
/**
* Carries out one of our commands.
*
* Returns true when the line was ours, so that the mods do not see it.
*/
public static boolean command(String line) {
if (line == null || !line.startsWith("/")) {
return false;
}
String[] parts = line.substring(1).trim().split("\\s+");
if (parts.length < 2 || !"rsx".equalsIgnoreCase(parts[0])
|| !"hitboxes".equalsIgnoreCase(parts[1])) {
return false;
}
String what = parts.length > 2 ? parts[2].toLowerCase() : "";
if ("on".equals(what)) {
visible = true;
say("hitboxes on (" + MODELS.size() + " models)");
return true;
}
if ("off".equals(what)) {
visible = false;
say("hitboxes off");
return true;
}
if ("reload".equals(what)) {
net.rustnotmc.client.net.RsxNetworkManager.sendHitboxesReload();
say("the server was asked to read its file again");
return true;
}
if ("list".equals(what)) {
StringBuilder names = new StringBuilder();
for (String name : MODELS.keySet()) {
if (names.length() > 0) {
names.append(", ");
}
names.append(name);
}
say("models: " + (names.length() == 0 ? "none" : names.toString()));
return true;
}
if ("show".equals(what)) {
if (parts.length < 4) {
say("usage: /rsx hitboxes show NAME");
return true;
}
preview = parts[3];
visible = true;
say("showing " + preview);
return true;
}
say("usage: /rsx hitboxes on|off|reload|list|show NAME");
return true;
}
private static void say(String message) {
net.rustnotmc.loader.Log.info("hitboxes: " + message);
Minecraft mc = Minecraft.getMinecraft();
if (mc.ingameGUI != null) {
mc.ingameGUI.addChatMessage(net.minecraft.util.text.ChatType.CHAT,
new net.minecraft.util.text.TextComponentString(message));
}
}
/**
* Draws the boxes.
*
* Called from the frame hook, so this is after the world and after the
* interface: the lines are drawn over both, which is what a debug view
* should do.
*/
public static void draw(Minecraft mc) {
if (!visible || mc.player == null || mc.world == null) {
return;
}
Entity view = mc.getRenderViewEntity();
if (view == null) {
return;
}
float partial = mc.getRenderPartialTicks();
double camX = view.lastTickPosX + (view.posX - view.lastTickPosX) * partial;
double camY = view.lastTickPosY + (view.posY - view.lastTickPosY) * partial;
double camZ = view.lastTickPosZ + (view.posZ - view.lastTickPosZ) * partial;
GlStateManager.enableBlend();
GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA,
GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
GlStateManager.glLineWidth(2.0F);
GlStateManager.disableTexture2D();
GlStateManager.depthMask(false);
for (int i = 0; i < INSTANCES.size(); i++) {
Packets.HitboxInstance instance = INSTANCES.get(i);
Packets.HitboxModel model = MODELS.get(instance.model);
if (model == null) {
continue;
}
drawModel(model, instance.x, instance.y, instance.z, instance.yaw,
camX, camY, camZ, 1.0F);
}
if (preview != null) {
Packets.HitboxModel model = MODELS.get(preview);
if (model != null) {
EntityPlayerSP player = mc.player;
drawModel(model, player.posX + 1.0D, player.posY, player.posZ,
player.rotationYaw, camX, camY, camZ, 1.0F);
}
}
// and the players, with the zones the damage is counted by
Packets.HitboxModel player = MODELS.get(PLAYER_MODEL);
if (player != null) {
List<Entity> players = mc.world.getLoadedEntityList();
for (int i = 0; i < players.size(); i++) {
Entity entity = players.get(i);
if (!(entity instanceof EntityPlayerSP)) {
continue;
}
float scale = entity.height / PLAYER_MODEL_HEIGHT;
drawModel(player, entity.posX, entity.posY, entity.posZ,
entity.rotationYaw, camX, camY, camZ, scale);
}
}
GlStateManager.depthMask(true);
GlStateManager.enableTexture2D();
GlStateManager.disableBlend();
}
/**
* Draws one model.
*
* The boxes of the model are turned by the yaw of the thing and moved to
* where it stands, and then drawn relative to the camera, because that is
* how the world is drawn too.
*/
private static void drawModel(Packets.HitboxModel model, double x, double y, double z,
float yaw, double camX, double camY, double camZ, float scale) {
double cos = Math.cos(Math.toRadians(yaw));
double sin = Math.sin(Math.toRadians(yaw));
for (int i = 0; i < model.boxes.length; i++) {
Hitbox box = model.boxes[i];
float[] colour = colourOf(box.part);
double minX = turn(box.minX * scale, box.minZ * scale, cos, sin);
double minZ = turn(box.minZ * scale, -box.minX * scale, cos, sin);
double maxX = turn(box.maxX * scale, box.maxZ * scale, cos, sin);
double maxZ = turn(box.maxZ * scale, -box.maxX * scale, cos, sin);
double minY = box.minY * scale;
double maxY = box.maxY * scale;
RenderGlobal.drawBoundingBox(
x + minX - camX, y + minY - camY, z + minZ - camZ,
x + maxX - camX, y + maxY - camY, z + maxZ - camZ,
colour[0], colour[1], colour[2], 0.9F);
if (!box.name.isEmpty()) {
DebugRenderer.renderDebugText(box.name,
x + minX, y + maxY, z + minZ, 0.02F, 0xFFFFFFFF);
}
}
}
/** Turns a point around the vertical axis. */
private static double turn(double along, double across, double cos, double sin) {
return along * cos + across * sin;
}
/** The colour of a box: the head is red, the body yellow, the legs blue. */
private static float[] colourOf(String part) {
if (Hitbox.PART_HEAD.equals(part)) {
return new float[]{1.0F, 0.25F, 0.25F};
}
if (Hitbox.PART_BODY.equals(part)) {
return new float[]{1.0F, 0.9F, 0.2F};
}
if (Hitbox.PART_LEGS.equals(part)) {
return new float[]{0.3F, 0.55F, 1.0F};
}
return new float[]{0.3F, 1.0F, 0.4F};
}
}