Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/update-protocols.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Update Minecraft protocols

# Régénère le mapping protocole -> version depuis l'upstream PrismarineJS
# (branche `master`, auto-mise à jour par bot) et ouvre une PR si ça change.
# La PR passe par la CI normale (typecheck/lint/test/build) avant merge :
# l'automatisation propose, l'humain dispose.
on:
schedule:
# Chaque lundi à 06:00 UTC (les releases Minecraft sortent le mardi,
# le mapping est donc à jour pour la semaine).
- cron: "0 6 * * 1"
workflow_dispatch: # déclenchement manuel depuis l'onglet Actions

permissions:
contents: write
pull-requests: write

jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Regenerate protocol mapping
working-directory: frontend
run: node scripts/update-minecraft-protocols.mjs

- name: Open PR if mapping changed
uses: peter-evans/create-pull-request@v7
with:
commit-message: "chore: update Minecraft protocol mapping"
title: "chore: update Minecraft protocol mapping"
body: |
Mapping `JAVA_RELEASE_PROTOCOL_VERSIONS` régénéré depuis l'upstream
[PrismarineJS/minecraft-data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/common/protocolVersions.json).

Généré par `bun run update:protocols` (voir `frontend/scripts/update-minecraft-protocols.mjs`).
La CI (typecheck/lint/test/build) doit être verte avant merge.
branch: chore/update-minecraft-protocols
base: dev
delete-branch: true
add-paths: frontend/app/core/lib/minecraft-protocols.ts
98 changes: 98 additions & 0 deletions frontend/app/core/lib/minecraft-protocols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Mapping protocole réseau Java <-> version Minecraft affichable.
*
* Source de vérité (upstream, maintenu par bot à chaque sortie) :
* https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/common/protocolVersions.json
* (URL stable sur la branche `master`, auto-mise à jour — voir l'historique
* "Add 1.21.x to pc protocolVersions.json").
*
* Pourquoi une copie vendored plutôt qu'un fetch runtime ?
* - Déterminisme SSR + client : la même table est utilisée des deux côtés, donc
* aucun risque de mismatch d'hydratation (#418) ni de dépendance réseau externe
* au rendu (cf. AGENTS.md : le data-fetching reste côté client, le rendu doit
* rester fluide et synchrone).
* - Pas de latence / panne d'un CDN tiers sur le chemin critique d'affichage.
*
* Mise à jour (à relancer à chaque version mineure de Minecraft, ou via CI) :
* bun run update:protocols
* Le script `scripts/update-minecraft-protocols.mjs` régénère le bloc
* `JAVA_RELEASE_PROTOCOL_VERSIONS` ci-dessous depuis l'URL upstream.
*
* Périmètre : releases Java uniquement (`usesNetty: true`, `releaseType: "release"`).
* Les snapshots / pre-releases / RCs partagent le même espace de numéros avec des
* valeurs sentinelles (>= 2^30) et ne sont pas affichés. Bedrock utilise une
* numérotation différente : aucun fallback protocole pour `type: "bedrock"`.
*/

// <-- BEGIN GENERATED BLOCK (ne pas éditer à la main, voir `bun run update:protocols`) -->
export const JAVA_RELEASE_PROTOCOL_VERSIONS: Record<number, string> = {
4: "1.7.2 - 1.7.5",
5: "1.7.6 - 1.7.10",
47: "1.8 - 1.8.9",
107: "1.9",
108: "1.9.1",
109: "1.9.2",
110: "1.9.3 - 1.9.4",
210: "1.10 - 1.10.2",
315: "1.11",
316: "1.11.1 - 1.11.2",
335: "1.12",
338: "1.12.1",
340: "1.12.2",
393: "1.13",
401: "1.13.1",
404: "1.13.2",
477: "1.14",
480: "1.14.1",
485: "1.14.2",
490: "1.14.3",
498: "1.14.4",
573: "1.15",
575: "1.15.1",
578: "1.15.2",
735: "1.16",
736: "1.16.1",
751: "1.16.2",
753: "1.16.3",
754: "1.16.4 - 1.16.5",
755: "1.17",
756: "1.17.1",
757: "1.18 - 1.18.1",
758: "1.18.2",
759: "1.19",
760: "1.19.1 - 1.19.2",
761: "1.19.3",
762: "1.19.4",
763: "1.20 - 1.20.1",
764: "1.20.2",
765: "1.20.3 - 1.20.4",
766: "1.20.5 - 1.20.6",
767: "1.21 - 1.21.1",
768: "1.21.2 - 1.21.3",
769: "1.21.4",
770: "1.21.5",
771: "1.21.6",
772: "1.21.7 - 1.21.8",
773: "1.21.9 - 1.21.10",
774: "1.21.11",
775: "26.1 - 26.1.2",
776: "26.2",
777: "26.3",
}
// <-- END GENERATED BLOCK -->

/**
* Résout un numéro de protocole réseau en version Minecraft affichable.
*
* @returns la version (ex. "1.21.4", "1.21.7 - 1.21.8"), ou `null` si :
* - le protocole est absent, inconnu ou sentinelle (<= 0 : maintenance, MOTD custom),
* - le serveur est Bedrock (numérotation différente, non couverte).
*/
export function resolveProtocolVersion(
protocolVersion: number | null | undefined,
serverType?: string | null,
): string | null {
if (protocolVersion == null || protocolVersion <= 0) return null
if (serverType === "bedrock") return null
return JAVA_RELEASE_PROTOCOL_VERSIONS[protocolVersion] ?? null
}
71 changes: 71 additions & 0 deletions frontend/app/core/lib/minecraft-version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect } from "vitest"
import { formatMinecraftVersion, resolveServerVersion } from "./utils"
import { resolveProtocolVersion } from "./minecraft-protocols"

describe("formatMinecraftVersion", () => {
it("returns null for missing versions", () => {
expect(formatMinecraftVersion(null)).toBeNull()
expect(formatMinecraftVersion(undefined)).toBeNull()
expect(formatMinecraftVersion("")).toBeNull()
})

it("never returns raw custom names (reported bug)", () => {
// Serveur avec un nom de version custom : aucun motif X.Y(.Z) -> null,
// jamais la chaîne brute ("Version ᴏɴʟɪɴᴇ: 12136 ...").
expect(formatMinecraftVersion("ᴏɴʟɪɴᴇ: 12136 ɢʀᴀᴄᴢʏ")).toBeNull()
expect(formatMinecraftVersion("Online: 12136")).toBeNull()
expect(formatMinecraftVersion("Maintenance")).toBeNull()
})

it("extracts single versions", () => {
expect(formatMinecraftVersion("1.20.1")).toBe("1.20.1")
expect(formatMinecraftVersion("Paper 1.21.4")).toBe("1.21.4")
expect(formatMinecraftVersion("1.21.44")).toBe("1.21.44")
})

it("extracts ranges as first - last", () => {
expect(formatMinecraftVersion("BungeeCord 1.8 - 1.21")).toBe("1.8 - 1.21")
expect(formatMinecraftVersion("1.8.9 - 1.21.4")).toBe("1.8.9 - 1.21.4")
})
})

describe("resolveProtocolVersion", () => {
it("returns null for missing or sentinel protocols", () => {
expect(resolveProtocolVersion(null, "java")).toBeNull()
expect(resolveProtocolVersion(undefined, "java")).toBeNull()
// Protocole sentinelle (maintenance / MOTD custom) -> badge masqué
expect(resolveProtocolVersion(-1, "java")).toBeNull()
expect(resolveProtocolVersion(0, "java")).toBeNull()
})

it("maps known Java protocols to release versions", () => {
expect(resolveProtocolVersion(769, "java")).toBe("1.21.4")
expect(resolveProtocolVersion(772, "java")).toBe("1.21.7 - 1.21.8")
expect(resolveProtocolVersion(47, "java")).toBe("1.8 - 1.8.9")
})

it("returns null for unknown protocols", () => {
expect(resolveProtocolVersion(999999, "java")).toBeNull()
})

it("never applies the Java table to Bedrock (different numbering)", () => {
expect(resolveProtocolVersion(769, "bedrock")).toBeNull()
})
})

describe("resolveServerVersion", () => {
it("prefers the declared version when it is a real one", () => {
expect(resolveServerVersion("Paper 1.20.4", 769, "java")).toBe("1.20.4")
})

it("falls back to the protocol for custom names", () => {
expect(resolveServerVersion("ᴏɴʟɪɴᴇ: 12136 ɢʀᴀᴄᴢʏ", 769, "java")).toBe("1.21.4")
expect(resolveServerVersion(null, 772, "java")).toBe("1.21.7 - 1.21.8")
})

it("returns null when nothing displayable (badge hidden)", () => {
expect(resolveServerVersion("ᴏɴʟɪɴᴇ: 12136", -1, "java")).toBeNull()
expect(resolveServerVersion("Maintenance", 999999, "java")).toBeNull()
expect(resolveServerVersion(null, null, "java")).toBeNull()
})
})
32 changes: 25 additions & 7 deletions frontend/app/core/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { resolveProtocolVersion } from "./minecraft-protocols"
import { translate, type Language } from "./i18n"

/** Normalise une langue libre en une `Language` du registre i18n (défaut: fr). */
Expand Down Expand Up @@ -97,19 +98,36 @@ export function parseMinecraftVersionRange(versionName: string): [string, string
return [first, last];
}

export function formatMinecraftVersion(versionName: string | null | undefined, stripColors = true): string | null {
export function formatMinecraftVersion(versionName: string | null | undefined): string | null {
if (!versionName) return null;
const range = parseMinecraftVersionRange(versionName);
// Pas de motif `X.Y(.Z)` : nom de version custom (ex. "Online: 12136") —
// on ne renvoie JAMAIS la chaîne brute pour éviter d'afficher du texte
// arbitraire contrôlé par l'opérateur du serveur dans les badges.
if (!range) {
// Fallback to original if parsing fails
if (stripColors) {
return versionName.replace(/(§x(?:§[0-9a-fA-F]){6}|§[0-9a-fk-orA-FK-OR]|&#[0-9a-fA-F]{6}|&f{[^}]+};|&s{[^}]+};|&h{[^}]*};)/g, '');
}
return versionName;
return null;
}
if (range[0] === range[1]) {
return range[0];
}

return `${range[0]} - ${range[1]}`;
}

/**
* Version affichable d'un serveur : d'abord la version déclarée (`last_version`
* nettoyée par `formatMinecraftVersion`), puis — si c'est un nom custom sans
* motif de version — le numéro de protocole réseau (`last_protocol_version`,
* fiable car envoyé par le handshake, pas customisable en texte libre).
*
* @returns `null` quand rien d'affichable (protocole sentinelle <= 0 comme en
* maintenance, protocole inconnu, ou serveur Bedrock sans version lisible) :
* les appelants doivent alors masquer le badge / afficher "Version inconnue".
*/
export function resolveServerVersion(
versionName: string | null | undefined,
protocolVersion: number | null | undefined,
serverType?: string | null,
): string | null {
return formatMinecraftVersion(versionName) ?? resolveProtocolVersion(protocolVersion, serverType);
}
4 changes: 2 additions & 2 deletions frontend/app/pages/admin/components/ServersTab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Button } from "@/ui/components/button"
import { Badge } from "@/ui/components/badge"
import { Link } from "react-router"
import { Pagination } from "@/ui/components/pagination"
import { formatMinecraftVersion } from "@/core/lib/utils"
import { resolveServerVersion } from "@/core/lib/utils"
import type { Server, User } from "@/core/lib/api"

interface ServersTabProps {
Expand Down Expand Up @@ -294,7 +294,7 @@ export function ServersTab({
</Badge>
)}
</span>
<span className="text-2xs text-muted-foreground mt-0.5 font-normal">v{formatMinecraftVersion(server.last_version) || "unknown"}</span>
<span className="text-2xs text-muted-foreground mt-0.5 font-normal">v{resolveServerVersion(server.last_version, server.last_protocol_version, server.type) || "unknown"}</span>
</div>
</div>
</td>
Expand Down
8 changes: 5 additions & 3 deletions frontend/app/pages/home/components/ServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, lazy, Suspense } from "react"
import type { Server } from "@/core/lib/api"
const MiniChart = lazy(() => import("./MiniChart").then(m => ({ default: m.MiniChart })))
import { MiniChartSkeleton } from "./MiniChartSkeleton"
import { cn, getServerIp, copyServerIp, formatMinecraftVersion, formatNumber } from "@/core/lib/utils"
import { cn, getServerIp, copyServerIp, resolveServerVersion, formatNumber } from "@/core/lib/utils"
import { ServerIcon } from "@/ui/components/ServerIcon"
import { lunarLogoClass, labyLogoClass } from "@/core/lib/theme-colors"
import { Check, Copy, Wifi, WifiOff } from "lucide-react"
Expand Down Expand Up @@ -36,6 +36,8 @@ export function ServerCard({ server, to }: ServerCardProps) {
const isOffline = server.last_status === "offline"

const { displayIp } = getServerIp(server.ip, server.port, server.type)

const displayVersion = resolveServerVersion(server.last_version, server.last_protocol_version, server.type)

const labyInfo = server.client_infos?.laby;
const lunarInfo = server.client_infos?.lunar;
Expand Down Expand Up @@ -160,9 +162,9 @@ export function ServerCard({ server, to }: ServerCardProps) {
<div className="flex flex-row items-center justify-between gap-2 w-full pt-2 border-t border-border/30">
<div className="flex flex-row items-center gap-1.5 truncate">
{server.type && <PlatformBadge type={server.type} />}
{server.last_version ? (
{displayVersion ? (
<span className="inline-flex items-center rounded-lg border border-border/50 px-2 py-0.5 text-2xs font-semibold bg-muted/50 text-muted-foreground shadow-xs whitespace-nowrap">
{t("common.version", { version: formatMinecraftVersion(server.last_version) })}
{t("common.version", { version: displayVersion })}
</span>
) : (
<span className="inline-flex items-center rounded-lg border border-border/40 px-2 py-0.5 text-2xs font-semibold bg-muted/30 text-muted-foreground shadow-xs whitespace-nowrap">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Server } from "@/core/lib/api"
import { Button } from "@/ui/components/button"
import { Badge } from "@/ui/components/badge"
import { ArrowLeft, Copy, Check, User as UserIcon, Calendar } from "lucide-react"
import { cn, getServerIp, copyServerIp, formatMinecraftVersion, formatDate } from "@/core/lib/utils"
import { cn, getServerIp, copyServerIp, resolveServerVersion, formatDate } from "@/core/lib/utils"
import { ServerIcon } from "@/ui/components/ServerIcon"
import { lunarLogoClass } from "@/core/lib/theme-colors"
import { LunarLogo } from "@/ui/components/LunarLogo"
Expand All @@ -30,6 +30,9 @@ export function ServerDetailHeader({ server, t, language }: ServerDetailHeaderPr

const isOnline = server.last_status === "online"
const { displayIp } = getServerIp(server.ip, server.port, server.type)
// Nom custom sans motif de version -> fallback protocole ; `null` (maintenance,
// protocole sentinelle <= 0, Bedrock illisible...) -> badge masqué.
const displayVersion = resolveServerVersion(server.last_version, server.last_protocol_version, server.type)

const handleCopy = () => {
copyServerIp(server.ip, server.port, server.type)
Expand Down Expand Up @@ -102,9 +105,9 @@ export function ServerDetailHeader({ server, t, language }: ServerDetailHeaderPr
<Copy className="h-2.5 w-2.5 opacity-0 group-hover/copy:opacity-100 group-focus-visible/copy:opacity-100 transition-opacity flex-shrink-0" />
)}
</button>
{server.last_version && (
{displayVersion && (
<Badge variant="secondary" className="font-mono text-xs whitespace-nowrap">
v{formatMinecraftVersion(server.last_version)}
v{displayVersion}
</Badge>
)}
{server.user && (
Expand Down
10 changes: 5 additions & 5 deletions frontend/app/pages/server-detail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { BarChart, WifiOff } from "lucide-react"

import { useLanguage } from "@/core/contexts/LanguageContext"
import { getTimeRanges, getIntervals } from "@/core/lib/chartUtils"
import { formatMinecraftVersion, formatNumber } from "@/core/lib/utils"
import { formatNumber } from "@/core/lib/utils"

import { MinecraftMotd } from "@/ui/motd"
import { ServerSidebar } from "@/pages/server-detail/components/ServerSidebar"
Expand Down Expand Up @@ -296,10 +296,10 @@ export default function ServerDetail() {
currentPlayers={
server.last_protocol_version != null &&
server.last_protocol_version <= 0
? formatMinecraftVersion(
server.last_version,
false
) || "Version"
// Protocole sentinelle (maintenance...) : le client Minecraft
// affiche le texte brut du serveur dans la colonne joueurs.
// On reproduit fidèlement ce comportement ici (pas de badge).
? (server.last_version?.trim() || "Version")
: (server.last_connected ?? 0)
}
maxPlayers={
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "react-router build",
"lint": "eslint .",
"test": "vitest run",
"update:protocols": "node scripts/update-minecraft-protocols.mjs",
"start": "bun run server.ts",
"typecheck": "react-router typegen && tsc",
"preview": "bun run build && bun run start"
Expand Down
Loading
Loading