Skip to content

Commit 554c0b6

Browse files
committed
Add account status badge and addon API across Forge targets
1 parent de509d4 commit 554c0b6

45 files changed

Lines changed: 969 additions & 342 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,21 @@ Allows a short same-IP reconnect grace period after a verified player disconnect
159159
auth.showJoinFeedback = true
160160
```
161161

162-
Show join feedback Title/chat messages for premium, skin-site, offline fallback, and single-player states. Set to `false` to silence those messages without changing authentication or skin refresh behavior.
162+
Show the join feedback chat message for premium, skin-site, offline fallback, and single-player states. Set to `false` to silence it without changing authentication or skin refresh behavior.
163+
164+
```toml
165+
auth.showJoinTitle = false
166+
```
167+
168+
Additionally show a full-screen title/subtitle on join. Off by default, because the persistent account-status overlay below already reports the same state without interrupting the screen. Set to `true` to restore the old title behavior.
163169

164170
```toml
165171
auth.showAccountOverlay = true
166172
```
167173

168-
Shows a small top-left client badge after a TrueUUID handshake. Green means
169-
premium verified; red means the server accepted its configured offline
170-
fallback. It is client-local and only appears after a TrueUUID-enabled server
171-
has responded.
174+
Shows a small client-side badge after a TrueUUID handshake: green `Premium` or
175+
red `Offline`, drawn as plain corner text with no backdrop. It is client-local
176+
and only appears after a TrueUUID-enabled server has responded.
172177

173178
Default feedback and disconnect messages are sent as Minecraft translation keys and rendered by the player's client language files (`en_us` / `zh_cn`). If you previously generated a config with custom bilingual strings, change those message values back to `trueuuid.*` keys or regenerate the config to use client-side localization.
174179

@@ -190,6 +195,60 @@ auth.yggdrasil.apiRootWhitelist = []
190195

191196
Allowlist for Yggdrasil/authlib-injector `hasJoined` hosts. An empty list rejects all client-reported endpoints and keeps Mojang as the safe default. Add an exact host such as `"littleskin.cn"`, or an explicit wildcard such as `"*.example.com"`. Custom endpoints must pass HTTPS/443, path, DNS/IP, response-size, timeout, and no-redirect checks.
192197

198+
The account-status badge position is configurable:
199+
200+
```toml
201+
auth.overlayCorner = "bottom_right"
202+
auth.overlayOffsetX = 0
203+
auth.overlayOffsetY = 0
204+
auth.overlayScale = 1.25
205+
```
206+
207+
`overlayCorner` accepts `top_left`, `top_right`, `bottom_left`, or `bottom_right`.
208+
`bottom_right` is the default because vanilla keeps status effects and
209+
advancement toasts in the top right, chat in the bottom left, and other mods
210+
commonly claim the top left. The offsets nudge the badge by that many pixels
211+
(positive = right/down) if it still collides with another mod's HUD.
212+
`overlayScale` sizes the padlock and label together; whole numbers (`1.0`, `2.0`)
213+
keep Minecraft's bitmap font perfectly crisp, values in between are slightly soft.
214+
215+
These options behave identically on every supported target.
216+
217+
## Addon API
218+
219+
TrueUUID exposes a small server-side API (`cn.alini.trueuuid.api`) so other mods
220+
can branch on whether an online player authenticated as premium or through
221+
offline fallback — for example, sending offline players to a separate spawn.
222+
223+
`AccountStatus` is one of `PREMIUM_VERIFIED`, `ONLINE_MODE`, `OFFLINE_FALLBACK`,
224+
or `UNKNOWN` (with `isPremium()` / `isOffline()` helpers).
225+
226+
Query a player's status at any time while they are online:
227+
228+
```java
229+
import cn.alini.trueuuid.api.TrueuuidApi;
230+
import cn.alini.trueuuid.api.AccountStatus;
231+
232+
AccountStatus status = TrueuuidApi.getStatus(serverPlayer);
233+
if (status.isOffline()) {
234+
// e.g. restrict permissions, or teleport to an offline-only world
235+
}
236+
```
237+
238+
Or register a callback fired on the server thread the moment a player's status
239+
is known at join (register once during your mod's setup):
240+
241+
```java
242+
TrueuuidApi.registerLoginCallback((player, status) -> {
243+
if (status.isOffline()) {
244+
// first-join spawn handling for offline accounts
245+
}
246+
});
247+
```
248+
249+
Also available: `TrueuuidApi.isKnownPremiumName(name)` and
250+
`TrueuuidApi.getPremiumUuid(name)` for the persistent verified-name registry.
251+
193252
## Compatibility Notes
194253

195254
- Proxies: Mojang's `hasJoined` IP parameter is optional. Verification can still work when the real client IP is hidden by a proxy.

‎platform/forge-1.20.1/src/main/java/cn/alini/trueuuid/client/ClientAccountStatus.java‎

Lines changed: 153 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,82 @@
1111
import net.minecraftforge.eventbus.api.SubscribeEvent;
1212
import net.minecraftforge.fml.common.Mod;
1313

14-
/** Client-local account state derived from the TrueUUID login handshake. */
14+
/**
15+
* Client-local account badge: a closed padlock + green "Premium", or an open
16+
* padlock + red "Offline".
17+
*
18+
* <p>1.20.1 is an independent adapter, so this mirrors the modern Forge line's
19+
* {@code platform/forge-common} badge deliberately — same sprite, colours, config
20+
* and layout — so every supported target looks identical. Keep the two in step
21+
* when either changes. The padlock is painted with {@code fill} rather than a blit
22+
* texture to match that shared implementation.
23+
*/
1524
@Mod.EventBusSubscriber(modid = Trueuuid.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
1625
public final class ClientAccountStatus {
1726
private static volatile Status status = Status.NONE;
1827

28+
// Sprites generated from the source PNGs. Palette: 1-4 shackle, 5/6/8/9 body,
29+
// 7 body highlight, g/G green check, r/R red cross. ' ' is transparent.
30+
// Both frames share one 11x16 grid: the open shackle sits a pixel higher, and
31+
// a shared grid keeps the bodies aligned so the badge does not jump.
32+
private static final String[] LOCK_CLOSED = {
33+
" ",
34+
" 11111 ",
35+
" 1233331 ",
36+
" 121111431 ",
37+
" 121 431 ",
38+
"55555555556",
39+
"57777777786",
40+
"57999999g56",
41+
"5799999gG56",
42+
"57g999gG956",
43+
"57Gg9gG9956",
44+
"579GgG99956",
45+
"5799G999956",
46+
"57999999956",
47+
"68555555556",
48+
"66666666666",
49+
};
50+
private static final String[] LOCK_OPEN = {
51+
" 11111 ",
52+
" 1233331 ",
53+
" 121111431 ",
54+
" 121 431 ",
55+
" 121 ",
56+
"55555555556",
57+
"57777777786",
58+
"579R999R956",
59+
"57RrR9RrR56",
60+
"579RrRrR956",
61+
"5799RrR9956",
62+
"579RrRrR956",
63+
"57RrR9RrR56",
64+
"579R999R956",
65+
"68555555556",
66+
"66666666666",
67+
};
68+
69+
private static final int ICON_W = 11;
70+
private static final int ICON_H = 16;
71+
private static final int GAP = 3;
72+
private static final int MARGIN = 4;
73+
/**
74+
* Rows spanned by the padlock body, the shackle sitting above it. The label is
75+
* centred on the body rather than on the whole sprite: the sprite's midpoint
76+
* lands inside the shackle, which makes the text visibly ride high.
77+
*/
78+
private static final int BODY_TOP = 5;
79+
private static final int BODY_BOTTOM = 16;
80+
/**
81+
* Label scale relative to the padlock. The art is {@link #ICON_H}px tall while
82+
* Minecraft's font is 8px, so the lock reads larger than its label. Fix that by
83+
* redrawing the source PNGs at roughly the font's height and regenerating the
84+
* sprite below -- do not resample here: the art has one-pixel shackle lines and
85+
* a one-pixel mark, so nearest-neighbour shreds them and a smooth filter blends
86+
* the green/red mark into the yellow body.
87+
*/
88+
private static final float TEXT_SCALE = 1.0F;
89+
1990
public static void markPremium() { status = Status.PREMIUM; }
2091
public static void markOffline() { status = Status.OFFLINE; }
2192

@@ -27,18 +98,91 @@ public static void registerOverlay(RegisterGuiOverlaysEvent event) {
2798
private static void render(ForgeGui gui, GuiGraphics graphics, float partialTick, int width, int height) {
2899
Minecraft minecraft = Minecraft.getInstance();
29100
if (!TrueuuidConfig.showAccountOverlay() || minecraft.player == null || status == Status.NONE) return;
30-
Component text = Component.translatable("trueuuid.overlay.label", Component.translatable(status.translationKey));
31-
int textWidth = minecraft.font.width(text);
32-
int x = 6;
33-
int y = 6;
34-
graphics.fill(x - 3, y - 3, x + textWidth + 3, y + 12, 0xA0000000);
35-
graphics.drawString(minecraft.font, text, x, y, status.color, true);
101+
102+
Component text = Component.translatable(status.translationKey);
103+
float lockScale = TrueuuidConfig.overlayScale();
104+
float textScale = lockScale * TEXT_SCALE;
105+
106+
// Lay the badge out in screen pixels, since the padlock and the label are
107+
// drawn at different scales.
108+
float lockWidth = ICON_W * lockScale;
109+
float gapWidth = GAP * lockScale;
110+
float textWidth = minecraft.font.width(text) * textScale;
111+
float drawnWidth = lockWidth + gapWidth + textWidth;
112+
float drawnHeight = ICON_H * lockScale;
113+
114+
boolean right = TrueuuidConfig.overlayCorner().endsWith("right");
115+
boolean bottom = TrueuuidConfig.overlayCorner().startsWith("bottom");
116+
float screenX = (right ? width - MARGIN - drawnWidth : MARGIN) + TrueuuidConfig.overlayOffsetX();
117+
float screenY = (bottom ? height - MARGIN - drawnHeight : MARGIN) + TrueuuidConfig.overlayOffsetY();
118+
119+
// Each element is drawn inside its own scale and positioned by converting
120+
// the screen anchor back into that scale's units.
121+
graphics.pose().pushPose();
122+
try {
123+
graphics.pose().scale(lockScale, lockScale, 1.0F);
124+
drawSprite(graphics, Math.round(screenX / lockScale), Math.round(screenY / lockScale),
125+
status == Status.PREMIUM ? LOCK_CLOSED : LOCK_OPEN);
126+
} finally {
127+
graphics.pose().popPose();
128+
}
129+
130+
float bodyCentre = screenY + (BODY_TOP + BODY_BOTTOM) / 2.0F * lockScale;
131+
float textTop = bodyCentre - minecraft.font.lineHeight * textScale / 2.0F;
132+
graphics.pose().pushPose();
133+
try {
134+
graphics.pose().scale(textScale, textScale, 1.0F);
135+
graphics.drawString(minecraft.font, text,
136+
Math.round((screenX + lockWidth + gapWidth) / textScale),
137+
Math.round(textTop / textScale),
138+
0xFF000000 | status.color, true);
139+
} finally {
140+
graphics.pose().popPose();
141+
}
142+
}
143+
144+
/** Paints one pixel-art sprite, coalescing each row into horizontal runs. */
145+
private static void drawSprite(GuiGraphics graphics, int x, int y, String[] rows) {
146+
for (int row = 0; row < rows.length; row++) {
147+
String line = rows[row];
148+
int column = 0;
149+
while (column < line.length()) {
150+
char pixel = line.charAt(column);
151+
if (pixel == ' ') {
152+
column++;
153+
continue;
154+
}
155+
int run = 1;
156+
while (column + run < line.length() && line.charAt(column + run) == pixel) run++;
157+
graphics.fill(x + column, y + row, x + column + run, y + row + 1, colorOf(pixel));
158+
column += run;
159+
}
160+
}
161+
}
162+
163+
private static int colorOf(char pixel) {
164+
return switch (pixel) {
165+
case '1' -> 0xFF575C71;
166+
case '2' -> 0xFFC9CBD8;
167+
case '3' -> 0xFFACB0C1;
168+
case '4' -> 0xFF3F4656;
169+
case '5' -> 0xFFB26411;
170+
case '6' -> 0xFF752702;
171+
case '7' -> 0xFFFDF55F;
172+
case '8' -> 0xFFDC9613;
173+
case '9' -> 0xFFE9B114;
174+
case 'g' -> 0xFF4AB85C;
175+
case 'G' -> 0xFF259B4A;
176+
case 'r' -> 0xFFEE3934;
177+
case 'R' -> 0xFFBD2E2E;
178+
default -> 0;
179+
};
36180
}
37181

38182
private enum Status {
39183
NONE("", 0),
40-
PREMIUM("trueuuid.overlay.premium", 0x55FF55),
41-
OFFLINE("trueuuid.overlay.offline", 0xFF5555);
184+
PREMIUM("trueuuid.overlay.premium", 0x259B4A),
185+
OFFLINE("trueuuid.overlay.offline", 0xBD2E2E);
42186

43187
private final String translationKey;
44188
private final int color;

‎platform/forge-1.20.1/src/main/java/cn/alini/trueuuid/config/TrueuuidConfig.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public static void register() {
3434
public static String onlineShortSubtitle() { return COMMON.onlineShortSubtitle.get(); }
3535
public static boolean showJoinFeedback() { return COMMON.showJoinFeedback.get(); }
3636
public static boolean showAccountOverlay() { return COMMON.showAccountOverlay.get(); }
37+
public static String overlayCorner() { return COMMON.overlayCorner.get(); }
38+
public static int overlayOffsetX() { return COMMON.overlayOffsetX.get(); }
39+
public static int overlayOffsetY() { return COMMON.overlayOffsetY.get(); }
40+
public static float overlayScale() { return COMMON.overlayScale.get().floatValue(); }
3741

3842
// 新增:策略相关 (Added: Strategy related)
3943
public static boolean knownPremiumDenyOffline() { return COMMON.knownPremiumDenyOffline.get(); }
@@ -58,6 +62,10 @@ public static final class Common {
5862
public final ForgeConfigSpec.ConfigValue<String> onlineShortSubtitle;
5963
public final ForgeConfigSpec.BooleanValue showJoinFeedback;
6064
public final ForgeConfigSpec.BooleanValue showAccountOverlay;
65+
public final ForgeConfigSpec.ConfigValue<String> overlayCorner;
66+
public final ForgeConfigSpec.IntValue overlayOffsetX;
67+
public final ForgeConfigSpec.IntValue overlayOffsetY;
68+
public final ForgeConfigSpec.DoubleValue overlayScale;
6169

6270
// authlib-injector / Yggdrasil 皮肤站白名单
6371
public final ForgeConfigSpec.ConfigValue<List<? extends String>> apiRootWhitelist;
@@ -93,6 +101,16 @@ public static final class Common {
93101
.define("showJoinFeedback", true);
94102
showAccountOverlay = b.comment("Show a small client account-status overlay after a TrueUUID login handshake. / 在 TrueUUID 登录握手后显示小型客户端账号状态覆盖层。")
95103
.define("showAccountOverlay", true);
104+
// Keep these identical to the modern Forge line (platform/forge-common)
105+
// so the badge behaves the same on every supported target.
106+
overlayCorner = b.comment("Screen corner for the account-status badge: top_left, top_right, bottom_left, bottom_right. Default bottom_right: vanilla keeps status effects and advancement toasts in the top right, chat in the bottom left, and mods commonly take the top left. / 账号状态角标所在屏幕角落。默认 bottom_right。")
107+
.defineInList("overlayCorner", "bottom_right", java.util.List.of("top_left", "top_right", "bottom_left", "bottom_right"));
108+
overlayOffsetX = b.comment("Extra horizontal pixels for the badge, to dodge another mod's HUD. Positive moves right. / 角标水平像素偏移,正值向右。")
109+
.defineInRange("overlayOffsetX", 0, -4096, 4096);
110+
overlayOffsetY = b.comment("Extra vertical pixels for the badge, to dodge another mod's HUD. Positive moves down. / 角标垂直像素偏移,正值向下。")
111+
.defineInRange("overlayOffsetY", 0, -4096, 4096);
112+
overlayScale = b.comment("Size of the account-status badge, scaling the padlock and label together. Whole numbers (1.0, 2.0) keep the bitmap font crisp. / 账号状态角标缩放,整数值字体最清晰。")
113+
.defineInRange("overlayScale", 1.0D, 0.5D, 4.0D);
96114

97115
// 策略项 (Strategy items)
98116
knownPremiumDenyOffline = b.comment("Once a name has been verified as premium/skin-site, deny later offline fallback for that name. / 一旦该名字成功验证过正版或皮肤站,后续鉴权失败时禁止以离线身份进入。")

‎platform/forge-1.20.1/src/main/resources/assets/trueuuid/lang/en_us.json‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
"trueuuid.chat.offline_fallback": "Notice: You joined in Offline Mode. If you own a premium account, verification may have failed because of network issues. Please relog and try again. If you continue playing, player data may need migration after a later successful verification.",
1111
"trueuuid.chat.premium": "[TrueUUID] Premium account verified.",
1212
"trueuuid.chat.skin_site": "[TrueUUID] Skin-site account verified by %s.",
13-
"trueuuid.overlay.label": "Account: %s",
14-
"trueuuid.overlay.premium": "Premium verified",
15-
"trueuuid.overlay.offline": "Offline fallback",
13+
"trueuuid.overlay.premium": "Premium",
14+
"trueuuid.overlay.offline": "Offline",
1615
"trueuuid.auth_source.premium": "Premium verification",
1716
"trueuuid.auth_source.skin_site": "Skin-site login",
1817
"trueuuid.auth_source.skin_site.with_name": "Skin-site login (%s)",

‎platform/forge-1.20.1/src/main/resources/assets/trueuuid/lang/zh_cn.json‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
"trueuuid.chat.offline_fallback": "注意:你当前以离线模式进入服务器;如果你是正版账号,可能是网络原因导致无法成功鉴权,请重新登录重试。继续游玩后,若后续鉴权成功,可能需要迁移玩家数据。",
1111
"trueuuid.chat.premium": "[TrueUUID] 正版账号验证成功。",
1212
"trueuuid.chat.skin_site": "[TrueUUID] 已通过 %s 验证皮肤站账号。",
13-
"trueuuid.overlay.label": "账号:%s",
14-
"trueuuid.overlay.premium": "正版验证通过",
15-
"trueuuid.overlay.offline": "离线兜底",
13+
"trueuuid.overlay.premium": "正版",
14+
"trueuuid.overlay.offline": "离线",
1615
"trueuuid.auth_source.premium": "正版验证",
1716
"trueuuid.auth_source.skin_site": "皮肤站登录",
1817
"trueuuid.auth_source.skin_site.with_name": "皮肤站登录(%s)",

‎platform/forge-1.21.1/build.gradle‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,12 @@ minecraft {
5858
}
5959
}
6060
}
61-
client { property 'forge.enabledGameTestNamespaces', 'trueuuid' }
61+
client {
62+
workingDirectory project.file('run/client')
63+
property 'forge.enabledGameTestNamespaces', 'trueuuid'
64+
}
6265
server {
66+
workingDirectory project.file('run/server')
6367
property 'forge.enabledGameTestNamespaces', 'trueuuid'
6468
args '--nogui'
6569
}

‎platform/forge-1.21.1/src/main/java/cn/alini/trueuuid/TrueuuidForgeEvents.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
2121
ForgeAdapterRuntime.onPlayerLoggedIn(event);
2222
}
2323

24+
@SubscribeEvent
25+
public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) {
26+
ForgeAdapterRuntime.onPlayerLoggedOut(event);
27+
}
28+
2429
@SubscribeEvent
2530
public static void onServerStopping(ServerStoppingEvent event) {
2631
ForgeAdapterRuntime.shutdown();

0 commit comments

Comments
 (0)