diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-05-06 06:09:43 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-05-06 06:09:43 +0900 |
| commit | c092edbab0e8b516faaa459f6d0915c7df3cd06f (patch) | |
| tree | 10e9394dcd35270c5374f5396bf8b86b7a711c2b /website/.vitepress | |
| parent | bbd63b0032965ae58f7a20988312ff5012be543f (diff) | |
| download | LunaticChat-c092edbab0e8b516faaa459f6d0915c7df3cd06f.tar.gz LunaticChat-c092edbab0e8b516faaa459f6d0915c7df3cd06f.tar.bz2 LunaticChat-c092edbab0e8b516faaa459f6d0915c7df3cd06f.zip | |
docs: clarify paper/velocity compatibility
Add a dynamic compatibility matrix (GitHub Releases + ProtocolVersion.kt at
each tag) shown on the home page, download page, and a new reference doc.
Split protocol theory and rolling-update rules out of the Velocity feature
guide into the new compatibility reference, and simplify the download notice.
Also set explicit GitHub release titles in the Paper / Velocity workflows so
the awkward paper/v* tag prefix is not the user-facing label.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'website/.vitepress')
| -rw-r--r-- | website/.vitepress/config/en.ts | 4 | ||||
| -rw-r--r-- | website/.vitepress/config/ja.ts | 4 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/CompatibilityMatrix.vue | 313 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/DownloadCard.vue | 20 | ||||
| -rw-r--r-- | website/.vitepress/theme/components/useCompatibilityData.ts | 167 | ||||
| -rw-r--r-- | website/.vitepress/theme/index.ts | 2 |
6 files changed, 505 insertions, 5 deletions
diff --git a/website/.vitepress/config/en.ts b/website/.vitepress/config/en.ts index 3a05016..6bc755b 100644 --- a/website/.vitepress/config/en.ts +++ b/website/.vitepress/config/en.ts @@ -67,6 +67,10 @@ export const en: DefaultTheme.Config = { link: '/en/docs/reference/player-settings', text: 'Player Settings', }, + { + link: '/en/docs/reference/compatibility', + text: 'Paper / Velocity Compatibility', + }, ], }, ], diff --git a/website/.vitepress/config/ja.ts b/website/.vitepress/config/ja.ts index e74102c..2335509 100644 --- a/website/.vitepress/config/ja.ts +++ b/website/.vitepress/config/ja.ts @@ -67,6 +67,10 @@ export const ja: DefaultTheme.Config = { link: '/docs/reference/player-settings', text: 'プレイヤー設定', }, + { + link: '/docs/reference/compatibility', + text: 'Paper / Velocity 互換性', + }, ], }, ], diff --git a/website/.vitepress/theme/components/CompatibilityMatrix.vue b/website/.vitepress/theme/components/CompatibilityMatrix.vue new file mode 100644 index 0000000..0e91edc --- /dev/null +++ b/website/.vitepress/theme/components/CompatibilityMatrix.vue @@ -0,0 +1,313 @@ +<script setup lang="ts"> +import { computed } from 'vue'; +import { useData } from 'vitepress'; +import { + useCompatibilityData, + checkCompatibility, + formatProtocol, + type CompatibilityResult, + type PlatformReleaseEntry, +} from './useCompatibilityData'; + +const { lang } = useData(); +const isEn = computed(() => lang.value === 'en-US'); +const { data, loading, error } = useCompatibilityData(); + +const t = computed(() => + isEn.value + ? { + loading: 'Loading compatibility data...', + error: 'Failed to load compatibility data. Please check ', + errorSuffix: ' directly.', + emptyPaper: 'No Paper releases yet.', + emptyVelocity: 'No Velocity releases yet.', + empty: 'No releases yet. Compatibility matrix will appear once releases are published.', + paperVersion: 'Paper version', + velocityVersion: 'Velocity version', + protocol: 'Protocol', + unknown: 'unknown', + compatible: 'Compatible', + incompatible: 'Incompatible', + compatibleShort: 'OK', + compatibilityHeader: 'Compatibility', + reasonMajorMismatch: 'Major version mismatch', + reasonPaperTooNew: 'Paper newer — update Velocity first', + reasonVelocityTooNew: 'Velocity newer — update Paper', + reasonPaperTooOld: 'Paper too old', + reasonVelocityTooOld: 'Velocity too old', + legend: 'Legend', + legendCompatible: 'Compatible — both can connect.', + legendIncompatible: 'Incompatible — handshake will be rejected.', + } + : { + loading: '互換性情報を取得中...', + error: '互換性情報の取得に失敗しました.', + errorSuffix: ' を直接ご確認ください.', + emptyPaper: 'Paper のリリースはまだありません.', + emptyVelocity: 'Velocity のリリースはまだありません.', + empty: 'リリースがまだありません.リリース後に互換性マトリクスが表示されます.', + paperVersion: 'Paper バージョン', + velocityVersion: 'Velocity バージョン', + protocol: 'プロトコル', + unknown: '不明', + compatible: '互換', + incompatible: '非互換', + compatibleShort: 'OK', + compatibilityHeader: '互換性', + reasonMajorMismatch: 'MAJOR バージョン不一致', + reasonPaperTooNew: 'Paper が新しすぎる — Velocity を先に更新', + reasonVelocityTooNew: 'Velocity が新しすぎる — Paper を更新', + reasonPaperTooOld: 'Paper が古すぎる', + reasonVelocityTooOld: 'Velocity が古すぎる', + legend: '凡例', + legendCompatible: '互換 — 接続可能.', + legendIncompatible: '非互換 — ハンドシェイクで拒否されます.', + }, +); + +const sortedPaper = computed<PlatformReleaseEntry[]>(() => + [...data.value.paper].sort((a, b) => compareVersion(b.version, a.version)), +); +const sortedVelocity = computed<PlatformReleaseEntry[]>(() => + [...data.value.velocity].sort((a, b) => compareVersion(b.version, a.version)), +); + +function compareVersion(a: string, b: string): number { + const pa = a.split('.').map((n) => Number.parseInt(n, 10)); + const pb = b.split('.').map((n) => Number.parseInt(n, 10)); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const da = pa[i] ?? 0; + const db = pb[i] ?? 0; + if (da !== db) return da - db; + } + return 0; +} + +function reasonLabel(result: CompatibilityResult): string { + switch (result) { + case 'major-mismatch': + return t.value.reasonMajorMismatch; + case 'paper-too-new': + return t.value.reasonPaperTooNew; + case 'velocity-too-new': + return t.value.reasonVelocityTooNew; + case 'paper-too-old': + return t.value.reasonPaperTooOld; + case 'velocity-too-old': + return t.value.reasonVelocityTooOld; + default: + return ''; + } +} + +function cellResult( + paper: PlatformReleaseEntry, + velocity: PlatformReleaseEntry, +): { ok: boolean; reason: string } { + if (!paper.protocol || !velocity.protocol) { + return { ok: false, reason: t.value.unknown }; + } + const r = checkCompatibility(paper.protocol, velocity.protocol); + return { ok: r === 'compatible', reason: r === 'compatible' ? '' : reasonLabel(r) }; +} +</script> + +<template> + <div class="compat-matrix"> + <div v-if="loading" class="compat-loading"> + <p>{{ t.loading }}</p> + </div> + + <div v-else-if="error" class="compat-error"> + <p> + {{ t.error + }}<a href="https://github.com/m1sk9/LunaticChat/releases" target="_blank" rel="noopener">GitHub Releases</a>{{ t.errorSuffix }} + </p> + </div> + + <div v-else-if="sortedPaper.length === 0 && sortedVelocity.length === 0" class="compat-empty"> + <p>{{ t.empty }}</p> + </div> + + <div v-else class="compat-content"> + <div class="compat-table-wrapper"> + <table class="compat-table"> + <thead> + <tr> + <th class="compat-corner"> + <span class="compat-axis-paper">{{ t.paperVersion }}</span> + <span class="compat-axis-divider">/</span> + <span class="compat-axis-velocity">{{ t.velocityVersion }}</span> + </th> + <th v-for="v in sortedVelocity" :key="v.tag" class="compat-col-header"> + <div class="compat-version">v{{ v.version }}</div> + <div class="compat-protocol"> + {{ t.protocol }}: {{ v.protocol ? formatProtocol(v.protocol) : t.unknown }} + </div> + </th> + </tr> + </thead> + <tbody> + <tr v-if="sortedPaper.length === 0"> + <td colspan="100" class="compat-empty-cell">{{ t.emptyPaper }}</td> + </tr> + <tr v-for="p in sortedPaper" :key="p.tag"> + <th scope="row" class="compat-row-header"> + <div class="compat-version">v{{ p.version }}</div> + <div class="compat-protocol"> + {{ t.protocol }}: {{ p.protocol ? formatProtocol(p.protocol) : t.unknown }} + </div> + </th> + <td + v-for="v in sortedVelocity" + :key="v.tag" + :class="['compat-cell', cellResult(p, v).ok ? 'compat-ok' : 'compat-ng']" + :title="cellResult(p, v).reason || t.compatible" + > + <span v-if="cellResult(p, v).ok" class="compat-mark-ok" :aria-label="t.compatible">✓</span> + <span v-else class="compat-mark-ng" :aria-label="t.incompatible">✗</span> + </td> + <td v-if="sortedVelocity.length === 0" class="compat-empty-cell">{{ t.emptyVelocity }}</td> + </tr> + </tbody> + </table> + </div> + + <div class="compat-legend"> + <p class="compat-legend-title">{{ t.legend }}</p> + <ul> + <li><span class="compat-mark-ok">✓</span> {{ t.legendCompatible }}</li> + <li><span class="compat-mark-ng">✗</span> {{ t.legendIncompatible }}</li> + </ul> + </div> + </div> + </div> +</template> + +<style scoped> +.compat-matrix { + margin: 24px 0; +} + +.compat-loading, +.compat-empty { + text-align: center; + padding: 32px 0; + color: var(--vp-c-text-2); +} + +.compat-error { + border: 1px solid var(--vp-c-danger-soft); + background: var(--vp-c-danger-soft); + border-radius: 8px; + padding: 16px 20px; +} + +.compat-error a { + color: var(--vp-c-brand-1); + text-decoration: underline; +} + +.compat-table-wrapper { + overflow-x: auto; + border: 1px solid var(--vp-c-divider); + border-radius: 8px; +} + +.compat-table { + border-collapse: collapse; + width: 100%; + margin: 0; + font-size: 0.875rem; +} + +.compat-table th, +.compat-table td { + border: 1px solid var(--vp-c-divider); + padding: 8px 12px; + text-align: center; + vertical-align: middle; +} + +.compat-corner { + background: var(--vp-c-bg-soft); + font-weight: 500; + font-size: 0.75rem; + white-space: nowrap; + text-align: left !important; +} + +.compat-axis-paper, +.compat-axis-velocity { + display: inline-block; +} + +.compat-axis-divider { + margin: 0 4px; + color: var(--vp-c-text-3); +} + +.compat-col-header, +.compat-row-header { + background: var(--vp-c-bg-soft); + font-weight: 600; + white-space: nowrap; +} + +.compat-version { + font-size: 0.95rem; +} + +.compat-protocol { + font-size: 0.7rem; + color: var(--vp-c-text-2); + font-weight: 400; + margin-top: 2px; +} + +.compat-cell { + font-size: 1.1rem; + font-weight: 700; +} + +.compat-ok { + background: rgba(20, 200, 100, 0.08); +} + +.compat-ng { + background: rgba(220, 60, 60, 0.06); +} + +.compat-mark-ok { + color: rgb(20, 160, 90); +} + +.compat-mark-ng { + color: rgb(200, 60, 60); +} + +.compat-empty-cell { + color: var(--vp-c-text-3); + font-style: italic; +} + +.compat-legend { + margin-top: 16px; + font-size: 0.85rem; + color: var(--vp-c-text-2); +} + +.compat-legend-title { + font-weight: 600; + margin-bottom: 4px; +} + +.compat-legend ul { + margin: 0; + padding-left: 20px; +} + +.compat-legend li { + line-height: 1.7; +} +</style> diff --git a/website/.vitepress/theme/components/DownloadCard.vue b/website/.vitepress/theme/components/DownloadCard.vue index a03adfb..4337181 100644 --- a/website/.vitepress/theme/components/DownloadCard.vue +++ b/website/.vitepress/theme/components/DownloadCard.vue @@ -29,8 +29,11 @@ const t = computed(() => 'The latest build from the main branch is available from CI. Development builds are not guaranteed to be stable.', viewCiBuilds: 'View latest CI builds', compatNotice: - 'Paper and Velocity plugins are versioned with a protocol version. For MINOR version changes, update Velocity first. For MAJOR version changes, update all servers simultaneously.', - compatLink: 'See Velocity Integration - Protocol Version for details.', + 'Using the latest Paper and the latest Velocity always works. If you need to mix older versions, check the compatibility matrix below.', + compatLink: 'See Paper / Velocity Compatibility for details.', + compatMatrixTitle: 'Compatibility Matrix', + compatMatrixDesc: + 'Combinations marked ✓ can connect. Hover a cell for details.', spigotNotice: 'LunaticChat only supports Paper / Folia servers. It does not work on Spigot or BungeeCord, and there are no plans to support them in the future.', spigotAlt: @@ -57,8 +60,11 @@ const t = computed(() => '最新の main ブランチのビルドは CI から取得できます。開発ビルドは安定性が保証されていません。', viewCiBuilds: '最新の CI ビルドを確認', compatNotice: - 'Paper プラグインと Velocity プラグインはプロトコルバージョンで互換性が管理されています.MINOR バージョン変更時は Velocity を先にアップデートしてください.MAJOR バージョン変更時は全サーバーを同時にアップデートする必要があります.', - compatLink: '詳細は Velocity 連携 - プロトコルバージョン を参照してください.', + '両プラグインの最新版同士は常に互換性があります.古いバージョンを混ぜる場合のみ,下の互換性マトリクスを確認してください.', + compatLink: '詳細は Paper / Velocity 互換性 を参照してください.', + compatMatrixTitle: '互換性マトリクス', + compatMatrixDesc: + '✓ の組み合わせは接続可能です.セルにホバーすると詳細が表示されます.', spigotNotice: 'LunaticChat は Paper / Folia サーバーのみをサポートしています.Spigot / BungeeCord では動作せず,今後も対応予定はありません.', spigotAlt: @@ -107,7 +113,7 @@ function formatDate(dateStr: string | null): string { <div class="download-compat-notice"> <p class="download-compat-title">{{ t.compatible }}</p> <p>{{ t.compatNotice }}</p> - <p><a :href="isEn ? '/en/docs/features/velocity#protocol-version' : '/docs/features/velocity#プロトコルバージョン'">{{ t.compatLink }}</a></p> + <p><a :href="isEn ? '/en/docs/reference/compatibility' : '/docs/reference/compatibility'">{{ t.compatLink }}</a></p> </div> <div v-if="loading" class="download-loading"> @@ -188,6 +194,10 @@ function formatDate(dateStr: string | null): string { </div> </div> + <h2>{{ t.compatMatrixTitle }}</h2> + <p>{{ t.compatMatrixDesc }}</p> + <CompatibilityMatrix /> + <h2>Modrinth</h2> <p>{{ t.modrinthDesc }}</p> <div class="download-ci"> diff --git a/website/.vitepress/theme/components/useCompatibilityData.ts b/website/.vitepress/theme/components/useCompatibilityData.ts new file mode 100644 index 0000000..452e8b4 --- /dev/null +++ b/website/.vitepress/theme/components/useCompatibilityData.ts @@ -0,0 +1,167 @@ +import { ref, onMounted } from 'vue'; + +const REPO = 'm1sk9/LunaticChat'; +const PROTOCOL_FILE_PATH = + 'engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt'; + +interface GitHubRelease { + tag_name: string; + published_at: string; + html_url: string; +} + +export interface ProtocolVersion { + major: number; + minor: number; + patch: number; + minSupportedMinor: number; +} + +export interface PlatformReleaseEntry { + platform: 'paper' | 'velocity'; + version: string; + tag: string; + publishedAt: string; + releaseUrl: string; + protocol: ProtocolVersion | null; +} + +export interface CompatibilityData { + paper: PlatformReleaseEntry[]; + velocity: PlatformReleaseEntry[]; +} + +function parseProtocolVersion(source: string): ProtocolVersion | null { + const match = (key: string): number | null => { + const re = new RegExp(`const\\s+val\\s+${key}\\s*=\\s*(\\d+)`); + const m = source.match(re); + return m ? Number.parseInt(m[1], 10) : null; + }; + + const major = match('MAJOR'); + const minor = match('MINOR'); + const patch = match('PATCH'); + const minSupportedMinor = match('MIN_SUPPORTED_MINOR'); + + if ( + major === null || + minor === null || + patch === null || + minSupportedMinor === null + ) { + return null; + } + + return { major, minor, patch, minSupportedMinor }; +} + +async function fetchProtocolAtTag(tag: string): Promise<ProtocolVersion | null> { + const url = `https://raw.githubusercontent.com/${REPO}/${encodeURIComponent(tag)}/${PROTOCOL_FILE_PATH}`; + try { + const res = await fetch(url); + if (!res.ok) return null; + const text = await res.text(); + return parseProtocolVersion(text); + } catch { + return null; + } +} + +function buildEntry( + release: GitHubRelease, + platform: 'paper' | 'velocity', +): PlatformReleaseEntry { + const version = release.tag_name.replace(/^(paper\/|velocity\/)?v/, ''); + return { + platform, + version, + tag: release.tag_name, + publishedAt: release.published_at, + releaseUrl: release.html_url, + protocol: null, + }; +} + +export function useCompatibilityData() { + const data = ref<CompatibilityData>({ paper: [], velocity: [] }); + const loading = ref(true); + const error = ref(false); + + onMounted(async () => { + try { + const res = await fetch( + `https://api.github.com/repos/${REPO}/releases?per_page=100`, + ); + if (!res.ok) { + error.value = true; + return; + } + + const releases: GitHubRelease[] = await res.json(); + + const isUnified = (tag: string) => /^v\d/.test(tag); + const isPaperTag = (tag: string) => + tag.startsWith('paper/v') || isUnified(tag); + const isVelocityTag = (tag: string) => + tag.startsWith('velocity/v') || isUnified(tag); + + const paper = releases + .filter((r) => isPaperTag(r.tag_name)) + .map((r) => buildEntry(r, 'paper')); + const velocity = releases + .filter((r) => isVelocityTag(r.tag_name)) + .map((r) => buildEntry(r, 'velocity')); + + const all = [...paper, ...velocity]; + const protocols = await Promise.all( + all.map((entry) => fetchProtocolAtTag(entry.tag)), + ); + all.forEach((entry, i) => { + entry.protocol = protocols[i]; + }); + + data.value = { + paper: paper.filter((e) => e.protocol !== null), + velocity: velocity.filter((e) => e.protocol !== null), + }; + } catch { + error.value = true; + } finally { + loading.value = false; + } + }); + + return { data, loading, error }; +} + +export type CompatibilityResult = 'compatible' | 'major-mismatch' | 'paper-too-new' | 'velocity-too-new' | 'paper-too-old' | 'velocity-too-old'; + +export function checkCompatibility( + paper: ProtocolVersion, + velocity: ProtocolVersion, +): CompatibilityResult { + if (paper.major !== velocity.major) return 'major-mismatch'; + + const paperAcceptsVelocity = + velocity.minor >= paper.minSupportedMinor && velocity.minor <= paper.minor; + const velocityAcceptsPaper = + paper.minor >= velocity.minSupportedMinor && paper.minor <= velocity.minor; + + if (paperAcceptsVelocity && velocityAcceptsPaper) return 'compatible'; + if (!velocityAcceptsPaper && paper.minor > velocity.minor) return 'paper-too-new'; + if (!paperAcceptsVelocity && velocity.minor > paper.minor) return 'velocity-too-new'; + if (!velocityAcceptsPaper && paper.minor < velocity.minSupportedMinor) return 'paper-too-old'; + if (!paperAcceptsVelocity && velocity.minor < paper.minSupportedMinor) return 'velocity-too-old'; + return 'major-mismatch'; +} + +export function isCompatible( + paper: ProtocolVersion, + velocity: ProtocolVersion, +): boolean { + return checkCompatibility(paper, velocity) === 'compatible'; +} + +export function formatProtocol(p: ProtocolVersion): string { + return `${p.major}.${p.minor}.${p.patch}`; +} diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts index bc82522..75bda89 100644 --- a/website/.vitepress/theme/index.ts +++ b/website/.vitepress/theme/index.ts @@ -1,5 +1,6 @@ import type { Theme } from 'vitepress'; import DefaultTheme from 'vitepress/theme'; +import CompatibilityMatrix from './components/CompatibilityMatrix.vue'; import DownloadCard from './components/DownloadCard.vue'; import './custom.css'; @@ -7,5 +8,6 @@ export default { extends: DefaultTheme, enhanceApp({ app }) { app.component('DownloadCard', DownloadCard); + app.component('CompatibilityMatrix', CompatibilityMatrix); }, } satisfies Theme; |
