From 7b6a49b70dc8d44bf665f96215b700db876953ed Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:00:59 -0500 Subject: [PATCH 01/12] fix(mc): auto-trim dead space in resize64 script (#7249) * fix(mc): auto-trim transparent dead space before resizing to 64x64 Crops to the content bounding box before scaling so sprites fill the full 64x64 frame instead of shrinking into a tiny centered dot. * feat(mc): add kbve_scythe custom item to resource pack Trimmed and resized large_sythe.png to 64x64 via resize64 script, added as kbve_scythe with item definition, model, and texture. --- .../assets/kbve/items/kbve_scythe.json | 6 ++++++ .../assets/kbve/models/item/kbve_scythe.json | 6 ++++++ .../assets/kbve/textures/item/kbve_scythe.png | Bin 0 -> 4187 bytes apps/mc/data/resource-pack/pack.mcmeta | 2 +- apps/mc/scripts/resize64/resize.py | 6 ++++++ 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_scythe.png diff --git a/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json b/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json new file mode 100644 index 0000000000..a65688ee0d --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "kbve:item/kbve_scythe" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json b/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json new file mode 100644 index 0000000000..6f8d24e350 --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "kbve:item/kbve_scythe" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_scythe.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_scythe.png new file mode 100644 index 0000000000000000000000000000000000000000..9673dbffb294edaadd011dda263a76009e1ac5c5 GIT binary patch literal 4187 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 literal 0 HcmV?d00001 diff --git a/apps/mc/data/resource-pack/pack.mcmeta b/apps/mc/data/resource-pack/pack.mcmeta index 3c7237e686..fe194eeedb 100644 --- a/apps/mc/data/resource-pack/pack.mcmeta +++ b/apps/mc/data/resource-pack/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "pack_format": 46, - "description": "KBVE Custom Items - Coin & Sword" + "description": "KBVE Custom Items - Coin, Sword & Scythe" } } diff --git a/apps/mc/scripts/resize64/resize.py b/apps/mc/scripts/resize64/resize.py index 14beb35b5a..bc8a85ef05 100644 --- a/apps/mc/scripts/resize64/resize.py +++ b/apps/mc/scripts/resize64/resize.py @@ -25,6 +25,12 @@ def resize_image(src: Path, dst: Path) -> None: with Image.open(src) as img: img = img.convert("RGBA") + + # Auto-trim transparent dead space + bbox = img.getbbox() + if bbox: + img = img.crop(bbox) + img = img.resize(TARGET_SIZE, Image.LANCZOS) dst.parent.mkdir(parents=True, exist_ok=True) img.save(dst, format="PNG") From c6366c3c889ac1f7daeff03444158a3c3032bfe8 Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:14:49 -0500 Subject: [PATCH 02/12] fix(kbve.sh): initialize submodules when creating worktrees (#7251) Both create_worktree and atomic_function now run git submodule update --init --recursive after worktree creation so submodules like apps/mc/pumpkin are checked out automatically. --- kbve.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/kbve.sh b/kbve.sh index e152dd92e7..079fabcb35 100755 --- a/kbve.sh +++ b/kbve.sh @@ -224,6 +224,12 @@ atomic_function() { echo "Branch: $branch_name (based on dev)" git worktree add "$worktree_dir" -b "$branch_name" "origin/dev" + # Initialize submodules in the worktree + if [ -f "$worktree_dir/.gitmodules" ]; then + echo "Initializing submodules in worktree..." + git -C "$worktree_dir" submodule update --init --recursive + fi + # Copy .env if it exists in the main repo if [ -f "$main_repo/.env" ]; then echo "Copying .env from main repo..." @@ -322,6 +328,12 @@ create_worktree() { echo "Branch: $branch_name (based on $base_branch)" git worktree add "$worktree_dir" -b "$branch_name" "origin/$base_branch" + # Initialize submodules in the worktree + if [ -f "$worktree_dir/.gitmodules" ]; then + echo "Initializing submodules in worktree..." + git -C "$worktree_dir" submodule update --init --recursive + fi + # Copy .env if it exists in the main repo (gitignored, won't be in worktree) if [ -f "$main_repo/.env" ]; then echo "Copying .env from main repo..." From 8bb0a46c58872181ab7f47d6e670ba312bca77a6 Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:29:45 -0500 Subject: [PATCH 03/12] fix(mc-e2e): use project-relative vitest include path and add resource pack test (#7252) vitest.config.ts used a monorepo-root-relative include path which broke test discovery when @nx/vitest inferred the test target from the project directory. Also adds an e2e spec for the HTTP resource pack endpoint on port 8080. --- apps/mc/mc-e2e/e2e/resource-pack.spec.ts | 35 ++++++++++++++++++++++++ apps/mc/mc-e2e/project.json | 7 +++-- apps/mc/mc-e2e/vitest.config.ts | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 apps/mc/mc-e2e/e2e/resource-pack.spec.ts diff --git a/apps/mc/mc-e2e/e2e/resource-pack.spec.ts b/apps/mc/mc-e2e/e2e/resource-pack.spec.ts new file mode 100644 index 0000000000..decf090195 --- /dev/null +++ b/apps/mc/mc-e2e/e2e/resource-pack.spec.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from 'vitest'; + +const MC_HOST = process.env['MC_HOST'] ?? '127.0.0.1'; +const PACK_PORT = Number(process.env['MC_PACK_PORT'] ?? 8080); +const PACK_PATH = '/kbve-resource-pack.zip'; + +describe('MC Resource Pack HTTP Server', () => { + it('should serve the resource pack as a ZIP file', async () => { + const url = `http://${MC_HOST}:${PACK_PORT}${PACK_PATH}`; + const res = await fetch(url); + + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + + const contentType = res.headers.get('content-type'); + expect(contentType).toContain('application/zip'); + + const body = await res.arrayBuffer(); + expect(body.byteLength).toBeGreaterThan(0); + + // Validate ZIP magic bytes (PK\x03\x04) + const header = new Uint8Array(body, 0, 4); + expect(header[0]).toBe(0x50); // P + expect(header[1]).toBe(0x4b); // K + expect(header[2]).toBe(0x03); + expect(header[3]).toBe(0x04); + }); + + it('should return 404 for unknown paths', async () => { + const url = `http://${MC_HOST}:${PACK_PORT}/nonexistent`; + const res = await fetch(url); + + expect(res.ok).toBe(false); + }); +}); diff --git a/apps/mc/mc-e2e/project.json b/apps/mc/mc-e2e/project.json index 032d2c4ed2..c8dcf517be 100644 --- a/apps/mc/mc-e2e/project.json +++ b/apps/mc/mc-e2e/project.json @@ -11,10 +11,11 @@ "options": { "commands": [ "docker rm -f mc-e2e-test 2>/dev/null || true", - "docker run -d --name mc-e2e-test -p 25565:25565 kbve/mc:local", - "npx vitest run --config apps/mc/mc-e2e/vitest.config.ts; EC=$?; docker rm -f mc-e2e-test 2>/dev/null || true; exit $EC" + "docker run -d --name mc-e2e-test -p 25565:25565 -p 8080:8080 kbve/mc:local", + "npx vitest run; EC=$?; docker rm -f mc-e2e-test 2>/dev/null || true; exit $EC" ], - "parallel": false + "parallel": false, + "cwd": "apps/mc/mc-e2e" } } }, diff --git a/apps/mc/mc-e2e/vitest.config.ts b/apps/mc/mc-e2e/vitest.config.ts index 4c840e7d1b..8c37267970 100644 --- a/apps/mc/mc-e2e/vitest.config.ts +++ b/apps/mc/mc-e2e/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['apps/mc/mc-e2e/e2e/**/*.spec.ts'], + include: ['e2e/**/*.spec.ts'], testTimeout: 30_000, hookTimeout: 60_000, }, From be99fa5f50fac9c6cf0754dcb1ac3e8350f7c5bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:33:42 -0500 Subject: [PATCH 04/12] feat(mc): add registry-based Docker build caching via nx-container (#7253) The MC server was using a bare `docker build` command without any cross-run caching. Every CI build recompiled all Pumpkin cargo dependencies from scratch because BuildKit cache mounts are ephemeral on GitHub Actions runners. Switch to `@nx-tools/nx-container:build` executor with GHCR registry-based caching (cache-from/cache-to), matching the pattern already used by all Axum apps (discordsh, herbmail, memes, irc-gateway). Cargo-chef layers and dependency builds are now persisted between CI runs via the `:buildcache` tag. Co-authored-by: Al @h0lybyte <5599058+h0lybyte@users.noreply.github.com> --- apps/mc/project.json | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/mc/project.json b/apps/mc/project.json index a6a83d92ed..3fa157c6fc 100644 --- a/apps/mc/project.json +++ b/apps/mc/project.json @@ -6,16 +6,52 @@ "targets": { "container": { "executor": "nx:run-commands", + "defaultConfiguration": "local", "options": { - "commands": [ - "VERSION=$(grep '^version' apps/mc/plugins/kbve-mc-plugin/Cargo.toml | head -1 | sed 's/.*\"\\(.*\\)\"/\\1/') && docker build -t kbve/mc:$VERSION -t kbve/mc:latest -f apps/mc/Dockerfile apps/mc" - ], "parallel": false }, "configurations": { "local": { "commands": [ - "VERSION=$(grep '^version' apps/mc/plugins/kbve-mc-plugin/Cargo.toml | head -1 | sed 's/.*\"\\(.*\\)\"/\\1/') && docker build -t kbve/mc:$VERSION -t kbve/mc:local -f apps/mc/Dockerfile apps/mc" + "./kbve.sh -nx mc:containerx", + "VERSION=$(grep '^version' apps/mc/plugins/kbve-mc-plugin/Cargo.toml | head -1 | sed 's/.*\"\\(.*\\)\"/\\1/') && docker tag kbve/mc:latest kbve/mc:$VERSION && docker tag kbve/mc:latest kbve/mc:local && echo \"Tagged kbve/mc:$VERSION and kbve/mc:local\"" + ] + }, + "production": { + "commands": [ + "./kbve.sh -nx mc:containerx --configuration=production", + "VERSION=$(grep '^version' apps/mc/plugins/kbve-mc-plugin/Cargo.toml | head -1 | sed 's/.*\"\\(.*\\)\"/\\1/') && docker tag kbve/mc:latest kbve/mc:$VERSION && docker tag ghcr.io/kbve/mc:latest ghcr.io/kbve/mc:$VERSION && echo \"Tagged kbve/mc:$VERSION and ghcr.io/kbve/mc:$VERSION\"" + ] + } + } + }, + "containerx": { + "executor": "@nx-tools/nx-container:build", + "defaultConfiguration": "local", + "options": { + "engine": "docker", + "context": "apps/mc", + "file": "apps/mc/Dockerfile", + "load": true + }, + "configurations": { + "local": { + "load": true, + "push": false, + "tags": ["kbve/mc:latest"] + }, + "production": { + "load": true, + "push": false, + "metadata": { + "images": ["ghcr.io/kbve/mc", "kbve/mc"], + "tags": ["latest"] + }, + "cache-from": [ + "type=registry,ref=ghcr.io/kbve/mc:buildcache" + ], + "cache-to": [ + "type=registry,ref=ghcr.io/kbve/mc:buildcache,mode=max" ] } } From e02ddd65296b88b8bd127410db3365275a2f15f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:53:44 -0500 Subject: [PATCH 05/12] chore(kube): update discordsh to v0.1.9 (#7254) Co-authored-by: github-actions[bot] --- apps/kube/discordsh/manifest/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/kube/discordsh/manifest/deployment.yaml b/apps/kube/discordsh/manifest/deployment.yaml index 3d92712039..b425e09a35 100644 --- a/apps/kube/discordsh/manifest/deployment.yaml +++ b/apps/kube/discordsh/manifest/deployment.yaml @@ -20,7 +20,7 @@ spec: serviceAccountName: discordsh-sa containers: - name: discordsh - image: ghcr.io/kbve/discordsh:0.1.8 + image: ghcr.io/kbve/discordsh:0.1.9 imagePullPolicy: Always ports: - name: http From 2e10416414e51bc6da175574851b74d99df9116d Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:59:43 -0500 Subject: [PATCH 06/12] refactor(mc): organize resource pack textures into category folders (#7255) Move models and textures into weapons/ and rewards/ subdirectories for better organization as the item catalog grows. --- .../resource-pack/assets/kbve/items/kbve_coin.json | 2 +- .../assets/kbve/items/kbve_scythe.json | 2 +- .../resource-pack/assets/kbve/items/kbve_sword.json | 2 +- .../kbve/models/item/{ => rewards}/kbve_coin.json | 2 +- .../kbve/models/item/{ => weapons}/kbve_scythe.json | 2 +- .../kbve/models/item/{ => weapons}/kbve_sword.json | 2 +- .../kbve/textures/item/{ => rewards}/kbve_coin.png | Bin .../textures/item/{ => weapons}/kbve_scythe.png | Bin .../kbve/textures/item/{ => weapons}/kbve_sword.png | Bin apps/mc/data/resource-pack/pack.mcmeta | 2 +- 10 files changed, 7 insertions(+), 7 deletions(-) rename apps/mc/data/resource-pack/assets/kbve/models/item/{ => rewards}/kbve_coin.json (59%) rename apps/mc/data/resource-pack/assets/kbve/models/item/{ => weapons}/kbve_scythe.json (57%) rename apps/mc/data/resource-pack/assets/kbve/models/item/{ => weapons}/kbve_sword.json (58%) rename apps/mc/data/resource-pack/assets/kbve/textures/item/{ => rewards}/kbve_coin.png (100%) rename apps/mc/data/resource-pack/assets/kbve/textures/item/{ => weapons}/kbve_scythe.png (100%) rename apps/mc/data/resource-pack/assets/kbve/textures/item/{ => weapons}/kbve_sword.png (100%) diff --git a/apps/mc/data/resource-pack/assets/kbve/items/kbve_coin.json b/apps/mc/data/resource-pack/assets/kbve/items/kbve_coin.json index 6750d8f454..743f90622b 100644 --- a/apps/mc/data/resource-pack/assets/kbve/items/kbve_coin.json +++ b/apps/mc/data/resource-pack/assets/kbve/items/kbve_coin.json @@ -1,6 +1,6 @@ { "model": { "type": "minecraft:model", - "model": "kbve:item/kbve_coin" + "model": "kbve:item/rewards/kbve_coin" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json b/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json index a65688ee0d..3e3af6deac 100644 --- a/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json +++ b/apps/mc/data/resource-pack/assets/kbve/items/kbve_scythe.json @@ -1,6 +1,6 @@ { "model": { "type": "minecraft:model", - "model": "kbve:item/kbve_scythe" + "model": "kbve:item/weapons/kbve_scythe" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/items/kbve_sword.json b/apps/mc/data/resource-pack/assets/kbve/items/kbve_sword.json index 2cd2ddb075..fe7bf506c2 100644 --- a/apps/mc/data/resource-pack/assets/kbve/items/kbve_sword.json +++ b/apps/mc/data/resource-pack/assets/kbve/items/kbve_sword.json @@ -1,6 +1,6 @@ { "model": { "type": "minecraft:model", - "model": "kbve:item/kbve_sword" + "model": "kbve:item/weapons/kbve_sword" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_coin.json b/apps/mc/data/resource-pack/assets/kbve/models/item/rewards/kbve_coin.json similarity index 59% rename from apps/mc/data/resource-pack/assets/kbve/models/item/kbve_coin.json rename to apps/mc/data/resource-pack/assets/kbve/models/item/rewards/kbve_coin.json index a757e0e524..2dab3dcc33 100644 --- a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_coin.json +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/rewards/kbve_coin.json @@ -1,6 +1,6 @@ { "parent": "minecraft:item/generated", "textures": { - "layer0": "kbve:item/kbve_coin" + "layer0": "kbve:item/rewards/kbve_coin" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_scythe.json similarity index 57% rename from apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json rename to apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_scythe.json index 6f8d24e350..534f32e754 100644 --- a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_scythe.json +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_scythe.json @@ -1,6 +1,6 @@ { "parent": "minecraft:item/handheld", "textures": { - "layer0": "kbve:item/kbve_scythe" + "layer0": "kbve:item/weapons/kbve_scythe" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_sword.json b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_sword.json similarity index 58% rename from apps/mc/data/resource-pack/assets/kbve/models/item/kbve_sword.json rename to apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_sword.json index b4e6d5d0ab..769a0b3c11 100644 --- a/apps/mc/data/resource-pack/assets/kbve/models/item/kbve_sword.json +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_sword.json @@ -1,6 +1,6 @@ { "parent": "minecraft:item/handheld", "textures": { - "layer0": "kbve:item/kbve_sword" + "layer0": "kbve:item/weapons/kbve_sword" } } diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_coin.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/rewards/kbve_coin.png similarity index 100% rename from apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_coin.png rename to apps/mc/data/resource-pack/assets/kbve/textures/item/rewards/kbve_coin.png diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_scythe.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_scythe.png similarity index 100% rename from apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_scythe.png rename to apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_scythe.png diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_sword.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_sword.png similarity index 100% rename from apps/mc/data/resource-pack/assets/kbve/textures/item/kbve_sword.png rename to apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_sword.png diff --git a/apps/mc/data/resource-pack/pack.mcmeta b/apps/mc/data/resource-pack/pack.mcmeta index fe194eeedb..912c5b5028 100644 --- a/apps/mc/data/resource-pack/pack.mcmeta +++ b/apps/mc/data/resource-pack/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "pack_format": 46, - "description": "KBVE Custom Items - Coin, Sword & Scythe" + "description": "KBVE Custom Items - Weapons & Rewards" } } From ee414186084c2451199606af1e7ec96acc59c2b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:06:13 -0500 Subject: [PATCH 07/12] Atomic: docker cargo cache (#7256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mc): layer Dockerfile into parallel build stages for pumpkin and plugin Split the monolithic builder stage into separate deps, builder, and plugin-builder stages so BuildKit can compile the server binary and plugin cdylib concurrently. The plugin stage reuses cooked third-party deps via CARGO_TARGET_DIR, eliminating redundant compilation. * feat(mc): tier workspace crate builds into foundation and core layers Pre-build workspace crates in dependency order across isolated Docker layers so only the changed tier recompiles on rebuild: deps → foundation (nbt, api-macros, util, data, macros, config) → core (world, protocol, inventory) → builder | plugin-builder (parallel via BuildKit) When only the root pumpkin crate or plugin source changes, foundation and core layers are fully cached. --------- Co-authored-by: Al @h0lybyte <5599058+h0lybyte@users.noreply.github.com> --- apps/mc/Dockerfile | 53 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/mc/Dockerfile b/apps/mc/Dockerfile index a0ccef292b..f32a5b018d 100644 --- a/apps/mc/Dockerfile +++ b/apps/mc/Dockerfile @@ -11,28 +11,61 @@ RUN rustup component add rustfmt FROM chef AS planner RUN cargo chef prepare --recipe-path recipe.json -# --- Pumpkin: cook dependencies (cached until Cargo.toml/lock changes) --- -FROM chef AS builder +# --- Cook: compile third-party deps (cached until Cargo.toml/lock changes) --- +FROM chef AS deps COPY --from=planner /pumpkin/recipe.json recipe.json RUN --mount=type=cache,target=/usr/local/cargo/git/db \ --mount=type=cache,target=/usr/local/cargo/registry/ \ cargo chef cook --release --recipe-path recipe.json -# Build Pumpkin server (only recompiles changed source, deps are cached) -COPY ./pumpkin /pumpkin +# --- Foundation: leaf + data crates (rarely change, cached aggressively) --- +FROM deps AS foundation +COPY ./pumpkin/pumpkin-nbt /pumpkin/pumpkin-nbt +COPY ./pumpkin/pumpkin-api-macros /pumpkin/pumpkin-api-macros +COPY ./pumpkin/pumpkin-util /pumpkin/pumpkin-util +COPY ./pumpkin/pumpkin-data /pumpkin/pumpkin-data +COPY ./pumpkin/pumpkin-macros /pumpkin/pumpkin-macros +COPY ./pumpkin/pumpkin-config /pumpkin/pumpkin-config +RUN --mount=type=cache,target=/usr/local/cargo/git/db \ + --mount=type=cache,target=/usr/local/cargo/registry/ \ + cargo build --release \ + -p pumpkin-nbt \ + -p pumpkin-api-macros \ + -p pumpkin-util \ + -p pumpkin-data \ + -p pumpkin-macros \ + -p pumpkin-config + +# --- Core: engine crates (protocol, world, inventory) --- +FROM foundation AS core +COPY ./pumpkin/pumpkin-world /pumpkin/pumpkin-world +COPY ./pumpkin/pumpkin-protocol /pumpkin/pumpkin-protocol +COPY ./pumpkin/pumpkin-inventory /pumpkin/pumpkin-inventory +RUN --mount=type=cache,target=/usr/local/cargo/git/db \ + --mount=type=cache,target=/usr/local/cargo/registry/ \ + cargo build --release \ + -p pumpkin-world \ + -p pumpkin-protocol \ + -p pumpkin-inventory + +# --- Pumpkin: build server (parallel with plugin) --- +FROM core AS builder +COPY ./pumpkin/pumpkin /pumpkin/pumpkin RUN --mount=type=cache,target=/usr/local/cargo/git/db \ --mount=type=cache,target=/usr/local/cargo/registry/ \ cargo build --release && cp target/release/pumpkin ./pumpkin.release -# Build plugins (path deps to pumpkin crates, use mount cache) +# --- Plugin: build (parallel with pumpkin, reuses all pre-compiled crates) --- +FROM core AS plugin-builder +COPY ./pumpkin/pumpkin /pumpkin/pumpkin COPY ./plugins /plugins -RUN --mount=type=cache,sharing=private,target=/plugins/kbve-mc-plugin/target \ - --mount=type=cache,target=/usr/local/cargo/git/db \ +ENV CARGO_TARGET_DIR=/pumpkin/target +RUN --mount=type=cache,target=/usr/local/cargo/git/db \ --mount=type=cache,target=/usr/local/cargo/registry/ \ cargo build --release --manifest-path /plugins/kbve-mc-plugin/Cargo.toml \ && mkdir -p /built-plugins \ - && cp /plugins/kbve-mc-plugin/target/release/libkbve_mc_plugin.so /built-plugins/ 2>/dev/null \ - || cp /plugins/kbve-mc-plugin/target/release/libkbve_mc_plugin.dylib /built-plugins/ 2>/dev/null \ + && cp /pumpkin/target/release/libkbve_mc_plugin.so /built-plugins/ 2>/dev/null \ + || cp /pumpkin/target/release/libkbve_mc_plugin.dylib /built-plugins/ 2>/dev/null \ || true # --- Resource pack --- @@ -47,7 +80,7 @@ RUN SHA1=$(sha1sum /kbve-resource-pack.zip | awk '{print $1}') && \ FROM alpine:3.23 COPY --from=builder /pumpkin/pumpkin.release /bin/pumpkin -COPY --from=builder /built-plugins/ /pumpkin/plugins/ +COPY --from=plugin-builder /built-plugins/ /pumpkin/plugins/ WORKDIR /pumpkin From cbcf266713d39949b18d2657cf02c2edf83743b8 Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:11:18 -0500 Subject: [PATCH 08/12] fix(mc-e2e): disable auto-inferred vitest test target (#7257) The @nx/vitest plugin infers a bare `test` target from vitest.config.ts, which runs vitest without the Docker container. This causes CI failures in `nx affected --target=test` since the MC server and resource pack HTTP server are only available inside the Docker image. The proper e2e pipeline (`nx e2e mc`) handles container lifecycle and remains unchanged. --- apps/mc/mc-e2e/project.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/mc/mc-e2e/project.json b/apps/mc/mc-e2e/project.json index c8dcf517be..996c3861fd 100644 --- a/apps/mc/mc-e2e/project.json +++ b/apps/mc/mc-e2e/project.json @@ -5,6 +5,10 @@ "sourceRoot": "apps/mc/mc-e2e/e2e", "implicitDependencies": ["mc"], "targets": { + "test": { + "executor": "nx:noop", + "cache": false + }, "e2e": { "executor": "nx:run-commands", "cache": false, From 5bd97c0571d20eb2a754e1dc8b94a0fb67b9607c Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:21:30 -0500 Subject: [PATCH 09/12] feat(ci): add Rust to CodeQL security analysis matrix (#7258) --- .github/workflows/ci-security.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-security.yml b/.github/workflows/ci-security.yml index 1088e0d6ba..aabf9099ca 100644 --- a/.github/workflows/ci-security.yml +++ b/.github/workflows/ci-security.yml @@ -31,6 +31,7 @@ jobs: language: - javascript-typescript - python + - rust steps: - name: Checkout From 70d326c54499f274ea78800c7fcd928d3aaa7bbb Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:44:50 -0500 Subject: [PATCH 10/12] feat(ci): harden CodeQL with PR gates, scheduled scans, and dependency review (#7259) - Add dev to pull_request branches so PRs are scanned before merge - Add weekly scheduled scan (Mondays 06:30 UTC) for newly disclosed CVEs - Upgrade query suite from default to security-and-quality - Add dependency-review job to block PRs introducing high-severity deps --- .github/workflows/ci-security.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/ci-security.yml b/.github/workflows/ci-security.yml index aabf9099ca..a94e1ec5a0 100644 --- a/.github/workflows/ci-security.yml +++ b/.github/workflows/ci-security.yml @@ -7,8 +7,11 @@ on: - main pull_request: branches: + - dev - staging - main + schedule: + - cron: '30 6 * * 1' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,6 +44,7 @@ jobs: uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} + queries: security-and-quality - name: Auto-build uses: github/codeql-action/autobuild@v4 @@ -49,3 +53,22 @@ jobs: uses: github/codeql-action/analyze@v4 with: category: /language:${{ matrix.language }} + + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: always From c7067ba8b083530fdaf7f86cbb93522be5a4b971 Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:45:11 -0500 Subject: [PATCH 11/12] fix(mc): restore workspace manifests after cargo-chef cook and update Pumpkin submodule (#7260) cargo-chef cook strips workspace.lints from the root Cargo.toml, breaking crates that inherit lints. Building individual crates with -p also breaks tokio feature unification (pumpkin-protocol needs tokio/net transitively). Fix both by restoring Cargo.toml/Cargo.lock after cook and switching to full workspace builds at each layer. Also update Pumpkin submodule to latest upstream master (69a37afd). --- apps/mc/Dockerfile | 17 ++++++----------- apps/mc/pumpkin | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/apps/mc/Dockerfile b/apps/mc/Dockerfile index f32a5b018d..e05a53ac09 100644 --- a/apps/mc/Dockerfile +++ b/apps/mc/Dockerfile @@ -19,7 +19,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/git/db \ cargo chef cook --release --recipe-path recipe.json # --- Foundation: leaf + data crates (rarely change, cached aggressively) --- +# cargo-chef cook strips workspace.lints, so restore the real manifests. +# Full workspace build ensures feature unification is correct. FROM deps AS foundation +COPY ./pumpkin/Cargo.toml /pumpkin/Cargo.toml +COPY ./pumpkin/Cargo.lock /pumpkin/Cargo.lock COPY ./pumpkin/pumpkin-nbt /pumpkin/pumpkin-nbt COPY ./pumpkin/pumpkin-api-macros /pumpkin/pumpkin-api-macros COPY ./pumpkin/pumpkin-util /pumpkin/pumpkin-util @@ -28,13 +32,7 @@ COPY ./pumpkin/pumpkin-macros /pumpkin/pumpkin-macros COPY ./pumpkin/pumpkin-config /pumpkin/pumpkin-config RUN --mount=type=cache,target=/usr/local/cargo/git/db \ --mount=type=cache,target=/usr/local/cargo/registry/ \ - cargo build --release \ - -p pumpkin-nbt \ - -p pumpkin-api-macros \ - -p pumpkin-util \ - -p pumpkin-data \ - -p pumpkin-macros \ - -p pumpkin-config + cargo build --release # --- Core: engine crates (protocol, world, inventory) --- FROM foundation AS core @@ -43,10 +41,7 @@ COPY ./pumpkin/pumpkin-protocol /pumpkin/pumpkin-protocol COPY ./pumpkin/pumpkin-inventory /pumpkin/pumpkin-inventory RUN --mount=type=cache,target=/usr/local/cargo/git/db \ --mount=type=cache,target=/usr/local/cargo/registry/ \ - cargo build --release \ - -p pumpkin-world \ - -p pumpkin-protocol \ - -p pumpkin-inventory + cargo build --release # --- Pumpkin: build server (parallel with plugin) --- FROM core AS builder diff --git a/apps/mc/pumpkin b/apps/mc/pumpkin index e610310bfb..69a37afd94 160000 --- a/apps/mc/pumpkin +++ b/apps/mc/pumpkin @@ -1 +1 @@ -Subproject commit e610310bfb377fcd6825d33afd9345816f67323f +Subproject commit 69a37afd94fe5e5df724d060293bbf56a2bc0e1c From 8eddea31f88418e63313b654e3a3a7cb31df744b Mon Sep 17 00:00:00 2001 From: "Al @h0lybyte" <5599058+h0lybyte@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:07:25 -0500 Subject: [PATCH 12/12] feat(mc): add rust stone and spartan shield with DashMap item registry (#7261) Refactor give commands from individual executors to a DashMap-backed item registry. New items only need a single map.insert() entry. - Add rust_stone block texture (64x64 via resize64) with blockstate, model, and item definitions - Add spartan_shield item texture (64x64) with model and item defs - Spartan shield: mid-tier durability (200 vs vanilla 336), reflects 3 thorns damage on hit via ShieldBlockHandler event, flame particles - Register ShieldBlockHandler for PlayerInteractEntityEvent - Add dashmap and pumpkin-protocol dependencies --- .../assets/kbve/blockstates/rust_stone.json | 7 + .../assets/kbve/items/rust_stone.json | 6 + .../assets/kbve/items/spartan_shield.json | 6 + .../assets/kbve/models/block/rust_stone.json | 6 + .../models/item/weapons/spartan_shield.json | 6 + .../assets/kbve/textures/block/rust_stone.png | Bin 0 -> 7421 bytes .../textures/item/weapons/spartan_shield.png | Bin 0 -> 9422 bytes apps/mc/data/resource-pack/pack.mcmeta | 2 +- apps/mc/plugins/kbve-mc-plugin/Cargo.toml | 2 + apps/mc/plugins/kbve-mc-plugin/src/lib.rs | 296 ++++++++++++++---- 10 files changed, 264 insertions(+), 67 deletions(-) create mode 100644 apps/mc/data/resource-pack/assets/kbve/blockstates/rust_stone.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/items/rust_stone.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/items/spartan_shield.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/models/block/rust_stone.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/models/item/weapons/spartan_shield.json create mode 100644 apps/mc/data/resource-pack/assets/kbve/textures/block/rust_stone.png create mode 100644 apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/spartan_shield.png diff --git a/apps/mc/data/resource-pack/assets/kbve/blockstates/rust_stone.json b/apps/mc/data/resource-pack/assets/kbve/blockstates/rust_stone.json new file mode 100644 index 0000000000..e5f5539dd9 --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/blockstates/rust_stone.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "kbve:block/rust_stone" + } + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/items/rust_stone.json b/apps/mc/data/resource-pack/assets/kbve/items/rust_stone.json new file mode 100644 index 0000000000..52862c8fda --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/items/rust_stone.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "kbve:block/rust_stone" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/items/spartan_shield.json b/apps/mc/data/resource-pack/assets/kbve/items/spartan_shield.json new file mode 100644 index 0000000000..59ca1321d6 --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/items/spartan_shield.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "kbve:item/weapons/spartan_shield" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/models/block/rust_stone.json b/apps/mc/data/resource-pack/assets/kbve/models/block/rust_stone.json new file mode 100644 index 0000000000..5e09e8734d --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/models/block/rust_stone.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "kbve:block/rust_stone" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/spartan_shield.json b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/spartan_shield.json new file mode 100644 index 0000000000..87f93b2cbe --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/spartan_shield.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "kbve:item/weapons/spartan_shield" + } +} diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/block/rust_stone.png b/apps/mc/data/resource-pack/assets/kbve/textures/block/rust_stone.png new file mode 100644 index 0000000000000000000000000000000000000000..34cbe2770068fec8388b5a1677f5b97864a4bb73 GIT binary patch literal 7421 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 literal 0 HcmV?d00001 diff --git a/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/spartan_shield.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/spartan_shield.png new file mode 100644 index 0000000000000000000000000000000000000000..d3a34fa8504c28b15619736e1574d855084dba77 GIT binary patch literal 9422 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 for WelcomeHandler { } // --------------------------------------------------------------------------- -// /kbve give command +// Item registry — DashMap keyed by command name // --------------------------------------------------------------------------- -struct GiveCoinExecutor; -struct GiveSwordExecutor; +struct ItemDef { + base_item_key: &'static str, + model: &'static str, + display_name: &'static str, + message_color: NamedColor, + particle: Option<(Particle, i32)>, + max_damage: Option, +} -fn give_custom_item(item: &'static Item, model: &str, name: &str) -> ItemStack { - let mut stack = ItemStack::new(1, item); - stack.patch.push(( - DataComponent::ItemModel, - Some( - ItemModelImpl { - model: model.to_string(), - } - .to_dyn(), - ), - )); - stack.patch.push(( - DataComponent::CustomName, - Some( - CustomNameImpl { - name: name.to_string(), - } - .to_dyn(), - ), - )); - stack +static ITEM_REGISTRY: LazyLock> = LazyLock::new(|| { + let map = DashMap::new(); + map.insert( + "coin", + ItemDef { + base_item_key: "gold_nugget", + model: "kbve:kbve_coin", + display_name: "KBVE Coin", + message_color: NamedColor::Gold, + particle: None, + max_damage: None, + }, + ); + map.insert( + "sword", + ItemDef { + base_item_key: "diamond_sword", + model: "kbve:kbve_sword", + display_name: "KBVE Sword", + message_color: NamedColor::Aqua, + particle: None, + max_damage: None, + }, + ); + map.insert( + "rust_stone", + ItemDef { + base_item_key: "stone", + model: "kbve:rust_stone", + display_name: "Rust Stone", + message_color: NamedColor::Red, + particle: Some((Particle::Flame, 15)), + max_damage: None, + }, + ); + map.insert( + "spartan_shield", + ItemDef { + base_item_key: "shield", + model: "kbve:spartan_shield", + display_name: "Spartan Shield", + message_color: NamedColor::DarkRed, + particle: Some((Particle::Flame, 10)), + // Vanilla shield = 336; mid-tier, breaks faster + max_damage: Some(200), + }, + ); + map +}); + +// --------------------------------------------------------------------------- +// /kbve give — generic executor backed by ITEM_REGISTRY +// --------------------------------------------------------------------------- + +struct GiveItemExecutor { + item_key: &'static str, } -impl CommandExecutor for GiveCoinExecutor { +impl CommandExecutor for GiveItemExecutor { fn execute<'a>( &'a self, sender: &'a CommandSender, @@ -205,71 +255,182 @@ impl CommandExecutor for GiveCoinExecutor { args: &'a ConsumedArgs<'a>, ) -> CommandResult<'a> { Box::pin(async move { + let def = ITEM_REGISTRY + .get(self.item_key) + .ok_or_else(|| CommandError::CommandFailed(TextComponent::text("Unknown item")))?; + let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let item = Item::from_registry_key("gold_nugget").ok_or_else(|| { - CommandError::CommandFailed(TextComponent::text("gold_nugget not found")) + let item = Item::from_registry_key(def.base_item_key).ok_or_else(|| { + CommandError::CommandFailed(TextComponent::text(format!( + "{} not found", + def.base_item_key + ))) })?; for target in targets { - let mut stack = give_custom_item(item, "kbve:kbve_coin", "KBVE Coin"); + let mut stack = ItemStack::new(1, item); + stack.patch.push(( + DataComponent::ItemModel, + Some( + ItemModelImpl { + model: def.model.to_string(), + } + .to_dyn(), + ), + )); + stack.patch.push(( + DataComponent::CustomName, + Some( + CustomNameImpl { + name: def.display_name.to_string(), + } + .to_dyn(), + ), + )); + + if let Some(max_dmg) = def.max_damage { + stack.patch.push(( + DataComponent::MaxDamage, + Some( + MaxDamageImpl { + max_damage: max_dmg, + } + .to_dyn(), + ), + )); + stack.patch.push(( + DataComponent::Damage, + Some(DamageImpl { damage: 0 }.to_dyn()), + )); + } + target.inventory().insert_stack_anywhere(&mut stack).await; if !stack.is_empty() { target.drop_item(stack).await; } + + if let Some((particle, count)) = &def.particle { + let pos = target.living_entity.entity.pos.load(); + target + .world() + .spawn_particle( + Vector3::new(pos.x, pos.y + 1.0, pos.z), + Vector3::new(0.3, 0.5, 0.3), + 0.05, + *count, + *particle, + ) + .await; + } } sender - .send_message(TextComponent::text("Gave KBVE Coin!").color_named(NamedColor::Gold)) + .send_message( + TextComponent::text(format!("Gave {}!", def.display_name)) + .color_named(def.message_color), + ) .await; Ok(1) }) } } -impl CommandExecutor for GiveSwordExecutor { - fn execute<'a>( +fn kbve_command_tree() -> CommandTree { + let mut give = literal("give"); + for entry in ITEM_REGISTRY.iter() { + let key = *entry.key(); + give = give.then( + literal(key).then( + argument_default_name(PlayersArgumentConsumer) + .execute(GiveItemExecutor { item_key: key }), + ), + ); + } + CommandTree::new(["kbve"], "KBVE custom items").then(give) +} + +// --------------------------------------------------------------------------- +// Shield block handler — reflects damage when attacker hits a shield holder +// --------------------------------------------------------------------------- + +struct ShieldBlockHandler; + +impl EventHandler for ShieldBlockHandler { + fn handle<'a>( &'a self, - sender: &'a CommandSender, - _server: &'a Server, - args: &'a ConsumedArgs<'a>, - ) -> CommandResult<'a> { + _server: &'a Arc, + event: &'a PlayerInteractEntityEvent, + ) -> BoxFuture<'a, ()> { + let attacker = Arc::clone(&event.player); + let target = Arc::clone(&event.target); + let action = event.action.clone(); + Box::pin(async move { - let targets = PlayersArgumentConsumer.find_arg_default_name(args)?; - let item = Item::from_registry_key("diamond_sword").ok_or_else(|| { - CommandError::CommandFailed(TextComponent::text("diamond_sword not found")) - })?; + if !matches!(action, ActionType::Attack) { + return; + } - for target in targets { - let mut stack = give_custom_item(item, "kbve:kbve_sword", "KBVE Sword"); - target.inventory().insert_stack_anywhere(&mut stack).await; - if !stack.is_empty() { - target.drop_item(stack).await; + let Some(target_player) = target.get_player() else { + return; + }; + + // Check both hands for the Spartan Shield + let has_shield = { + let off_hand = target_player + .inventory() + .get_stack_in_hand(Hand::Left) + .await; + let stack = off_hand.lock().await; + let off = stack + .get_data_component::() + .is_some_and(|m| m.model == "kbve:spartan_shield"); + + if off { + true + } else { + let main_hand = target_player.inventory().held_item(); + let stack = main_hand.lock().await; + stack + .get_data_component::() + .is_some_and(|m| m.model == "kbve:spartan_shield") } + }; + + if !has_shield { + return; } - sender - .send_message(TextComponent::text("Gave KBVE Sword!").color_named(NamedColor::Aqua)) + // Deal 3 thorns damage back to the attacker + attacker + .damage_with_context( + &*attacker, + 3.0, + DamageType::THORNS, + None, + Some(target_player), + None, + ) + .await; + + // Degrade the shield (1 durability per reflected hit) + target_player.damage_held_item(1).await; + + // Visual feedback — flame burst on the attacker + let pos = attacker.living_entity.entity.pos.load(); + attacker + .world() + .spawn_particle( + Vector3::new(pos.x, pos.y + 1.0, pos.z), + Vector3::new(0.2, 0.3, 0.2), + 0.02, + 8, + Particle::Flame, + ) .await; - Ok(1) }) } } -fn kbve_command_tree() -> CommandTree { - CommandTree::new(["kbve"], "KBVE custom items").then( - literal("give") - .then( - literal("coin") - .then(argument_default_name(PlayersArgumentConsumer).execute(GiveCoinExecutor)), - ) - .then( - literal("sword").then( - argument_default_name(PlayersArgumentConsumer).execute(GiveSwordExecutor), - ), - ), - ) -} - // --------------------------------------------------------------------------- // Axum resource pack server // --------------------------------------------------------------------------- @@ -326,11 +487,14 @@ impl KbveMcPlugin { async fn on_load(&mut self, context: Arc) -> Result<(), String> { eprintln!("[kbve-mc-plugin] on_load START"); - // Register welcome event handler + // Register event handlers context .register_event(Arc::new(WelcomeHandler), EventPriority::Normal, false) .await; - eprintln!("[kbve-mc-plugin] Welcome handler registered"); + context + .register_event(Arc::new(ShieldBlockHandler), EventPriority::Normal, false) + .await; + eprintln!("[kbve-mc-plugin] Event handlers registered"); // Register /kbve command permission (Allow = all players can use it) if let Err(e) = context