summaryrefslogtreecommitdiff
path: root/website/.vitepress
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-05-06 06:09:43 +0900
committerSho Sakuma <me@m1sk9.dev>2026-05-06 06:09:43 +0900
commitc092edbab0e8b516faaa459f6d0915c7df3cd06f (patch)
tree10e9394dcd35270c5374f5396bf8b86b7a711c2b /website/.vitepress
parentbbd63b0032965ae58f7a20988312ff5012be543f (diff)
downloadLunaticChat-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.ts4
-rw-r--r--website/.vitepress/config/ja.ts4
-rw-r--r--website/.vitepress/theme/components/CompatibilityMatrix.vue313
-rw-r--r--website/.vitepress/theme/components/DownloadCard.vue20
-rw-r--r--website/.vitepress/theme/components/useCompatibilityData.ts167
-rw-r--r--website/.vitepress/theme/index.ts2
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;