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
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
target-branch: "dev"
schedule:
interval: "weekly"
day: "monday"
Expand All @@ -22,6 +23,7 @@ updates:

- package-ecosystem: "github-actions"
directory: "/"
target-branch: "dev"
schedule:
interval: "weekly"
day: "monday"
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ name: Build
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
workflow_dispatch:

jobs:
Expand Down
201 changes: 140 additions & 61 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,104 @@
name: Release

on:
push:
tags:
- "v*"
pull_request:
branches:
- main
types:
- closed
workflow_dispatch:
inputs:
tag:
description: "Existing tag to publish a release for (e.g. v0.9.1-beta.1)"
required: true
type: string

concurrency:
group: release-main
queue: max

permissions:
contents: write
contents: read

jobs:
prepare-release:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'dev' && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
outputs:
prerelease: ${{ steps.metadata.outputs.prerelease }}
source_sha: ${{ steps.metadata.outputs.source_sha }}
tag: ${{ steps.metadata.outputs.tag }}
version: ${{ steps.metadata.outputs.version }}

steps:
- name: Checkout release source
uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event.pull_request.merge_commit_sha }}
fetch-depth: 0

- name: Resolve and validate release metadata
id: metadata
shell: bash
env:
REQUESTED_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail

SOURCE_SHA="$(git rev-parse HEAD)"
BUILD_VERSION="$(sed -n 's/^version = "\([^"]*\)"/\1/p' build.gradle.kts | head -n 1)"
if [[ ! "$BUILD_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::build.gradle.kts has an invalid release version: $BUILD_VERSION"
exit 1
fi
if [[ "$BUILD_VERSION" == *SNAPSHOT* ]]; then
echo "::error::Snapshot versions cannot be released: $BUILD_VERSION"
exit 1
fi

TAG="v$BUILD_VERSION"
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
if [[ ! "$REQUESTED_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid release tag: $REQUESTED_TAG"
exit 1
fi
if [[ "$REQUESTED_TAG" != "$TAG" ]]; then
echo "::error::Requested tag $REQUESTED_TAG does not match build.gradle.kts version $BUILD_VERSION"
exit 1
fi
fi

if [[ ! -f CHANGELOG.md ]] || ! grep -Eq "^## ${BUILD_VERSION//./\\.}([[:space:]]|$)" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no section for $BUILD_VERSION"
exit 1
fi

REMOTE_TAG_SHA="$(git ls-remote --tags origin "refs/tags/$TAG^{}" | awk 'NR == 1 { print $1 }')"
if [[ -z "$REMOTE_TAG_SHA" ]]; then
REMOTE_TAG_SHA="$(git ls-remote --tags origin "refs/tags/$TAG" | awk 'NR == 1 { print $1 }')"
fi

if [[ -n "$REMOTE_TAG_SHA" && "$REMOTE_TAG_SHA" != "$SOURCE_SHA" ]]; then
echo "::error::$TAG already points to $REMOTE_TAG_SHA instead of release source $SOURCE_SHA"
exit 1
fi
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && -z "$REMOTE_TAG_SHA" ]]; then
echo "::error::Manual recovery requires an existing tag: $TAG"
exit 1
fi

if [[ "$BUILD_VERSION" == *-* ]]; then
PRERELEASE=true
else
PRERELEASE=false
fi
echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$BUILD_VERSION" >> "$GITHUB_OUTPUT"

build-platform:
needs: prepare-release
strategy:
fail-fast: false
matrix:
Expand All @@ -41,7 +125,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.ref }}
ref: ${{ needs.prepare-release.outputs.source_sha }}

- name: Set up Windows native toolchain
if: runner.os == 'Windows'
Expand Down Expand Up @@ -124,6 +208,7 @@ jobs:
with:
name: mahjong-paper-jar-${{ matrix.artifact }}
path: build/libs/*.jar
if-no-files-found: error

package-universal:
needs: build-platform
Expand Down Expand Up @@ -200,69 +285,64 @@ jobs:
with:
name: mahjong-paper-jars
path: build/universal/*.jar
if-no-files-found: error

release:
needs: package-universal
needs:
- prepare-release
- package-universal
permissions:
contents: write
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.tag || github.ref }}
# Need the full history so the fast-forward step can verify ancestry between main and the tag commit.
ref: ${{ needs.prepare-release.outputs.source_sha }}
fetch-depth: 0

- name: Resolve release tag
id: tag
shell: bash
run: |
set -euo pipefail
if [[ -n "${{ github.event.inputs.tag }}" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
if [[ "$TAG" == *-beta* || "$TAG" == *-alpha* || "$TAG" == *-rc* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi

- name: Extract changelog notes
id: notes
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }}
run: |
set -euo pipefail
VERSION="${{ steps.tag.outputs.tag }}"
VERSION="$RELEASE_TAG"
VERSION="${VERSION#v}"
NOTES_FILE="release-notes.md"
if [[ -f CHANGELOG.md ]]; then
awk -v ver="$VERSION" '
$0 ~ "^## " ver " " { capture=1; next }
$1 == "##" && $2 == ver { capture=1; next }
capture && /^## / { exit }
capture { print }
' CHANGELOG.md > "$NOTES_FILE"
fi
if [[ ! -s "$NOTES_FILE" ]]; then
echo "Release ${{ steps.tag.outputs.tag }}" > "$NOTES_FILE"
echo "Release $RELEASE_TAG" > "$NOTES_FILE"
fi
echo "file=$NOTES_FILE" >> "$GITHUB_OUTPUT"

- name: Verify release version metadata
- name: Revalidate release source metadata
shell: bash
env:
RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }}
RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }}
SOURCE_SHA: ${{ needs.prepare-release.outputs.source_sha }}
run: |
set -euo pipefail
VERSION="${{ steps.tag.outputs.tag }}"
VERSION="${VERSION#v}"
if [[ "$(git rev-parse HEAD)" != "$SOURCE_SHA" ]]; then
echo "::error::Checked out source does not match prepared release commit $SOURCE_SHA"
exit 1
fi
BUILD_VERSION="$(sed -n 's/^version = "\([^"]*\)"/\1/p' build.gradle.kts | head -n 1)"
if [[ -z "$BUILD_VERSION" || "$BUILD_VERSION" != "$VERSION" ]]; then
echo "::error::Release tag version $VERSION does not match build.gradle.kts version $BUILD_VERSION"
if [[ -z "$BUILD_VERSION" || "$BUILD_VERSION" != "$RELEASE_VERSION" || "$RELEASE_TAG" != "v$BUILD_VERSION" ]]; then
echo "::error::Release metadata changed after preparation"
exit 1
fi
if [[ ! -f CHANGELOG.md ]] || ! grep -Eq "^## ${VERSION//./\\.}([[:space:]]|$)" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no section for $VERSION"
if [[ ! -f CHANGELOG.md ]] || ! grep -Eq "^## ${BUILD_VERSION//./\\.}([[:space:]]|$)" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no section for $BUILD_VERSION"
exit 1
fi

Expand All @@ -272,34 +352,33 @@ jobs:
name: mahjong-paper-jars
path: release-artifacts

- name: Publish GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
body_path: ${{ steps.notes.outputs.file }}
prerelease: ${{ steps.tag.outputs.prerelease }}
files: release-artifacts/*.jar

- name: Fast-forward main to released tag
if: steps.tag.outputs.prerelease == 'false'
- name: Create release tag
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.prepare-release.outputs.tag }}
SOURCE_SHA: ${{ needs.prepare-release.outputs.source_sha }}
run: |
set -euo pipefail
TAG="${{ steps.tag.outputs.tag }}"
TAG_SHA="$(git rev-parse "$TAG"^{commit})"
echo "Tag $TAG resolves to commit $TAG_SHA"
git fetch origin main:refs/remotes/origin/main
MAIN_SHA="$(git rev-parse origin/main)"
if [[ "$MAIN_SHA" == "$TAG_SHA" ]]; then
echo "main is already at $TAG_SHA, nothing to do."
exit 0
fi
if ! git merge-base --is-ancestor "$MAIN_SHA" "$TAG_SHA"; then
echo "::warning::main ($MAIN_SHA) is not an ancestor of $TAG ($TAG_SHA); skipping fast-forward."
exit 0
git fetch origin --tags --force
if git rev-parse --verify --quiet "refs/tags/$RELEASE_TAG" >/dev/null; then
EXISTING_SHA="$(git rev-list -n 1 "$RELEASE_TAG")"
if [[ "$EXISTING_SHA" != "$SOURCE_SHA" ]]; then
echo "::error::$RELEASE_TAG moved to $EXISTING_SHA after preparation; expected $SOURCE_SHA"
exit 1
fi
echo "$RELEASE_TAG already points to $SOURCE_SHA; reusing it."
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "$RELEASE_TAG" "$SOURCE_SHA" -m "Release $RELEASE_TAG"
git push origin "refs/tags/$RELEASE_TAG"
fi
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$TAG_SHA:refs/heads/main"
echo "Fast-forwarded main to $TAG_SHA"

- name: Publish GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.prepare-release.outputs.tag }}
name: ${{ needs.prepare-release.outputs.tag }}
body_path: ${{ steps.notes.outputs.file }}
prerelease: ${{ needs.prepare-release.outputs.prerelease }}
files: release-artifacts/*.jar
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ name: Test
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
workflow_dispatch:

jobs:
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## 1.5.0 - 2026-07-16

Feature release for table interaction, river viewing, rules accuracy, storage integration, runtime compatibility, and performance.

中文更新日志:

- **升级要求**:发行包改用 Java 21 字节码,并要求 CraftEngine 26.7 或更高版本。更换 MahjongPaper、CraftEngine 或 InvSync 后必须完整重启服务器。
- **牌河俯视与操作恢复**:新增无需客户端 Mod 的平滑客户端俯视镜头。按 Shift 返回座位后会按当前实时状态恢复定缺、吃碰杠、出牌等操作;俯视期间 ActionBar 倒计时继续运行,超时操作仍会正常执行。
- **倒计时与托管**:连续自动出牌后的下一次时限依次为 60、30、15、10 秒,之后保持 10 秒;玩家手动出牌后恢复 60 秒。只有实际离座或掉线才会交给机器人托管,快速按 Shift 或切换俯视镜头不再夺走玩家控制权。
- **射线交互与隐藏信息**:麻将牌和扁平文字改由精确服务端射线盒决定操作,并仅向相关观察者发送必要的客户端代理,修复加入、定缺、吃碰和出牌偶发无法点击的问题。自己的手牌保持明牌,其他玩家手牌和牌墙只发送未知牌面。
- **牌桌显示与反馈**:修复牌墙上层失去下层支撑后仍悬空、牌面和交互区域不一致、提示文字重叠等布局问题,并统一 ActionBar、聊天提示、牌桌音效和玩家操作反馈。
- **三种玩法校正**:GB 模式对齐 144 张牌、花牌、8 个非花番门槛、单和与固定 16 局流程;立直模式修正鸣牌优先级、振听、杠宝牌时机、流局与延长赛边界;四川模式对齐 T/TFMJ 的直接定缺、无换三张、三番封顶、血战到底、杠分转移和查叫流程。
- **段位与服务器集成**:新增可选 InvSync 2.x 玩家段位存储,并在 InvSync 缺失或不兼容时仅向已启用且健康的 SQL 后端回退。当前自动段位与统计更新仍只适用于四名真人完成的立直对局。
- **资源与本地化**:加入 GB、立直和四川模式对应的摸牌、出牌、鸣牌、流局与和牌音效;新增完整日语游戏消息、安装文档和三种玩法说明。
- **性能与稳定性**:减少牌桌区域更新、展示实体复用、观察者集合和 GB Bot 决策热路径中的排序、装箱及临时分配;缓存调度器反射解析,并加入 GitHub Actions 配对 A/B 性能回归门禁。
- **分发说明**:发行 JAR 现在内嵌并重定位 Sparrow Reflection 与 ASM。第三方声明和运行软件分发条款已更新,重新分发时应保留对应许可证与源码义务说明。

English Release Notes:

- **Upgrade requirements**: Release artifacts now use Java 21 bytecode and require CraftEngine 26.7 or newer. Fully restart the server after replacing MahjongPaper, CraftEngine, or InvSync.
- **River view and control restoration**: Added a smooth client-only overhead camera that requires no client mod. Returning with Shift rebuilds ding-que, reaction, and discard controls from live game state. ActionBar deadlines continue running while viewing the river, and expired decisions still resolve normally.
- **Deadlines and bot ownership**: Consecutive automatic discards shorten the next turn from 60 to 30, 15, and then 10 seconds; later automatic turns remain at 10 seconds. A manual discard restores 60 seconds. Bot control now begins only after a real dismount or disconnect, not a quick Shift press or camera transition.
- **Ray interaction and hidden information**: Mahjong tiles and flat text now use exact server-side ray geometry, backed only by the per-viewer client proxies needed to make vanilla clients report a click. This fixes intermittent join, ding-que, reaction, and discard input failures. Owners receive their visible hand while other hands and wall tiles use unknown faces.
- **Table rendering and feedback**: Fixed unsupported upper wall tiles remaining suspended, mismatched visual and interaction geometry, overlapping controls, and inconsistent ActionBar, chat, sound, and table feedback.
- **Rules corrections**: GB now follows the 144-tile flower flow, eight non-flower-fan floor, single-ron priority, and fixed 16-hand profile. Riichi fixes cover reaction priority, furiten, kan-dora timing, draws, and match extensions. Sichuan now follows the documented T/TFMJ direct ding-que, no exchange-three, three-fan cap, Bloody Battle, kong-transfer, and cha-jiao flow.
- **Rank and server integrations**: Added optional InvSync 2.x player-rank storage, with fallback only to an enabled and healthy SQL backend. Automatic rank and personal-stat updates remain limited to completed four-human Riichi matches.
- **Assets and localization**: Added mode-specific draw, discard, reaction, draw-result, and win sounds for GB, Riichi, and Sichuan, plus complete Japanese game messages and player documentation.
- **Performance and reliability**: Reduced sorting, boxing, and temporary allocations in region updates, display reconciliation, viewer membership handling, and GB bot decisions; cached scheduler reflection resolution and added paired GitHub Actions A/B performance gates.
- **Distribution notice**: Release jars now embed and relocate Sparrow Reflection and ASM. Third-party notices and runnable-distribution terms were updated; redistributors must preserve the applicable license and source-obligation notices.

## 1.4.1 - 2026-06-18

Hotfix for game room exit countdown behavior.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ plugins {
}

group = "top.ellan"
version = "1.4.1"
version = "1.5.0"
val minimumPaperDevBundleVersion = "1.20.1-R0.1-SNAPSHOT"
val paperDevBundleVersion =
providers
Expand Down
2 changes: 1 addition & 1 deletion docs/performance-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ python perf/live/download_locked.py `
--destination build/live-cache/craft-engine-paper-plugin-26.7.3.jar
python perf/live/run_server.py `
--paper-jar build/live-cache/server.jar `
--plugin-jar build/libs/mahjong-paper-1.4.1.jar `
--plugin-jar build/libs/mahjong-paper-1.5.0.jar `
--craftengine-jar build/live-cache/craft-engine-paper-plugin-26.7.3.jar `
--required-plugin MahjongPaper `
--scenario-name majsoul-hanchan-four-bot-spectator `
Expand Down
Loading