Skip to content

Commit d372ca1

Browse files
authored
Merge branch 'main' into cursor/always-render-nametags-2580
2 parents 01fe5aa + 60ed9aa commit d372ca1

56 files changed

Lines changed: 3391 additions & 32 deletions

File tree

Some content is hidden

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

‎.gitattributes‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ gradlew text eol=lf
2525
*.ear binary
2626
*.jar binary
2727
*.jks binary
28+
*.mrpack binary
2829
*.png binary
30+
*.dat binary
31+
*.zip binary
2932
*.so binary
3033
*.war binary

‎.github/workflows/build.yml‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,26 @@ jobs:
3232
id: version
3333
run: echo "version=$(grep -E '^version=' gradle.properties | cut -d= -f2)" >> "$GITHUB_OUTPUT"
3434

35-
- name: Stage release artifact
35+
- name: Stage mrpack
3636
run: |
3737
mkdir -p build/upload
38-
find versions/*/build/libs -maxdepth 1 -name '*.jar' \
39-
! -name '*-dev*.jar' ! -name '*-sources*.jar' \
40-
-exec cp -v {} build/upload/ \;
41-
if [ -z "$(ls -A build/upload)" ]; then
42-
echo "::error::No release jar found under versions/*/build/libs"
38+
mapfile -t jars < <(find versions/*/build/libs -maxdepth 1 -name '*.jar' \
39+
! -name '*-dev*.jar' ! -name '*-sources*.jar' | sort)
40+
if [ "${#jars[@]}" -ne 1 ]; then
41+
echo "::error::Expected one release jar, found ${#jars[@]}"
42+
printf '%s\n' "${jars[@]}"
4343
exit 1
4444
fi
45+
python3 pack/build_mrpack.py \
46+
--dolphin-jar "${jars[0]}" \
47+
--output "build/upload/Dolphin Optimized v${{ steps.version.outputs.version }}.mrpack" \
48+
--check
4549
4650
- name: Upload release artifact
4751
uses: actions/upload-artifact@v7
4852
with:
4953
name: DolphinClient-${{ steps.version.outputs.version }}-${{ github.sha }}
50-
path: build/upload/
54+
path: build/upload/*.mrpack
5155
if-no-files-found: error
5256

5357
- name: Upload test reports

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Dolphin Optimized is an **insanely** optimized Minecraft client/mod collection for Hypixel **1.8.9**.
44

5-
Download [Dolphin Optimized Pink.zip](https://github.com/dsnsgithub/AxolotlClient-Fork/releases), then drag it into Prism Launcher to run.
5+
Download [Dolphin Optimized.mrpack](https://github.com/dsnsgithub/AxolotlClient-Fork/releases), then drag it into Prism Launcher to run.
66

77
<img width="1000" alt="2026-08-26_15 20 15" src="https://github.com/user-attachments/assets/6bfbac4f-85e6-42da-98d7-ef623f73ca2e" />
88

‎common/src/main/java/io/github/axolotlclient/modules/hypixel/HypixelAbstractionLayer.java‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@
2626
import java.util.UUID;
2727
import java.util.concurrent.CompletableFuture;
2828

29+
import io.github.axolotlclient.DolphinClientCommon;
2930
import io.github.axolotlclient.util.CachedAPI;
31+
import io.github.axolotlclient.util.ThreadExecuter;
32+
import io.github.axolotlclient.util.UUIDHelper;
3033
import lombok.Getter;
3134

3235
/**
33-
* Local cache for Hypixel stats. Online lookups were removed with the backend,
34-
* so every request completes empty and callers fall back to in-game data.
36+
* Cache for Hypixel stats. Network level is loaded from hypixel.dsns.dev.
37+
* The other lookups have no endpoint and complete empty.
3538
*/
3639
public class HypixelAbstractionLayer {
3740
@Getter
@@ -45,13 +48,16 @@ private static <V> CachedAPI<String, V> empty() {
4548
private final CachedAPI<String, BedwarsData> bedwarsDataApi = empty();
4649

4750
@Getter
48-
private final CachedAPI<String, Integer> networkLevelApi = empty();
51+
private final CachedAPI<String, Double> networkLevelApi = new CachedAPI<>(uuid -> CompletableFuture
52+
.supplyAsync(() -> HypixelLevelClient.requestLevel(uuid, HypixelLevelClient.userAgent(DolphinClientCommon.VERSION)),
53+
ThreadExecuter.service())
54+
.exceptionally(error -> Optional.empty()), 128, true);
4955

5056
@Getter
51-
private final CachedAPI<String, Integer> bedwarsLevelApi = empty();
57+
private final CachedAPI<String, Double> bedwarsLevelApi = empty();
5258

5359
@Getter
54-
private final CachedAPI<String, Integer> skywarsExpApi = empty();
60+
private final CachedAPI<String, Double> skywarsExpApi = empty();
5561

5662
@Getter
5763
private final CachedAPI<String, PlayerData> playerDataApi = empty();
@@ -65,7 +71,7 @@ public void clearPlayerData() {
6571
}
6672

6773
public void handleDisconnectEvents(UUID uuid) {
68-
String key = uuid.toString();
74+
String key = UUIDHelper.toUndashed(uuid);
6975
bedwarsDataApi.invalidate(key);
7076
networkLevelApi.invalidate(key);
7177
bedwarsLevelApi.invalidate(key);
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright © 2026 DSNS <dominic@seung.dev>
3+
*
4+
* This file is part of DolphinClient.
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*
20+
* For more information, see the LICENSE file.
21+
*/
22+
23+
package io.github.axolotlclient.modules.hypixel;
24+
25+
import java.io.IOException;
26+
import java.net.URI;
27+
import java.net.http.HttpClient;
28+
import java.net.http.HttpRequest;
29+
import java.net.http.HttpResponse;
30+
import java.nio.charset.StandardCharsets;
31+
import java.time.Duration;
32+
import java.util.Optional;
33+
34+
import io.github.axolotlclient.util.UUIDHelper;
35+
36+
/**
37+
* Loads a player's Hypixel level from {@code https://hypixel.dsns.dev/level/{uuid}}.
38+
* The endpoint returns a single number and only responds when the user agent contains
39+
* {@code DolphinClient} followed by the mod version.
40+
*/
41+
public final class HypixelLevelClient {
42+
public static final String LEVEL_ENDPOINT = "https://hypixel.dsns.dev/level/";
43+
private static final int MAX_ATTEMPTS = 3;
44+
45+
private static final HttpClient HTTP = HttpClient.newBuilder()
46+
.connectTimeout(Duration.ofSeconds(5))
47+
.followRedirects(HttpClient.Redirect.NORMAL)
48+
.build();
49+
50+
private HypixelLevelClient() {
51+
}
52+
53+
public static String userAgent(String modVersion) {
54+
return "DolphinClient " + modVersion;
55+
}
56+
57+
public static String formatLevel(Number level) {
58+
double value = level.doubleValue();
59+
if (!Double.isFinite(value) || value < 0) {
60+
return "???";
61+
}
62+
return Long.toString((long) Math.floor(value));
63+
}
64+
65+
public static String levelUrl(String uuid) {
66+
return LEVEL_ENDPOINT + dashedUuid(uuid);
67+
}
68+
69+
static String dashedUuid(String uuid) {
70+
String trimmed = uuid.trim();
71+
if (trimmed.indexOf('-') >= 0) {
72+
return trimmed;
73+
}
74+
return UUIDHelper.fromUndashed(trimmed).toString();
75+
}
76+
77+
public static Optional<Double> parseLevel(int status, String body) {
78+
if (status != 200 || body == null) {
79+
return Optional.empty();
80+
}
81+
String trimmed = body.trim();
82+
if (trimmed.isEmpty() || trimmed.charAt(0) == '{' || trimmed.charAt(0) == '<') {
83+
return Optional.empty();
84+
}
85+
try {
86+
double level = Double.parseDouble(trimmed);
87+
if (!Double.isFinite(level) || level < 0) {
88+
return Optional.empty();
89+
}
90+
return Optional.of(level);
91+
} catch (NumberFormatException ignored) {
92+
return Optional.empty();
93+
}
94+
}
95+
96+
public static Optional<Double> requestLevel(String uuid, String userAgent) {
97+
final HttpRequest request;
98+
try {
99+
request = HttpRequest.newBuilder()
100+
.uri(URI.create(levelUrl(uuid)))
101+
.timeout(Duration.ofSeconds(8))
102+
.header("User-Agent", userAgent)
103+
.header("Accept", "text/plain, application/json")
104+
.GET()
105+
.build();
106+
} catch (RuntimeException ignored) {
107+
return Optional.empty();
108+
}
109+
110+
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
111+
try {
112+
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
113+
int status = response.statusCode();
114+
if ((status == 429 || status >= 500) && attempt < MAX_ATTEMPTS) {
115+
if (!pause(attempt)) {
116+
return Optional.empty();
117+
}
118+
continue;
119+
}
120+
return parseLevel(status, response.body());
121+
} catch (InterruptedException e) {
122+
Thread.currentThread().interrupt();
123+
return Optional.empty();
124+
} catch (IOException e) {
125+
if (attempt == MAX_ATTEMPTS || !pause(attempt)) {
126+
return Optional.empty();
127+
}
128+
}
129+
}
130+
return Optional.empty();
131+
}
132+
133+
private static boolean pause(int attempt) {
134+
try {
135+
Thread.sleep(200L * attempt);
136+
return true;
137+
} catch (InterruptedException e) {
138+
Thread.currentThread().interrupt();
139+
return false;
140+
}
141+
}
142+
}

‎common/src/main/java/io/github/axolotlclient/modules/hypixel/LevelHead.java‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,22 @@
3737
public class LevelHead implements AbstractHypixelMod {
3838
@Getter
3939
private static final LevelHead Instance = new LevelHead();
40-
public final ForceableBooleanOption enabled = new ForceableBooleanOption("enabled", "levelhead.enabled.tooltip", false);
40+
public final ForceableBooleanOption enabled = new ForceableBooleanOption("enabled", "levelhead.enabled.tooltip", true);
4141
public final BooleanOption background = new BooleanOption("background", false);
4242
public final ColorOption textColor = new ColorOption("textcolor", ClientColors.GOLD);
4343
public final EnumOption<Mode> mode = new EnumOption<>("levelHeadMode", Mode.class, Mode.NETWORK);
4444
private final OptionCategory category = OptionCategory.create("levelhead");
4545

4646
public static String getDisplayString(Mode mode, String uuid) {
47-
return "Level: " + mode.getApi().getAsyncNow(uuid).map(() -> "...", () -> "???", val -> {
48-
if (mode == Mode.BEDWARS) {
49-
return val + "☆";
50-
} else {
51-
return String.valueOf(val);
52-
}
53-
});
47+
return "Level: " + mode.getApi().getAsyncNow(uuid).map(() -> "...", () -> "???", val -> decorate(mode, val));
48+
}
49+
50+
static String decorate(Mode mode, Number level) {
51+
String text = HypixelLevelClient.formatLevel(level);
52+
if (mode == Mode.BEDWARS) {
53+
return text + "☆";
54+
}
55+
return text;
5456
}
5557

5658
public String getDisplayString(String uuid) {
@@ -60,7 +62,6 @@ public String getDisplayString(String uuid) {
6062

6163
@Override
6264
public void init() {
63-
enabled.setForceOff(true, "levelhead.enabled.tooltip");
6465
category.add(enabled);
6566
category.add(textColor);
6667
category.add(background);
@@ -80,6 +81,6 @@ public enum Mode {
8081
BEDWARS(HypixelAbstractionLayer.getInstance().getBedwarsLevelApi()),
8182
SKYWARS(HypixelAbstractionLayer.getInstance().getSkywarsExpApi());
8283

83-
private final CachedAPI<String, Integer> api;
84+
private final CachedAPI<String, Double> api;
8485
}
8586
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright © 2026 DSNS <dominic@seung.dev>
3+
*
4+
* This file is part of DolphinClient.
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*
20+
* For more information, see the LICENSE file.
21+
*/
22+
23+
package io.github.axolotlclient.modules.hypixel;
24+
25+
import java.util.Arrays;
26+
27+
/**
28+
* How far above the username the level line needs to sit so it clears server nametags.
29+
* Hypixel draws extra lines there: the below-name scoreboard row, and holograms stacked over the player.
30+
*/
31+
public final class LevelHeadPlacement {
32+
/** Scale used by the vanilla nametag renderer ({@code 1.6 * 0.016666668}). */
33+
public static final double NAMETAG_SCALE = 0.02666667;
34+
/** Same gap {@code PlayerRenderer} leaves between the name and the scoreboard line. */
35+
public static final double LINE_FACTOR = 1.15;
36+
37+
private LevelHeadPlacement() {
38+
}
39+
40+
/**
41+
* @param fontHeight vanilla font height, normally 9
42+
* @param belowNameLine the below-name scoreboard objective is currently drawn
43+
* @param nameAnchor world Y of the player's nametag anchor
44+
* @param tagsAbove world Y of other nametag anchors stacked above that one
45+
* @param baseAbove pixels the level is already placed above the name
46+
* @return extra pixels to move the level up
47+
*/
48+
public static int extraPixels(int fontHeight, boolean belowNameLine, double nameAnchor, double[] tagsAbove, int baseAbove) {
49+
double line = fontHeight * LINE_FACTOR;
50+
int count = (belowNameLine ? 1 : 0) + (tagsAbove == null ? 0 : tagsAbove.length);
51+
if (count == 0) {
52+
return 0;
53+
}
54+
double[] rises = new double[count];
55+
int n = 0;
56+
if (belowNameLine) {
57+
rises[n++] = line * NAMETAG_SCALE;
58+
}
59+
if (tagsAbove != null) {
60+
for (double tag : tagsAbove) {
61+
double rise = tag - nameAnchor;
62+
if (rise > 0.001) {
63+
rises[n++] = rise;
64+
}
65+
}
66+
}
67+
if (n == 0) {
68+
return 0;
69+
}
70+
double[] stacked = n == rises.length ? rises : Arrays.copyOf(rises, n);
71+
Arrays.sort(stacked);
72+
// Only the lines that actually touch the name. A sign floating well above the head is not part of the nametag.
73+
double maxGap = line * NAMETAG_SCALE * 2.5;
74+
double top = 0.0;
75+
double cursor = 0.0;
76+
for (double rise : stacked) {
77+
if (rise - cursor > maxGap) {
78+
break;
79+
}
80+
top = rise;
81+
cursor = rise;
82+
}
83+
if (top <= 0.001) {
84+
return 0;
85+
}
86+
double needed = top / NAMETAG_SCALE + line;
87+
return Math.max(0, (int) Math.ceil(needed - baseAbove));
88+
}
89+
}

‎common/src/main/resources/assets/dolphinclient/lang/en_us.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@
507507
"debugcounters.pcount": "Particle Debug Info",
508508
"bedwars_resources.renderWhenRelevant": "Only Render When Relevant",
509509
"bedwars_resources": "Resources HUD",
510-
"levelhead.enabled.tooltip": "Level Head is currently unavailable.<br>It required DolphinClient's online features to fetch Hypixel levels.",
510+
"levelhead.enabled.tooltip": "Shows a player's Hypixel level above their nametag.",
511511
"bedwars.enabled.tooltip": "Whether this module is enabled or not.<br>Some functions of this module require DolphinClient's online features<br>in order to communicate with the backend and the Hypixel API.",
512512
"keystrokes.animation_time": "Animation Time (Milliseconds)",
513513
"autoboop.filters.configure": "Configure Filters",

0 commit comments

Comments
 (0)