diff --git a/.github/workflows/ci-security.yml b/.github/workflows/ci-security.yml index 1088e0d6ba..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 }} @@ -31,6 +34,7 @@ jobs: language: - javascript-typescript - python + - rust steps: - name: Checkout @@ -40,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 @@ -48,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 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 diff --git a/apps/mc/Dockerfile b/apps/mc/Dockerfile index a0ccef292b..e05a53ac09 100644 --- a/apps/mc/Dockerfile +++ b/apps/mc/Dockerfile @@ -11,28 +11,56 @@ 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) --- +# 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 +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 + +# --- 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 + +# --- 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 +75,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 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/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 new file mode 100644 index 0000000000..3e3af6deac --- /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/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/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/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/weapons/kbve_scythe.json b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_scythe.json new file mode 100644 index 0000000000..534f32e754 --- /dev/null +++ b/apps/mc/data/resource-pack/assets/kbve/models/item/weapons/kbve_scythe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "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/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 0000000000..34cbe27700 Binary files /dev/null and b/apps/mc/data/resource-pack/assets/kbve/textures/block/rust_stone.png differ 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/weapons/kbve_scythe.png b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_scythe.png new file mode 100644 index 0000000000..9673dbffb2 Binary files /dev/null and b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/kbve_scythe.png differ 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/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 0000000000..d3a34fa850 Binary files /dev/null and b/apps/mc/data/resource-pack/assets/kbve/textures/item/weapons/spartan_shield.png differ diff --git a/apps/mc/data/resource-pack/pack.mcmeta b/apps/mc/data/resource-pack/pack.mcmeta index 3c7237e686..944f71d4f7 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 Assets - Weapons, Rewards & Blocks" } } 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..996c3861fd 100644 --- a/apps/mc/mc-e2e/project.json +++ b/apps/mc/mc-e2e/project.json @@ -5,16 +5,21 @@ "sourceRoot": "apps/mc/mc-e2e/e2e", "implicitDependencies": ["mc"], "targets": { + "test": { + "executor": "nx:noop", + "cache": false + }, "e2e": { "executor": "nx:run-commands", "cache": false, "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, }, diff --git a/apps/mc/plugins/kbve-mc-plugin/Cargo.toml b/apps/mc/plugins/kbve-mc-plugin/Cargo.toml index 6594c92a29..28ca0bead8 100644 --- a/apps/mc/plugins/kbve-mc-plugin/Cargo.toml +++ b/apps/mc/plugins/kbve-mc-plugin/Cargo.toml @@ -15,6 +15,8 @@ pumpkin-api-macros = { path = "../../pumpkin/pumpkin-api-macros" } pumpkin-macros = { path = "../../pumpkin/pumpkin-macros" } pumpkin-util = { path = "../../pumpkin/pumpkin-util" } pumpkin-data = { path = "../../pumpkin/pumpkin-data" } +pumpkin-protocol = { path = "../../pumpkin/pumpkin-protocol" } pumpkin-world = { path = "../../pumpkin/pumpkin-world" } tokio = { version = "1", features = ["rt-multi-thread"] } axum = "0.8" +dashmap = "6" diff --git a/apps/mc/plugins/kbve-mc-plugin/src/lib.rs b/apps/mc/plugins/kbve-mc-plugin/src/lib.rs index 4597cf4046..afd838bd54 100644 --- a/apps/mc/plugins/kbve-mc-plugin/src/lib.rs +++ b/apps/mc/plugins/kbve-mc-plugin/src/lib.rs @@ -1,23 +1,32 @@ #![allow(unused_imports, clippy::async_yields_async)] -use std::sync::Arc; use std::sync::atomic::Ordering; +use std::sync::{Arc, LazyLock}; use std::time::Duration; +use dashmap::DashMap; use pumpkin::command::args::players::PlayersArgumentConsumer; use pumpkin::command::args::{ConsumedArgs, FindArgDefaultName}; use pumpkin::command::dispatcher::CommandError; use pumpkin::command::tree::CommandTree; use pumpkin::command::tree::builder::{argument_default_name, literal}; use pumpkin::command::{CommandExecutor, CommandResult, CommandSender}; +use pumpkin::entity::EntityBase; use pumpkin::plugin::api::Context; +use pumpkin::plugin::player::player_interact_entity_event::PlayerInteractEntityEvent; use pumpkin::plugin::player::player_join::PlayerJoinEvent; use pumpkin::plugin::{BoxFuture, EventHandler, EventPriority}; use pumpkin::server::Server; use pumpkin_api_macros::plugin_impl; +use pumpkin_data::damage::DamageType; use pumpkin_data::data_component::DataComponent; -use pumpkin_data::data_component_impl::{CustomNameImpl, DataComponentImpl, ItemModelImpl}; +use pumpkin_data::data_component_impl::{ + CustomNameImpl, DamageImpl, DataComponentImpl, ItemModelImpl, MaxDamageImpl, +}; use pumpkin_data::item::Item; +use pumpkin_data::particle::Particle; +use pumpkin_protocol::java::server::play::ActionType; +use pumpkin_util::Hand; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector2::Vector2; use pumpkin_util::math::vector3::Vector3; @@ -168,36 +177,77 @@ impl EventHandler 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 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" ] } } 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 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") 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..."