diff --git a/src/main/java/top/fpsmaster/modules/client/api/PromotedServersService.java b/src/main/java/top/fpsmaster/modules/client/api/PromotedServersService.java
new file mode 100644
index 00000000..26632804
--- /dev/null
+++ b/src/main/java/top/fpsmaster/modules/client/api/PromotedServersService.java
@@ -0,0 +1,258 @@
+package top.fpsmaster.modules.client.api;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import top.fpsmaster.exception.FileException;
+import top.fpsmaster.modules.client.api.model.ApiResponse;
+import top.fpsmaster.modules.client.api.model.FPSMasterGson;
+import top.fpsmaster.modules.client.api.model.PromotedServerView;
+import top.fpsmaster.modules.logger.ClientLogger;
+import top.fpsmaster.utils.io.FileUtils;
+import top.fpsmaster.utils.io.HttpRequest;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Featured/partner servers for the multiplayer screen, backed by
+ * {@code GET /api/v1/launcher/servers}, plus the player's local overrides: promoted entries the
+ * player deleted (never inserted again) and the player's own pinned servers.
+ *
+ *
The fetch is fully asynchronous — the GUI only ever reads the cached snapshot via
+ * {@link #promotedServers()} and watches {@link #revision()} for changes, so an offline client
+ * simply keeps an empty promoted list and the local server list stays usable.
+ *
+ *
Overrides persist in a sidecar JSON ({@code promoted_servers.json} in the client data dir);
+ * {@code servers.dat} is never touched with new keys.
+ */
+public final class PromotedServersService {
+ private static final PromotedServersService INSTANCE = new PromotedServersService();
+ private static final String SIDECAR_FILE = "promoted_servers.json";
+ private static final String HIDDEN_KEY = "hiddenPromoted";
+ private static final String PINNED_KEY = "pinnedAddresses";
+ /** Hidden entries without a backend id fall back to the address, marked with this prefix. */
+ private static final String ADDRESS_KEY_PREFIX = "addr:";
+ private static final long REFRESH_INTERVAL_MS = 60_000L;
+ private static final String SERVERS_ENDPOINT =
+ FPSMasterConstants.API_BASE_URL + FPSMasterConstants.API_VERSION + "/launcher/servers";
+
+ private volatile List promoted = Collections.emptyList();
+ private final AtomicLong revision = new AtomicLong(0);
+ private final AtomicBoolean fetching = new AtomicBoolean(false);
+ private volatile long lastFetchAt = 0L;
+
+ private final Object stateLock = new Object();
+ private boolean stateLoaded;
+ private final Set hiddenKeys = new HashSet<>();
+ private final Set pinnedAddresses = new HashSet<>();
+
+ private PromotedServersService() {
+ }
+
+ public static PromotedServersService getInstance() {
+ return INSTANCE;
+ }
+
+ /** Bumped once per successful fetch; the GUI rebuilds its entry list when it changes. */
+ public long revision() {
+ return revision.get();
+ }
+
+ /** Cached snapshot in backend order; never blocks, empty until a fetch succeeds. */
+ public List promotedServers() {
+ return promoted;
+ }
+
+ /**
+ * Kicks off a background fetch unless one is in flight or a recent one already ran.
+ * Safe to call from {@code initGui()} — resizing the window re-runs it every time.
+ */
+ public void refreshIfStale() {
+ long now = System.currentTimeMillis();
+ long last = lastFetchAt;
+ if (last != 0L && now - last >= 0L && now - last < REFRESH_INTERVAL_MS) {
+ return;
+ }
+ if (!fetching.compareAndSet(false, true)) {
+ return;
+ }
+ lastFetchAt = now;
+ CompletableFuture.runAsync(this::fetch).whenComplete((unused, error) -> {
+ fetching.set(false);
+ if (error != null) {
+ ClientLogger.warn("Promoted servers refresh failed: " + error);
+ }
+ });
+ }
+
+ private void fetch() {
+ try {
+ Map headers = new HashMap<>();
+ headers.put("User-Agent", FPSMasterConstants.USER_AGENT);
+ headers.put("Accept", "application/json");
+ HttpRequest.HttpResponseResult response = HttpRequest.get(SERVERS_ENDPOINT, headers);
+ if (!response.isSuccess() || response.getBody() == null || response.getBody().isEmpty()) {
+ ClientLogger.warn("Promoted servers request failed: HTTP " + response.getStatusCode());
+ return;
+ }
+ JsonObject json = FPSMasterGson.getInstance().fromJson(response.getBody(), JsonObject.class);
+ ApiResponse apiResponse =
+ ApiResponse.fromJson(json, PromotedServerView[].class, response.getStatusCode());
+ if (!apiResponse.isSuccess() || apiResponse.getData() == null) {
+ ClientLogger.warn("Promoted servers response rejected: " + apiResponse.getMessage());
+ return;
+ }
+ List fetched = new ArrayList<>();
+ for (PromotedServerView view : apiResponse.getData()) {
+ if (view != null && view.getAddress() != null && !view.getAddress().trim().isEmpty()) {
+ fetched.add(view);
+ }
+ }
+ promoted = Collections.unmodifiableList(fetched);
+ revision.incrementAndGet();
+ } catch (IOException e) {
+ ClientLogger.warn("Promoted servers request failed: " + e.getMessage());
+ } catch (RuntimeException e) {
+ ClientLogger.warn("Promoted servers response could not be parsed: " + e.getMessage());
+ }
+ }
+
+ // ================== Local overrides ================== //
+
+ public boolean isHidden(PromotedServerView view) {
+ synchronized (stateLock) {
+ ensureStateLoaded();
+ return hiddenKeys.contains(hideKey(view));
+ }
+ }
+
+ /**
+ * Records the promoted server matching {@code address} as deleted, so it is never inserted
+ * into the list again. Prefers the backend id; falls back to the address when absent.
+ */
+ public void hideByAddress(String address) {
+ String normalized = normalizeAddress(address);
+ if (normalized.isEmpty()) {
+ return;
+ }
+ String key = ADDRESS_KEY_PREFIX + normalized;
+ for (PromotedServerView view : promoted) {
+ if (normalizeAddress(view.getAddress()).equals(normalized)) {
+ key = hideKey(view);
+ break;
+ }
+ }
+ synchronized (stateLock) {
+ ensureStateLoaded();
+ if (hiddenKeys.add(key)) {
+ saveState();
+ }
+ }
+ }
+
+ public boolean isPinned(String address) {
+ String normalized = normalizeAddress(address);
+ if (normalized.isEmpty()) {
+ return false;
+ }
+ synchronized (stateLock) {
+ ensureStateLoaded();
+ return pinnedAddresses.contains(normalized);
+ }
+ }
+
+ /** Pin state for the player's own servers, keyed by address. */
+ public void setPinned(String address, boolean pinned) {
+ String normalized = normalizeAddress(address);
+ if (normalized.isEmpty()) {
+ return;
+ }
+ synchronized (stateLock) {
+ ensureStateLoaded();
+ boolean changed = pinned ? pinnedAddresses.add(normalized) : pinnedAddresses.remove(normalized);
+ if (changed) {
+ saveState();
+ }
+ }
+ }
+
+ public static String normalizeAddress(String address) {
+ return address == null ? "" : address.trim().toLowerCase(Locale.ROOT);
+ }
+
+ private String hideKey(PromotedServerView view) {
+ if (view.getId() != null && !view.getId().trim().isEmpty()) {
+ return view.getId().trim();
+ }
+ return ADDRESS_KEY_PREFIX + normalizeAddress(view.getAddress());
+ }
+
+ /** Callers hold {@code stateLock}. */
+ private void ensureStateLoaded() {
+ if (stateLoaded) {
+ return;
+ }
+ stateLoaded = true;
+ try {
+ String content = FileUtils.readFile(SIDECAR_FILE);
+ if (content == null || content.trim().isEmpty()) {
+ return;
+ }
+ JsonObject json = FPSMasterGson.getInstance().fromJson(content, JsonObject.class);
+ if (json == null) {
+ return;
+ }
+ readStrings(json, HIDDEN_KEY, hiddenKeys);
+ readStrings(json, PINNED_KEY, pinnedAddresses);
+ } catch (FileException e) {
+ ClientLogger.warn("Failed to read promoted servers state: " + e.getMessage());
+ } catch (RuntimeException e) {
+ ClientLogger.warn("Promoted servers state is invalid JSON, starting fresh: " + e.getMessage());
+ }
+ }
+
+ private void readStrings(JsonObject json, String key, Set target) {
+ if (!json.has(key) || !json.get(key).isJsonArray()) {
+ return;
+ }
+ for (JsonElement element : json.getAsJsonArray(key)) {
+ if (element.isJsonPrimitive()) {
+ String value = element.getAsString();
+ if (value != null && !value.isEmpty()) {
+ target.add(value);
+ }
+ }
+ }
+ }
+
+ /** Callers hold {@code stateLock}. */
+ private void saveState() {
+ JsonObject json = new JsonObject();
+ JsonArray hidden = new JsonArray();
+ for (String key : hiddenKeys) {
+ hidden.add(new com.google.gson.JsonPrimitive(key));
+ }
+ json.add(HIDDEN_KEY, hidden);
+ JsonArray pinned = new JsonArray();
+ for (String address : pinnedAddresses) {
+ pinned.add(new com.google.gson.JsonPrimitive(address));
+ }
+ json.add(PINNED_KEY, pinned);
+ try {
+ FileUtils.saveFile(SIDECAR_FILE, FPSMasterGson.getInstance().toJson(json));
+ } catch (FileException e) {
+ ClientLogger.error("Failed to save promoted servers state: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/top/fpsmaster/modules/client/api/model/PromotedServerView.java b/src/main/java/top/fpsmaster/modules/client/api/model/PromotedServerView.java
new file mode 100644
index 00000000..019eb34e
--- /dev/null
+++ b/src/main/java/top/fpsmaster/modules/client/api/model/PromotedServerView.java
@@ -0,0 +1,33 @@
+package top.fpsmaster.modules.client.api.model;
+
+/**
+ * One entry of {@code GET /api/v1/launcher/servers} — the official featured/partner server list.
+ * Unknown backend fields are ignored by Gson.
+ */
+public class PromotedServerView {
+ private String id;
+ private String name;
+ private String address;
+ private String description;
+ private boolean active;
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+}
diff --git a/src/main/java/top/fpsmaster/ui/mc/GuiMultiplayer.java b/src/main/java/top/fpsmaster/ui/mc/GuiMultiplayer.java
index 15cacb35..f2eb0ff2 100644
--- a/src/main/java/top/fpsmaster/ui/mc/GuiMultiplayer.java
+++ b/src/main/java/top/fpsmaster/ui/mc/GuiMultiplayer.java
@@ -17,6 +17,8 @@
import org.lwjgl.opengl.GL11;
import top.fpsmaster.FPSMaster;
import top.fpsmaster.font.impl.UFontRenderer;
+import top.fpsmaster.modules.client.api.PromotedServersService;
+import top.fpsmaster.modules.client.api.model.PromotedServerView;
import top.fpsmaster.ui.click.ClickGuiTheme;
import top.fpsmaster.ui.click.UiChrome;
import top.fpsmaster.ui.click.component.ScrollContainer;
@@ -32,7 +34,13 @@
import java.awt.*;
import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
public class GuiMultiplayer extends ScaledGuiScreen {
private ServerData selectedServer;
@@ -46,6 +54,10 @@ public class GuiMultiplayer extends ScaledGuiScreen {
private TextField searchField;
private String lastQuery = "";
+ /** Synthetic {@link ServerData} per promoted-only address, reused across rebuilds so each is pinged once. */
+ private final Map promotedServerData = new HashMap<>();
+ private long promotedRevisionSeen = -1L;
+
private void joinSelected() {
if (selectedServer != null) {
this.mc.displayGuiScreen(new GuiConnecting(this, this.mc, selectedServer));
@@ -100,16 +112,13 @@ public void initGui() {
}
}
loadServerList();
+ PromotedServersService promotedService = PromotedServersService.getInstance();
+ promotedService.refreshIfStale();
+ promotedRevisionSeen = promotedService.revision();
+ rebuildServerEntries();
// Prototype opens with the first server already selected so the detail column is
// never an empty pane when there is anything to show.
- selectedServer = servers.isEmpty() ? null : servers.get(0);
-
- serverListInternet.clear();
- for (ServerData server : servers) {
- this.serverListInternet.add(new ServerListEntry(this, server));
- }
- serverListDisplay.clear();
- serverListDisplay.addAll(serverListInternet);
+ selectedServer = serverListInternet.isEmpty() ? null : serverListInternet.get(0).getServerData();
if (searchField == null) {
searchField = new TextField(
FPSMaster.fontManager.getFont(12),
@@ -136,6 +145,11 @@ public void confirmClicked(boolean result, int id) {
saveServerList();
break;
case "remove":
+ // Deleting a promoted row is a local dismissal: remember it so it is never
+ // inserted again, without touching the backend.
+ if (isPromotedEntry(selectedServer)) {
+ PromotedServersService.getInstance().hideByAddress(selectedServer.serverIP);
+ }
servers.remove(selectedServer);
saveServerList();
break;
@@ -375,7 +389,9 @@ private void renderDetail(float x, float y, float width, float height, int mouse
ping < 0 ? FPSMaster.i18n.get("multiplayer.offline") : FPSMaster.i18n.get("multiplayer.online"),
FPSMaster.i18n.get("multiplayer.status"));
- // Actions pinned to the bottom
+ // Actions pinned to the bottom. Promoted-only rows are not the player's servers:
+ // no edit and no pin, delete just dismisses the promotion locally.
+ boolean ownServer = servers.contains(selectedServer);
float joinH = 21f;
float rowH = 18f;
float rowY = y + height - pad - rowH;
@@ -390,14 +406,29 @@ private void renderDetail(float x, float y, float width, float height, int mouse
joinSelected();
}
- float halfW = (width - pad * 2f - 4f) / 2f;
- if (UiChrome.buttonClicked(this, x + pad, rowY, halfW, rowH, "rename",
- FPSMaster.i18n.get("multiplayer.edit"), UiChrome.Style.DEFAULT, mouseX, mouseY)) {
- editSelected();
- }
- if (UiChrome.buttonClicked(this, x + pad + halfW + 4f, rowY, halfW, rowH, "delete",
- FPSMaster.i18n.get("multiplayer.delete"), UiChrome.Style.DANGER, mouseX, mouseY)) {
- removeSelected();
+ if (ownServer) {
+ boolean pinned = PromotedServersService.getInstance().isPinned(selectedServer.serverIP);
+ float pinY = joinY - 4f - rowH;
+ if (UiChrome.buttonClicked(this, x + pad, pinY, width - pad * 2f, rowH, null,
+ FPSMaster.i18n.get(pinned ? "multiplayer.unpin" : "multiplayer.pin"),
+ UiChrome.Style.DEFAULT, mouseX, mouseY)) {
+ PromotedServersService.getInstance().setPinned(selectedServer.serverIP, !pinned);
+ rebuildServerEntries();
+ }
+ float halfW = (width - pad * 2f - 4f) / 2f;
+ if (UiChrome.buttonClicked(this, x + pad, rowY, halfW, rowH, "rename",
+ FPSMaster.i18n.get("multiplayer.edit"), UiChrome.Style.DEFAULT, mouseX, mouseY)) {
+ editSelected();
+ }
+ if (UiChrome.buttonClicked(this, x + pad + halfW + 4f, rowY, halfW, rowH, "delete",
+ FPSMaster.i18n.get("multiplayer.delete"), UiChrome.Style.DANGER, mouseX, mouseY)) {
+ removeSelected();
+ }
+ } else {
+ if (UiChrome.buttonClicked(this, x + pad, rowY, width - pad * 2f, rowH, "delete",
+ FPSMaster.i18n.get("multiplayer.delete"), UiChrome.Style.DANGER, mouseX, mouseY)) {
+ removeSelected();
+ }
}
}
@@ -415,6 +446,14 @@ private void drawStat(float x, float y, float width, String icon, String value,
@Override
public void updateScreen() {
super.updateScreen();
+ long promotedRevision = PromotedServersService.getInstance().revision();
+ if (promotedRevision != promotedRevisionSeen) {
+ promotedRevisionSeen = promotedRevision;
+ rebuildServerEntries();
+ if (selectedServer == null && !serverListInternet.isEmpty()) {
+ selectedServer = serverListInternet.get(0).getServerData();
+ }
+ }
// Forge's FMLClientHandler.setupServerList() only wires LAN/Realms discovery, which this
// custom multiplayer screen does not use; the vanilla ping loop below is all we need.
// Guard the ping loop: OldServerPinger's legacy-ping fallback can surface a netty NPE
@@ -453,6 +492,88 @@ public void loadServerList() {
}
}
+
+ /**
+ * Rebuilds the entry list in display order: the player's pinned servers first, then visible
+ * promoted servers in backend order, then the remaining own servers in {@code servers.dat}
+ * order. A promoted server whose address matches an own server collapses into one row — styled
+ * as user-pinned when the player pinned it, as promoted otherwise.
+ */
+ private void rebuildServerEntries() {
+ PromotedServersService service = PromotedServersService.getInstance();
+ serverListInternet.clear();
+
+ Set placed = Collections.newSetFromMap(new IdentityHashMap<>());
+ for (ServerData server : servers) {
+ if (service.isPinned(server.serverIP)) {
+ serverListInternet.add(new ServerListEntry(this, server, false, true, null));
+ placed.add(server);
+ }
+ }
+
+ Set promotedAddressesSeen = new HashSet<>();
+ for (PromotedServerView view : service.promotedServers()) {
+ String address = PromotedServersService.normalizeAddress(view.getAddress());
+ if (!view.isActive() || service.isHidden(view) || !promotedAddressesSeen.add(address)) {
+ continue;
+ }
+ ServerData ownServer = findServerByAddress(address);
+ if (ownServer != null) {
+ // User-pinned styling wins for a pinned own server; the promoted row is dropped.
+ if (!placed.contains(ownServer)) {
+ serverListInternet.add(new ServerListEntry(this, ownServer, true, false, view.getDescription()));
+ placed.add(ownServer);
+ }
+ } else {
+ serverListInternet.add(new ServerListEntry(this, promotedServerData(view), true, false, view.getDescription()));
+ }
+ }
+
+ for (ServerData server : servers) {
+ if (!placed.contains(server)) {
+ serverListInternet.add(new ServerListEntry(this, server, false, false, null));
+ }
+ }
+
+ serverListDisplay.clear();
+ serverListDisplay.addAll(serverListInternet);
+ // Force applySearchFilter to re-filter the fresh entries on the next frame.
+ lastQuery = null;
+ }
+
+ private ServerData findServerByAddress(String normalizedAddress) {
+ for (ServerData server : servers) {
+ if (PromotedServersService.normalizeAddress(server.serverIP).equals(normalizedAddress)) {
+ return server;
+ }
+ }
+ return null;
+ }
+
+ private ServerData promotedServerData(PromotedServerView view) {
+ String key = PromotedServersService.normalizeAddress(view.getAddress());
+ ServerData data = promotedServerData.get(key);
+ if (data == null) {
+ String name = view.getName() == null || view.getName().trim().isEmpty()
+ ? view.getAddress().trim()
+ : view.getName().trim();
+ data = new ServerData(name, view.getAddress().trim(), false);
+ promotedServerData.put(key, data);
+ }
+ return data;
+ }
+
+ private boolean isPromotedEntry(ServerData server) {
+ if (server == null) {
+ return false;
+ }
+ for (ServerListEntry entry : serverListInternet) {
+ if (entry.getServerData() == server) {
+ return entry.isPromoted();
+ }
+ }
+ return false;
+ }
}
diff --git a/src/main/java/top/fpsmaster/ui/mc/ServerListEntry.java b/src/main/java/top/fpsmaster/ui/mc/ServerListEntry.java
index e2cc2bcf..3cb43015 100644
--- a/src/main/java/top/fpsmaster/ui/mc/ServerListEntry.java
+++ b/src/main/java/top/fpsmaster/ui/mc/ServerListEntry.java
@@ -41,12 +41,25 @@ public class ServerListEntry {
private DynamicTexture field_148305_h;
private final GuiMultiplayer owner;
private long lastClick = 0;
+ /** Backend-featured row: drawn with the accent "featured" badge, delete dismisses locally. */
+ private final boolean promoted;
+ /** Player-pinned own server: drawn with the neutral "pinned" chip, sorted to the very top. */
+ private final boolean pinned;
+ private final String promotedDescription;
+
+ protected ServerListEntry(GuiMultiplayer multiplayer, ServerData server) {
+ this(multiplayer, server, false, false, null);
+ }
- protected ServerListEntry(GuiMultiplayer multiplayer, ServerData p_i45048_2_) {
+ protected ServerListEntry(GuiMultiplayer multiplayer, ServerData server, boolean promoted,
+ boolean pinned, String promotedDescription) {
this.owner = multiplayer;
- this.server = p_i45048_2_;
+ this.server = server;
+ this.promoted = promoted;
+ this.pinned = pinned;
+ this.promotedDescription = promotedDescription;
this.mc = Minecraft.getMinecraft();
- this.serverIcon = new ResourceLocation("servers/" + p_i45048_2_.serverIP + "/icon");
+ this.serverIcon = new ResourceLocation("servers/" + server.serverIP + "/icon");
this.field_148305_h = (DynamicTexture) this.mc.getTextureManager().getTexture(this.serverIcon);
}
@@ -89,9 +102,30 @@ public void drawEntry(int x, int y, int listWidth, int rowHeight, int mouseX, in
float textW = listWidth - (textX - x) - rightW - 5f;
UFontRenderer nameFont = FPSMaster.fontManager.s14;
UFontRenderer subFont = FPSMaster.fontManager.getFont(12);
- nameFont.drawString(this.server.serverName, textX, y + 5f,
+ // Two distinct markers next to the name: featured rows get the accent badge pill,
+ // player-pinned rows get the neutral bordered chip. Both are existing theme widgets.
+ String markerText = promoted
+ ? FPSMaster.i18n.get("multiplayer.promoted.badge")
+ : pinned ? FPSMaster.i18n.get("multiplayer.pinned.badge") : null;
+ float nameW = textW;
+ if (markerText != null) {
+ nameW -= UiChrome.keyChipWidth(markerText) + 4f;
+ }
+ String name = nameFont.trimStringToWidth(this.server.serverName, nameW);
+ nameFont.drawString(name, textX, y + 5f,
(unreachable ? ClickGuiTheme.textSecondary() : ClickGuiTheme.textPrimary()).getRGB());
+ if (markerText != null) {
+ float markerX = textX + nameFont.getStringWidth(name) + 4f;
+ if (promoted) {
+ UiChrome.badge(markerX, y + 3.5f, markerText);
+ } else {
+ UiChrome.keyChip(markerX, y + 3.5f, UiChrome.keyChipWidth(markerText), 10f, markerText, false, false);
+ }
+ }
String motd = this.server.serverMOTD == null ? "" : this.server.serverMOTD.replaceAll("§.", "");
+ if (motd.isEmpty() && promoted && promotedDescription != null) {
+ motd = promotedDescription;
+ }
if (unreachable) {
subFont.drawString(subFont.trimStringToWidth(motd, textW), textX, y + 15f,
ClickGuiTheme.danger().getRGB());
@@ -178,6 +212,10 @@ public ServerData getServerData() {
return this.server;
}
+ public boolean isPromoted() {
+ return this.promoted;
+ }
+
public void triggerClick() {
if (Minecraft.getSystemTime() - lastClick < 250L) {
this.mc.displayGuiScreen(new GuiConnecting(owner, this.mc, getServerData()));
diff --git a/src/main/resources/assets/minecraft/client/lang/en_us.lang b/src/main/resources/assets/minecraft/client/lang/en_us.lang
index 1c83fc8f..7819d04a 100644
--- a/src/main/resources/assets/minecraft/client/lang/en_us.lang
+++ b/src/main/resources/assets/minecraft/client/lang/en_us.lang
@@ -35,6 +35,10 @@ multiplayer.players=Players
multiplayer.ping=Ping
multiplayer.version=Version
multiplayer.offline=Offline
+multiplayer.promoted.badge=Featured
+multiplayer.pinned.badge=Pinned
+multiplayer.pin=Pin
+multiplayer.unpin=Unpin
backgroundselector.title=Background Settings
backgroundselector.pick=Choose Image
backgroundselector.preview.image=IMG
diff --git a/src/main/resources/assets/minecraft/client/lang/zh_cn.lang b/src/main/resources/assets/minecraft/client/lang/zh_cn.lang
index a26c4b09..e8c68b30 100644
--- a/src/main/resources/assets/minecraft/client/lang/zh_cn.lang
+++ b/src/main/resources/assets/minecraft/client/lang/zh_cn.lang
@@ -35,6 +35,10 @@ multiplayer.players=在线玩家
multiplayer.ping=延迟
multiplayer.version=版本
multiplayer.offline=离线
+multiplayer.promoted.badge=推荐
+multiplayer.pinned.badge=置顶
+multiplayer.pin=置顶
+multiplayer.unpin=取消置顶
backgroundselector.title=背景设置
backgroundselector.pick=选择图片
backgroundselector.preview.image=IMG