diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-04-04 21:57:48 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-04 21:57:48 +0900 |
| commit | bd92ba60d39b4956a580cc56b83ecf55dd348aea (patch) | |
| tree | 6b548cf832405c008a8370efb52468fa71ad86b3 | |
| parent | 78a6d7ae3fc3c6fcb98a048889cd489d34a10978 (diff) | |
| parent | a20c385528fc1d7515ded0c0978cda05e66c3c98 (diff) | |
| download | LunaticChat-bd92ba60d39b4956a580cc56b83ecf55dd348aea.tar.gz LunaticChat-bd92ba60d39b4956a580cc56b83ecf55dd348aea.tar.bz2 LunaticChat-bd92ba60d39b4956a580cc56b83ecf55dd348aea.zip | |
Merge pull request #165 from m1sk9/feat/velocity-support
feat: Improving Velocity's Cycling Compatibility
21 files changed, 638 insertions, 175 deletions
diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000..ff3edc1 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,33 @@ +--- +description: リリースの準備とタグ作成を行う。引数に paper, velocity, both のいずれかを指定。 +--- + +# Release Command + +引数: `$ARGUMENTS` + +## 手順 + +1. `gradle.properties` から `paperVersion` と `velocityVersion` を読み取る +2. 引数に応じてリリース対象を決定: + - `paper` → Paper のみリリース + - `velocity` → Velocity のみリリース + - `both` → 両方同時リリース + - 引数なし → ユーザーに確認する +3. リリース前チェック: + - `./gradlew ktlintCheck` が通ること + - `./gradlew test` が通ること + - 対象プラットフォームの `shadowJar` がビルドできること + - git working tree がクリーンであること + - 該当タグがまだ存在しないこと +4. チェックがすべて通ったら、タグの作成をユーザーに提案する: + - `paper` → `git tag paper/v{paperVersion}` + - `velocity` → `git tag velocity/v{velocityVersion}` + - `both` → `git tag v{paperVersion}` (Paper/Velocity バージョンが異なる場合は注意を促す) +5. タグを push するかユーザーに確認する (`git push origin {tag}`) + +## 注意事項 + +- タグの作成と push は必ずユーザーの確認を得てから行うこと +- `both` の場合、Paper と Velocity のバージョンが異なる場合はその旨を明示すること +- プロトコルバージョン (`ProtocolVersion.kt`) の変更がある場合は、後方互換テスト (`ProtocolBackwardCompatibilityTest`) が通っていることを確認すること diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml deleted file mode 100644 index 33de763..0000000 --- a/.github/workflows/nightly.yaml +++ /dev/null @@ -1,111 +0,0 @@ -name: Nightly Release - -on: - push: - branches: - - main - paths-ignore: - - 'docs/**' - - '*.md' - - 'LICENSE' - -concurrency: - group: nightly-release - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-24.04 - outputs: - version: ${{ steps.version.outputs.version }} - paper_jar_name: ${{ steps.version.outputs.paper_jar_name }} - velocity_jar_name: ${{ steps.version.outputs.velocity_jar_name }} - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Compute nightly version - id: version - run: | - BASE_VERSION=$(grep '?: "' build.gradle.kts | head -1 | sed 's/.*?: "\(.*\)"/\1/') - if [ -z "$BASE_VERSION" ]; then - BASE_VERSION="1.0.0" - fi - SHORT_HASH=$(git rev-parse --short HEAD) - NIGHTLY_VERSION="${BASE_VERSION}-nightly.${SHORT_HASH}" - echo "version=$NIGHTLY_VERSION" >> $GITHUB_OUTPUT - echo "paper_jar_name=LunaticChat-${NIGHTLY_VERSION}.jar" >> $GITHUB_OUTPUT - echo "velocity_jar_name=LunaticChat-${NIGHTLY_VERSION}-velocity.jar" >> $GITHUB_OUTPUT - echo "Nightly version: $NIGHTLY_VERSION" - - - name: Setup Development Environment - uses: jdx/mise-action@v4 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with shadowJar - run: | - ./gradlew :platform-paper:shadowJar :platform-velocity:shadowJar \ - -Pversion="${{ steps.version.outputs.version }}" \ - -PisNightly=true \ - --parallel --no-daemon - - - name: Upload build artifacts - uses: actions/upload-artifact@v7 - with: - name: LunaticChat-nightly-${{ steps.version.outputs.version }} - path: | - platform-paper/build/libs/${{ steps.version.outputs.paper_jar_name }} - platform-velocity/build/libs/${{ steps.version.outputs.velocity_jar_name }} - retention-days: 7 - - release: - runs-on: ubuntu-24.04 - needs: build - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Download build artifact - uses: actions/download-artifact@v8 - with: - name: LunaticChat-nightly-${{ needs.build.outputs.version }} - - - name: Delete existing nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - gh release delete nightly --yes --cleanup-tag 2>/dev/null || true - - - name: Create nightly release - env: - GH_TOKEN: ${{ github.token }} - run: | - VERSION=${{ needs.build.outputs.version }} - PAPER_JAR_NAME=${{ needs.build.outputs.paper_jar_name }} - VELOCITY_JAR_NAME=${{ needs.build.outputs.velocity_jar_name }} - - gh release create nightly \ - "platform-paper/build/libs/$PAPER_JAR_NAME" \ - "platform-velocity/build/libs/$VELOCITY_JAR_NAME" \ - --title "Nightly Build ($VERSION)" \ - --prerelease \ - --notes "$(cat <<'EOF' - ## ⚠️ Nightly Build - - This is an automatically generated nightly build from the latest `main` branch. - - **Warning:** This build may be unstable or contain bugs. Do not use in production environments. - - - **Version:** `${{ needs.build.outputs.version }}` - - **Commit:** ${{ github.sha }} - - If you encounter any issues, please report them on [GitHub Issues](https://github.com/m1sk9/LunaticChat/issues). - EOF - )" diff --git a/.github/workflows/release-paper.yaml b/.github/workflows/release-paper.yaml new file mode 100644 index 0000000..49c9af8 --- /dev/null +++ b/.github/workflows/release-paper.yaml @@ -0,0 +1,140 @@ +name: Release Paper + +on: + push: + tags: + - 'paper/v*' + +jobs: + validate: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.get_version.outputs.version }} + jar_name: ${{ steps.get_version.outputs.jar_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Extract version from tag and validate + id: get_version + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/paper/v} + GRADLE_VERSION=$(grep '^paperVersion=' gradle.properties | cut -d'=' -f2) + + if [ "$TAG_VERSION" != "$GRADLE_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match gradle.properties paperVersion ($GRADLE_VERSION)" + exit 1 + fi + + echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT + echo "jar_name=LunaticChat-$TAG_VERSION.jar" >> $GITHUB_OUTPUT + echo "Version validation passed: $TAG_VERSION" + + - name: Check if release already exists + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + if gh release view "$TAG_NAME" &> /dev/null; then + echo "Error: Release $TAG_NAME already exists" + exit 1 + fi + echo "Release validation passed: $TAG_NAME does not exist yet" + + build: + runs-on: ubuntu-24.04 + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Development Environment + uses: jdx/mise-action@v4 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Paper shadowJar + run: ./gradlew :platform-paper:shadowJar --no-daemon + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: LunaticChat-paper-v${{ needs.validate.outputs.version }} + path: platform-paper/build/libs/${{ needs.validate.outputs.jar_name }} + retention-days: 90 + + release: + runs-on: ubuntu-24.04 + needs: [validate, build] + permissions: + contents: write + env: + MODRINTH_CHANNEL: release + MODRINTH_PAPER_GAME_VERSIONS: "26.1.x" + MODRINTH_MC_VERSION: "26.1" + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: LunaticChat-paper-v${{ needs.validate.outputs.version }} + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${{ needs.validate.outputs.version }} + JAR_NAME=${{ needs.validate.outputs.jar_name }} + + cat > /tmp/release-notes.md <<EOF + LunaticChat Paper/Folia v${VERSION} has been released. + + ## Download + + - [GitHub](https://github.com/m1sk9/LunaticChat/releases/tag/${TAG_NAME}) + - [Modrinth (Paper/Folia)](https://modrinth.com/plugin/lunaticchat/version/${VERSION}) + + ## What's new + + ### Highlights + + ### New Features + + ### Improvements + + ### Changes + + ### Bug Fixes + + ### Notes + + EOF + + gh release create "$TAG_NAME" \ + "platform-paper/build/libs/$JAR_NAME" \ + --title "$TAG_NAME" \ + --draft \ + --notes-file /tmp/release-notes.md + + - name: Publish to Modrinth (Paper/Folia) + uses: cloudnode-pro/modrinth-publish@0be4916ad5f081d936eb5615aa35ce3f0949979c # v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + name: v${{ needs.validate.outputs.version }} (Paper/Folia v${{ env.MODRINTH_MC_VERSION }}) + project: ${{ secrets.MODRINTH_PROJECT_ID }} + version: ${{ needs.validate.outputs.version }} + channel: ${{ env.MODRINTH_CHANNEL }} + changelog: |- + Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/paper/v${{ needs.validate.outputs.version }}) for update details. + loaders: |- + paper + folia + game-versions: ${{ env.MODRINTH_PAPER_GAME_VERSIONS }} + files: platform-paper/build/libs/${{ needs.validate.outputs.jar_name }} diff --git a/.github/workflows/release-velocity.yaml b/.github/workflows/release-velocity.yaml new file mode 100644 index 0000000..6a78ea5 --- /dev/null +++ b/.github/workflows/release-velocity.yaml @@ -0,0 +1,139 @@ +name: Release Velocity + +on: + push: + tags: + - 'velocity/v*' + +jobs: + validate: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.get_version.outputs.version }} + jar_name: ${{ steps.get_version.outputs.jar_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Extract version from tag and validate + id: get_version + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/velocity/v} + GRADLE_VERSION=$(grep '^velocityVersion=' gradle.properties | cut -d'=' -f2) + + if [ "$TAG_VERSION" != "$GRADLE_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match gradle.properties velocityVersion ($GRADLE_VERSION)" + exit 1 + fi + + echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT + echo "jar_name=LunaticChat-$TAG_VERSION-velocity.jar" >> $GITHUB_OUTPUT + echo "Version validation passed: $TAG_VERSION" + + - name: Check if release already exists + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + if gh release view "$TAG_NAME" &> /dev/null; then + echo "Error: Release $TAG_NAME already exists" + exit 1 + fi + echo "Release validation passed: $TAG_NAME does not exist yet" + + build: + runs-on: ubuntu-24.04 + needs: validate + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Development Environment + uses: jdx/mise-action@v4 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build Velocity shadowJar + run: ./gradlew :platform-velocity:shadowJar --no-daemon + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: LunaticChat-velocity-v${{ needs.validate.outputs.version }} + path: platform-velocity/build/libs/${{ needs.validate.outputs.jar_name }} + retention-days: 90 + + release: + runs-on: ubuntu-24.04 + needs: [validate, build] + permissions: + contents: write + env: + MODRINTH_CHANNEL: release + MODRINTH_VELOCITY_GAME_VERSIONS: |- + 1.21.x + 26.1.x + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download build artifact + uses: actions/download-artifact@v8 + with: + name: LunaticChat-velocity-v${{ needs.validate.outputs.version }} + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${{ needs.validate.outputs.version }} + JAR_NAME=${{ needs.validate.outputs.jar_name }} + + cat > /tmp/release-notes.md <<EOF + LunaticChat Velocity v${VERSION} has been released. + + ## Download + + - [GitHub](https://github.com/m1sk9/LunaticChat/releases/tag/${TAG_NAME}) + - [Modrinth (Velocity)](https://modrinth.com/plugin/lunaticchat/version/${VERSION}-velocity) + + ## What's new + + ### Highlights + + ### New Features + + ### Improvements + + ### Changes + + ### Bug Fixes + + ### Notes + + EOF + + gh release create "$TAG_NAME" \ + "platform-velocity/build/libs/$JAR_NAME" \ + --title "$TAG_NAME" \ + --draft \ + --notes-file /tmp/release-notes.md + + - name: Publish to Modrinth (Velocity) + uses: cloudnode-pro/modrinth-publish@0be4916ad5f081d936eb5615aa35ce3f0949979c # v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + name: v${{ needs.validate.outputs.version }} (Velocity) + project: ${{ secrets.MODRINTH_PROJECT_ID }} + version: ${{ needs.validate.outputs.version }}-velocity + channel: ${{ env.MODRINTH_CHANNEL }} + changelog: |- + Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/velocity/v${{ needs.validate.outputs.version }}) for update details. + loaders: velocity + game-versions: ${{ env.MODRINTH_VELOCITY_GAME_VERSIONS }} + files: platform-velocity/build/libs/${{ needs.validate.outputs.jar_name }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aaa22a3..f8000b4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,32 +9,25 @@ jobs: validate: runs-on: ubuntu-24.04 outputs: - version: ${{ steps.get_version.outputs.version }} + paper_version: ${{ steps.get_version.outputs.paper_version }} + velocity_version: ${{ steps.get_version.outputs.velocity_version }} paper_jar_name: ${{ steps.get_version.outputs.paper_jar_name }} velocity_jar_name: ${{ steps.get_version.outputs.velocity_jar_name }} steps: - name: Checkout repository uses: actions/checkout@v6 - - name: Extract version from build.gradle.kts + - name: Extract versions from gradle.properties id: get_version run: | - VERSION=$(grep ' version = ' build.gradle.kts | sed 's/.*version = "\(.*\)"/\1/') - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "paper_jar_name=LunaticChat-$VERSION.jar" >> $GITHUB_OUTPUT - echo "velocity_jar_name=LunaticChat-$VERSION-velocity.jar" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" + PAPER_VERSION=$(grep '^paperVersion=' gradle.properties | cut -d'=' -f2) + VELOCITY_VERSION=$(grep '^velocityVersion=' gradle.properties | cut -d'=' -f2) - - name: Validate tag matches version - run: | - TAG_VERSION=${GITHUB_REF#refs/tags/v} - GRADLE_VERSION="${{ steps.get_version.outputs.version }}" - - if [ "$TAG_VERSION" != "$GRADLE_VERSION" ]; then - echo "Error: Tag version ($TAG_VERSION) does not match build.gradle.kts version ($GRADLE_VERSION)" - exit 1 - fi - echo "Version validation passed: $TAG_VERSION" + echo "paper_version=$PAPER_VERSION" >> $GITHUB_OUTPUT + echo "velocity_version=$VELOCITY_VERSION" >> $GITHUB_OUTPUT + echo "paper_jar_name=LunaticChat-$PAPER_VERSION.jar" >> $GITHUB_OUTPUT + echo "velocity_jar_name=LunaticChat-$VELOCITY_VERSION-velocity.jar" >> $GITHUB_OUTPUT + echo "Paper version: $PAPER_VERSION, Velocity version: $VELOCITY_VERSION" - name: Check if release already exists env: @@ -69,7 +62,7 @@ jobs: - name: Upload build artifacts uses: actions/upload-artifact@v7 with: - name: LunaticChat-v${{ needs.validate.outputs.version }} + name: LunaticChat-${{ github.ref_name }} path: | platform-paper/build/libs/${{ needs.validate.outputs.paper_jar_name }} platform-velocity/build/libs/${{ needs.validate.outputs.velocity_jar_name }} @@ -94,28 +87,30 @@ jobs: - name: Download build artifact uses: actions/download-artifact@v8 with: - name: LunaticChat-v${{ needs.validate.outputs.version }} - - - name: Get current date - id: get_date - run: echo "current_date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + name: LunaticChat-${{ github.ref_name }} - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} run: | TAG_NAME=${GITHUB_REF#refs/tags/} - VERSION=${{ needs.validate.outputs.version }} + PAPER_VERSION=${{ needs.validate.outputs.paper_version }} + VELOCITY_VERSION=${{ needs.validate.outputs.velocity_version }} PAPER_JAR_NAME=${{ needs.validate.outputs.paper_jar_name }} VELOCITY_JAR_NAME=${{ needs.validate.outputs.velocity_jar_name }} cat > /tmp/release-notes.md <<EOF - LunaticChat v${VERSION} has been released. + LunaticChat ${TAG_NAME} has been released. + + | Platform | Version | + |----------|---------| + | Paper/Folia | ${PAPER_VERSION} | + | Velocity | ${VELOCITY_VERSION} | ## Download - - [GitHub](https://github.com/m1sk9/LunaticChat/releases/tag/v${VERSION}) - - Modrinth: [Paper/Folia](https://modrinth.com/plugin/lunaticchat/version/${VERSION}), [Velocity](https://modrinth.com/plugin/lunaticchat/version/${VERSION}-velocity) + - [GitHub](https://github.com/m1sk9/LunaticChat/releases/tag/${TAG_NAME}) + - Modrinth: [Paper/Folia](https://modrinth.com/plugin/lunaticchat/version/${PAPER_VERSION}), [Velocity](https://modrinth.com/plugin/lunaticchat/version/${VELOCITY_VERSION}-velocity) ## What's new @@ -144,12 +139,12 @@ jobs: uses: cloudnode-pro/modrinth-publish@0be4916ad5f081d936eb5615aa35ce3f0949979c # v2 with: token: ${{ secrets.MODRINTH_TOKEN }} - name: v${{ needs.validate.outputs.version }} (Paper/Folia v${{ env.MODRINTH_MC_VERSION }}) + name: v${{ needs.validate.outputs.paper_version }} (Paper/Folia v${{ env.MODRINTH_MC_VERSION }}) project: ${{ secrets.MODRINTH_PROJECT_ID }} - version: ${{ needs.validate.outputs.version }} + version: ${{ needs.validate.outputs.paper_version }} channel: ${{ env.MODRINTH_CHANNEL }} changelog: |- - Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/v${{ needs.validate.outputs.version }}) for update details. + Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/${{ github.ref_name }}) for update details. loaders: |- paper folia @@ -160,13 +155,12 @@ jobs: uses: cloudnode-pro/modrinth-publish@0be4916ad5f081d936eb5615aa35ce3f0949979c # v2 with: token: ${{ secrets.MODRINTH_TOKEN }} - name: v${{ needs.validate.outputs.version }} (Velocity) + name: v${{ needs.validate.outputs.velocity_version }} (Velocity) project: ${{ secrets.MODRINTH_PROJECT_ID }} - version: ${{ needs.validate.outputs.version }}-velocity + version: ${{ needs.validate.outputs.velocity_version }}-velocity channel: ${{ env.MODRINTH_CHANNEL }} changelog: |- - Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/v${{ needs.validate.outputs.version }}) for update details. + Please refer to the [GitHub release](https://github.com/m1sk9/LunaticChat/releases/tag/${{ github.ref_name }}) for update details. loaders: velocity game-versions: ${{ env.MODRINTH_VELOCITY_GAME_VERSIONS }} files: platform-velocity/build/libs/${{ needs.validate.outputs.velocity_jar_name }} - diff --git a/CHANGELOG.md b/CHANGELOG.md index 73cf134..09a5710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,7 @@ - Support for Paper 1.21.X, Folia 1.21.X (and later) has been dropped. - We have added warnings when running `/lc status` or `/lcv status`, or when logging in, if you are using the Nightly version. - Fixed an issue where, while Romaji conversion was enabled, chat events were not registered under certain conditions, making Romaji conversion unavailable. -- We have started distributing Nightly releases - - Whenever updates are made to `main` (the default branch), we will distribute the current build via GitHub Releases - - Since the Nightly version is a work-in-progress, it may be unstable or contain bugs. If you encounter any issues, please report them on GitHub Issues - - **The Nightly version is not available on Modrinth**. +- Improving Velocity's Cycling Compatibility. ### v0.11.0 @@ -84,6 +84,43 @@ docs/ → VitePress documentation site | `listener` | Event listeners (chat, join/quit, plugin messages) | | `velocity` | Cross-server integration | +## Versioning & Release + +### Version Management + +Paper and Velocity have independent versions, managed in `gradle.properties`: + +```properties +paperVersion=1.0.0 +velocityVersion=1.0.0 +``` + +### Release Tags + +| Tag Pattern | Workflow | Target | +|-------------|----------|--------| +| `paper/v1.1.0` | `release-paper.yaml` | Paper/Folia JAR only | +| `velocity/v1.0.1` | `release-velocity.yaml` | Velocity JAR only | +| `v1.2.0` | `release.yaml` | Both (e.g., engine changes) | + +### Protocol Version (`engine/protocol/ProtocolVersion.kt`) + +Paper-Velocity compatibility is gated by protocol version only. Plugin version is not used for compatibility checks. + +| Level | When to bump | Deployment order | +|-------|-------------|-----------------| +| **PATCH** | Add optional fields, new sub-channels | Any order | +| **MINOR** | Add required fields | Velocity first, then Paper | +| **MAJOR** | Wire format changes | Simultaneous | + +`MIN_SUPPORTED_MINOR` controls the acceptance window for older MINOR versions. + +### Backward Compatibility + +- `PluginMessageCodec` uses `ignoreUnknownKeys = true` to ignore unknown fields +- `ProtocolBackwardCompatibilityTest` verifies backward compat via JSON snapshots +- Protocol changes must include snapshot tests + ## Code Conventions - Follows [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html), enforced by Ktlint diff --git a/build.gradle.kts b/build.gradle.kts index 1228449..b4196d7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,6 @@ plugins { allprojects { group = "dev.m1sk9" - version = findProperty("version")?.toString()?.takeIf { it != "unspecified" } ?: "1.0.0" repositories { mavenCentral() diff --git a/codecov.yml b/codecov.yml index e23de4b..453e954 100644 --- a/codecov.yml +++ b/codecov.yml @@ -21,6 +21,9 @@ ignore: # VelocityStatusCommand is tightly coupled to VelocityConnectionManager and has no unit-test # infrastructure; it is exercised via end-to-end multi-server tests. - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt" + # VelocityConnectionManager is tightly coupled to the Paper/Bukkit runtime (Plugin, Player, + # PluginMessageListener) and is exercised via end-to-end multi-server tests. + - "platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt" # The Velocity platform has no unit-test infrastructure; it is exercised via end-to-end # multi-server tests (docker compose). - "platform-velocity/src/main/kotlin/**" diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt index cee7837..f7090da 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessage.kt @@ -34,6 +34,9 @@ sealed interface PluginMessage { val compatible: Boolean, val velocityVersion: String, val error: String? = null, + val protocolMajor: Int = ProtocolVersion.MAJOR, + val protocolMinor: Int = ProtocolVersion.MINOR, + val protocolPatch: Int = ProtocolVersion.PATCH, ) : PluginMessage /** diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt index a57d3a7..3cbb2cd 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersion.kt @@ -4,26 +4,63 @@ package dev.m1sk9.lunaticChat.engine.protocol * LunaticChat protocol version definition * * Manages version of communication protocol between Paper and Velocity. - * MAJOR.MINOR must match; differences in PATCH are compatible. + * + * ## Version Bump Rules + * + * **PATCH** (e.g., 1.0.0 -> 1.0.1): + * - Add optional fields with default values to existing message types. + * - Add new sub-channels that peers can safely ignore. + * - No deployment coordination required. + * + * **MINOR** (e.g., 1.0.x -> 1.1.0): + * - Add required fields to existing messages. + * - Add sub-channels whose absence degrades functionality. + * - Deployment order: update Velocity first, then Paper servers. + * - Set [MIN_SUPPORTED_MINOR] to control the deprecation window. + * + * **MAJOR** (e.g., 1.x.x -> 2.0.0): + * - Remove or rename existing sub-channels or fields. + * - Change wire format or encoding. + * - Requires simultaneous deployment of all components. + * + * ## Adding a New Message Type (sub-channel) + * 1. Add the data class to [PluginMessage]. + * 2. Add a sub-channel constant to [PluginMessageCodec.SubChannel]. + * 3. Add encode/decode branches in [PluginMessageCodec]. + * 4. Add a backward compatibility snapshot to ProtocolBackwardCompatibilityTest. + * 5. Bump PATCH if the new sub-channel is optional, MINOR if it is required. */ object ProtocolVersion { const val MAJOR = 1 const val MINOR = 0 const val PATCH = 0 + /** + * Minimum MINOR version this build can interoperate with (same MAJOR). + * + * When bumping MINOR, set this to the oldest MINOR version that should still + * be accepted. This allows a controlled deprecation window for rolling updates. + */ + const val MIN_SUPPORTED_MINOR = 0 + val version: String = "$MAJOR.$MINOR.$PATCH" /** * Checks if specified protocol version is compatible * + * Compatibility rules: + * - MAJOR must match exactly. + * - Remote MINOR must be >= [MIN_SUPPORTED_MINOR] and <= [MINOR]. + * - PATCH is always ignored. + * * @param major Major version * @param minor Minor version - * @return true if MAJOR and MINOR match + * @return true if compatible */ fun isCompatible( major: Int, minor: Int, - ): Boolean = MAJOR == major && MINOR == minor + ): Boolean = MAJOR == major && minor in MIN_SUPPORTED_MINOR..MINOR /** * Checks compatibility from version string diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt index c792aad..e848780 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt @@ -45,6 +45,28 @@ class PluginMessageCodecTest { } @Test + fun `encode and decode HandshakeResponse with protocol version fields`() { + val original = + PluginMessage.HandshakeResponse( + compatible = true, + velocityVersion = "0.11.0", + protocolMajor = 1, + protocolMinor = 1, + protocolPatch = 0, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.HandshakeResponse>(decoded) + assertEquals(original.compatible, decoded.compatible) + assertEquals(original.velocityVersion, decoded.velocityVersion) + assertEquals(1, decoded.protocolMajor) + assertEquals(1, decoded.protocolMinor) + assertEquals(0, decoded.protocolPatch) + } + + @Test fun `encode and decode HandshakeResponse incompatible round-trip`() { val original = PluginMessage.HandshakeResponse( diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt new file mode 100644 index 0000000..217baef --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolBackwardCompatibilityTest.kt @@ -0,0 +1,140 @@ +package dev.m1sk9.lunaticChat.engine.protocol + +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Backward compatibility tests using fixed JSON snapshots. + * + * These snapshots represent the wire format of protocol version 1.0.0. + * DO NOT MODIFY these constants after commit — they are the compatibility contract. + * If a test fails after modifying a @Serializable class, it means backward compatibility is broken. + */ +class ProtocolBackwardCompatibilityTest { + companion object { + // Protocol 1.0.0 snapshots — NEVER MODIFY after commit + const val HANDSHAKE_V1_0_0 = + """{"pluginVersion":"0.10.0","protocolMajor":1,"protocolMinor":0,"protocolPatch":0}""" + + const val HANDSHAKE_RESPONSE_V1_0_0 = + """{"compatible":true,"velocityVersion":"0.10.0"}""" + + const val HANDSHAKE_RESPONSE_INCOMPATIBLE_V1_0_0 = + """{"compatible":false,"velocityVersion":"0.10.0","error":"Version mismatch"}""" + + const val STATUS_RESPONSE_V1_0_0 = + """{"velocityVersion":"0.10.0","protocolVersion":"1.0.0","online":true}""" + + const val GLOBAL_CHAT_V1_0_0 = + """{"messageId":"abc-123","serverName":"lobby","playerId":"00000001-0000-0000-0000-000000000000","playerName":"TestPlayer","message":"Hello, world!","timestamp":1000}""" + } + + private fun buildRawMessage( + subChannel: String, + json: String, + ): ByteArray { + val out = ByteArrayOutputStream() + val dataOut = DataOutputStream(out) + dataOut.writeUTF(subChannel) + dataOut.writeUTF(json) + return out.toByteArray() + } + + @Test + fun `current codec can decode protocol 1_0_0 Handshake`() { + val data = buildRawMessage("handshake", HANDSHAKE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.Handshake>(decoded) + assertEquals("0.10.0", decoded.pluginVersion) + assertEquals(1, decoded.protocolMajor) + assertEquals(0, decoded.protocolMinor) + assertEquals(0, decoded.protocolPatch) + } + + @Test + fun `current codec can decode HandshakeResponse without protocol version fields`() { + val data = buildRawMessage("handshake_response", HANDSHAKE_RESPONSE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.HandshakeResponse>(decoded) + assertTrue(decoded.compatible) + assertEquals("0.10.0", decoded.velocityVersion) + assertNull(decoded.error) + // Protocol fields should get defaults when missing from old format + assertEquals(ProtocolVersion.MAJOR, decoded.protocolMajor) + assertEquals(ProtocolVersion.MINOR, decoded.protocolMinor) + assertEquals(ProtocolVersion.PATCH, decoded.protocolPatch) + } + + @Test + fun `current codec can decode HandshakeResponse incompatible without protocol version fields`() { + val data = buildRawMessage("handshake_response", HANDSHAKE_RESPONSE_INCOMPATIBLE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.HandshakeResponse>(decoded) + assertEquals(false, decoded.compatible) + assertEquals("Version mismatch", decoded.error) + } + + @Test + fun `current codec can decode protocol 1_0_0 StatusResponse`() { + val data = buildRawMessage("status_response", STATUS_RESPONSE_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.StatusResponse>(decoded) + assertEquals("0.10.0", decoded.velocityVersion) + assertEquals("1.0.0", decoded.protocolVersion) + assertTrue(decoded.online) + } + + @Test + fun `current codec can decode protocol 1_0_0 GlobalChatMessage`() { + val data = buildRawMessage("global_chat", GLOBAL_CHAT_V1_0_0) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.GlobalChatMessage>(decoded) + assertEquals("abc-123", decoded.messageId) + assertEquals("lobby", decoded.serverName) + assertEquals("00000001-0000-0000-0000-000000000000", decoded.playerId) + assertEquals("TestPlayer", decoded.playerName) + assertEquals("Hello, world!", decoded.message) + assertEquals(1000L, decoded.timestamp) + } + + @Test + fun `current codec ignores unknown fields in Handshake`() { + val json = + """{"pluginVersion":"0.10.0","protocolMajor":1,"protocolMinor":0,"protocolPatch":0,"futureField":"value"}""" + val data = buildRawMessage("handshake", json) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.Handshake>(decoded) + assertEquals("0.10.0", decoded.pluginVersion) + } + + @Test + fun `current codec ignores unknown fields in GlobalChatMessage`() { + val json = + """{"messageId":"abc","serverName":"lobby","playerId":"pid","playerName":"Test","message":"Hello","timestamp":1000,"futureField":"value","anotherField":42}""" + val data = buildRawMessage("global_chat", json) + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.GlobalChatMessage>(decoded) + assertEquals("abc", decoded.messageId) + assertEquals("Hello", decoded.message) + } + + @Test + fun `StatusRequest sub-channel decodes correctly with empty JSON`() { + val data = buildRawMessage("status_request", "{}") + val decoded = PluginMessageCodec.decode(data) + + assertIs<PluginMessage.StatusRequest>(decoded) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt index 7f05039..e8b9df8 100644 --- a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt @@ -54,4 +54,38 @@ class ProtocolVersionTest { fun `isCompatible string with two parts should return false`() { assertFalse(ProtocolVersion.isCompatible("1.0")) } + + @Test + fun `MIN_SUPPORTED_MINOR should be less than or equal to MINOR`() { + assertTrue(ProtocolVersion.MIN_SUPPORTED_MINOR <= ProtocolVersion.MINOR) + } + + @Test + fun `isCompatible with minor equal to MINOR should return true`() { + assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR)) + } + + @Test + fun `isCompatible with minor equal to MIN_SUPPORTED_MINOR should return true`() { + assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MIN_SUPPORTED_MINOR)) + } + + @Test + fun `isCompatible with minor above MINOR should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR + 1)) + } + + @Test + fun `isCompatible with minor below MIN_SUPPORTED_MINOR should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MIN_SUPPORTED_MINOR - 1)) + } + + @Test + fun `isCompatible string with minor below MIN_SUPPORTED_MINOR should return false`() { + assertFalse( + ProtocolVersion.isCompatible( + "${ProtocolVersion.MAJOR}.${ProtocolVersion.MIN_SUPPORTED_MINOR - 1}.0", + ), + ) + } } diff --git a/gradle.properties b/gradle.properties index 7750c23..9ab6267 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,3 +3,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + +# Platform versions (can be released independently) +paperVersion=1.0.0 +velocityVersion=1.0.0 diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts index 9e494c8..6194c5a 100644 --- a/platform-paper/build.gradle.kts +++ b/platform-paper/build.gradle.kts @@ -6,6 +6,8 @@ plugins { id("org.jetbrains.dokka") } +version = findProperty("paperVersion")?.toString() ?: "0.0.0" + repositories { maven("https://repo.papermc.io/repository/maven-public/") { name = "papermc-repo" diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt index 439a29a..6c0596e 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lcv/VelocityStatusCommand.kt @@ -4,7 +4,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.engine.protocol.ProtocolVersion -import dev.m1sk9.lunaticChat.paper.BuildInfo import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.command.annotation.Command import dev.m1sk9.lunaticChat.paper.command.annotation.Permission @@ -140,15 +139,6 @@ class VelocityStatusCommand( ), ) - // Nightly warning - if (BuildInfo.isNightly) { - sendMessage( - MessageFormatter - .format(languageManager.getMessage("general.nightlyWarning")) - .color(NamedTextColor.YELLOW), - ) - } - // Connection status details connectionStatus.forEach { line -> line?.let { sendMessage(it) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt index 1a7e667..a93cb9f 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/velocity/VelocityConnectionManager.kt @@ -197,7 +197,10 @@ class VelocityConnectionManager( if (response.compatible) { state = ConnectionState.CONNECTED velocityVersion = response.velocityVersion - logger.info("Successfully connected to Velocity (version: ${response.velocityVersion})") + logger.info( + "Successfully connected to Velocity (version: ${response.velocityVersion}, " + + "protocol: ${response.protocolMajor}.${response.protocolMinor}.${response.protocolPatch})", + ) future.complete(HandshakeResult.Success(response.velocityVersion)) } else { state = ConnectionState.FAILED diff --git a/platform-velocity/build.gradle.kts b/platform-velocity/build.gradle.kts index f6a4e57..dcd9f9a 100644 --- a/platform-velocity/build.gradle.kts +++ b/platform-velocity/build.gradle.kts @@ -4,6 +4,8 @@ plugins { id("com.gradleup.shadow") } +version = findProperty("velocityVersion")?.toString() ?: "0.0.0" + repositories { maven("https://repo.papermc.io/repository/maven-public/") { name = "papermc-repo" diff --git a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt index 74da9f2..382eba4 100644 --- a/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt +++ b/platform-velocity/src/main/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandler.kt @@ -84,16 +84,7 @@ class PluginMessageHandler( "Plugin=${handshake.pluginVersion}, Protocol=${handshake.protocolMajor}.${handshake.protocolMinor}.${handshake.protocolPatch}", ) - // Plugin version check (exact match required) - val versionMatch = handshake.pluginVersion == pluginVersion - if (!versionMatch) { - val error = "Plugin version mismatch: Paper=${handshake.pluginVersion}, Velocity=$pluginVersion" - logger.error(error) - sendHandshakeResponse(connection, false, error) - return - } - - // Protocol version check (MAJOR.MINOR must match) + // Protocol version check (MAJOR must match, MINOR within supported range) val protocolCompatible = ProtocolVersion.isCompatible( handshake.protocolMajor, @@ -126,6 +117,9 @@ class PluginMessageHandler( compatible = compatible, velocityVersion = pluginVersion, error = error, + protocolMajor = ProtocolVersion.MAJOR, + protocolMinor = ProtocolVersion.MINOR, + protocolPatch = ProtocolVersion.PATCH, ) val data = PluginMessageCodec.encode(response) diff --git a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt index 876eec6..3342331 100644 --- a/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt +++ b/platform-velocity/src/test/kotlin/dev/m1sk9/lunaticChat/velocity/messaging/PluginMessageHandlerTest.kt @@ -69,7 +69,7 @@ class PluginMessageHandlerTest { } @Test - fun `onPluginMessage should reject version mismatch handshake`() { + fun `onPluginMessage should accept different plugin version with matching protocol`() { val (handler, _, _) = createHandler(pluginVersion = "0.10.0") val connection = createServerConnection() @@ -85,6 +85,7 @@ class PluginMessageHandlerTest { handler.onPluginMessage(event) + // Should still send a response (successful handshake) verify { connection.sendPluginMessage(any<ChannelIdentifier>(), any<ByteArray>()) } } |
