diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 00000000..ef41d8ab
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,91 @@
+name: A-B Performance Benchmark
+
+on:
+ workflow_dispatch:
+ pull_request:
+ paths:
+ - '.github/workflows/benchmark.yml'
+ - 'benchmark/**'
+ - 'build.gradle.kts'
+ - 'common/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemDisplay.java'
+ - 'common/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemSpatialIndex.java'
+ - 'common/src/main/java/com/loohp/interactionvisualizer/entities/VisibilityTokenBucket.java'
+
+permissions:
+ contents: read
+
+jobs:
+ benchmark:
+ name: Paper 26.1.2 A-B benchmark
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Java 25
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "25"
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build disposable benchmark plugin
+ run: ./gradlew benchmarkJar --no-daemon --no-build-cache --rerun-tasks
+
+ - name: Download stable Paper 26.1.2
+ env:
+ PAPER_USER_AGENT: InteractionVisualizer-Benchmark/1.0 (https://github.com/EllanServer/InteractionVisualizer)
+ run: |
+ mkdir -p benchmark-server/plugins
+ BUILDS=$(curl --fail --silent --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ https://fill.papermc.io/v3/projects/paper/versions/26.1.2/builds)
+ PAPER_URL=$(echo "$BUILDS" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty')
+ test -n "$PAPER_URL"
+ curl --fail --location --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ --output benchmark-server/server.jar "$PAPER_URL"
+ cp build/libs/InteractionVisualizer-*-benchmark.jar benchmark-server/plugins/
+
+ - name: Configure isolated benchmark server
+ working-directory: benchmark-server
+ run: |
+ echo 'eula=true' > eula.txt
+ cat > server.properties <<'EOF'
+ online-mode=false
+ level-name=benchmark-world
+ level-type=minecraft:flat
+ generate-structures=false
+ spawn-protection=0
+ view-distance=3
+ simulation-distance=3
+ max-players=1
+ max-tick-time=-1
+ sync-chunk-writes=false
+ EOF
+
+ - name: Run alternating A-B measurements
+ working-directory: benchmark-server
+ run: |
+ java -Xms2G -Xmx2G -XX:+UseG1GC -jar server.jar --nogui | tee benchmark-server.log
+ test -f plugins/InteractionVisualizerBenchmark/benchmark-results.jsonl
+ test -f plugins/InteractionVisualizerBenchmark/benchmark-complete.txt
+ EXPECTED_RESULTS=$(cat plugins/InteractionVisualizerBenchmark/benchmark-complete.txt)
+ ACTUAL_RESULTS=$(wc -l < plugins/InteractionVisualizerBenchmark/benchmark-results.jsonl)
+ test "$ACTUAL_RESULTS" -eq "$EXPECTED_RESULTS"
+
+ - name: Publish benchmark results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: interactionvisualizer-ab-${{ github.sha }}
+ path: |
+ benchmark-server/plugins/InteractionVisualizerBenchmark/benchmark-results.jsonl
+ benchmark-server/plugins/InteractionVisualizerBenchmark/benchmark-complete.txt
+ benchmark-server/benchmark-server.log
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3670cfdf..9d64c6e5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -29,6 +29,59 @@ jobs:
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
+ - name: Validate performance tooling
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $scripts = @(
+ 'tools/perf/phase2-pktmon.ps1',
+ 'tools/perf/analyze-presentmon.ps1',
+ 'tools/perf/analyze-minecraft-debug-profile.ps1',
+ 'tools/perf/analyze-jfr-socket-writes.ps1',
+ 'tools/perf/analyze-phase2-abba.ps1',
+ 'tools/perf/analyze-phase2-pcap.ps1'
+ )
+ foreach ($script in $scripts) {
+ [void][scriptblock]::Create((Get-Content -Raw -LiteralPath $script))
+ }
+ $pktmonSelfTestJson = & ./tools/perf/phase2-pktmon.ps1 selftest | Out-String
+ $pktmonSelfTest = $pktmonSelfTestJson | ConvertFrom-Json
+ if (-not $pktmonSelfTest.passed) {
+ throw 'PktMon wrapper self-test did not report passed=true.'
+ }
+ $selfTestJson = & ./tools/perf/analyze-presentmon.ps1 -SelfTest | Out-String
+ $selfTest = $selfTestJson | ConvertFrom-Json
+ if (-not $selfTest.passed) {
+ throw 'PresentMon analyzer self-test did not report passed=true.'
+ }
+ $debugProfileSelfTestJson = & ./tools/perf/analyze-minecraft-debug-profile.ps1 -SelfTest | Out-String
+ $debugProfileSelfTest = $debugProfileSelfTestJson | ConvertFrom-Json
+ if (-not $debugProfileSelfTest.passed) {
+ throw 'Minecraft debug-profile analyzer self-test did not report passed=true.'
+ }
+ $jfrSelfTestJson = & ./tools/perf/analyze-jfr-socket-writes.ps1 -SelfTest | Out-String
+ $jfrSelfTest = $jfrSelfTestJson | ConvertFrom-Json
+ if (-not $jfrSelfTest.passed) {
+ throw 'JFR socket-write analyzer self-test did not report passed=true.'
+ }
+ $abbaSelfTestJson = & ./tools/perf/analyze-phase2-abba.ps1 -SelfTest | Out-String
+ $abbaSelfTest = $abbaSelfTestJson | ConvertFrom-Json
+ if (-not $abbaSelfTest.passed) {
+ throw 'Phase 2 ABBA analyzer self-test did not report passed=true.'
+ }
+ $pcapSelfTestJson = & ./tools/perf/analyze-phase2-pcap.ps1 -SelfTest | Out-String
+ $pcapSelfTest = $pcapSelfTestJson | ConvertFrom-Json
+ if (-not $pcapSelfTest.passed) {
+ throw 'Phase 2 pcap analyzer self-test did not report passed=true.'
+ }
+
+ - name: Validate runtime harness syntax
+ shell: bash
+ run: |
+ bash -n tools/perf/prepare-phase2-protocol-client.sh
+ bash -n tools/perf/run-phase2-runtime-once.sh
+ node --check tools/perf/phase2-protocol-client.js
+
- name: Run checks and build production jar
run: ./gradlew clean check shadowJar --no-daemon --no-build-cache --rerun-tasks
diff --git a/.github/workflows/phase2-packet-capture.yml b/.github/workflows/phase2-packet-capture.yml
new file mode 100644
index 00000000..c1793b7b
--- /dev/null
+++ b/.github/workflows/phase2-packet-capture.yml
@@ -0,0 +1,361 @@
+name: Phase 2 Packet Capture A-B
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, labeled]
+ workflow_dispatch:
+ inputs:
+ scenario:
+ description: "Independent network scenario"
+ required: true
+ default: "static-spawn"
+ type: choice
+ options: ["static-spawn", "visibility-return", "visibility-itemdisplay-return", "visibility-textdisplay-return"]
+ runs:
+ description: "Restart-isolated runs (4, 8, or formal 12)"
+ required: true
+ default: "12"
+ type: choice
+ options: ["4", "8", "12"]
+ items:
+ description: "Logical items"
+ required: true
+ default: "4096"
+ type: string
+ warmup_seconds:
+ description: "Warmup after the real TCP client is ready"
+ required: true
+ default: "60"
+ type: string
+ settle_seconds:
+ description: "Pre-window settling time"
+ required: true
+ default: "10"
+ type: string
+ measure_seconds:
+ description: "Packet capture window"
+ required: true
+ default: "10"
+ type: string
+ snaplen:
+ description: "tcpdump snapshot length; 128 for transport metrics, 0 for full compatibility capture"
+ required: true
+ default: "128"
+ type: choice
+ options: ["128", "0"]
+
+permissions:
+ contents: read
+
+jobs:
+ packet-capture-ab:
+ name: Paper 26.1.2 packet ABBA
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.event.action != 'labeled' ||
+ github.event.label.name == 'phase2-packet-visibility-smoke' ||
+ github.event.label.name == 'phase2-packet-static-formal' ||
+ github.event.label.name == 'phase2-packet-visibility-formal' ||
+ github.event.label.name == 'phase2-packet-visibility-itemdisplay-smoke' ||
+ github.event.label.name == 'phase2-packet-visibility-itemdisplay-formal' ||
+ github.event.label.name == 'phase2-packet-visibility-textdisplay-smoke' ||
+ github.event.label.name == 'phase2-packet-visibility-textdisplay-formal'
+ runs-on: ubuntu-latest
+ timeout-minutes: 100
+ env:
+ CAMPAIGN_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || contains(github.event.label.name, 'visibility-itemdisplay') && 'visibility-itemdisplay-return' || contains(github.event.label.name, 'visibility-textdisplay') && 'visibility-textdisplay-return' || contains(github.event.label.name, 'visibility') && 'visibility-return' || 'static-spawn' }}
+ CAMPAIGN_RUNS: ${{ github.event_name == 'workflow_dispatch' && inputs.runs || contains(github.event.label.name, '-formal') && '12' || '4' }}
+ CAMPAIGN_ITEMS: ${{ github.event_name == 'workflow_dispatch' && inputs.items || contains(github.event.label.name, '-formal') && '4096' || '1024' }}
+ CAMPAIGN_WARMUP_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || contains(github.event.label.name, '-formal') && '60' || '10' }}
+ CAMPAIGN_SETTLE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || contains(github.event.label.name, '-formal') && '10' || '5' }}
+ CAMPAIGN_MEASURE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || contains(github.event.label.name, '-formal') && '10' || contains(github.event.label.name, 'visibility') && '10' || '5' }}
+ CAMPAIGN_SNAPLEN: ${{ github.event_name == 'workflow_dispatch' && inputs.snaplen || '128' }}
+
+ steps:
+ - name: Check out immutable test source
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Set up Java 25
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "25"
+
+ - name: Set up Node 24 for the isolated protocol peer
+ uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Install capture readers
+ env:
+ DEBIAN_FRONTEND: noninteractive
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes --no-install-recommends tcpdump tshark
+ tcpdump --version
+ tshark --version
+
+ - name: Validate harness source
+ run: |
+ bash -n tools/perf/prepare-phase2-protocol-client.sh
+ bash -n tools/perf/run-phase2-runtime-once.sh
+ node --check tools/perf/phase2-protocol-client.js
+ node --check tools/perf/analyze-phase2-protocol-trace.js
+ node tools/perf/analyze-phase2-protocol-trace.js --self-test
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 -SelfTest
+ pwsh -NoProfile -File tools/perf/analyze-phase2-pcap.ps1 -SelfTest
+
+ - name: Build and test the production plugin
+ run: ./gradlew clean check shadowJar --no-daemon --no-build-cache --rerun-tasks
+
+ - name: Download stable Paper 26.1.2
+ env:
+ PAPER_USER_AGENT: InteractionVisualizer-Phase2/1.0 (https://github.com/EllanServer/InteractionVisualizer)
+ run: |
+ mkdir -p phase2-dependencies
+ BUILDS=$(curl --fail --silent --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ https://fill.papermc.io/v3/projects/paper/versions/26.1.2/builds)
+ PAPER_URL=$(echo "$BUILDS" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty')
+ test -n "$PAPER_URL"
+ curl --fail --location --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ --output phase2-dependencies/paper.jar "$PAPER_URL"
+
+ - name: Prepare immutable protocol client artifact
+ run: bash tools/perf/prepare-phase2-protocol-client.sh phase2-dependencies/protocol-client
+
+ - name: Run restart-isolated packet ABBA campaign
+ id: packet_campaign
+ env:
+ SCENARIO: ${{ env.CAMPAIGN_SCENARIO }}
+ RUNS: ${{ env.CAMPAIGN_RUNS }}
+ ITEMS: ${{ env.CAMPAIGN_ITEMS }}
+ WARMUP_SECONDS: ${{ env.CAMPAIGN_WARMUP_SECONDS }}
+ SETTLE_SECONDS: ${{ env.CAMPAIGN_SETTLE_SECONDS }}
+ MEASURE_SECONDS: ${{ env.CAMPAIGN_MEASURE_SECONDS }}
+ SNAPLEN: ${{ env.CAMPAIGN_SNAPLEN }}
+ run: |
+ set -euo pipefail
+ [[ "$SCENARIO" == static-spawn || "$SCENARIO" == visibility-return || \
+ "$SCENARIO" == visibility-itemdisplay-return || "$SCENARIO" == visibility-textdisplay-return ]]
+ [[ "$RUNS" =~ ^(4|8|12)$ ]]
+ [[ "$ITEMS" =~ ^[0-9]+$ ]] && (( ITEMS >= 1 && ITEMS <= 8192 ))
+ [[ "$WARMUP_SECONDS" =~ ^[0-9]+$ ]] && (( WARMUP_SECONDS >= 10 ))
+ [[ "$SETTLE_SECONDS" =~ ^[0-9]+$ ]] && (( SETTLE_SECONDS >= 5 ))
+ [[ "$MEASURE_SECONDS" =~ ^[0-9]+$ ]] && (( MEASURE_SECONDS >= 5 ))
+ [[ "$SNAPLEN" == 0 || "$SNAPLEN" == 128 ]]
+ if [[ "$SCENARIO" == visibility-* ]]; then
+ (( MEASURE_SECONDS >= 10 ))
+ fi
+
+ PLUGIN_JAR=$(find build/libs -maxdepth 1 -type f -name 'InteractionVisualizer-*.jar' ! -name '*-sources.jar' ! -name '*-benchmark.jar' -print -quit)
+ test -n "$PLUGIN_JAR"
+ mkdir -p phase2-results/packet
+ MANIFEST=phase2-results/packet/abba-manifest.csv
+ PROTOCOL_MANIFEST=phase2-results/packet/protocol-trace-abba-manifest.csv
+ printf 'Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$MANIFEST"
+ printf 'Scenario,Block,Position,Variant,RunId,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$PROTOCOL_MANIFEST"
+ ARTIFACT_SHA=$(sha256sum "$PLUGIN_JAR" | awk '{print $1}')
+ STACK_SHA=$(
+ {
+ sha256sum phase2-dependencies/paper.jar
+ sha256sum phase2-dependencies/protocol-client/client-build-manifest.json
+ sha256sum tools/perf/phase2-protocol-client.js
+ sha256sum tools/perf/analyze-phase2-protocol-trace.js
+ sha256sum tools/perf/analyze-phase2-pcap.ps1
+ sha256sum tools/perf/analyze-phase2-abba.ps1
+ sha256sum tools/perf/run-phase2-runtime-once.sh
+ java -version 2>&1
+ printf '%s\n' \
+ 'java=-Xms2G -Xmx2G -XX:+UseG1GC -XX:+AlwaysPreTouch -Dinteractionvisualizer.performance.allowBlockScene=true -Xlog:gc*=info,safepoint=info:file=jvm-gc-safepoint.log:time,uptime,level,tags:filecount=0 -Dfile.encoding=UTF-8' \
+ "scenario=$SCENARIO" "snaplen=$SNAPLEN" 'protocolTrace=semantic-lifecycle-v1' \
+ 'sparkProfile=none'
+ } | sha256sum | awk '{print $1}'
+ )
+
+ for run_number in $(seq 1 "$RUNS"); do
+ block=$(( (run_number - 1) / 4 + 1 ))
+ position=$(( (run_number - 1) % 4 + 1 ))
+ if (( block % 2 == 1 )); then pattern=ABBA; else pattern=BAAB; fi
+ variant=${pattern:$((position - 1)):1}
+ run_id=$(printf '%s_%s_%02d' "${SCENARIO//-/_}" "$variant" "$run_number")
+
+ PHASE2_PLUGIN_JAR="$PLUGIN_JAR" \
+ PHASE2_PAPER_JAR=phase2-dependencies/paper.jar \
+ PHASE2_CLIENT_ROOT=phase2-dependencies/protocol-client \
+ PHASE2_OUTPUT_ROOT=phase2-results/packet \
+ PHASE2_RUN_ID="$run_id" \
+ PHASE2_SCENARIO="$SCENARIO" \
+ PHASE2_VARIANT="$variant" \
+ PHASE2_ITEM_COUNT="$ITEMS" \
+ PHASE2_WARMUP_SECONDS="$WARMUP_SECONDS" \
+ PHASE2_SETTLE_SECONDS="$SETTLE_SECONDS" \
+ PHASE2_MEASURE_SECONDS="$MEASURE_SECONDS" \
+ PHASE2_CAPTURE_ENABLED=1 \
+ PHASE2_CAPTURE_SNAPLEN="$SNAPLEN" \
+ PHASE2_PROTOCOL_TRACE_ENABLED=1 \
+ PHASE2_SPARK_PROFILE_MODE=none \
+ bash tools/perf/run-phase2-runtime-once.sh
+
+ printf '%s,%d,%d,%s,%s,%s,%s,tcpdump-lo-s%s,%s/%s.pcap-analysis.json\n' \
+ "$SCENARIO" "$block" "$position" "$variant" "$run_id" "$STACK_SHA" "$ARTIFACT_SHA" \
+ "$SNAPLEN" "$run_id" "$run_id" >> "$MANIFEST"
+ printf '%s,%d,%d,%s,%s,%s,%s,protocol-client-memory,%s/%s.protocol-trace-analysis.json\n' \
+ "$SCENARIO" "$block" "$position" "$variant" "$run_id" "$STACK_SHA" "$ARTIFACT_SHA" \
+ "$run_id" "$run_id" >> "$PROTOCOL_MANIFEST"
+ done
+
+ minimum_seconds=$(( MEASURE_SECONDS - 1 ))
+ incomplete=()
+ if [[ "$RUNS" != 12 ]]; then incomplete=(-AllowIncomplete); fi
+ # Loopback capture preserves TCP payload volume/timing for paired
+ # regression checks, but its synthetic link headers and offload
+ # behavior are not evidence of physical on-wire frame bytes.
+ for metric in downstream.tcpPayloadBytes downstream.peak50ms.tcpPayloadBytes downstream.peak1s.tcpPayloadBytes; do
+ safe_metric=${metric//./_}
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$MANIFEST" \
+ -Scenario "$SCENARIO" -Metric "$metric" -Direction LowerIsBetter \
+ -MinimumSeconds "$minimum_seconds" "${incomplete[@]}" \
+ -OutputJson "phase2-results/packet/$safe_metric.analysis.json" -Overwrite
+ done
+
+ # These semantic lifecycle counts are generated from the same
+ # explicit half-open window as the pcap evidence. They supplement,
+ # and never replace, the transport-level byte and retransmit gates.
+ for metric in traceCoverage.windowEventCount identity.spawn.observations spawnPeaks.epochAligned50ms.spawnedEntityIdCount spawnPeaks.epochAligned1s.spawnedEntityIdCount; do
+ safe_metric=${metric//./_}
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$PROTOCOL_MANIFEST" \
+ -Scenario "$SCENARIO" -Metric "$metric" -Direction LowerIsBetter \
+ "${incomplete[@]}" \
+ -OutputJson "phase2-results/packet/protocol_$safe_metric.analysis.json" -Overwrite
+ done
+
+ - name: Summarize semantic protocol evidence
+ if: always()
+ env:
+ SCENARIO: ${{ env.CAMPAIGN_SCENARIO }}
+ EXPECTED_RUNS: ${{ env.CAMPAIGN_RUNS }}
+ CAMPAIGN_OUTCOME: ${{ steps.packet_campaign.outcome }}
+ run: |
+ python3 - <<'PY'
+ import json
+ import os
+ import re
+ from pathlib import Path
+
+ results_root = Path("phase2-results/packet")
+ results_root.mkdir(parents=True, exist_ok=True)
+ suffix = ".protocol-trace-analysis.json"
+ records = []
+ errors = []
+ for analysis_path in sorted(results_root.glob(f"*/*{suffix}")):
+ run_id = analysis_path.name[:-len(suffix)]
+ variant_match = re.search(r"_([AB])_[0-9]+$", run_id)
+ try:
+ analysis = json.loads(analysis_path.read_text(encoding="utf-8"))
+ status = analysis.get("status", {})
+ identity = analysis.get("identity", {})
+ spawn = identity.get("spawn", {})
+ destroy = identity.get("destroy", {})
+ peaks = analysis.get("spawnPeaks", {})
+ records.append({
+ "runId": run_id,
+ "variant": variant_match.group(1) if variant_match else None,
+ "sourcePath": analysis_path.as_posix(),
+ "formalEvidenceReady": status.get("formalEvidenceReady") is True,
+ "windowCovered": status.get("windowCovered") is True,
+ "windowDurationMs": analysis.get("window", {}).get("durationMs"),
+ "windowEventCount": analysis.get("traceCoverage", {}).get("windowEventCount"),
+ "spawnObservations": spawn.get("observations"),
+ "uniqueSpawnIds": spawn.get("uniqueIdCount"),
+ "destroyObservations": destroy.get("observations"),
+ "peak50msSpawnIds": peaks.get("epochAligned50ms", {}).get("spawnedEntityIdCount"),
+ "peak1sSpawnIds": peaks.get("epochAligned1s", {}).get("spawnedEntityIdCount"),
+ "duplicateLiveSpawnObservations": identity.get("duplicateLiveSpawnObservations"),
+ "destroyWithoutKnownLiveObservations": identity.get("destroyWithoutKnownLiveObservations"),
+ })
+ except Exception as error:
+ errors.append({"sourcePath": analysis_path.as_posix(), "error": str(error)})
+
+ expected_runs = int(os.environ["EXPECTED_RUNS"])
+ run_ids = [record["runId"] for record in records]
+ unique_run_ids = len(run_ids) == len(set(run_ids))
+ complete_run_set = len(records) == expected_runs and unique_run_ids and not errors
+ formal_evidence_ready = complete_run_set and all(
+ record["formalEvidenceReady"] for record in records
+ )
+ summary = {
+ "schemaVersion": 1,
+ "scenario": os.environ["SCENARIO"],
+ "campaignOutcome": os.environ.get("CAMPAIGN_OUTCOME", "unknown"),
+ "requestedRuns": expected_runs,
+ "observedRuns": len(records),
+ "formalEvidenceReady": formal_evidence_ready,
+ "formalCampaignReady": (
+ formal_evidence_ready
+ and expected_runs == 12
+ and os.environ.get("CAMPAIGN_OUTCOME") == "success"
+ ),
+ "errors": errors,
+ "runs": records,
+ }
+ summary_path = results_root / "protocol-trace-summary.json"
+ summary_path.write_text(
+ json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
+ encoding="utf-8",
+ )
+
+ step_summary = os.environ.get("GITHUB_STEP_SUMMARY")
+ if step_summary:
+ lines = [
+ "### Phase 2 semantic protocol trace",
+ "",
+ f"- Scenario: `{summary['scenario']}`",
+ f"- Explicit-window runs: `{summary['observedRuns']}/{summary['requestedRuns']}`",
+ f"- Per-run `formalEvidenceReady`: `{'ready' if formal_evidence_ready else 'not-ready'}`",
+ f"- Formal 12-run ABBA campaign: `{'ready' if summary['formalCampaignReady'] else 'incomplete'}`",
+ "",
+ "| Run | Variant | Ready | Window events | Spawn IDs | Unique IDs | Peak 50 ms | Peak 1 s |",
+ "|---|---:|---:|---:|---:|---:|---:|---:|",
+ ]
+ for record in records:
+ lines.append(
+ "| {runId} | {variant} | {ready} | {events} | {spawn} | {unique} | {peak50} | {peak1s} |".format(
+ runId=record["runId"],
+ variant=record["variant"] or "?",
+ ready="yes" if record["formalEvidenceReady"] else "no",
+ events=record["windowEventCount"],
+ spawn=record["spawnObservations"],
+ unique=record["uniqueSpawnIds"],
+ peak50=record["peak50msSpawnIds"],
+ peak1s=record["peak1sSpawnIds"],
+ )
+ )
+ if errors:
+ lines.extend(["", f"Trace summary errors: `{len(errors)}`"])
+ with open(step_summary, "a", encoding="utf-8", newline="\n") as stream:
+ stream.write("\n".join(lines) + "\n")
+
+ if summary["campaignOutcome"] == "success" and not formal_evidence_ready:
+ raise SystemExit("successful campaign is missing complete formal-ready protocol trace evidence")
+ PY
+
+ - name: Publish packet evidence
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: phase2-packet-${{ env.CAMPAIGN_SCENARIO }}-${{ github.sha }}-${{ github.run_id }}
+ path: |
+ phase2-results/packet
+ phase2-dependencies/protocol-client/client-build-manifest.json
+ phase2-dependencies/protocol-client/client-files.sha256
+ phase2-dependencies/protocol-client/node-minecraft-protocol/package-lock.json
+ phase2-dependencies/protocol-client/node-minecraft-protocol/production-lock-inventory.json
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/.github/workflows/phase2-runtime-ab.yml b/.github/workflows/phase2-runtime-ab.yml
new file mode 100644
index 00000000..ef11386b
--- /dev/null
+++ b/.github/workflows/phase2-runtime-ab.yml
@@ -0,0 +1,767 @@
+name: Phase 2 Runtime A-B
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, labeled]
+ workflow_dispatch:
+ inputs:
+ ab_factor:
+ description: "Independent A/B factor (text cache requires block-active)"
+ required: true
+ default: "scenario-config"
+ type: choice
+ options: ["scenario-config", "legacy-text-component-cache"]
+ scenario:
+ description: "Runtime workload (choose block-active for text cache)"
+ required: true
+ default: "static-steady"
+ type: choice
+ options: ["static-steady", "block-idle", "block-active", "block-direct-write"]
+ runs:
+ description: "Restart-isolated runs (4, 8, or formal 12)"
+ required: true
+ default: "12"
+ type: choice
+ options: ["4", "8", "12"]
+ items:
+ description: "Logical items (max 8192) or fully tracked workload blocks (max 1024)"
+ required: true
+ default: "1024"
+ type: string
+ warmup_seconds:
+ description: "Warmup after the real TCP client is ready"
+ required: true
+ default: "120"
+ type: string
+ settle_seconds:
+ description: "Scene settling time before sampling"
+ required: true
+ default: "20"
+ type: string
+ measure_seconds:
+ description: "MSPT/TPS sampling window (diagnostic when Spark is enabled)"
+ required: true
+ default: "180"
+ type: string
+ spark_profile_mode:
+ description: "Optional 4-run Spark diagnostic (cpu=slow ticks, cpu-all=steady-state)"
+ required: true
+ default: "none"
+ type: choice
+ options: ["none", "cpu", "cpu-all", "alloc"]
+
+permissions:
+ contents: read
+
+jobs:
+ clean-runtime-ab:
+ name: Paper 26.1.2 runtime ABBA
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ github.event.action != 'labeled' ||
+ github.event.label.name == 'phase2-runtime-formal'
+ runs-on: ubuntu-latest
+ timeout-minutes: 130
+ env:
+ CAMPAIGN_AB_FACTOR: ${{ github.event_name == 'workflow_dispatch' && inputs.ab_factor || 'scenario-config' }}
+ CAMPAIGN_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || 'static-steady' }}
+ CAMPAIGN_RUNS: ${{ github.event_name == 'workflow_dispatch' && inputs.runs || github.event.action == 'labeled' && '12' || '4' }}
+ CAMPAIGN_ITEMS: ${{ github.event_name == 'workflow_dispatch' && inputs.items || github.event.action == 'labeled' && '4096' || '1024' }}
+ CAMPAIGN_WARMUP_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.warmup_seconds || github.event.action == 'labeled' && '120' || '10' }}
+ CAMPAIGN_SETTLE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.settle_seconds || github.event.action == 'labeled' && '20' || '5' }}
+ CAMPAIGN_MEASURE_SECONDS: ${{ github.event_name == 'workflow_dispatch' && inputs.measure_seconds || github.event.action == 'labeled' && '180' || '10' }}
+ CAMPAIGN_SPARK_PROFILE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode || 'none' }}
+ CAMPAIGN_EVIDENCE_KIND: ${{ github.event_name == 'workflow_dispatch' && inputs.spark_profile_mode != 'none' && 'diagnostic' || 'clean' }}
+
+ steps:
+ - name: Check out immutable test source
+ uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+
+ - name: Set up Java 25
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "25"
+
+ - name: Set up Node 24 for the isolated protocol peer
+ uses: actions/setup-node@v4
+ with:
+ node-version: "24"
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Validate harness source
+ run: |
+ bash -n tools/perf/prepare-phase2-protocol-client.sh
+ bash -n tools/perf/run-phase2-runtime-once.sh
+ node --check tools/perf/phase2-protocol-client.js
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 -SelfTest
+ pwsh -NoProfile -File tools/perf/analyze-phase2-pcap.ps1 -SelfTest
+
+ - name: Build and test the production plugin
+ run: ./gradlew clean check shadowJar --no-daemon --no-build-cache --rerun-tasks
+
+ - name: Download stable Paper 26.1.2
+ env:
+ PAPER_USER_AGENT: InteractionVisualizer-Phase2/1.0 (https://github.com/EllanServer/InteractionVisualizer)
+ run: |
+ mkdir -p phase2-dependencies
+ BUILDS=$(curl --fail --silent --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ https://fill.papermc.io/v3/projects/paper/versions/26.1.2/builds)
+ PAPER_URL=$(echo "$BUILDS" | jq -r 'first(.[] | select(.channel == "STABLE") | .downloads."server:default".url) // empty')
+ test -n "$PAPER_URL"
+ curl --fail --location --show-error \
+ -H "User-Agent: $PAPER_USER_AGENT" \
+ --output phase2-dependencies/paper.jar "$PAPER_URL"
+
+ - name: Prepare immutable protocol client artifact
+ run: bash tools/perf/prepare-phase2-protocol-client.sh phase2-dependencies/protocol-client
+
+ - name: Run restart-isolated runtime campaign
+ env:
+ AB_FACTOR: ${{ env.CAMPAIGN_AB_FACTOR }}
+ SCENARIO: ${{ env.CAMPAIGN_SCENARIO }}
+ RUNS: ${{ env.CAMPAIGN_RUNS }}
+ ITEMS: ${{ env.CAMPAIGN_ITEMS }}
+ WARMUP_SECONDS: ${{ env.CAMPAIGN_WARMUP_SECONDS }}
+ SETTLE_SECONDS: ${{ env.CAMPAIGN_SETTLE_SECONDS }}
+ MEASURE_SECONDS: ${{ env.CAMPAIGN_MEASURE_SECONDS }}
+ SPARK_PROFILE_MODE: ${{ env.CAMPAIGN_SPARK_PROFILE_MODE }}
+ EVIDENCE_KIND: ${{ env.CAMPAIGN_EVIDENCE_KIND }}
+ run: |
+ set -euo pipefail
+ [[ "$AB_FACTOR" == scenario-config || "$AB_FACTOR" == legacy-text-component-cache ]]
+ [[ "$SCENARIO" == static-steady || "$SCENARIO" == block-idle || \
+ "$SCENARIO" == block-active || "$SCENARIO" == block-direct-write ]]
+ [[ "$RUNS" =~ ^(4|8|12)$ ]]
+ [[ "$ITEMS" =~ ^[0-9]+$ ]]
+ if [[ "$SCENARIO" == block-* ]]; then
+ # TileEntityUpdate.CheckingRange=1 covers the centered 32x32
+ # block footprint completely; larger command scenes are valid but
+ # are not full-workload clean ABBA evidence.
+ (( ITEMS >= 1 && ITEMS <= 1024 ))
+ else
+ (( ITEMS >= 1 && ITEMS <= 8192 ))
+ fi
+ [[ "$WARMUP_SECONDS" =~ ^[0-9]+$ ]] && (( WARMUP_SECONDS >= 10 ))
+ [[ "$SETTLE_SECONDS" =~ ^[0-9]+$ ]] && (( SETTLE_SECONDS >= 5 ))
+ [[ "$MEASURE_SECONDS" =~ ^[0-9]+$ ]] && (( MEASURE_SECONDS >= 10 ))
+ [[ "$SPARK_PROFILE_MODE" =~ ^(none|cpu|cpu-all|alloc)$ ]]
+ if [[ "$AB_FACTOR" == legacy-text-component-cache ]]; then
+ if [[ "$SCENARIO" != block-active ]]; then
+ echo "legacy-text-component-cache A/B is isolated to block-active" >&2
+ exit 64
+ fi
+ if [[ "$SPARK_PROFILE_MODE" == none && "$RUNS" != 12 ]]; then
+ echo "clean legacy-text-component-cache evidence is formal-only and requires runs=12" >&2
+ exit 64
+ fi
+ if [[ "$SPARK_PROFILE_MODE" != none && "$RUNS" != 4 ]]; then
+ echo "profiled legacy-text-component-cache diagnostics require runs=4" >&2
+ exit 64
+ fi
+ if (( ITEMS < 100 )); then
+ echo "legacy-text-component-cache A/B requires at least 100 workload blocks" >&2
+ exit 64
+ fi
+ fi
+ if [[ "$SPARK_PROFILE_MODE" == cpu-all && \
+ "$SCENARIO" != block-active && "$SCENARIO" != block-direct-write ]]; then
+ echo "Spark cpu-all profiling is isolated to block-active or block-direct-write" >&2
+ exit 64
+ fi
+ if [[ "$SPARK_PROFILE_MODE" != none && "$SPARK_PROFILE_MODE" != cpu-all && \
+ "$SCENARIO" != block-direct-write ]]; then
+ echo "Spark cpu/alloc profiling is isolated to block-direct-write" >&2
+ exit 64
+ fi
+ if [[ "$SPARK_PROFILE_MODE" != none && "$RUNS" != 4 ]]; then
+ echo "Spark profiling is diagnostic-only and requires runs=4" >&2
+ exit 64
+ fi
+ if [[ "$SCENARIO" == block-direct-write ]]; then
+ # Direct BlockState writes intentionally emit no Bukkit event.
+ # Allow the 600-tick audit to begin plus enough ticks for every
+ # per-display iterator to cover the <=1024-block scene.
+ (( MEASURE_SECONDS >= 45 ))
+ fi
+
+ PLUGIN_JAR=$(find build/libs -maxdepth 1 -type f -name 'InteractionVisualizer-*.jar' ! -name '*-sources.jar' ! -name '*-benchmark.jar' -print -quit)
+ test -n "$PLUGIN_JAR"
+ if [[ "$SPARK_PROFILE_MODE" == none ]]; then
+ [[ "$EVIDENCE_KIND" == clean ]]
+ CAPTURE_METHOD=none
+ else
+ [[ "$EVIDENCE_KIND" == diagnostic ]]
+ CAPTURE_METHOD="spark-$SPARK_PROFILE_MODE"
+ fi
+ EVIDENCE_ROOT="phase2-results/$EVIDENCE_KIND"
+ mkdir -p "$EVIDENCE_ROOT"
+ MANIFEST="$EVIDENCE_ROOT/abba-manifest.csv"
+ printf 'Scenario,Block,Position,Variant,RunId,AbFactor,LegacyTextComponentCacheDisableProperty,LegacyTextComponentCacheEnabled,LegacyTextCacheRequests,LegacyTextCacheMisses,LegacyTextCacheHits,LegacyTextCacheHitRate,LegacyTextSameRawFastPaths,ConfigSha256,JvmArgumentsSha256,JvmArgumentsNormalizedSha256,StackSha256,ArtifactSha256,CaptureMethod,SourcePath\n' > "$MANIFEST"
+ ARTIFACT_SHA=$(sha256sum "$PLUGIN_JAR" | awk '{print $1}')
+ STACK_SHA=$(
+ {
+ sha256sum phase2-dependencies/paper.jar
+ sha256sum phase2-dependencies/protocol-client/client-build-manifest.json
+ sha256sum tools/perf/run-phase2-runtime-once.sh
+ java -version 2>&1
+ printf '%s\n' \
+ "abFactor=$AB_FACTOR" "scenario=$SCENARIO" "sparkProfile=$SPARK_PROFILE_MODE"
+ } | sha256sum | awk '{print $1}'
+ )
+
+ for run_number in $(seq 1 "$RUNS"); do
+ block=$(( (run_number - 1) / 4 + 1 ))
+ position=$(( (run_number - 1) % 4 + 1 ))
+ if (( block % 2 == 1 )); then pattern=ABBA; else pattern=BAAB; fi
+ variant=${pattern:$((position - 1)):1}
+ if [[ "$AB_FACTOR" == scenario-config ]]; then
+ run_id=$(printf '%s_%s_%02d' "${SCENARIO//-/_}" "$variant" "$run_number")
+ else
+ run_id=$(printf '%s_text_cache_%s_%02d' "${SCENARIO//-/_}" "$variant" "$run_number")
+ fi
+
+ PHASE2_PLUGIN_JAR="$PLUGIN_JAR" \
+ PHASE2_PAPER_JAR=phase2-dependencies/paper.jar \
+ PHASE2_CLIENT_ROOT=phase2-dependencies/protocol-client \
+ PHASE2_OUTPUT_ROOT="$EVIDENCE_ROOT" \
+ PHASE2_RUN_ID="$run_id" \
+ PHASE2_SCENARIO="$SCENARIO" \
+ PHASE2_VARIANT="$variant" \
+ PHASE2_AB_FACTOR="$AB_FACTOR" \
+ PHASE2_ITEM_COUNT="$ITEMS" \
+ PHASE2_WARMUP_SECONDS="$WARMUP_SECONDS" \
+ PHASE2_SETTLE_SECONDS="$SETTLE_SECONDS" \
+ PHASE2_MEASURE_SECONDS="$MEASURE_SECONDS" \
+ PHASE2_CAPTURE_ENABLED=0 \
+ PHASE2_PROTOCOL_TRACE_ENABLED=0 \
+ PHASE2_SPARK_PROFILE_MODE="$SPARK_PROFILE_MODE" \
+ bash tools/perf/run-phase2-runtime-once.sh
+
+ python3 - "$MANIFEST" "$EVIDENCE_ROOT" "$SCENARIO" "$block" "$position" \
+ "$variant" "$run_id" "$AB_FACTOR" "$STACK_SHA" "$ARTIFACT_SHA" \
+ "$CAPTURE_METHOD" <<'PY'
+ import csv
+ import json
+ from pathlib import Path
+ import sys
+
+ (
+ manifest_text,
+ evidence_root_text,
+ scenario,
+ block,
+ position,
+ variant,
+ run_id,
+ ab_factor,
+ stack_sha,
+ artifact_sha,
+ capture_method,
+ ) = sys.argv[1:]
+ manifest_path = Path(manifest_text)
+ evidence_root = Path(evidence_root_text)
+ metrics_path = evidence_root / run_id / "iv-perf.json"
+ run_manifest_path = evidence_root / run_id / "run-manifest.json"
+ metrics = json.loads(metrics_path.read_text(encoding="utf-8"))
+ provenance = json.loads(run_manifest_path.read_text(encoding="utf-8"))
+ cache = provenance.get("legacyTextComponentCache", {})
+ if provenance.get("abFactor") != ab_factor or metrics.get("abFactor") != ab_factor:
+ raise SystemExit(f"A/B factor provenance mismatch for {run_id}")
+ cache_metric_mapping = {
+ "enabled": "legacyTextComponentCache",
+ "requests": "legacyTextCacheRequests",
+ "misses": "legacyTextCacheMisses",
+ "hits": "legacyTextCacheHits",
+ "hitRate": "legacyTextCacheHitRate",
+ "sameRawFastPaths": "legacyTextSameRawFastPaths",
+ }
+ for provenance_field, metrics_field in cache_metric_mapping.items():
+ if cache.get(provenance_field) != metrics.get(metrics_field):
+ raise SystemExit(
+ f"cache provenance mismatch for {run_id}: {provenance_field}/{metrics_field}"
+ )
+ if cache.get("disableProperty") != metrics.get("legacyTextComponentCacheDisableProperty"):
+ raise SystemExit(f"cache property provenance mismatch for {run_id}")
+ if provenance.get("jvmArgumentsSha256") != metrics.get("jvmArgumentsSha256"):
+ raise SystemExit(f"JVM argument provenance mismatch for {run_id}")
+ if (provenance.get("jvmArgumentsNormalizedSha256")
+ != metrics.get("jvmArgumentsNormalizedSha256")):
+ raise SystemExit(f"normalized JVM argument provenance mismatch for {run_id}")
+ process_command_line = provenance.get("jvmDiagnostics", {}).get("processCommandLine", {})
+ if (process_command_line.get("formalEvidenceReady") is not True
+ or process_command_line.get("capturedFromProcCmdline") is not True
+ or process_command_line.get("jvmArgumentsSha256")
+ != provenance.get("jvmArgumentsSha256")
+ or process_command_line.get("jvmArgumentsNormalizedSha256")
+ != provenance.get("jvmArgumentsNormalizedSha256")):
+ raise SystemExit(f"live JVM process command-line provenance mismatch for {run_id}")
+ row = [
+ scenario,
+ block,
+ position,
+ variant,
+ run_id,
+ ab_factor,
+ str(cache.get("disableProperty")).lower(),
+ str(cache.get("enabled")).lower(),
+ cache.get("requests"),
+ cache.get("misses"),
+ cache.get("hits"),
+ cache.get("hitRate"),
+ cache.get("sameRawFastPaths"),
+ provenance.get("configSha256"),
+ provenance.get("jvmArgumentsSha256"),
+ provenance.get("jvmArgumentsNormalizedSha256"),
+ stack_sha,
+ artifact_sha,
+ capture_method,
+ f"{run_id}/iv-perf.json",
+ ]
+ with manifest_path.open("a", encoding="utf-8", newline="") as stream:
+ csv.writer(stream, lineterminator="\n").writerow(row)
+ PY
+ done
+
+ if [[ "$SPARK_PROFILE_MODE" == none ]]; then
+ minimum_seconds=$(( MEASURE_SECONDS - 2 ))
+ incomplete=()
+ if [[ "$RUNS" != 12 ]]; then incomplete=(-AllowIncomplete); fi
+ for metric in msptP95 msptP99 msptP999 msptMean; do
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$MANIFEST" \
+ -Scenario "$SCENARIO" -Metric "$metric" -Direction LowerIsBetter \
+ -MinimumSeconds "$minimum_seconds" "${incomplete[@]}" \
+ -OutputJson "$EVIDENCE_ROOT/$metric.analysis.json" -Overwrite
+ done
+ # At a healthy 20 TPS cap this is only an overload/non-regression
+ # diagnostic. MSPT remains the optimization effect-size metric.
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$MANIFEST" \
+ -Scenario "$SCENARIO" -Metric observedTps -Direction HigherIsBetter \
+ -MinimumSeconds "$minimum_seconds" "${incomplete[@]}" \
+ -OutputJson "$EVIDENCE_ROOT/observedTps.analysis.json" -Overwrite
+ fi
+
+ if [[ "$SCENARIO" == block-* ]]; then
+ # Active/direct-write CPU time has a meaningful LowerIsBetter
+ # direction after lifecycle/config assertions pass. An idle
+ # candidate can legitimately perform zero checks in a short
+ # window, so it stays diagnostic instead of entering log ratios.
+ # Raw check counts never carry a universal performance direction.
+ if [[ "$SPARK_PROFILE_MODE" == none && "$SCENARIO" != block-idle ]]; then
+ pwsh -NoProfile -File tools/perf/analyze-phase2-abba.ps1 "$MANIFEST" \
+ -Scenario "$SCENARIO" -Metric blockUpdateMs -Direction LowerIsBetter \
+ -MinimumSeconds "$minimum_seconds" "${incomplete[@]}" \
+ -OutputJson "$EVIDENCE_ROOT/blockUpdateMs.analysis.json" -Overwrite
+ fi
+
+ python3 - "$MANIFEST" "$SCENARIO" "$RUNS" "$ITEMS" "$CAPTURE_METHOD" "$AB_FACTOR" \
+ "$EVIDENCE_ROOT/blockUpdateChecks.evidence.json" <<'PY'
+ from pathlib import Path
+ import csv
+ import json
+ import math
+ import os
+ import re
+ import statistics
+ import sys
+
+ manifest_path = Path(sys.argv[1])
+ scenario = sys.argv[2]
+ expected_runs = int(sys.argv[3])
+ expected_items = int(sys.argv[4])
+ capture_method = sys.argv[5]
+ ab_factor = sys.argv[6]
+ output_path = Path(sys.argv[7])
+ records = []
+ with manifest_path.open(encoding="utf-8", newline="") as stream:
+ rows = list(csv.DictReader(stream))
+ if len(rows) != expected_runs:
+ raise SystemExit(f"block evidence expected {expected_runs} runs, found {len(rows)}")
+
+ def parse_bool(value, field, run_id):
+ if value == "true":
+ return True
+ if value == "false":
+ return False
+ raise SystemExit(f"invalid {field} for {run_id}: {value!r}")
+
+ def parse_nonnegative_int(value, field, run_id):
+ try:
+ parsed = int(value)
+ except (TypeError, ValueError) as error:
+ raise SystemExit(f"invalid {field} for {run_id}: {value!r}") from error
+ if parsed < 0:
+ raise SystemExit(f"invalid {field} for {run_id}: {parsed!r}")
+ return parsed
+
+ for row in rows:
+ if row["Scenario"] != scenario:
+ raise SystemExit(f"manifest scenario drift: {row['Scenario']} != {scenario}")
+ if row["CaptureMethod"] != capture_method:
+ raise SystemExit(
+ f"manifest capture-method drift: {row['CaptureMethod']} != {capture_method}"
+ )
+ if row["AbFactor"] != ab_factor:
+ raise SystemExit(f"manifest A/B factor drift: {row['AbFactor']} != {ab_factor}")
+ source_path = manifest_path.parent / row["SourcePath"]
+ metrics = json.loads(source_path.read_text(encoding="utf-8"))
+ run_id = row["RunId"]
+ if metrics.get("label") != run_id:
+ raise SystemExit(f"metrics label mismatch for {run_id}")
+ checks = metrics.get("blockUpdateChecks")
+ elapsed_ms = metrics.get("blockUpdateMs")
+ if isinstance(checks, bool) or not isinstance(checks, int) or checks < 0:
+ raise SystemExit(f"invalid blockUpdateChecks for {run_id}: {checks!r}")
+ if (isinstance(elapsed_ms, bool) or not isinstance(elapsed_ms, (int, float))
+ or not math.isfinite(elapsed_ms) or elapsed_ms < 0):
+ raise SystemExit(f"invalid blockUpdateMs for {run_id}: {elapsed_ms!r}")
+ if checks > 0 and elapsed_ms <= 0:
+ raise SystemExit(f"{run_id} recorded {checks} checks but no elapsed time")
+ tick_samples = metrics.get("tickSamples")
+ if isinstance(tick_samples, bool) or not isinstance(tick_samples, int) or tick_samples <= 0:
+ raise SystemExit(f"invalid tickSamples for {run_id}: {tick_samples!r}")
+ variant = row["Variant"]
+ expected_event_driven = (
+ ab_factor == "legacy-text-component-cache" or variant == "B"
+ )
+ if metrics.get("eventDrivenBlockUpdates") is not expected_event_driven:
+ raise SystemExit(f"event-driven config mismatch for {run_id}")
+ disable_property = parse_bool(
+ row["LegacyTextComponentCacheDisableProperty"],
+ "LegacyTextComponentCacheDisableProperty",
+ run_id,
+ )
+ cache_enabled = parse_bool(
+ row["LegacyTextComponentCacheEnabled"],
+ "LegacyTextComponentCacheEnabled",
+ run_id,
+ )
+ expected_disable_property = (
+ ab_factor == "legacy-text-component-cache" and variant == "A"
+ )
+ if disable_property is not expected_disable_property:
+ raise SystemExit(f"legacy text cache property mismatch for {run_id}")
+ if cache_enabled is not (not disable_property):
+ raise SystemExit(f"legacy text cache enabled state mismatch for {run_id}")
+ requests = parse_nonnegative_int(row["LegacyTextCacheRequests"], "requests", run_id)
+ misses = parse_nonnegative_int(row["LegacyTextCacheMisses"], "misses", run_id)
+ hits = parse_nonnegative_int(row["LegacyTextCacheHits"], "hits", run_id)
+ same_raw_fast_paths = parse_nonnegative_int(
+ row["LegacyTextSameRawFastPaths"], "sameRawFastPaths", run_id
+ )
+ try:
+ hit_rate = float(row["LegacyTextCacheHitRate"])
+ except ValueError as error:
+ raise SystemExit(f"invalid cache hit rate for {run_id}") from error
+ if (not math.isfinite(hit_rate) or not 0.0 <= hit_rate <= 1.0
+ or misses > requests or hits != requests - misses):
+ raise SystemExit(f"inconsistent legacy text cache metrics for {run_id}")
+ derived_hit_rate = hits / requests if requests else 0.0
+ if not math.isclose(hit_rate, derived_hit_rate, rel_tol=0.0, abs_tol=0.000001):
+ raise SystemExit(f"legacy text cache hit rate mismatch for {run_id}")
+ metric_expectations = {
+ "abFactor": ab_factor,
+ "jvmArgumentsSha256": row["JvmArgumentsSha256"],
+ "jvmArgumentsNormalizedSha256": row["JvmArgumentsNormalizedSha256"],
+ "legacyTextComponentCacheDisableProperty": disable_property,
+ "legacyTextComponentCache": cache_enabled,
+ "legacyTextCacheRequests": requests,
+ "legacyTextCacheMisses": misses,
+ "legacyTextCacheHits": hits,
+ "legacyTextSameRawFastPaths": same_raw_fast_paths,
+ }
+ for field, expected in metric_expectations.items():
+ if metrics.get(field) != expected:
+ raise SystemExit(f"{field} evidence mismatch for {run_id}")
+ if not math.isclose(
+ metrics.get("legacyTextCacheHitRate", -1), hit_rate,
+ rel_tol=0.0, abs_tol=0.000001,
+ ):
+ raise SystemExit(f"legacyTextCacheHitRate evidence mismatch for {run_id}")
+ config_sha = row["ConfigSha256"]
+ jvm_arguments_sha = row["JvmArgumentsSha256"]
+ jvm_arguments_normalized_sha = row["JvmArgumentsNormalizedSha256"]
+ for field, value in (
+ ("ConfigSha256", config_sha),
+ ("JvmArgumentsSha256", jvm_arguments_sha),
+ ("JvmArgumentsNormalizedSha256", jvm_arguments_normalized_sha),
+ ):
+ if re.fullmatch(r"[0-9a-f]{64}", value) is None:
+ raise SystemExit(f"invalid {field} for {run_id}: {value!r}")
+ provenance_path = source_path.parent / "run-manifest.json"
+ provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
+ if (provenance.get("abFactor") != ab_factor
+ or provenance.get("configSha256") != config_sha
+ or provenance.get("jvmArgumentsSha256") != jvm_arguments_sha
+ or provenance.get("jvmArgumentsNormalizedSha256")
+ != jvm_arguments_normalized_sha):
+ raise SystemExit(f"run provenance mismatch for {run_id}")
+ cache_provenance = provenance.get("legacyTextComponentCache", {})
+ expected_cache_provenance = {
+ "propertyName": "interactionvisualizer.disableLegacyTextComponentCache",
+ "disableProperty": disable_property,
+ "enabled": cache_enabled,
+ "requests": requests,
+ "misses": misses,
+ "hits": hits,
+ "hitRate": hit_rate,
+ "sameRawFastPaths": same_raw_fast_paths,
+ }
+ if cache_provenance != expected_cache_provenance:
+ raise SystemExit(f"legacy text cache provenance mismatch for {run_id}")
+ records.append({
+ "block": int(row["Block"]),
+ "position": int(row["Position"]),
+ "variant": variant,
+ "runId": run_id,
+ "abFactor": ab_factor,
+ "configSha256": config_sha,
+ "jvmArgumentsSha256": jvm_arguments_sha,
+ "jvmArgumentsNormalizedSha256": jvm_arguments_normalized_sha,
+ "legacyTextComponentCacheDisableProperty": disable_property,
+ "legacyTextComponentCacheEnabled": cache_enabled,
+ "legacyTextCacheRequests": requests,
+ "legacyTextCacheMisses": misses,
+ "legacyTextCacheHits": hits,
+ "legacyTextCacheHitRate": hit_rate,
+ "legacyTextSameRawFastPaths": same_raw_fast_paths,
+ "blockUpdateChecks": checks,
+ "tickSamples": tick_samples,
+ "checksPerTick": checks / tick_samples,
+ "blockUpdateMs": elapsed_ms,
+ "msPerCheck": elapsed_ms / checks if checks else None,
+ "sourcePath": source_path.as_posix(),
+ })
+
+ def summarize(variant):
+ selected = [record for record in records if record["variant"] == variant]
+ checks = [record["blockUpdateChecks"] for record in selected]
+ checks_per_tick = [record["checksPerTick"] for record in selected]
+ times = [record["blockUpdateMs"] for record in selected]
+ cache_requests = [record["legacyTextCacheRequests"] for record in selected]
+ cache_misses = [record["legacyTextCacheMisses"] for record in selected]
+ cache_hits = [record["legacyTextCacheHits"] for record in selected]
+ cache_hit_rates = [record["legacyTextCacheHitRate"] for record in selected]
+ same_raw_fast_paths = [record["legacyTextSameRawFastPaths"] for record in selected]
+ return {
+ "runCount": len(selected),
+ "blockUpdateChecks": {
+ "values": checks,
+ "median": statistics.median(checks),
+ "mean": statistics.fmean(checks),
+ },
+ "checksPerTick": {
+ "values": checks_per_tick,
+ "median": statistics.median(checks_per_tick),
+ "mean": statistics.fmean(checks_per_tick),
+ },
+ "blockUpdateMs": {
+ "values": times,
+ "median": statistics.median(times),
+ "mean": statistics.fmean(times),
+ },
+ "legacyTextComponentCache": {
+ "enabled": sorted({record["legacyTextComponentCacheEnabled"] for record in selected}),
+ "disableProperty": sorted({
+ record["legacyTextComponentCacheDisableProperty"] for record in selected
+ }),
+ "requests": cache_requests,
+ "misses": cache_misses,
+ "hits": cache_hits,
+ "hitRates": cache_hit_rates,
+ "sameRawFastPaths": same_raw_fast_paths,
+ "meanHitRate": statistics.fmean(cache_hit_rates),
+ },
+ }
+
+ by_variant = {"A": summarize("A"), "B": summarize("B")}
+ config_hashes = sorted({record["configSha256"] for record in records})
+ config_hashes_by_variant = {
+ variant: sorted({
+ record["configSha256"]
+ for record in records if record["variant"] == variant
+ })
+ for variant in ("A", "B")
+ }
+ jvm_hashes_by_variant = {
+ variant: sorted({
+ record["jvmArgumentsSha256"]
+ for record in records if record["variant"] == variant
+ })
+ for variant in ("A", "B")
+ }
+ normalized_jvm_hashes = sorted({
+ record["jvmArgumentsNormalizedSha256"] for record in records
+ })
+ if any(len(hashes) != 1 for hashes in jvm_hashes_by_variant.values()):
+ raise SystemExit("JVM argument SHA drifted within a variant")
+ if any(len(hashes) != 1 for hashes in config_hashes_by_variant.values()):
+ raise SystemExit("config SHA drifted within a variant")
+ if len(normalized_jvm_hashes) != 1:
+ raise SystemExit("normalized JVM argument SHA drifted across the campaign")
+ if ab_factor == "legacy-text-component-cache":
+ if len(config_hashes) != 1:
+ raise SystemExit("cache A/B variants do not share the same config SHA")
+ if jvm_hashes_by_variant["A"] == jvm_hashes_by_variant["B"]:
+ raise SystemExit("cache A/B variants unexpectedly share one JVM argument SHA")
+ for record in records:
+ if record["legacyTextCacheRequests"] <= 0:
+ raise SystemExit(f"cache A/B workload was not exercised in {record['runId']}")
+ if (record["variant"] == "A"
+ and record["legacyTextCacheMisses"] != record["legacyTextCacheRequests"]):
+ raise SystemExit(f"disabled cache reported hits in {record['runId']}")
+ if (record["variant"] == "B"
+ and record["legacyTextCacheHitRate"] < 0.90):
+ raise SystemExit(
+ "enabled cache missed the 90% steady-state hit-rate guard in "
+ f"{record['runId']}: {record['legacyTextCacheHitRate']:.6f}"
+ )
+ else:
+ if jvm_hashes_by_variant["A"] != jvm_hashes_by_variant["B"]:
+ raise SystemExit("scenario-config variants unexpectedly changed JVM arguments")
+ if config_hashes_by_variant["A"] == config_hashes_by_variant["B"]:
+ raise SystemExit("scenario-config variants unexpectedly share one config SHA")
+ cadence_guard = {
+ "evaluated": False,
+ "reason": "only block-active workloads with at least 100 blocks have a stable mixed-type cadence",
+ }
+ if scenario == "block-active" and expected_items >= 100:
+ baseline = by_variant["A"]["checksPerTick"]["median"]
+ candidate = by_variant["B"]["checksPerTick"]["median"]
+ if baseline <= 0:
+ raise SystemExit("block-active baseline recorded no checks")
+ ratio = candidate / baseline
+ if ab_factor == "legacy-text-component-cache":
+ minimum_ratio = 0.95
+ maximum_ratio = 1.05
+ cadence_interpretation = (
+ "Both cache variants use the same event-driven configuration, so their executed "
+ "block-check cadence must remain equivalent."
+ )
+ else:
+ minimum_ratio = 0.60
+ maximum_ratio = 0.85
+ cadence_interpretation = (
+ "The active candidate aggregate rate must remain consistent with all three furnace "
+ "cadences while eliminating idle bee polling and noisy level-event invalidations."
+ )
+ cadence_guard = {
+ "evaluated": True,
+ "minimumCandidateToBaselineRatio": minimum_ratio,
+ "maximumCandidateToBaselineRatio": maximum_ratio,
+ "candidateToBaselineRatio": ratio,
+ "passed": minimum_ratio <= ratio <= maximum_ratio,
+ "interpretation": cadence_interpretation,
+ }
+
+ if capture_method == "none":
+ interpretation = (
+ "blockUpdateChecks is workload evidence, not a standalone optimization objective; "
+ "fewer checks can mean either effective event-driven scheduling or missing work. "
+ "For block-idle, zero candidate checks/time in a short window is valid and blockUpdateMs "
+ "has no ratio analysis. Interpret all scenarios with machine-validated scene actions, "
+ "blockUpdateMs, MSPT and TPS."
+ )
+ else:
+ interpretation = (
+ "Instrumented Spark workload evidence only. blockUpdateChecks can establish executed work, "
+ "but MSPT, TPS and blockUpdateMs were collected under profiler overhead and must not be "
+ "used as clean performance effect sizes. Use the saved profile to attribute CPU cost, then "
+ "confirm any optimization in a separate captureMethod=none campaign."
+ )
+
+ evidence = {
+ "schemaVersion": 2,
+ "analysisType": "diagnostic-work-evidence",
+ "scenario": scenario,
+ "abFactor": ab_factor,
+ "requestedRuns": expected_runs,
+ "expectedItems": expected_items,
+ "captureMethod": capture_method,
+ "performanceEvidenceReady": capture_method == "none",
+ "formalComplete": expected_runs == 12 and capture_method == "none",
+ "blockUpdateChecksDirection": None,
+ "blockUpdateMsDirection": (
+ "LowerIsBetter" if capture_method == "none" and scenario != "block-idle" else None
+ ),
+ "interpretation": interpretation,
+ "activeCadenceGuard": cadence_guard,
+ "provenance": {
+ "configSha256": config_hashes,
+ "configSha256ByVariant": config_hashes_by_variant,
+ "jvmArgumentsSha256ByVariant": jvm_hashes_by_variant,
+ "jvmArgumentsNormalizedSha256": normalized_jvm_hashes[0],
+ "legacyTextComponentCacheProperty": (
+ "interactionvisualizer.disableLegacyTextComponentCache"
+ ),
+ "legacyTextProcessingTreatmentScope": [
+ "sharedComponentCache",
+ "perEntitySameRawFastPath",
+ ],
+ },
+ "byVariant": by_variant,
+ "runs": records,
+ }
+ output_path.write_text(
+ json.dumps(evidence, ensure_ascii=False, indent=2) + "\n",
+ encoding="utf-8",
+ )
+
+ step_summary = os.environ.get("GITHUB_STEP_SUMMARY")
+ if step_summary:
+ with open(step_summary, "a", encoding="utf-8", newline="\n") as stream:
+ stream.write(f"### {scenario} / {ab_factor} block update evidence\n\n")
+ stream.write(
+ f"Capture method: `{capture_method}`; clean performance evidence ready: "
+ f"`{str(capture_method == 'none').lower()}`.\n\n"
+ )
+ stream.write("`blockUpdateChecks` is diagnostic work evidence (no universal direction).\n\n")
+ stream.write(
+ "| Run | Variant | Checks/tick | Cache | Requests | Misses | Hits | Hit rate | Same raw |\n"
+ )
+ stream.write("|---|---:|---:|---:|---:|---:|---:|---:|---:|\n")
+ for record in records:
+ stream.write(
+ f"| {record['runId']} | {record['variant']} | "
+ f"{record['checksPerTick']:.6f} | "
+ f"{'on' if record['legacyTextComponentCacheEnabled'] else 'off'} | "
+ f"{record['legacyTextCacheRequests']} | {record['legacyTextCacheMisses']} | "
+ f"{record['legacyTextCacheHits']} | "
+ f"{record['legacyTextCacheHitRate']:.6f} | "
+ f"{record['legacyTextSameRawFastPaths']} |\n"
+ )
+ if cadence_guard["evaluated"]:
+ stream.write(
+ "\nActive cadence guard: "
+ f"`{'pass' if cadence_guard['passed'] else 'fail'}`; "
+ f"candidate/baseline=`{cadence_guard['candidateToBaselineRatio']:.6f}` "
+ f"(required `{cadence_guard['minimumCandidateToBaselineRatio']:.2f}.."
+ f"{cadence_guard['maximumCandidateToBaselineRatio']:.2f}`).\n"
+ )
+
+ if cadence_guard["evaluated"] and not cadence_guard["passed"]:
+ raise SystemExit(
+ "block-active candidate checks/tick escaped the expected mixed-type cadence: "
+ f"ratio={cadence_guard['candidateToBaselineRatio']:.6f}"
+ )
+ PY
+ fi
+
+ - name: Publish runtime evidence
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: phase2-${{ env.CAMPAIGN_EVIDENCE_KIND }}-runtime-${{ env.CAMPAIGN_SCENARIO }}-${{ env.CAMPAIGN_AB_FACTOR }}-${{ github.sha }}-${{ github.run_id }}
+ path: |
+ phase2-results/${{ env.CAMPAIGN_EVIDENCE_KIND }}
+ phase2-dependencies/protocol-client/client-build-manifest.json
+ phase2-dependencies/protocol-client/client-files.sha256
+ phase2-dependencies/protocol-client/node-minecraft-protocol/package-lock.json
+ phase2-dependencies/protocol-client/node-minecraft-protocol/production-lock-inventory.json
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/BillboardDisplayEntity.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/BillboardDisplayEntity.java
index 62e5e3ce..5241f4c5 100644
--- a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/BillboardDisplayEntity.java
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/BillboardDisplayEntity.java
@@ -17,11 +17,15 @@
import org.bukkit.util.Vector;
/**
- * Shared billboard replacement for the former per-viewer moving hologram.
- * Client-side billboard rotation replaces O(players) teleport packets.
+ * Text-display replacement for the former per-viewer moving hologram.
+ * Viewer positions are calculated exactly as the legacy surrounding-plane
+ * marker entity and synchronized only after that viewer actually moves.
*/
public final class BillboardDisplayEntity extends DisplayEntity implements DynamicVisualizerEntity {
+ private static final double RIGHT_ANGLE = Math.PI / 2.0D;
+ private static final double FORTY_FIVE_DEGREES = RIGHT_ANGLE / 2.0D;
+
private double radius;
private PathType path;
@@ -29,12 +33,77 @@ public BillboardDisplayEntity(Location location, double radius, PathType path) {
super(location);
this.radius = radius;
this.path = path;
- setBillboard(Display.Billboard.VERTICAL);
+ setBillboard(Display.Billboard.CENTER);
}
@Override
public Location getViewingLocation(Location from, Vector direction) {
- return getLocation();
+ Location location = getLocation();
+ if (!from.getWorld().equals(location.getWorld())) {
+ throw new IllegalArgumentException("Cannot view billboard in "
+ + location.getWorld().getName() + " from " + from.getWorld().getName());
+ }
+ return location.add(getViewingVector(location, from, direction, radius, path));
+ }
+
+ private static Vector getViewingVector(Location location, Location from, Vector direction,
+ double radius, PathType path) {
+ Location leveled = from.clone();
+ leveled.setY(location.getY());
+ if (location.distanceSquared(leveled) < radius * radius) {
+ Vector vector = direction.clone().setY(0.0D);
+ if (vector.getX() == 0.0D && vector.getZ() == 0.0D) {
+ vector.setX(0.001D);
+ }
+ vector.normalize().multiply(radius + 2.0D);
+ return getViewingVector(location, leveled.add(vector), direction, radius, path);
+ }
+
+ Vector vector;
+ switch (path) {
+ case SQUARE:
+ Vector axis = location.clone().add(1.0D, 0.0D, 0.0D).toVector()
+ .subtract(location.toVector()).normalize();
+ vector = leveled.toVector().subtract(location.toVector()).normalize();
+ double rawAngle = Math.abs(axis.angle(vector));
+ double angle = rawAngle % FORTY_FIVE_DEGREES;
+ if (rawAngle % RIGHT_ANGLE > FORTY_FIVE_DEGREES) {
+ angle = FORTY_FIVE_DEGREES - angle;
+ }
+ vector.multiply(radius / Math.cos(angle));
+ break;
+ case CIRCLE:
+ vector = leveled.toVector().subtract(location.toVector()).normalize().multiply(radius);
+ break;
+ case FACE:
+ default:
+ Vector facing = leveled.toVector().subtract(location.toVector()).normalize();
+ Location origin = location.clone().setDirection(facing);
+ origin.setYaw(getCardinalDirection(origin));
+ vector = origin.getDirection().normalize().multiply(radius);
+ break;
+ }
+ return vector;
+ }
+
+ static float getCardinalDirection(Location location) {
+ double rotation = (location.getYaw() - 90.0F) % 360.0F;
+ if (rotation < 0.0D) {
+ rotation += 360.0D;
+ }
+ if (rotation < 45.0D) {
+ return 90.0F;
+ }
+ if (rotation < 135.0D) {
+ return 180.0F;
+ }
+ if (rotation < 225.0D) {
+ return -90.0F;
+ }
+ if (rotation < 315.0D) {
+ return 0.0F;
+ }
+ return 90.0F;
}
@Override
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/DisplayEntity.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/DisplayEntity.java
index b99457c0..c5429f01 100644
--- a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/DisplayEntity.java
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/DisplayEntity.java
@@ -12,9 +12,8 @@
package com.loohp.interactionvisualizer.entityholders;
-import com.loohp.interactionvisualizer.utils.ComponentFont;
+import com.loohp.interactionvisualizer.utils.LegacyTextComponentCache;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Display;
@@ -38,10 +37,18 @@ public class DisplayEntity extends VisualizerEntity {
private EulerAngle headPose;
private ItemStack helmet;
private ItemStack mainHand;
+ private boolean legacyRightHandItemTransform;
+ private boolean legacyNameTagGeometry;
+ private boolean legacyNameTagStyle;
private Component customName;
+ private String customNameRawSource;
+ private boolean customNameRawSourceKnown;
private boolean customNameVisible;
+ private boolean defaultBackground;
private Vector velocity;
private Display.Billboard billboard;
+ private float textScale;
+ private boolean unboundedTextWidth;
private float viewRange;
private int interpolationDuration;
private int teleportDuration;
@@ -55,8 +62,14 @@ public DisplayEntity(Location location) {
this.headPose = EulerAngle.ZERO;
this.helmet = ItemStack.empty();
this.mainHand = ItemStack.empty();
+ this.legacyRightHandItemTransform = false;
+ this.legacyNameTagGeometry = false;
+ this.legacyNameTagStyle = false;
+ this.defaultBackground = false;
this.velocity = new Vector();
this.billboard = Display.Billboard.FIXED;
+ this.textScale = 0.5F;
+ this.unboundedTextWidth = false;
this.viewRange = 1.0F;
this.interpolationDuration = 3;
this.teleportDuration = 3;
@@ -67,15 +80,46 @@ public Component getCustomName() {
}
public void setCustomName(String customName) {
- setCustomName(customName == null ? null : ComponentFont.parseFont(
- LegacyComponentSerializer.legacySection().deserialize(customName)));
+ updateCustomName(customName);
}
public void setCustomName(Component customName) {
+ customNameRawSource = null;
+ customNameRawSourceKnown = false;
+ assignCustomName(customName);
+ }
+
+ public boolean updateCustomName(String customName) {
+ if (LegacyTextComponentCache.isEnabled() && customNameRawSourceKnown
+ && java.util.Objects.equals(customNameRawSource, customName)) {
+ if (customName != null) {
+ LegacyTextComponentCache.recordSameRawFastPath();
+ }
+ return false;
+ }
+ Component parsed = customName == null ? null : LegacyTextComponentCache.parse(customName);
+ boolean changed = assignCustomName(parsed);
+ customNameRawSource = LegacyTextComponentCache.isEnabled() ? customName : null;
+ customNameRawSourceKnown = LegacyTextComponentCache.isEnabled();
+ return changed;
+ }
+
+ public boolean updateCustomName(String customName, boolean visible) {
+ boolean changed = updateCustomName(customName);
+ if (customNameVisible != visible) {
+ setCustomNameVisible(visible);
+ return true;
+ }
+ return changed;
+ }
+
+ private boolean assignCustomName(Component customName) {
if (!java.util.Objects.equals(this.customName, customName)) {
this.customName = customName;
markDirty();
+ return true;
}
+ return false;
}
public boolean isCustomNameVisible() {
@@ -89,8 +133,25 @@ public void setCustomNameVisible(boolean visible) {
}
}
+ public boolean isDefaultBackground() {
+ return defaultBackground;
+ }
+
+ public void setDefaultBackground(boolean defaultBackground) {
+ if (this.defaultBackground != defaultBackground) {
+ this.defaultBackground = defaultBackground;
+ markDirty();
+ }
+ }
+
public boolean isTextDisplay() {
- return customNameVisible;
+ /*
+ * Legacy name-tag holders remain text-shaped even while their name is
+ * temporarily hidden. Otherwise a lectern (and every other toggled
+ * hologram) is first created as an empty ItemDisplay and must be
+ * destroyed/re-spawned as a TextDisplay when its text becomes visible.
+ */
+ return legacyNameTagGeometry || customNameVisible;
}
public ItemStack getDisplayItem() {
@@ -99,7 +160,65 @@ public ItemStack getDisplayItem() {
}
public ItemDisplay.ItemDisplayTransform getItemDisplayTransform() {
- return helmet.isEmpty() ? ItemDisplay.ItemDisplayTransform.FIXED : ItemDisplay.ItemDisplayTransform.HEAD;
+ if (helmet != null && !helmet.isEmpty()) {
+ return ItemDisplay.ItemDisplayTransform.HEAD;
+ }
+ return usesLegacyRightHandItemTransform()
+ ? ItemDisplay.ItemDisplayTransform.THIRDPERSON_RIGHTHAND
+ : ItemDisplay.ItemDisplayTransform.FIXED;
+ }
+
+ /**
+ * The public item-holding API still exposes its historical stand-shaped
+ * state. An explicit profile flag distinguishes that compatibility surface
+ * from ordinary fixed ItemDisplays without parsing its reserved custom name
+ * on every refresh.
+ */
+ public boolean usesLegacyRightHandItemTransform() {
+ return legacyRightHandItemTransform && (helmet == null || helmet.isEmpty());
+ }
+
+ public void setLegacyRightHandItemTransform(boolean legacyRightHandItemTransform) {
+ if (!lock && this.legacyRightHandItemTransform != legacyRightHandItemTransform) {
+ this.legacyRightHandItemTransform = legacyRightHandItemTransform;
+ markDirty();
+ }
+ }
+
+ /**
+ * Marks this text as a replacement for a visible custom name on the
+ * legacy marker entity. The client renders TextDisplay text from a
+ * different origin, so the renderer also uses this marker to apply the
+ * exact name-tag anchor compensation.
+ */
+ public boolean usesLegacyNameTagStyle() {
+ return legacyNameTagStyle;
+ }
+
+ /**
+ * Restores only the legacy name tag's 1:1 glyph size and attachment
+ * origin, leaving background and billboard choices untouched.
+ */
+ public boolean usesLegacyNameTagGeometry() {
+ return legacyNameTagGeometry;
+ }
+
+ public void useLegacyNameTagGeometry() {
+ if (!legacyNameTagGeometry) {
+ legacyNameTagGeometry = true;
+ markDirty();
+ }
+ setTextScale(1.0F);
+ }
+
+ public void useLegacyNameTagStyle() {
+ useLegacyNameTagGeometry();
+ if (!legacyNameTagStyle) {
+ legacyNameTagStyle = true;
+ markDirty();
+ }
+ setDefaultBackground(true);
+ setBillboard(Display.Billboard.CENTER);
}
public void setItemInMainHand(ItemStack item) {
@@ -215,6 +334,29 @@ public void setBillboard(Display.Billboard billboard) {
}
}
+ public float getTextScale() {
+ return textScale;
+ }
+
+ public void setTextScale(float textScale) {
+ float value = Float.isFinite(textScale) ? Math.max(0.0F, textScale) : 0.5F;
+ if (this.textScale != value) {
+ this.textScale = value;
+ markDirty();
+ }
+ }
+
+ public boolean usesUnboundedTextWidth() {
+ return unboundedTextWidth || legacyNameTagStyle;
+ }
+
+ public void setUnboundedTextWidth(boolean unboundedTextWidth) {
+ if (this.unboundedTextWidth != unboundedTextWidth) {
+ this.unboundedTextWidth = unboundedTextWidth;
+ markDirty();
+ }
+ }
+
public float getViewRange() {
return viewRange;
}
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/Item.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/Item.java
index 5f4fac9d..1fc7aba3 100644
--- a/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/Item.java
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/entityholders/Item.java
@@ -12,9 +12,8 @@
package com.loohp.interactionvisualizer.entityholders;
-import com.loohp.interactionvisualizer.utils.ComponentFont;
+import com.loohp.interactionvisualizer.utils.LegacyTextComponentCache;
import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
@@ -26,35 +25,124 @@
*/
public class Item extends VisualizerEntity {
+ /**
+ * Rendering presentation for one logical item.
+ *
+ *
{@link #DROPPED} preserves the Furnace/Sparrow fake-item path. The
+ * remaining modes use a fixed Paper ItemDisplay while sharing the same
+ * logical lifecycle, viewer filtering, updates, and removal API.
+ */
+ public enum RenderMode {
+ DROPPED,
+ ITEM,
+ BLOCK,
+ LOW_BLOCK,
+ TOOL,
+ STANDING,
+ BANNER,
+ FRAME;
+
+ public boolean isFixedDisplay() {
+ return this != DROPPED;
+ }
+ }
+
private ItemStack item;
+ private RenderMode renderMode;
+ private int frameRotation;
private boolean gravity;
private boolean glowing;
private int pickupDelay;
private Component customName;
+ private String customNameRawSource;
+ private boolean customNameRawSourceKnown;
private boolean customNameVisible;
private Vector velocity;
public Item(Location location) {
+ this(location, RenderMode.DROPPED);
+ }
+
+ public Item(Location location, RenderMode renderMode) {
super(location);
- this.item = ItemStack.of(Material.STONE);
+ this.renderMode = java.util.Objects.requireNonNull(renderMode, "renderMode");
+ this.item = renderMode.isFixedDisplay() ? ItemStack.empty() : ItemStack.of(Material.STONE);
this.gravity = false;
this.velocity = new Vector();
}
+ public RenderMode getRenderMode() {
+ return renderMode;
+ }
+
+ public void setRenderMode(RenderMode renderMode) {
+ if (lock) {
+ return;
+ }
+ RenderMode value = java.util.Objects.requireNonNull(renderMode, "renderMode");
+ if (this.renderMode != value) {
+ this.renderMode = value;
+ if (!value.isFixedDisplay() && item.isEmpty()) {
+ item = ItemStack.of(Material.STONE);
+ }
+ markDirty();
+ }
+ }
+
+ public boolean isFixedDisplay() {
+ return renderMode.isFixedDisplay();
+ }
+
+ public int getFrameRotation() {
+ return frameRotation;
+ }
+
+ public void setFrameRotation(int frameRotation) {
+ if (frameRotation < 0 || frameRotation > 7) {
+ throw new IllegalArgumentException("Item frame rotation must be between 0 and 7");
+ }
+ if (!lock && this.frameRotation != frameRotation) {
+ this.frameRotation = frameRotation;
+ markDirty();
+ }
+ }
+
public Component getCustomName() {
return customName;
}
public void setCustomName(String name) {
- setCustomName(name == null ? null : ComponentFont.parseFont(
- LegacyComponentSerializer.legacySection().deserialize(name)));
+ updateCustomName(name);
}
public void setCustomName(Component name) {
+ customNameRawSource = null;
+ customNameRawSourceKnown = false;
+ assignCustomName(name);
+ }
+
+ public boolean updateCustomName(String name) {
+ if (LegacyTextComponentCache.isEnabled() && customNameRawSourceKnown
+ && java.util.Objects.equals(customNameRawSource, name)) {
+ if (name != null) {
+ LegacyTextComponentCache.recordSameRawFastPath();
+ }
+ return false;
+ }
+ Component parsed = name == null ? null : LegacyTextComponentCache.parse(name);
+ boolean changed = assignCustomName(parsed);
+ customNameRawSource = LegacyTextComponentCache.isEnabled() ? name : null;
+ customNameRawSourceKnown = LegacyTextComponentCache.isEnabled();
+ return changed;
+ }
+
+ private boolean assignCustomName(Component name) {
if (!java.util.Objects.equals(customName, name)) {
customName = name;
markDirty();
+ return true;
}
+ return false;
}
public boolean isGlowing() {
@@ -92,7 +180,9 @@ public void setItemStack(ItemStack item) {
}
private void setItemInternal(ItemStack value) {
- ItemStack normalized = value == null || value.isEmpty() ? ItemStack.of(Material.STONE) : value.clone();
+ ItemStack normalized = value == null || value.isEmpty()
+ ? (renderMode.isFixedDisplay() ? ItemStack.empty() : ItemStack.of(Material.STONE))
+ : value.clone();
if (!item.equals(normalized)) {
item = normalized;
markDirty();
@@ -140,6 +230,7 @@ public void setPickupDelay(int pickupDelay) {
@Override
public double getHeight() {
- return 0.25;
+ return renderMode == RenderMode.BANNER ? 1.5
+ : (renderMode == RenderMode.FRAME ? 0.75 : (renderMode.isFixedDisplay() ? 0.5 : 0.25));
}
}
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/ChunkPosition.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/ChunkPosition.java
index dacee402..0a56472c 100644
--- a/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/ChunkPosition.java
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/ChunkPosition.java
@@ -61,10 +61,13 @@ public int getChunkZ() {
@Override
public boolean equals(Object object) {
- if (!(object instanceof ChunkPosition)) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof ChunkPosition other)) {
return false;
}
- return hashCode() == object.hashCode();
+ return x == other.x && z == other.z && world.equals(other.world);
}
@Override
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/SynchronizedFilteredCollection.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/SynchronizedFilteredCollection.java
index cdf9540f..b93a2942 100644
--- a/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/SynchronizedFilteredCollection.java
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/objectholders/SynchronizedFilteredCollection.java
@@ -21,7 +21,9 @@
package com.loohp.interactionvisualizer.objectholders;
import java.lang.reflect.Array;
+import java.util.AbstractCollection;
import java.util.Collection;
+import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
@@ -51,6 +53,39 @@ public static SynchronizedFilteredCollection from(Collection backingCo
return new SynchronizedFilteredCollection(backingCollection, e -> true);
}
+ /**
+ * Returns a read-only collection that preserves the snapshot-free
+ * {@link #anyMatch(Collection, Predicate)} path when the backing collection
+ * is a synchronized filtered view.
+ */
+ public static Collection unmodifiableCollection(Collection backingCollection) {
+ return new ReadOnlyCollection<>(Objects.requireNonNull(backingCollection, "backingCollection"));
+ }
+
+ /**
+ * Tests a collection without materializing an iterator snapshot when it is
+ * one of this class's filtered or read-only views. The matcher must be a
+ * side-effect-free read: mutating the same collection from the matcher would
+ * attempt a read-to-write lock upgrade.
+ */
+ @SuppressWarnings("unchecked")
+ public static boolean anyMatch(Collection collection, Predicate super E> matcher) {
+ Objects.requireNonNull(collection, "collection");
+ Objects.requireNonNull(matcher, "matcher");
+ if (collection instanceof SynchronizedFilteredCollection> filtered) {
+ return ((SynchronizedFilteredCollection) filtered).anyMatch(matcher);
+ }
+ if (collection instanceof ReadOnlyCollection> readOnly) {
+ return ((ReadOnlyCollection) readOnly).anyMatch(matcher);
+ }
+ for (E element : collection) {
+ if (matcher.test(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private final Collection backingCollection;
private final Predicate predicate;
private final ReentrantReadWriteLock lock;
@@ -73,6 +108,34 @@ public ReentrantReadWriteLock getLock() {
return lock;
}
+ /**
+ * Tests the live filtered view while holding the shared read lock, without
+ * materializing the snapshot used by {@link #iterator()} and {@link #stream()}.
+ */
+ public boolean anyMatch(Predicate super E> matcher) {
+ Objects.requireNonNull(matcher, "matcher");
+ try {
+ lock.readLock().lock();
+ return anyMatchUnlocked(matcher);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean anyMatchUnlocked(Predicate super E> matcher) {
+ if (backingCollection instanceof SynchronizedFilteredCollection> filtered) {
+ SynchronizedFilteredCollection nested = (SynchronizedFilteredCollection) filtered;
+ return nested.anyMatchUnlocked(element -> predicate.test(element) && matcher.test(element));
+ }
+ for (E element : backingCollection) {
+ if (predicate.test(element) && matcher.test(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public int size() {
try {
@@ -95,12 +158,7 @@ public boolean isEmpty() {
@Override
public boolean contains(Object o) {
- try {
- lock.readLock().lock();
- return backingCollection.stream().filter(predicate).anyMatch(each -> Objects.equals(each, o));
- } finally {
- lock.readLock().unlock();
- }
+ return anyMatch(each -> Objects.equals(each, o));
}
@Override
@@ -294,4 +352,84 @@ public Stream parallelStream() {
}
}
+ private static final class ReadOnlyCollection extends AbstractCollection {
+
+ private final Collection backingCollection;
+ private final Collection readOnlyDelegate;
+
+ private ReadOnlyCollection(Collection backingCollection) {
+ this.backingCollection = backingCollection;
+ this.readOnlyDelegate = Collections.unmodifiableCollection(backingCollection);
+ }
+
+ private boolean anyMatch(Predicate super E> matcher) {
+ return SynchronizedFilteredCollection.anyMatch(backingCollection, matcher);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return readOnlyDelegate.iterator();
+ }
+
+ @Override
+ public int size() {
+ return readOnlyDelegate.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return readOnlyDelegate.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ return readOnlyDelegate.contains(object);
+ }
+
+ @Override
+ public Object[] toArray() {
+ return readOnlyDelegate.toArray();
+ }
+
+ @Override
+ public T[] toArray(T[] array) {
+ return readOnlyDelegate.toArray(array);
+ }
+
+ @Override
+ public boolean add(E element) {
+ return readOnlyDelegate.add(element);
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ return readOnlyDelegate.remove(object);
+ }
+
+ @Override
+ public boolean addAll(Collection extends E> collection) {
+ return readOnlyDelegate.addAll(collection);
+ }
+
+ @Override
+ public boolean removeAll(Collection> collection) {
+ return readOnlyDelegate.removeAll(collection);
+ }
+
+ @Override
+ public boolean retainAll(Collection> collection) {
+ return readOnlyDelegate.retainAll(collection);
+ }
+
+ @Override
+ public boolean removeIf(Predicate super E> filter) {
+ return readOnlyDelegate.removeIf(filter);
+ }
+
+ @Override
+ public void clear() {
+ readOnlyDelegate.clear();
+ }
+ }
+
}
diff --git a/abstraction/src/main/java/com/loohp/interactionvisualizer/utils/LegacyTextComponentCache.java b/abstraction/src/main/java/com/loohp/interactionvisualizer/utils/LegacyTextComponentCache.java
new file mode 100644
index 00000000..be9e6aa0
--- /dev/null
+++ b/abstraction/src/main/java/com/loohp/interactionvisualizer/utils/LegacyTextComponentCache.java
@@ -0,0 +1,150 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.utils;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.atomic.LongAdder;
+
+/**
+ * Shared, bounded conversion cache for legacy display text.
+ *
+ * Progress displays tend to move through a small set of synchronized text
+ * states across hundreds of entities. Caching the immutable Adventure component
+ * keeps the exact legacy and {@code [font=...]} parsing semantics while avoiding
+ * the same flatten/compact work for every entity. Very large one-off strings
+ * bypass the cache, and the maximum size bounds all remaining key cardinality.
+ */
+public final class LegacyTextComponentCache {
+
+ public static final int MAXIMUM_SIZE = 1_024;
+ public static final int MAXIMUM_CACHEABLE_LENGTH = 2_048;
+ public static final int ENTRY_BASE_WEIGHT = 256;
+ public static final long MAXIMUM_WEIGHT = (long) MAXIMUM_SIZE * ENTRY_BASE_WEIGHT;
+ public static final String DISABLE_PROPERTY = "interactionvisualizer.disableLegacyTextComponentCache";
+
+ private static final boolean ENABLED = !Boolean.getBoolean(DISABLE_PROPERTY);
+ private static final Cache CACHE = Caffeine.newBuilder()
+ // The base weight preserves the 1,024-entry ceiling; raw length also
+ // prevents a small number of style-heavy strings from owning it.
+ .maximumWeight(MAXIMUM_WEIGHT)
+ .weigher((String rawText, Component ignored) -> ENTRY_BASE_WEIGHT + rawText.length())
+ // The cache is tiny; keep maintenance deterministic and off the JVM common pool.
+ .executor(Runnable::run)
+ .build();
+ private static final AtomicReference ACTIVE_MEASUREMENT = new AtomicReference<>();
+ private static volatile CacheMetrics latestMetrics = CacheMetrics.EMPTY;
+
+ private LegacyTextComponentCache() {
+ }
+
+ public static Component parse(String rawText) {
+ Objects.requireNonNull(rawText, "rawText");
+ Measurement measurement = ACTIVE_MEASUREMENT.get();
+ if (!ENABLED || rawText.length() > MAXIMUM_CACHEABLE_LENGTH) {
+ recordCacheOutcome(measurement, false);
+ return parseUncached(rawText);
+ }
+ Component cached = CACHE.getIfPresent(rawText);
+ if (cached != null) {
+ recordCacheOutcome(measurement, true);
+ return cached;
+ }
+ recordCacheOutcome(measurement, false);
+ return CACHE.get(rawText, LegacyTextComponentCache::parseUncached);
+ }
+
+ private static void recordCacheOutcome(Measurement measurement, boolean hit) {
+ if (measurement == null) {
+ return;
+ }
+ if (hit) {
+ measurement.hits.increment();
+ } else {
+ measurement.misses.increment();
+ }
+ }
+
+ private static Component parseUncached(String rawText) {
+ return ComponentFont.parseFont(LegacyComponentSerializer.legacySection().deserialize(rawText));
+ }
+
+ public static boolean isEnabled() {
+ return ENABLED;
+ }
+
+ public static void recordSameRawFastPath() {
+ Measurement measurement = ACTIVE_MEASUREMENT.get();
+ if (measurement != null) {
+ measurement.sameRawFastPaths.increment();
+ }
+ }
+
+ public static void startMeasurement() {
+ latestMetrics = CacheMetrics.EMPTY;
+ ACTIVE_MEASUREMENT.set(new Measurement());
+ }
+
+ public static CacheMetrics stopMeasurement() {
+ Measurement measurement = ACTIVE_MEASUREMENT.getAndSet(null);
+ if (measurement != null) {
+ latestMetrics = snapshot(measurement);
+ }
+ return latestMetrics;
+ }
+
+ public static CacheMetrics metrics() {
+ Measurement measurement = ACTIVE_MEASUREMENT.get();
+ return measurement == null ? latestMetrics : snapshot(measurement);
+ }
+
+ public static void invalidateAll() {
+ CACHE.invalidateAll();
+ CACHE.cleanUp();
+ }
+
+ static long estimatedSize() {
+ CACHE.cleanUp();
+ return CACHE.estimatedSize();
+ }
+
+ private static CacheMetrics snapshot(Measurement measurement) {
+ long hits = measurement.hits.sum();
+ long misses = measurement.misses.sum();
+ return new CacheMetrics(hits + misses, misses, measurement.sameRawFastPaths.sum());
+ }
+
+ private static final class Measurement {
+
+ private final LongAdder hits = new LongAdder();
+ private final LongAdder misses = new LongAdder();
+ private final LongAdder sameRawFastPaths = new LongAdder();
+ }
+
+ public record CacheMetrics(long requests, long misses, long sameRawFastPaths) {
+
+ private static final CacheMetrics EMPTY = new CacheMetrics(0L, 0L, 0L);
+
+ public long hits() {
+ return Math.max(0L, requests - misses);
+ }
+
+ public double hitRate() {
+ return requests == 0L ? 0.0D : (double) hits() / (double) requests;
+ }
+ }
+}
diff --git a/benchmark/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemBenchmarkPlugin.java b/benchmark/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemBenchmarkPlugin.java
new file mode 100644
index 00000000..909f4908
--- /dev/null
+++ b/benchmark/src/main/java/com/loohp/interactionvisualizer/entities/DroppedItemBenchmarkPlugin.java
@@ -0,0 +1,867 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.entities;
+
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.entity.Item;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.util.Vector;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.function.LongSupplier;
+
+/**
+ * Disposable benchmark plugin run only by the GitHub Actions performance job.
+ * It deliberately alternates A/B order and reports medians and p95 values.
+ */
+public final class DroppedItemBenchmarkPlugin extends JavaPlugin {
+
+ private static final int WARMUP_ROUNDS = 12;
+ private static final int MEASUREMENT_ROUNDS = 42;
+ private static final int VISIBILITY_SEEDS = 3;
+ private static final long SAMPLE_TARGET_NANOS = 2_000_000L;
+ private static final int MAX_SAMPLE_REPETITIONS = 4_096;
+ private static final double DEFAULT_VIEW_DISTANCE = 72.0D;
+ private static final List PRODUCTION_VISIBILITY_RANGES = List.of(64.0D, 80.0D);
+ private static final List PRODUCTION_GRID_PHASES = List.of(
+ new GridPhase("boundary", 0.0D, 0.0D),
+ new GridPhase("middle", 7.875D, 7.875D),
+ new GridPhase("near-boundary", 15.75D, 15.25D));
+ private static final UUID BENCHMARK_WORLD_ID = UUID.fromString("f17968b7-ad29-4e08-bc29-56dc57f74b90");
+ private static volatile long blackhole;
+
+ private final List results = new ArrayList<>();
+ private Path resultsOutput;
+ private Path completionOutput;
+
+ @Override
+ public void onEnable() {
+ Bukkit.getScheduler().runTask(this, () -> {
+ try {
+ runBenchmarks();
+ } catch (Throwable throwable) {
+ getLogger().severe("A/B benchmark failed: " + throwable.getMessage());
+ throwable.printStackTrace();
+ } finally {
+ Bukkit.shutdown();
+ }
+ });
+ }
+
+ private void runBenchmarks() throws IOException {
+ World world = Bukkit.getWorlds().getFirst();
+ world.setAutoSave(false);
+ world.getEntitiesByClass(Item.class).forEach(Item::remove);
+ Files.createDirectories(getDataFolder().toPath());
+ resultsOutput = getDataFolder().toPath().resolve("benchmark-results.jsonl");
+ completionOutput = getDataFolder().toPath().resolve("benchmark-complete.txt");
+ Files.writeString(resultsOutput, "");
+ Files.deleteIfExists(completionOutput);
+ assertDeterministicProductionRangePaths();
+
+ for (int itemCount : List.of(250, 1000, 2500)) {
+ benchmarkCramping(world, itemCount, false);
+ benchmarkCramping(world, itemCount, true);
+ }
+ for (String distribution : List.of("uniform", "hotspot", "no-hit", "enclosed-no-hit",
+ "diagonal-no-hit", "late-hit")) {
+ for (int itemCount : List.of(500, 2000, 8000)) {
+ for (int viewerCount : List.of(1, 8, 32, 64, 96, 128, 191, 192, 256, 384, 512, 768, 1024)) {
+ for (int seed = 0; seed < VISIBILITY_SEEDS; seed++) {
+ benchmarkVisibility(itemCount, viewerCount, distribution, seed);
+ }
+ }
+ }
+ }
+ for (double range : PRODUCTION_VISIBILITY_RANGES) {
+ for (String distribution : List.of("diagonal-no-hit", "late-hit")) {
+ for (int viewerCount : List.of(127, 128, 191, 192, 256)) {
+ for (GridPhase phase : PRODUCTION_GRID_PHASES) {
+ benchmarkVisibility(2000, viewerCount, distribution, 0,
+ range, phase, true);
+ }
+ }
+ }
+ }
+ for (GridPhase phase : PRODUCTION_GRID_PHASES) {
+ for (int seed = 0; seed < VISIBILITY_SEEDS; seed++) {
+ for (int itemCount : List.of(500, 8000)) {
+ benchmarkVisibility(itemCount, 128, "late-hit", seed,
+ 64.0D, phase, true);
+ benchmarkVisibility(itemCount, 191, "late-hit", seed,
+ 80.0D, phase, true);
+ }
+ }
+ for (double range : PRODUCTION_VISIBILITY_RANGES) {
+ benchmarkVisibility(8000, 191, "uniform", 0,
+ range, phase, true);
+ }
+ }
+ for (int pending : List.of(100, 500, 2000, 8000)) {
+ reportTokenBucket(pending, 128, 32);
+ }
+
+ Files.writeString(completionOutput, Integer.toString(results.size()));
+ getLogger().info("Wrote " + results.size() + " A/B results to " + resultsOutput.toAbsolutePath());
+ }
+
+ private void benchmarkCramping(World world, int itemCount, boolean clustered) {
+ List- items = spawnItems(world, itemCount, clustered);
+ UUID worldId = world.getUID();
+ LongSupplier baseline = () -> paperNearbyQueries(items);
+ LongSupplier candidate = () -> indexedNearbyQueries(items, worldId);
+ Comparison comparison = compare(baseline, candidate);
+ String result = String.format(Locale.ROOT,
+ "{\"benchmark\":\"cramping\",\"distribution\":\"%s\",\"items\":%d," +
+ "\"baselineMedianNs\":%d,\"baselineP95Ns\":%d," +
+ "\"candidateMedianNs\":%d,\"candidateP95Ns\":%d,\"speedup\":%.3f}",
+ clustered ? "clustered" : "sparse", itemCount,
+ comparison.baseline().median(), comparison.baseline().p95(),
+ comparison.candidate().median(), comparison.candidate().p95(), comparison.speedup());
+ report(result);
+ items.forEach(Item::remove);
+ }
+
+ private List
- spawnItems(World world, int itemCount, boolean clustered) {
+ int logicalCount = clustered ? (itemCount + 7) / 8 : itemCount;
+ int columns = (int) Math.ceil(Math.sqrt(logicalCount));
+ double spacing = clustered ? 2.0D : 1.25D;
+ List
- items = new ArrayList<>(itemCount);
+ for (int index = 0; index < itemCount; index++) {
+ int logicalIndex = clustered ? index / 8 : index;
+ double baseX = 8.0D + (logicalIndex % columns) * spacing;
+ double baseZ = 8.0D + (logicalIndex / columns) * spacing;
+ int clusterIndex = clustered ? index % 8 : 0;
+ double x = baseX + (clusterIndex & 1) * 0.12D;
+ double y = 80.0D + ((clusterIndex >>> 1) & 1) * 0.12D;
+ double z = baseZ + ((clusterIndex >>> 2) & 1) * 0.12D;
+ world.getChunkAt((int) Math.floor(x) >> 4, (int) Math.floor(z) >> 4).load();
+ ItemStack stack = new ItemStack(Material.STONE);
+ int itemId = index;
+ stack.editMeta(meta -> meta.customName(Component.text("iv-benchmark-" + itemId)));
+ Item item = world.dropItem(new Location(world, x, y, z), stack, entity -> {
+ entity.setGravity(false);
+ entity.setVelocity(new Vector());
+ entity.setPickupDelay(Integer.MAX_VALUE);
+ entity.setWillAge(false);
+ });
+ items.add(item);
+ }
+ return items;
+ }
+
+ private static long paperNearbyQueries(List
- items) {
+ long cramped = 0;
+ for (Item item : items) {
+ if (item.getWorld().getNearbyEntitiesByType(Item.class, item.getLocation(), 0.5D, 0.5D, 0.5D)
+ .stream().limit(7L).count() > 6L) {
+ cramped++;
+ }
+ }
+ blackhole = cramped;
+ return cramped;
+ }
+
+ private static long indexedNearbyQueries(List
- items, UUID worldId) {
+ DroppedItemSpatialIndex index = new DroppedItemSpatialIndex();
+ List locations = new ArrayList<>(items.size());
+ for (Item item : items) {
+ Location location = item.getLocation();
+ locations.add(location);
+ index.addItem(worldId, location.getX(), location.getY(), location.getZ());
+ }
+ long cramped = 0;
+ for (Location location : locations) {
+ if (index.exceedsItemLimit(worldId, location.getX(), location.getY(), location.getZ(), 6)) {
+ cramped++;
+ }
+ }
+ blackhole = cramped;
+ return cramped;
+ }
+
+ private void benchmarkVisibility(int itemCount, int viewerCount, String distribution, int seed) {
+ benchmarkVisibility(itemCount, viewerCount, distribution, seed,
+ DEFAULT_VIEW_DISTANCE, new GridPhase("default", 0.0D, 0.0D), false);
+ }
+
+ private void benchmarkVisibility(int itemCount, int viewerCount, String distribution, int seed,
+ double range, GridPhase gridPhase, boolean productionRangeProbe) {
+ Random random = new Random(0x51A71A1L + itemCount * 31L + viewerCount * 17L
+ + distribution.hashCode() * 13L + seed * 0x9E3779B9L);
+ List items;
+ List viewers;
+ switch (distribution) {
+ case "uniform" -> {
+ items = randomPoints(random, itemCount, 0.0D, 1024.0D);
+ viewers = randomPoints(random, viewerCount, 0.0D, 1024.0D);
+ }
+ case "hotspot" -> {
+ items = randomPoints(random, itemCount, 448.0D, 128.0D);
+ viewers = randomPoints(random, viewerCount, 448.0D, 128.0D);
+ }
+ case "no-hit" -> {
+ items = randomPoints(random, itemCount, 0.0D, 256.0D);
+ viewers = randomPoints(random, viewerCount, 768.0D, 256.0D);
+ }
+ case "enclosed-no-hit" -> {
+ items = randomPoints(random, itemCount, 448.0D, 128.0D);
+ viewers = enclosingRingPoints(random, viewerCount);
+ }
+ case "diagonal-no-hit" -> {
+ items = centralPoints(random, itemCount, 4.0D);
+ viewers = diagonalMissPoints(random, viewerCount, range);
+ }
+ case "late-hit" -> {
+ items = centralPoints(random, itemCount, 4.0D);
+ viewers = lateHitPoints(random, viewerCount);
+ }
+ default -> throw new IllegalArgumentException("Unknown visibility distribution: " + distribution);
+ }
+ if (gridPhase.x() != 0.0D || gridPhase.z() != 0.0D) {
+ items = translatePoints(items, gridPhase.x(), gridPhase.z());
+ viewers = translatePoints(viewers, gridPhase.x(), gridPhase.z());
+ }
+ List benchmarkItems = items;
+ List benchmarkViewers = viewers;
+ LongSupplier linearReference = () -> linearActiveLabels(benchmarkItems, benchmarkViewers, range);
+ LongSupplier legacyProduction = () -> legacyGridActiveLabels(benchmarkItems, benchmarkViewers, range);
+ LongSupplier primitiveControl = () -> primitiveControlActiveLabels(benchmarkItems, benchmarkViewers, range);
+ LongSupplier candidate = () -> indexedActiveLabels(benchmarkItems, benchmarkViewers, range);
+ long expectedLabels = linearReference.getAsLong();
+ long legacyLabels = legacyProduction.getAsLong();
+ long controlLabels = primitiveControl.getAsLong();
+ long candidateLabels = candidate.getAsLong();
+ if (expectedLabels != legacyLabels || expectedLabels != controlLabels || expectedLabels != candidateLabels) {
+ throw new IllegalStateException("Visibility A/B mismatch: linear=" + expectedLabels
+ + ", legacy=" + legacyLabels + ", control=" + controlLabels
+ + ", candidate=" + candidateLabels);
+ }
+ if ((distribution.endsWith("no-hit") && expectedLabels != 0L)
+ || (distribution.equals("late-hit") && expectedLabels != itemCount)) {
+ throw new IllegalStateException("Invalid " + distribution + " fixture: active=" + expectedLabels);
+ }
+ int startingPermutation = Math.floorMod(itemCount * 31 + viewerCount * 17 + seed, 6);
+ ThreeWayComparison comparisons = compareThree(
+ legacyProduction, primitiveControl, candidate, startingPermutation);
+ AdaptivePathProbe pathProbe = probeAdaptivePaths(benchmarkItems, benchmarkViewers, range);
+ if (pathProbe.activeLabels() != expectedLabels) {
+ throw new IllegalStateException("Adaptive path probe mismatch: expected=" + expectedLabels
+ + ", probe=" + pathProbe.activeLabels());
+ }
+ if (productionRangeProbe) {
+ boolean mustRejectGrid = distribution.equals("diagonal-no-hit")
+ || distribution.equals("late-hit") && (viewerCount < 128
+ || range == 80.0D && viewerCount == 128);
+ if (mustRejectGrid && pathProbe.gridBuilt()) {
+ throw new IllegalStateException("Unexpected production-range grid build: range=" + range
+ + ", phase=" + gridPhase.name() + ", distribution=" + distribution
+ + ", viewers=" + viewerCount);
+ }
+ }
+ double reduction = itemCount == 0 ? 0.0D : 100.0D * (itemCount - expectedLabels) / itemCount;
+ reportVisibilityComparison(productionRangeProbe
+ ? "visibility-production-range-ab" : "visibility-production-ab",
+ "legacy-production-viewer-grid", true,
+ distribution, seed, itemCount, viewerCount, comparisons.legacyCandidate(),
+ pathProbe, expectedLabels, reduction, range, gridPhase);
+ reportVisibilityComparison(productionRangeProbe
+ ? "visibility-adaptive-range-control-ab" : "visibility-adaptive-control-ab",
+ "pure-primitive-soa-control", false,
+ distribution, seed, itemCount, viewerCount, comparisons.controlCandidate(),
+ pathProbe, expectedLabels, reduction, range, gridPhase);
+ }
+
+ private void reportVisibilityComparison(String benchmark, String baseline, boolean baselineUsesGrid,
+ String distribution, int seed, int itemCount, int viewerCount,
+ Comparison comparison, AdaptivePathProbe pathProbe,
+ long activeLabels, double reduction, double range,
+ GridPhase gridPhase) {
+ String result = String.format(Locale.ROOT,
+ "{\"benchmark\":\"%s\",\"distribution\":\"%s\",\"seed\":%d," +
+ "\"items\":%d,\"viewers\":%d," +
+ "\"range\":%.1f,\"gridPhase\":\"%s\"," +
+ "\"gridPhaseX\":%.3f,\"gridPhaseZ\":%.3f," +
+ "\"baseline\":\"%s\",\"baselineUsesGrid\":%b," +
+ "\"baselineUsesBounds\":false,\"sharedCandidateSample\":true," +
+ "\"candidate\":\"production-adaptive-primitive-viewer-index\"," +
+ "\"candidateStorage\":\"primitive-soa\"," +
+ "\"indexRebuiltPerOperation\":true,\"candidateUsesGrid\":true," +
+ "\"candidateUsesBounds\":true,\"candidateBuildsIndexesLazily\":true," +
+ "\"probeGridBuilt\":%b,\"probeGridActiveAtEnd\":%b," +
+ "\"probeBoundsActiveAtEnd\":%b," +
+ "\"sampleTargetNs\":%d,\"warmupRounds\":%d,\"measurementRounds\":%d," +
+ "\"roundOrder\":\"%s\"," +
+ "\"baselineMedianNs\":%d,\"baselineP95Ns\":%d," +
+ "\"candidateMedianNs\":%d,\"candidateP95Ns\":%d,\"speedup\":%.3f," +
+ "\"baselineFirstBaselineMedianNs\":%d," +
+ "\"baselineFirstCandidateMedianNs\":%d," +
+ "\"baselineFirstBaselineP95Ns\":%d," +
+ "\"baselineFirstCandidateP95Ns\":%d," +
+ "\"baselineFirstSpeedup\":%.3f," +
+ "\"candidateFirstBaselineMedianNs\":%d," +
+ "\"candidateFirstCandidateMedianNs\":%d," +
+ "\"candidateFirstBaselineP95Ns\":%d," +
+ "\"candidateFirstCandidateP95Ns\":%d," +
+ "\"candidateFirstSpeedup\":%.3f," +
+ "\"baselineRoundsNs\":%s,\"candidateRoundsNs\":%s," +
+ "\"allLabels\":%d,\"activeLabels\":%d," +
+ "\"labelReductionPct\":%.3f}",
+ benchmark, distribution, seed, itemCount, viewerCount, range,
+ gridPhase.name(), gridPhase.x(), gridPhase.z(), baseline, baselineUsesGrid,
+ pathProbe.gridBuilt(), pathProbe.gridActiveAtEnd(), pathProbe.boundsActiveAtEnd(),
+ SAMPLE_TARGET_NANOS, WARMUP_ROUNDS, MEASUREMENT_ROUNDS, comparison.roundOrder(),
+ comparison.baseline().median(), comparison.baseline().p95(),
+ comparison.candidate().median(), comparison.candidate().p95(), comparison.speedup(),
+ comparison.baselineFirst().baseline().median(),
+ comparison.baselineFirst().candidate().median(),
+ comparison.baselineFirst().baseline().p95(), comparison.baselineFirst().candidate().p95(),
+ comparison.baselineFirst().speedup(),
+ comparison.candidateFirst().baseline().median(),
+ comparison.candidateFirst().candidate().median(),
+ comparison.candidateFirst().baseline().p95(), comparison.candidateFirst().candidate().p95(),
+ comparison.candidateFirst().speedup(),
+ Arrays.toString(comparison.baselineRoundsNs()), Arrays.toString(comparison.candidateRoundsNs()),
+ itemCount, activeLabels, reduction);
+ report(result);
+ }
+
+ private static List translatePoints(List points, double deltaX, double deltaZ) {
+ List translated = new ArrayList<>(points.size());
+ for (Point point : points) {
+ translated.add(new Point(point.id(), point.x() + deltaX, point.y(), point.z() + deltaZ));
+ }
+ return translated;
+ }
+
+ private static void assertDeterministicProductionRangePaths() {
+ for (GridPhase phase : PRODUCTION_GRID_PHASES) {
+ Random random = new Random(0x26_01_02L);
+ List items = identicalPoints(2000, 512.0D, 80.0D, 512.0D);
+ items = translatePoints(items, phase.x(), phase.z());
+
+ List showThresholdViewers = translatePoints(
+ lateHitPoints(random, 128), phase.x(), phase.z());
+ AdaptivePathProbe showThreshold = probeAdaptivePaths(
+ items, showThresholdViewers, 64.0D);
+ if (!showThreshold.gridBuilt()) {
+ throw new IllegalStateException("64-block deterministic threshold did not build grid: phase="
+ + phase.name());
+ }
+
+ List hideThresholdViewers = translatePoints(
+ lateHitPoints(random, 191), phase.x(), phase.z());
+ AdaptivePathProbe hideThreshold = probeAdaptivePaths(
+ items, hideThresholdViewers, 80.0D);
+ if (!hideThreshold.gridBuilt()) {
+ throw new IllegalStateException("80-block deterministic threshold did not build grid: phase="
+ + phase.name());
+ }
+
+ List belowThresholdViewers = translatePoints(
+ lateHitPoints(random, 127), phase.x(), phase.z());
+ if (probeAdaptivePaths(items, belowThresholdViewers, 64.0D).gridBuilt()) {
+ throw new IllegalStateException("Below-threshold deterministic probe built grid: phase="
+ + phase.name());
+ }
+ if (probeAdaptivePaths(items, showThresholdViewers, 80.0D).gridBuilt()) {
+ throw new IllegalStateException("80-block cost rejection built grid at 128 viewers: phase="
+ + phase.name());
+ }
+ }
+
+ Random random = new Random(0x26_01_02L);
+ List boundaryViewers128 = lateHitPoints(random, 128);
+ List boundaryViewers191 = lateHitPoints(random, 191);
+ List boundaryViewers192 = lateHitPoints(random, 192);
+ List boundaryViewers256 = lateHitPoints(random, 256);
+ List favorableItems = identicalPoints(2000, 516.0D, 80.0D, 516.0D);
+ List adverseItems = identicalPoints(2000, 508.0D, 80.0D, 508.0D);
+ if (!probeAdaptivePaths(favorableItems, boundaryViewers128, 64.0D).gridBuilt()) {
+ throw new IllegalStateException("64-block favorable boundary fixture did not build at 128 viewers");
+ }
+ if (probeAdaptivePaths(adverseItems, boundaryViewers128, 64.0D).gridBuilt()) {
+ throw new IllegalStateException("64-block adverse boundary fixture built at 128 viewers");
+ }
+ if (!probeAdaptivePaths(adverseItems, boundaryViewers191, 64.0D).gridBuilt()) {
+ throw new IllegalStateException("64-block adverse boundary fixture did not build at 191 viewers");
+ }
+ if (!probeAdaptivePaths(favorableItems, boundaryViewers191, 80.0D).gridBuilt()) {
+ throw new IllegalStateException("80-block favorable boundary fixture did not build at 191 viewers");
+ }
+ if (probeAdaptivePaths(adverseItems, boundaryViewers192, 80.0D).gridBuilt()) {
+ throw new IllegalStateException("80-block adverse boundary fixture built at 192 viewers");
+ }
+ if (!probeAdaptivePaths(adverseItems, boundaryViewers256, 80.0D).gridBuilt()) {
+ throw new IllegalStateException("80-block adverse boundary fixture did not build at 256 viewers");
+ }
+ }
+
+ private static List identicalPoints(int count, double x, double y, double z) {
+ List points = new ArrayList<>(count);
+ for (int index = 0; index < count; index++) {
+ points.add(new Point(index, x, y, z));
+ }
+ return points;
+ }
+
+ private static List randomPoints(Random random, int count, double offset, double width) {
+ List points = new ArrayList<>(count);
+ for (int index = 0; index < count; index++) {
+ points.add(new Point(index, offset + random.nextDouble(width), 64.0D + random.nextDouble(32.0D),
+ offset + random.nextDouble(width)));
+ }
+ return points;
+ }
+
+ /**
+ * Places viewers around an empty central region. From eight viewers onward the item region is
+ * enclosed by the viewer AABB, while every viewer remains well beyond the visibility radius.
+ * This prevents an outer-bounds fast rejection from disguising worst-case miss scans.
+ */
+ private static List enclosingRingPoints(Random random, int count) {
+ List points = new ArrayList<>(count);
+ double phase = random.nextDouble(2.0D * Math.PI);
+ for (int index = 0; index < count; index++) {
+ double angle = phase + 2.0D * Math.PI * index / Math.max(1, count);
+ points.add(new Point(index,
+ 512.0D + Math.cos(angle) * 300.0D,
+ 64.0D + random.nextDouble(32.0D),
+ 512.0D + Math.sin(angle) * 300.0D));
+ }
+ return points;
+ }
+
+ private static List centralPoints(Random random, int count, double radius) {
+ List points = new ArrayList<>(count);
+ for (int index = 0; index < count; index++) {
+ points.add(new Point(index,
+ 512.0D + random.nextDouble(-radius, radius),
+ 80.0D,
+ 512.0D + random.nextDouble(-radius, radius)));
+ }
+ return points;
+ }
+
+ private static List diagonalMissPoints(Random random, int count, double range) {
+ List points = new ArrayList<>(count);
+ double minimumOffset = range - 12.0D;
+ double maximumOffset = range - 8.0D;
+ for (int index = 0; index < count; index++) {
+ int quadrant = index & 3;
+ double xSign = (quadrant & 1) == 0 ? -1.0D : 1.0D;
+ double zSign = (quadrant & 2) == 0 ? -1.0D : 1.0D;
+ points.add(new Point(index,
+ 512.0D + xSign * random.nextDouble(minimumOffset, maximumOffset),
+ 80.0D,
+ 512.0D + zSign * random.nextDouble(minimumOffset, maximumOffset)));
+ }
+ return points;
+ }
+
+ private static List lateHitPoints(Random random, int count) {
+ if (count == 0) {
+ return List.of();
+ }
+ List points = enclosingRingPoints(random, count - 1);
+ points.add(new Point(count - 1, 512.0D, 80.0D, 512.0D));
+ return points;
+ }
+
+ private static long linearActiveLabels(List items, List viewers, double range) {
+ List viewerSnapshot = new ArrayList<>(viewers.size());
+ for (Point viewer : viewers) {
+ viewerSnapshot.add(new Point(viewer.id(), viewer.x(), viewer.y(), viewer.z()));
+ }
+ long active = 0;
+ double rangeSquared = range * range;
+ for (Point item : items) {
+ for (Point viewer : viewerSnapshot) {
+ if (item.distanceSquared(viewer) <= rangeSquared) {
+ active++;
+ break;
+ }
+ }
+ }
+ blackhole = active;
+ return active;
+ }
+
+ private static long legacyGridActiveLabels(List items, List viewers, double range) {
+ LegacyViewerIndex index = new LegacyViewerIndex();
+ for (Point viewer : viewers) {
+ index.addViewer(BENCHMARK_WORLD_ID, viewer.x(), viewer.y(), viewer.z());
+ }
+ long active = 0;
+ for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+ Point item = items.get(itemIndex);
+ if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range)) {
+ active++;
+ }
+ }
+ blackhole = active;
+ return active;
+ }
+
+ private static long indexedActiveLabels(List items, List viewers, double range) {
+ DroppedItemSpatialIndex.ViewerIndex index = createViewerIndex(viewers);
+ long active = 0;
+ for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+ Point item = items.get(itemIndex);
+ if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range,
+ items.size() - itemIndex - 1)) {
+ active++;
+ }
+ }
+ blackhole = active;
+ return active;
+ }
+
+ private static long primitiveControlActiveLabels(List items, List viewers, double range) {
+ PrimitiveViewerIndex index = new PrimitiveViewerIndex(viewers.size());
+ for (Point viewer : viewers) {
+ index.addViewer(BENCHMARK_WORLD_ID, viewer.x(), viewer.y(), viewer.z());
+ }
+ long active = 0;
+ for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+ Point item = items.get(itemIndex);
+ if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range)) {
+ active++;
+ }
+ }
+ blackhole = active;
+ return active;
+ }
+
+ private static AdaptivePathProbe probeAdaptivePaths(List items, List viewers, double range) {
+ DroppedItemSpatialIndex.ViewerIndex index = createViewerIndex(viewers);
+ long active = 0;
+ for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+ Point item = items.get(itemIndex);
+ if (index.hasViewerWithin(BENCHMARK_WORLD_ID, item.x(), item.y(), item.z(), range,
+ items.size() - itemIndex - 1)) {
+ active++;
+ }
+ }
+ return new AdaptivePathProbe(active, index.hasAdaptiveGrid(BENCHMARK_WORLD_ID),
+ index.isUsingAdaptiveGrid(BENCHMARK_WORLD_ID),
+ index.hasActiveBounds(BENCHMARK_WORLD_ID));
+ }
+
+ private static DroppedItemSpatialIndex.ViewerIndex createViewerIndex(List viewers) {
+ DroppedItemSpatialIndex.ViewerIndex index =
+ new DroppedItemSpatialIndex.ViewerIndex(viewers.size());
+ for (Point viewer : viewers) {
+ index.addViewer(BENCHMARK_WORLD_ID, viewer.x(), viewer.y(), viewer.z());
+ }
+ return index;
+ }
+
+ private void reportTokenBucket(int pending, int capacity, int refill) {
+ int firstTick = Math.min(pending, capacity);
+ int remaining = Math.max(0, pending - firstTick);
+ int convergenceTicks = pending == 0 ? 0 : 1 + (remaining + refill - 1) / refill;
+ double peakReduction = pending == 0 ? 0.0D : 100.0D * (pending - firstTick) / pending;
+ report(String.format(Locale.ROOT,
+ "{\"benchmark\":\"visibility-burst\",\"pending\":%d," +
+ "\"baselinePeakOpsPerTick\":%d,\"candidatePeakOpsPerTick\":%d," +
+ "\"peakReductionPct\":%.3f,\"convergenceTicks\":%d}",
+ pending, pending, firstTick, peakReduction, convergenceTicks));
+ }
+
+ private static Comparison compare(LongSupplier baseline, LongSupplier candidate) {
+ return compare(baseline, candidate, true);
+ }
+
+ private static Comparison compare(LongSupplier baseline, LongSupplier candidate, boolean baselineStarts) {
+ for (int round = 0; round < WARMUP_ROUNDS; round++) {
+ boolean baselineFirst = ((round & 1) == 0) == baselineStarts;
+ if (baselineFirst) {
+ time(baseline);
+ time(candidate);
+ } else {
+ time(candidate);
+ time(baseline);
+ }
+ }
+ long[] baselineTimes = new long[MEASUREMENT_ROUNDS];
+ long[] candidateTimes = new long[MEASUREMENT_ROUNDS];
+ boolean[] baselineFirstRounds = new boolean[MEASUREMENT_ROUNDS];
+ for (int round = 0; round < MEASUREMENT_ROUNDS; round++) {
+ boolean baselineFirst = ((round & 1) == 0) == baselineStarts;
+ baselineFirstRounds[round] = baselineFirst;
+ if (baselineFirst) {
+ baselineTimes[round] = time(baseline);
+ candidateTimes[round] = time(candidate);
+ } else {
+ candidateTimes[round] = time(candidate);
+ baselineTimes[round] = time(baseline);
+ }
+ }
+ Samples baselineSamples = Samples.of(baselineTimes);
+ Samples candidateSamples = Samples.of(candidateTimes);
+ return new Comparison(baselineSamples, candidateSamples,
+ OrderStratum.of(baselineTimes, candidateTimes, baselineFirstRounds, true),
+ OrderStratum.of(baselineTimes, candidateTimes, baselineFirstRounds, false),
+ baselineStarts ? "ABBA" : "BAAB", baselineTimes, candidateTimes,
+ (double) baselineSamples.median() / Math.max(1L, candidateSamples.median()));
+ }
+
+ private static ThreeWayComparison compareThree(LongSupplier legacy, LongSupplier control,
+ LongSupplier candidate, int startingPermutation) {
+ LongSupplier[] operations = {legacy, control, candidate};
+ int[][] permutations = {
+ {0, 1, 2}, {0, 2, 1}, {1, 0, 2},
+ {1, 2, 0}, {2, 0, 1}, {2, 1, 0}
+ };
+ for (int round = 0; round < WARMUP_ROUNDS; round++) {
+ int[] order = permutations[(startingPermutation + round) % permutations.length];
+ for (int operation : order) {
+ time(operations[operation]);
+ }
+ }
+ long[][] times = new long[operations.length][MEASUREMENT_ROUNDS];
+ boolean[] legacyFirst = new boolean[MEASUREMENT_ROUNDS];
+ boolean[] controlFirst = new boolean[MEASUREMENT_ROUNDS];
+ for (int round = 0; round < MEASUREMENT_ROUNDS; round++) {
+ int[] order = permutations[(startingPermutation + round) % permutations.length];
+ int legacyPosition = 0;
+ int controlPosition = 0;
+ int candidatePosition = 0;
+ for (int position = 0; position < order.length; position++) {
+ int operation = order[position];
+ times[operation][round] = time(operations[operation]);
+ if (operation == 0) {
+ legacyPosition = position;
+ } else if (operation == 1) {
+ controlPosition = position;
+ } else {
+ candidatePosition = position;
+ }
+ }
+ legacyFirst[round] = legacyPosition < candidatePosition;
+ controlFirst[round] = controlPosition < candidatePosition;
+ }
+ String roundOrder = "BALANCED-6@" + startingPermutation;
+ return new ThreeWayComparison(
+ comparisonOf(times[0], times[2], legacyFirst, roundOrder),
+ comparisonOf(times[1], times[2], controlFirst, roundOrder));
+ }
+
+ private static Comparison comparisonOf(long[] baselineTimes, long[] candidateTimes,
+ boolean[] baselineFirstRounds, String roundOrder) {
+ Samples baselineSamples = Samples.of(baselineTimes);
+ Samples candidateSamples = Samples.of(candidateTimes);
+ return new Comparison(baselineSamples, candidateSamples,
+ OrderStratum.of(baselineTimes, candidateTimes, baselineFirstRounds, true),
+ OrderStratum.of(baselineTimes, candidateTimes, baselineFirstRounds, false),
+ roundOrder, baselineTimes, candidateTimes,
+ (double) baselineSamples.median() / Math.max(1L, candidateSamples.median()));
+ }
+
+ private static long time(LongSupplier operation) {
+ long started = System.nanoTime();
+ int repetitions = 0;
+ long elapsed;
+ do {
+ operation.getAsLong();
+ repetitions++;
+ elapsed = System.nanoTime() - started;
+ } while (elapsed < SAMPLE_TARGET_NANOS && repetitions < MAX_SAMPLE_REPETITIONS);
+ return elapsed / repetitions;
+ }
+
+ private void report(String result) {
+ results.add(result);
+ try {
+ Files.writeString(resultsOutput, result + System.lineSeparator(), StandardOpenOption.APPEND);
+ } catch (IOException exception) {
+ throw new UncheckedIOException("Unable to append benchmark result", exception);
+ }
+ getLogger().info("IV_BENCH_RESULT " + result);
+ }
+
+ private record Samples(long median, long p95) {
+
+ private static Samples of(long[] values) {
+ long[] sorted = Arrays.copyOf(values, values.length);
+ Arrays.sort(sorted);
+ return new Samples(sorted[sorted.length / 2], sorted[(int) Math.ceil(sorted.length * 0.95D) - 1]);
+ }
+ }
+
+ private record OrderStratum(Samples baseline, Samples candidate, double speedup) {
+
+ private static OrderStratum of(long[] baselineTimes, long[] candidateTimes,
+ boolean[] baselineFirstRounds, boolean baselineFirst) {
+ int size = 0;
+ for (boolean value : baselineFirstRounds) {
+ if (value == baselineFirst) {
+ size++;
+ }
+ }
+ long[] baseline = new long[size];
+ long[] candidate = new long[size];
+ int output = 0;
+ for (int index = 0; index < baselineFirstRounds.length; index++) {
+ if (baselineFirstRounds[index] == baselineFirst) {
+ baseline[output] = baselineTimes[index];
+ candidate[output] = candidateTimes[index];
+ output++;
+ }
+ }
+ Samples baselineSamples = Samples.of(baseline);
+ Samples candidateSamples = Samples.of(candidate);
+ return new OrderStratum(baselineSamples, candidateSamples,
+ (double) baselineSamples.median() / Math.max(1L, candidateSamples.median()));
+ }
+ }
+
+ private record Comparison(Samples baseline, Samples candidate, OrderStratum baselineFirst,
+ OrderStratum candidateFirst, String roundOrder, long[] baselineRoundsNs,
+ long[] candidateRoundsNs, double speedup) {
+ }
+
+ private record ThreeWayComparison(Comparison legacyCandidate, Comparison controlCandidate) {
+ }
+
+ private record AdaptivePathProbe(long activeLabels, boolean gridBuilt,
+ boolean gridActiveAtEnd, boolean boundsActiveAtEnd) {
+ }
+
+ private record GridPhase(String name, double x, double z) {
+ }
+
+ /** Strict pure primitive SoA lower-bound control without bounds or an adaptive grid. */
+ private static final class PrimitiveViewerIndex {
+
+ private final double[] xCoordinates;
+ private final double[] yCoordinates;
+ private final double[] zCoordinates;
+ private UUID worldId;
+ private int size;
+
+ private PrimitiveViewerIndex(int capacity) {
+ xCoordinates = new double[capacity];
+ yCoordinates = new double[capacity];
+ zCoordinates = new double[capacity];
+ }
+
+ private void addViewer(UUID worldId, double x, double y, double z) {
+ if (this.worldId == null) {
+ this.worldId = worldId;
+ }
+ xCoordinates[size] = x;
+ yCoordinates[size] = y;
+ zCoordinates[size] = z;
+ size++;
+ }
+
+ private boolean hasViewerWithin(UUID worldId, double x, double y, double z, double range) {
+ if (!java.util.Objects.equals(this.worldId, worldId)) {
+ return false;
+ }
+ double rangeSquared = range * range;
+ for (int index = 0; index < size; index++) {
+ double deltaX = xCoordinates[index] - x;
+ double deltaY = yCoordinates[index] - y;
+ double deltaZ = zCoordinates[index] - z;
+ if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= rangeSquared) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /** Exact copy of the viewer-grid strategy used by production before the optimized candidate. */
+ private static final class LegacyViewerIndex {
+
+ private static final double CELL_SIZE = 16.0D;
+ private final Map> viewerCells = new HashMap<>();
+
+ private void addViewer(UUID worldId, double x, double y, double z) {
+ viewerCells.computeIfAbsent(cell(worldId, x, z), ignored -> new ArrayList<>())
+ .add(new LegacyViewerPoint(x, y, z));
+ }
+
+ private boolean hasViewerWithin(UUID worldId, double x, double y, double z, double range) {
+ if (range < 0.0D) {
+ return false;
+ }
+ int minimumX = coordinate(x - range);
+ int maximumX = coordinate(x + range);
+ int minimumZ = coordinate(z - range);
+ int maximumZ = coordinate(z + range);
+ double rangeSquared = range * range;
+ for (int cellX = minimumX; cellX <= maximumX; cellX++) {
+ for (int cellZ = minimumZ; cellZ <= maximumZ; cellZ++) {
+ List points = viewerCells.get(new LegacyViewerCell(worldId, cellX, cellZ));
+ if (points == null) {
+ continue;
+ }
+ for (LegacyViewerPoint point : points) {
+ double deltaX = point.x() - x;
+ double deltaY = point.y() - y;
+ double deltaZ = point.z() - z;
+ if (deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ <= rangeSquared) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static LegacyViewerCell cell(UUID worldId, double x, double z) {
+ return new LegacyViewerCell(worldId, coordinate(x), coordinate(z));
+ }
+
+ private static int coordinate(double value) {
+ return (int) Math.floor(value / CELL_SIZE);
+ }
+ }
+
+ private record LegacyViewerCell(UUID worldId, int x, int z) {
+ }
+
+ private record LegacyViewerPoint(double x, double y, double z) {
+ }
+
+ private record Point(int id, double x, double y, double z) {
+
+ private double distanceSquared(Point other) {
+ double deltaX = x - other.x;
+ double deltaY = y - other.y;
+ double deltaZ = z - other.z;
+ return deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ;
+ }
+ }
+
+}
diff --git a/benchmark/src/main/resources/plugin.yml b/benchmark/src/main/resources/plugin.yml
new file mode 100644
index 00000000..d061923b
--- /dev/null
+++ b/benchmark/src/main/resources/plugin.yml
@@ -0,0 +1,5 @@
+name: InteractionVisualizerBenchmark
+version: '1.0'
+main: com.loohp.interactionvisualizer.entities.DroppedItemBenchmarkPlugin
+api-version: '26.1'
+description: Disposable A/B performance harness for InteractionVisualizer
diff --git a/build.gradle.kts b/build.gradle.kts
index f0af2b33..be787198 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,6 +14,7 @@ val paper26_1Version = "26.1.2.build.74-stable"
val paper26_2Version = "26.2.build.56-alpha"
val craftEngineVersion = "26.7.2"
val sparrowHeartVersion = "0.72"
+val caffeineVersion = "3.2.3"
val paper26_2CompileClasspath = configurations.create("paper26_2CompileClasspath") {
isCanBeConsumed = false
@@ -37,6 +38,7 @@ dependencies {
implementation("net.momirealms:sparrow-yaml:1.0.7")
implementation("net.momirealms:sparrow-heart:$sparrowHeartVersion")
+ implementation("com.github.ben-manes.caffeine:caffeine:$caffeineVersion")
testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
@@ -70,6 +72,24 @@ sourceSets {
}
}
+val benchmarkSourceSet = sourceSets.create("benchmark") {
+ java.setSrcDirs(listOf("benchmark/src/main/java"))
+ resources.setSrcDirs(listOf("benchmark/src/main/resources"))
+ compileClasspath += sourceSets.main.get().output + configurations.compileClasspath.get()
+ runtimeClasspath += output + compileClasspath
+}
+
+val benchmarkJar = tasks.register("benchmarkJar") {
+ description = "Builds the standalone Paper A/B benchmark plugin (never shipped in production)."
+ group = LifecycleBasePlugin.VERIFICATION_GROUP
+ dependsOn(benchmarkSourceSet.classesTaskName)
+ archiveClassifier = "benchmark"
+ from(benchmarkSourceSet.output)
+ from(sourceSets.main.get().output) {
+ include("com/loohp/interactionvisualizer/entities/DroppedItemSpatialIndex*.class")
+ }
+}
+
tasks.withType().configureEach {
options.release = 25
options.encoding = "UTF-8"
@@ -103,6 +123,22 @@ tasks.withType().configureEach {
classpath = files(testClasspathJar.flatMap { it.archiveFile })
}
+val legacyTextCacheDisableProperty = "interactionvisualizer.disableLegacyTextComponentCache"
+
+tasks.named("test") {
+ systemProperty(legacyTextCacheDisableProperty, "false")
+ exclude("**/LegacyTextComponentCacheDisabledTest.class")
+}
+
+val testLegacyTextComponentCacheDisabled = tasks.register("testLegacyTextComponentCacheDisabled") {
+ description = "Verifies the isolated JVM rollback path for legacy text component caching."
+ group = LifecycleBasePlugin.VERIFICATION_GROUP
+ testClassesDirs = sourceSets.test.get().output.classesDirs
+ include("**/LegacyTextComponentCacheDisabledTest.class")
+ systemProperty(legacyTextCacheDisableProperty, "true")
+ dependsOn(tasks.testClasses)
+}
+
val compilePaper26_2 = tasks.register("compilePaper26_2") {
description = "Compiles the same Paper-API-only sources against Paper 26.2."
group = LifecycleBasePlugin.VERIFICATION_GROUP
@@ -115,7 +151,7 @@ val compilePaper26_2 = tasks.register("compilePaper26_2") {
}
val verifyPaperOnlyArchitecture = tasks.register("verifyPaperOnlyArchitecture") {
- description = "Confines NMS reflection to the client pickup bridge and rejects legacy layers."
+ description = "Confines NMS reflection to the isolated client packet bridges and rejects legacy layers."
group = LifecycleBasePlugin.VERIFICATION_GROUP
val sources = sourceSets.main.get().allJava
inputs.files(sources)
@@ -141,11 +177,15 @@ val verifyPaperOnlyArchitecture = tasks.register("verifyPaperOnlyArchitecture")
val pickupBridge = file(
"common/src/main/java/com/loohp/interactionvisualizer/integration/packet/ClientPickupAnimationBridge.java",
).canonicalFile
+ val textDisplayBridge = file(
+ "common/src/main/java/com/loohp/interactionvisualizer/integration/packet/ClientTextDisplayBridge.java",
+ ).canonicalFile
+ val packetBridges = setOf(pickupBridge, textDisplayBridge)
val bridgeTokens = setOf("net.minecraft", "org.bukkit.craftbukkit")
val violations = sources.files.flatMap { source ->
val text = source.readText()
forbidden.filter(text::contains).mapNotNull { token ->
- val allowed = source.canonicalFile == pickupBridge && token in bridgeTokens
+ val allowed = source.canonicalFile in packetBridges && token in bridgeTokens
if (allowed) null else "${source.relativeTo(rootDir)}: $token"
}
}
@@ -196,6 +236,7 @@ tasks.check {
dependsOn(compilePaper26_2)
dependsOn(verifyPaperOnlyArchitecture)
dependsOn(verifyCustomContentIsolation)
+ dependsOn(testLegacyTextComponentCacheDisabled)
}
tasks.named("shadowJar") {
@@ -204,6 +245,9 @@ tasks.named("shadowJar") {
relocate("net.momirealms.sparrow.yaml", "com.loohp.interactionvisualizer.libs.sparrow.yaml")
relocate("net.momirealms.sparrow.heart", "com.loohp.interactionvisualizer.libs.sparrow.heart")
+ relocate("com.github.benmanes.caffeine", "com.loohp.interactionvisualizer.libs.caffeine")
+ relocate("org.jspecify", "com.loohp.interactionvisualizer.libs.jspecify")
+ relocate("com.google.errorprone.annotations", "com.loohp.interactionvisualizer.libs.errorprone.annotations")
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
@@ -238,6 +282,49 @@ tasks.named("shadowJar") {
check(jar.getEntry("META-INF/licenses/sparrow-heart.txt") != null) {
"The Sparrow Heart MIT license notice is missing"
}
+ check(jar.getEntry("META-INF/licenses/caffeine.txt") != null) {
+ "The Caffeine Apache-2.0 license notice is missing"
+ }
+ check(jar.getEntry("META-INF/licenses/jspecify.txt") != null) {
+ "The JSpecify Apache-2.0 license notice is missing"
+ }
+ check(jar.getEntry("META-INF/licenses/error-prone-annotations.txt") != null) {
+ "The Error Prone Annotations Apache-2.0 license notice is missing"
+ }
+ check(jar.getEntry(
+ "com/loohp/interactionvisualizer/libs/caffeine/cache/Caffeine.class",
+ ) != null) {
+ "The shaded Caffeine runtime is missing"
+ }
+ val unrelocatedCaffeine = jar.entries().asSequence()
+ .map { it.name }
+ .filter { it.startsWith("com/github/benmanes/caffeine/") }
+ .toList()
+ check(unrelocatedCaffeine.isEmpty()) {
+ "Unrelocated Caffeine classes were bundled:\n" +
+ unrelocatedCaffeine.joinToString("\n")
+ }
+ val unrelocatedAnnotationDependencies = jar.entries().asSequence()
+ .map { it.name }
+ .filter {
+ it.startsWith("org/jspecify/") ||
+ it.startsWith("com/google/errorprone/annotations/")
+ }
+ .toList()
+ check(unrelocatedAnnotationDependencies.isEmpty()) {
+ "Unrelocated Caffeine annotation dependencies were bundled:\n" +
+ unrelocatedAnnotationDependencies.joinToString("\n")
+ }
+ check(jar.getEntry(
+ "com/loohp/interactionvisualizer/libs/jspecify/annotations/NullMarked.class",
+ ) != null) {
+ "The relocated JSpecify annotations are missing"
+ }
+ check(jar.getEntry(
+ "com/loohp/interactionvisualizer/libs/errorprone/annotations/CanIgnoreReturnValue.class",
+ ) != null) {
+ "The relocated Error Prone annotations are missing"
+ }
}
}
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/Commands.java b/common/src/main/java/com/loohp/interactionvisualizer/Commands.java
index 8cbdecf8..36909215 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/Commands.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/Commands.java
@@ -21,9 +21,12 @@
package com.loohp.interactionvisualizer;
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
+import com.loohp.interactionvisualizer.debug.PerformanceBlockScene;
import com.loohp.interactionvisualizer.managers.MaterialManager;
import com.loohp.interactionvisualizer.managers.MusicManager;
import com.loohp.interactionvisualizer.managers.DisplayManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
+import com.loohp.interactionvisualizer.debug.PerformanceScene;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.updater.Updater;
import com.loohp.interactionvisualizer.updater.Updater.UpdaterResponse;
@@ -43,6 +46,8 @@
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
public class Commands implements CommandExecutor, TabCompleter {
@@ -60,6 +65,10 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
return true;
}
+ if (args[0].equalsIgnoreCase("perf")) {
+ return handlePerformanceCommand(sender, args);
+ }
+
if (args[0].equalsIgnoreCase("reload")) {
if (sender.hasPermission("interactionvisualizer.reload")) {
plugin.loadConfig();
@@ -261,6 +270,368 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
return true;
}
+ private boolean handlePerformanceCommand(CommandSender sender, String[] args) {
+ if (!sender.hasPermission("interactionvisualizer.performance")) {
+ sender.sendMessage(ChatColorUtils.translateAlternateColorCodes('&', plugin.getConfiguration().getString("Messages.NoPermission")));
+ return true;
+ }
+ if (args.length < 2) {
+ sender.sendMessage(Component.text("Usage: /iv perf "));
+ return true;
+ }
+ switch (args[1].toLowerCase()) {
+ case "start" -> {
+ String label = args.length >= 3 ? args[2] : "unnamed";
+ if (PerformanceMetrics.start(label)) {
+ sender.sendMessage(Component.text("[InteractionVisualizer] Performance sampling started: " + label));
+ } else {
+ sender.sendMessage(Component.text("[InteractionVisualizer] Performance sampling is already active."));
+ }
+ }
+ case "stop" -> {
+ PerformanceMetrics.Snapshot snapshot = PerformanceMetrics.stop();
+ if (snapshot == null) {
+ sender.sendMessage(Component.text("[InteractionVisualizer] Performance sampling is not active."));
+ } else {
+ sender.sendMessage(Component.text("[InteractionVisualizer] " + snapshot.summary()));
+ }
+ }
+ case "status" -> {
+ PerformanceMetrics.Snapshot snapshot = PerformanceMetrics.snapshot();
+ if (snapshot == null) {
+ sender.sendMessage(Component.text("[InteractionVisualizer] Performance sampling is not active."));
+ } else {
+ sender.sendMessage(Component.text("[InteractionVisualizer] " + snapshot.summary()));
+ }
+ }
+ case "scene" -> {
+ if (args.length < 4) {
+ sender.sendMessage(Component.text(
+ "Usage: /iv perf scene " +
+ " [lifetimeTicks] [player]"));
+ return true;
+ }
+ boolean moving = args[2].equalsIgnoreCase("motion");
+ boolean staticItem = args[2].equalsIgnoreCase("static");
+ boolean itemDisplay = args[2].equalsIgnoreCase("itemdisplay");
+ boolean textDisplay = args[2].equalsIgnoreCase("textdisplay");
+ if (!moving && !staticItem && !itemDisplay && !textDisplay) {
+ sender.sendMessage(Component.text(
+ "[InteractionVisualizer] Scene type must be static, motion, itemdisplay, or textdisplay."));
+ return true;
+ }
+ long defaultLifetime = moving ? 80L : 200L;
+ long lifetime = defaultLifetime;
+ String requestedPlayer = args.length >= 6 ? args[5] : null;
+ if (args.length >= 5) {
+ try {
+ lifetime = Long.parseLong(args[4]);
+ } catch (NumberFormatException ignored) {
+ if (args.length == 5) {
+ requestedPlayer = args[4];
+ } else {
+ sender.sendMessage(Component.text(
+ "[InteractionVisualizer] lifetimeTicks must be an integer."));
+ return true;
+ }
+ }
+ }
+ Player player = performanceScenePlayer(sender, requestedPlayer);
+ if (player == null) {
+ sender.sendMessage(Component.text(
+ "[InteractionVisualizer] Specify an online player for the benchmark scene."));
+ return true;
+ }
+ int count = parseInteger(args[3], 1);
+ int spawned = itemDisplay || textDisplay
+ ? PerformanceScene.spawnDisplay(player, textDisplay, count, lifetime)
+ : PerformanceScene.spawn(player, moving, count, lifetime);
+ String sceneName = moving ? "moving" : staticItem ? "static"
+ : itemDisplay ? "itemdisplay" : "textdisplay";
+ String entityLabel = staticItem || moving ? " benchmark items" : " benchmark entities";
+ sender.sendMessage(Component.text("[InteractionVisualizer] Spawned " + spawned + " "
+ + sceneName + entityLabel + " for " + lifetime + " ticks."));
+ }
+ case "clear" -> {
+ String requestedOwner = args.length >= 3 ? args[2] : null;
+ Player player = performanceScenePlayer(sender, requestedOwner);
+ UUID ownerId = player == null ? parseUuid(requestedOwner) : player.getUniqueId();
+ if (ownerId == null) {
+ sender.sendMessage(Component.text(
+ "[InteractionVisualizer] Specify an online player or owner UUID when clearing a scene from console."));
+ return true;
+ }
+ PerformanceScene.clear(ownerId);
+ String ownerName = player == null ? ownerId.toString() : player.getName();
+ sender.sendMessage(Component.text("[InteractionVisualizer] Cleared the benchmark scene for "
+ + ownerName + "."));
+ }
+ case "blockscene" -> handlePerformanceBlockScene(sender, args);
+ default -> sender.sendMessage(Component.text(
+ "Usage: /iv perf "));
+ }
+ return true;
+ }
+
+ private static void handlePerformanceBlockScene(CommandSender sender, String[] args) {
+ if (args.length < 3) {
+ sendBlockSceneUsage(sender);
+ return;
+ }
+ String action = args[2].toLowerCase();
+ try {
+ switch (action) {
+ case "create" -> createPerformanceBlockScene(sender, args);
+ case "mutate" -> mutatePerformanceBlockScene(sender, args);
+ case "status" -> inspectPerformanceBlockScene(sender, args, false);
+ case "clear" -> inspectPerformanceBlockScene(sender, args, true);
+ default -> {
+ sendBlockSceneRecord(sender, action, null,
+ "state=invalid reason=unknown_action");
+ sendBlockSceneUsage(sender);
+ }
+ }
+ } catch (RuntimeException exception) {
+ sendBlockSceneRecordForOwner(sender, action, blockSceneOwnerHint(sender, action, args),
+ "state=error error=" + summaryToken(exception.getClass().getSimpleName())
+ + " detail=" + summaryToken(exception.getMessage()));
+ }
+ }
+
+ private static void createPerformanceBlockScene(CommandSender sender, String[] args) {
+ if (args.length < 5 || args.length > 6) {
+ sendBlockSceneRecord(sender, "create", null, "state=invalid reason=usage");
+ sender.sendMessage(Component.text(
+ "Usage: /iv perf blockscene create [player]"));
+ return;
+ }
+ PerformanceBlockScene.Mode mode = performanceBlockMode(args[3]);
+ if (mode == null) {
+ sendBlockSceneRecord(sender, "create", null, "state=invalid reason=invalid_mode");
+ return;
+ }
+ Integer count = performanceBlockCount(args[4]);
+ if (count == null) {
+ sendBlockSceneRecord(sender, "create", null,
+ "state=invalid reason=invalid_count min=1 max=" + PerformanceBlockScene.MAX_BLOCKS);
+ return;
+ }
+ Player player = performanceScenePlayer(sender, args.length == 6 ? args[5] : null);
+ if (player == null) {
+ sendBlockSceneRecord(sender, "create", null, "state=invalid reason=player_not_online");
+ return;
+ }
+ PerformanceBlockScene.Snapshot snapshot = PerformanceBlockScene.create(player, count, mode);
+ sendBlockSceneRecord(sender, "create", player, snapshot.summary());
+ }
+
+ private static void mutatePerformanceBlockScene(CommandSender sender, String[] args) {
+ if (args.length > 6) {
+ sendBlockSceneRecord(sender, "mutate", null, "state=invalid reason=usage");
+ sender.sendMessage(Component.text(
+ "Usage: /iv perf blockscene mutate [count] [idle|active|direct-write] [player|owner-uuid]"));
+ return;
+ }
+
+ Integer count = null;
+ PerformanceBlockScene.Mode mode = null;
+ String requestedPlayer = null;
+ for (int index = 3; index < args.length; index++) {
+ String argument = args[index];
+ PerformanceBlockScene.Mode parsedMode = performanceBlockMode(argument);
+ if (parsedMode != null) {
+ if (mode != null) {
+ sendBlockSceneRecord(sender, "mutate", null, "state=invalid reason=duplicate_mode");
+ return;
+ }
+ mode = parsedMode;
+ continue;
+ }
+ Integer parsedCount = performanceBlockCount(argument);
+ if (parsedCount != null) {
+ if (count != null) {
+ sendBlockSceneRecord(sender, "mutate", null, "state=invalid reason=duplicate_count");
+ return;
+ }
+ count = parsedCount;
+ continue;
+ }
+ if (argument.matches("[+-]?\\d+")) {
+ sendBlockSceneRecord(sender, "mutate", null,
+ "state=invalid reason=count_out_of_range min=1 max=" + PerformanceBlockScene.MAX_BLOCKS);
+ return;
+ }
+ if (requestedPlayer == null) {
+ requestedPlayer = argument;
+ continue;
+ }
+ sendBlockSceneRecord(sender, "mutate", null, "state=invalid reason=ambiguous_arguments");
+ return;
+ }
+
+ BlockSceneOwner owner = performanceBlockSceneOwner(sender, requestedPlayer);
+ if (owner == null) {
+ sendBlockSceneRecord(sender, "mutate", null,
+ "state=invalid reason=online_player_or_owner_uuid_required");
+ return;
+ }
+ long mutationCommandStartNanos = System.nanoTime();
+ PerformanceBlockScene.Snapshot current = PerformanceBlockScene.snapshot(owner.ownerId());
+ long mutationPreflightElapsedNanos = Math.max(
+ 0L, System.nanoTime() - mutationCommandStartNanos);
+ if (current.state() == PerformanceBlockScene.SceneState.ABSENT) {
+ sendBlockSceneRecordForOwner(sender, "mutate", owner.displayName(), current.summary());
+ return;
+ }
+ if (count != null && count > current.placedCount()) {
+ sendBlockSceneRecordForOwner(sender, "mutate", owner.displayName(),
+ "state=invalid reason=count_exceeds_scene requested=" + count
+ + " max=" + current.placedCount());
+ return;
+ }
+
+ PerformanceBlockScene.Snapshot snapshot;
+ if (count != null && mode != null) {
+ snapshot = PerformanceBlockScene.mutate(owner.ownerId(), count, mode);
+ } else if (mode != null) {
+ snapshot = PerformanceBlockScene.mutate(owner.ownerId(), mode);
+ } else {
+ snapshot = PerformanceBlockScene.mutate(owner.ownerId(),
+ count == null ? current.placedCount() : count);
+ }
+ long mutationCommandElapsedNanos = Math.max(
+ 0L, System.nanoTime() - mutationCommandStartNanos);
+ String commandTiming = " mutationPreflightMs="
+ + String.format(Locale.ROOT, "%.6f", mutationPreflightElapsedNanos / 1_000_000.0D)
+ + " mutationCommandMs="
+ + String.format(Locale.ROOT, "%.6f", mutationCommandElapsedNanos / 1_000_000.0D);
+ sendBlockSceneRecordForOwner(sender, "mutate", owner.displayName(),
+ snapshot.summary() + commandTiming);
+ }
+
+ private static void inspectPerformanceBlockScene(CommandSender sender, String[] args, boolean clear) {
+ String action = clear ? "clear" : "status";
+ if (args.length > 4) {
+ sendBlockSceneRecord(sender, action, null, "state=invalid reason=usage");
+ sender.sendMessage(Component.text(
+ "Usage: /iv perf blockscene " + action + " [player|owner-uuid]"));
+ return;
+ }
+ BlockSceneOwner owner = performanceBlockSceneOwner(sender, args.length == 4 ? args[3] : null);
+ if (owner == null) {
+ sendBlockSceneRecord(sender, action, null,
+ "state=invalid reason=online_player_or_owner_uuid_required");
+ return;
+ }
+ PerformanceBlockScene.Snapshot snapshot = clear
+ ? PerformanceBlockScene.clear(owner.ownerId())
+ : PerformanceBlockScene.snapshot(owner.ownerId());
+ sendBlockSceneRecordForOwner(sender, action, owner.displayName(), snapshot.summary());
+ }
+
+ private static PerformanceBlockScene.Mode performanceBlockMode(String value) {
+ try {
+ return PerformanceBlockScene.Mode.parse(value);
+ } catch (IllegalArgumentException exception) {
+ return null;
+ }
+ }
+
+ private static Integer performanceBlockCount(String value) {
+ try {
+ int count = Integer.parseInt(value);
+ return count >= 1 && count <= PerformanceBlockScene.MAX_BLOCKS ? count : null;
+ } catch (NumberFormatException exception) {
+ return null;
+ }
+ }
+
+ private static void sendBlockSceneRecord(CommandSender sender, String action, Player player, String summary) {
+ sendBlockSceneRecordForOwner(sender, action, player == null ? null : player.getName(), summary);
+ }
+
+ private static void sendBlockSceneRecordForOwner(CommandSender sender, String action, String owner,
+ String summary) {
+ String record = "IV_BLOCK_SCENE action=" + summaryToken(action)
+ + " owner=" + summaryToken(owner) + " " + summary;
+ plugin.getLogger().info(record);
+ if (sender instanceof Player) {
+ sender.sendMessage(Component.text("[InteractionVisualizer] " + record));
+ }
+ }
+
+ private static String summaryToken(String value) {
+ if (value == null || value.isBlank()) {
+ return "none";
+ }
+ return value.replaceAll("[^A-Za-z0-9_.:-]", "_");
+ }
+
+ private static void sendBlockSceneUsage(CommandSender sender) {
+ sender.sendMessage(Component.text(
+ "Usage: /iv perf blockscene "));
+ }
+
+ private static Player performanceScenePlayer(CommandSender sender, String requestedName) {
+ if (requestedName != null && !requestedName.isBlank()) {
+ return Bukkit.getPlayerExact(requestedName);
+ }
+ return sender instanceof Player player ? player : null;
+ }
+
+ private static BlockSceneOwner performanceBlockSceneOwner(CommandSender sender, String requestedName) {
+ Player online = performanceScenePlayer(sender, requestedName);
+ if (online != null) {
+ return new BlockSceneOwner(online.getUniqueId(), online.getName());
+ }
+ UUID ownerId = parseUuid(requestedName);
+ return ownerId == null ? null : new BlockSceneOwner(ownerId, ownerId.toString());
+ }
+
+ private static String blockSceneOwnerHint(CommandSender sender, String action, String[] args) {
+ String requested = null;
+ if ("create".equals(action) && args.length >= 6) {
+ requested = args[5];
+ } else if (("status".equals(action) || "clear".equals(action)) && args.length >= 4) {
+ requested = args[3];
+ } else if ("mutate".equals(action)) {
+ for (int index = 3; index < args.length; index++) {
+ String argument = args[index];
+ if (performanceBlockMode(argument) == null && performanceBlockCount(argument) == null
+ && !argument.matches("[+-]?\\d+")) {
+ requested = argument;
+ break;
+ }
+ }
+ }
+ if (requested != null && !requested.isBlank()) {
+ return requested;
+ }
+ return sender instanceof Player player ? player.getName() : null;
+ }
+
+ private static UUID parseUuid(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ try {
+ return UUID.fromString(value);
+ } catch (IllegalArgumentException ignored) {
+ return null;
+ }
+ }
+
+ private record BlockSceneOwner(UUID ownerId, String displayName) {
+ }
+
+ private static int parseInteger(String value, int defaultValue) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException ignored) {
+ return defaultValue;
+ }
+ }
+
@Override
public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) {
List tab = new LinkedList<>();
@@ -304,8 +675,19 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe
tab.add("refresh");
}
}
+ if (sender.hasPermission("interactionvisualizer.performance") && "perf".startsWith(args[0].toLowerCase())) {
+ tab.add("perf");
+ }
return tab;
case 2:
+ if (args[0].equalsIgnoreCase("perf") && sender.hasPermission("interactionvisualizer.performance")) {
+ for (String option : List.of("start", "stop", "status", "scene", "clear", "blockscene")) {
+ if (option.startsWith(args[1].toLowerCase())) {
+ tab.add(option);
+ }
+ }
+ return tab;
+ }
if (args[0].equalsIgnoreCase("toggle")) {
if (sender.hasPermission("interactionvisualizer.toggle")) {
if ("itemstand".startsWith(args[1].toLowerCase())) {
@@ -324,6 +706,24 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe
}
return tab;
case 3:
+ if (args[0].equalsIgnoreCase("perf") && args[1].equalsIgnoreCase("scene")
+ && sender.hasPermission("interactionvisualizer.performance")) {
+ for (String option : List.of("static", "motion", "itemdisplay", "textdisplay")) {
+ if (option.startsWith(args[2].toLowerCase())) {
+ tab.add(option);
+ }
+ }
+ return tab;
+ }
+ if (args[0].equalsIgnoreCase("perf") && args[1].equalsIgnoreCase("blockscene")
+ && sender.hasPermission("interactionvisualizer.performance")) {
+ for (String option : List.of("create", "mutate", "status", "clear")) {
+ if (option.startsWith(args[2].toLowerCase())) {
+ tab.add(option);
+ }
+ }
+ return tab;
+ }
if (args[0].equalsIgnoreCase("toggle")) {
if (sender.hasPermission("interactionvisualizer.toggle")) {
if (args[1].equalsIgnoreCase("itemstand") || args[1].equalsIgnoreCase("itemdrop") || args[1].equalsIgnoreCase("hologram") || args[1].equalsIgnoreCase("all")) {
@@ -340,6 +740,17 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe
}
return tab;
case 4:
+ if (args[0].equalsIgnoreCase("perf") && args[1].equalsIgnoreCase("blockscene")
+ && sender.hasPermission("interactionvisualizer.performance")) {
+ if (args[2].equalsIgnoreCase("create") || args[2].equalsIgnoreCase("mutate")) {
+ addMatchingBlockModes(tab, args[3]);
+ }
+ if (args[2].equalsIgnoreCase("mutate") || args[2].equalsIgnoreCase("status")
+ || args[2].equalsIgnoreCase("clear")) {
+ addMatchingPlayers(tab, args[3]);
+ }
+ return tab;
+ }
if (args[0].equalsIgnoreCase("toggle")) {
if (sender.hasPermission("interactionvisualizer.toggle")) {
if (args[1].equalsIgnoreCase("itemstand") || args[1].equalsIgnoreCase("itemdrop") || args[1].equalsIgnoreCase("hologram") || args[1].equalsIgnoreCase("all")) {
@@ -354,6 +765,15 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe
}
return tab;
case 5:
+ if (args[0].equalsIgnoreCase("perf") && args[1].equalsIgnoreCase("blockscene")
+ && args[2].equalsIgnoreCase("mutate")
+ && sender.hasPermission("interactionvisualizer.performance")) {
+ if (performanceBlockCount(args[3]) != null) {
+ addMatchingBlockModes(tab, args[4]);
+ }
+ addMatchingPlayers(tab, args[4]);
+ return tab;
+ }
if (args[0].equalsIgnoreCase("toggle")) {
if (sender.hasPermission("interactionvisualizer.toggle")) {
if (args[1].equalsIgnoreCase("itemstand") || args[1].equalsIgnoreCase("itemdrop") || args[1].equalsIgnoreCase("hologram")) {
@@ -368,9 +788,35 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe
}
}
return tab;
+ case 6:
+ if (args[0].equalsIgnoreCase("perf") && args[1].equalsIgnoreCase("blockscene")
+ && sender.hasPermission("interactionvisualizer.performance")) {
+ if (args[2].equalsIgnoreCase("create")
+ || args[2].equalsIgnoreCase("mutate")) {
+ addMatchingPlayers(tab, args[5]);
+ }
+ return tab;
+ }
+ return tab;
default:
return tab;
}
}
+ private static void addMatchingBlockModes(List tab, String prefix) {
+ for (String option : List.of("idle", "active", "direct-write")) {
+ if (option.startsWith(prefix.toLowerCase())) {
+ tab.add(option);
+ }
+ }
+ }
+
+ private static void addMatchingPlayers(List tab, String prefix) {
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (player.getName().toLowerCase().startsWith(prefix.toLowerCase())) {
+ tab.add(player.getName());
+ }
+ }
+ }
+
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java b/common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java
index 624f61ed..633efe8e 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/InteractionVisualizer.java
@@ -23,6 +23,8 @@
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
import com.loohp.interactionvisualizer.config.Config;
import com.loohp.interactionvisualizer.database.Database;
+import com.loohp.interactionvisualizer.debug.PerformanceBlockScene;
+import com.loohp.interactionvisualizer.debug.PerformanceScene;
import com.loohp.interactionvisualizer.integration.CustomContentManager;
import com.loohp.interactionvisualizer.managers.AsyncExecutorManager;
import com.loohp.interactionvisualizer.managers.LangManager;
@@ -31,6 +33,7 @@
import com.loohp.interactionvisualizer.managers.MusicManager;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PreferenceManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TaskManager;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.metrics.Charts;
@@ -62,6 +65,7 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
import java.util.stream.Collectors;
public class InteractionVisualizer extends JavaPlugin {
@@ -102,6 +106,19 @@ public class InteractionVisualizer extends JavaPlugin {
public static boolean hideIfObstructed = false;
public static boolean defaultDisabledAll = false;
+ /** A/B switch: virtual item remains authoritative while an invisible tracker stays stationary. */
+ public static boolean staticVirtualItemAnchorsDuringAnimation = false;
+ /** A/B switch: eligible stationary virtual items are tracked and rendered entirely by packets. */
+ public static boolean packetOnlyStaticVirtualItems = false;
+ /** A/B switch: smooths visibility recovery bursts; hides are always immediate. */
+ public static boolean visibilityRateLimiting = false;
+ public static int visibilityRateLimitBucketSize = 128;
+ public static int visibilityRateLimitRestorePerTick = 32;
+ /** A/B switch: coalesces block changes and updates tracked blocks from fixed-budget loops. */
+ public static boolean eventDrivenBlockUpdates = false;
+ public static int blockUpdateMaxDirtyPerTick = 64;
+
+ private boolean blockUpdateModeInitialized;
public static ILightManager lightManager;
public static PreferenceManager preferenceManager;
@@ -188,6 +205,7 @@ public void onEnable() {
MaterialManager.setup();
getCommand("interactionvisualizer").setExecutor(new Commands());
+ PerformanceMetrics.register(this);
TaskManager.run();
@@ -232,6 +250,7 @@ public void onEnable() {
@Override
public void onDisable() {
+ shutdownPerformanceScenes();
CustomContentManager.shutdown();
if (preferenceManager != null) {
preferenceManager.close();
@@ -244,6 +263,33 @@ public void onDisable() {
"[InteractionVisualizer] Disabled; all display entities removed.", NamedTextColor.RED));
}
+ private void shutdownPerformanceScenes() {
+ try {
+ PerformanceBlockScene.ShutdownReport report = PerformanceBlockScene.shutdown();
+ if (report.unresolvedCount() != 0) {
+ getLogger().severe("Performance block-scene cleanup left blocks unresolved during disable: "
+ + report.summary() + "; unloaded or externally modified blocks were left untouched");
+ }
+ } catch (Throwable throwable) {
+ logPerformanceCleanupFailure("Performance block-scene cleanup failed; core disable will continue",
+ throwable);
+ }
+ try {
+ PerformanceScene.shutdown();
+ } catch (Throwable throwable) {
+ logPerformanceCleanupFailure("Performance display-scene cleanup failed; core disable will continue",
+ throwable);
+ }
+ }
+
+ private void logPerformanceCleanupFailure(String message, Throwable throwable) {
+ try {
+ getLogger().log(Level.SEVERE, message, throwable);
+ } catch (Throwable ignored) {
+ // Diagnostic logging must never prevent the plugin's core shutdown.
+ }
+ }
+
public SparrowConfiguration getConfiguration() {
return Config.getConfig(CONFIG_ID).getConfiguration();
}
@@ -285,6 +331,27 @@ public void loadConfig() {
}
defaultDisabledAll = getConfiguration().getBoolean("Settings.DefaultDisableAll");
+ staticVirtualItemAnchorsDuringAnimation = getConfiguration().getBoolean(
+ "Settings.Performance.VirtualItems.StaticAnchorDuringAnimation");
+ packetOnlyStaticVirtualItems = getConfiguration().getBoolean(
+ "Settings.Performance.VirtualItems.PacketOnlyStatic");
+ visibilityRateLimiting = getConfiguration().getBoolean(
+ "Settings.Performance.VisibilityRateLimit.Enabled");
+ visibilityRateLimitBucketSize = Math.max(1, getConfiguration().getInt(
+ "Settings.Performance.VisibilityRateLimit.BucketSize"));
+ visibilityRateLimitRestorePerTick = Math.max(1, getConfiguration().getInt(
+ "Settings.Performance.VisibilityRateLimit.RestorePerTick"));
+ boolean configuredEventDrivenBlockUpdates = getConfiguration().getBoolean(
+ "Settings.Performance.BlockUpdates.EventDriven");
+ if (!blockUpdateModeInitialized) {
+ eventDrivenBlockUpdates = configuredEventDrivenBlockUpdates;
+ } else if (eventDrivenBlockUpdates != configuredEventDrivenBlockUpdates) {
+ getLogger().warning("Settings.Performance.BlockUpdates.EventDriven is applied only at startup; "
+ + "restart the server to change the active block update mode.");
+ }
+ blockUpdateMaxDirtyPerTick = Math.max(1, getConfiguration().getInt(
+ "Settings.Performance.BlockUpdates.MaxDirtyPerTick"));
+ blockUpdateModeInitialized = true;
getServer().getPluginManager().callEvent(new InteractionVisualizerReloadEvent());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/api/InteractionVisualizerAPI.java b/common/src/main/java/com/loohp/interactionvisualizer/api/InteractionVisualizerAPI.java
index 5a272f83..e9f62391 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/api/InteractionVisualizerAPI.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/api/InteractionVisualizerAPI.java
@@ -120,7 +120,7 @@ public static Collection getPlayerModuleList(Modules module, EntryKey en
} else {
players = SynchronizedFilteredCollection.filter(players, each -> !excludedPlayers.contains(each));
}
- return Collections.unmodifiableCollection(players);
+ return SynchronizedFilteredCollection.unmodifiableCollection(players);
}
/**
@@ -424,66 +424,54 @@ public static DisplayEntity createDisplayEntityObject(Location location) {
/**
* Create a logical item display at the given location.
- * DOES NOT SPAWN THE ARMORSTAND.
+ * DOES NOT SPAWN THE DISPLAY.
*
* @return The InteractionVisualizer DisplayEntity object created.
*/
public static DisplayEntity createDisplayEntityItemHoldingObject(Location location) {
- Vector vector = rotateVectorAroundY(location.clone().getDirection().normalize().multiply(0.19), -100).add(location.clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity stand = new DisplayEntity(location.add(vector));
- setStand(stand, location.getYaw());
+ Location displayLocation = itemHoldingDisplayLocation(location);
+ DisplayEntity stand = new DisplayEntity(displayLocation);
+ setStand(stand, displayLocation.getYaw());
return stand;
}
+ static Location itemHoldingDisplayLocation(Location location) {
+ return location.clone();
+ }
+
/**
* Get the rotation mode for a mini item holding DisplayEntity.
- * ONLY WORKS WITH ARMORSTANDS CREATED USING createDisplayEntityItemHoldingObject(Location location)
+ * ONLY WORKS WITH DISPLAYS CREATED USING createDisplayEntityItemHoldingObject(Location location)
*
- * @return The same InteractionVisualizer DisplayEntity object.
+ * @return The current holding mode, or null when this is not a compatible item display.
*/
public static DisplayEntityHoldingMode getDisplayEntityItemHoldingObjectMode(DisplayEntity stand, DisplayEntityHoldingMode mode) {
- switch (getStandModeRaw(stand).toLowerCase()) {
- case "Item":
- return DisplayEntityHoldingMode.ITEM;
- case "LowBlock":
- return DisplayEntityHoldingMode.LOWBLOCK;
- case "Tool":
- return DisplayEntityHoldingMode.TOOL;
- case "Standing":
- return DisplayEntityHoldingMode.STANDING;
- }
- return null;
+ // Keep the unused parameter for binary and source compatibility with the
+ // historical public signature.
+ return getStandMode(stand);
}
/**
* Sets the rotation mode for a mini item holding DisplayEntity.
- * ONLY WORKS WITH ARMORSTANDS CREATED USING createDisplayEntityItemHoldingObject(Location location)
+ * ONLY WORKS WITH DISPLAYS CREATED USING createDisplayEntityItemHoldingObject(Location location)
*
* @return The same InteractionVisualizer DisplayEntity object.
*/
public static DisplayEntity rotateDisplayEntityItemHoldingObject(DisplayEntity stand, DisplayEntityHoldingMode mode) {
- toggleStandMode(stand, mode.toString());
+ String requestedMode = mode.toString();
+ if (!stand.isLocked()) {
+ toggleStandMode(stand, requestedMode);
+ }
return stand;
}
- private static Vector rotateVectorAroundY(Vector vector, double degrees) {
- double rad = Math.toRadians(degrees);
-
- double currentX = vector.getX();
- double currentZ = vector.getZ();
-
- double cosine = Math.cos(rad);
- double sine = Math.sin(rad);
-
- return new Vector((cosine * currentX - sine * currentZ), vector.getY(), (sine * currentX + cosine * currentZ));
- }
-
private static void setStand(DisplayEntity stand, float yaw) {
stand.setArms(true);
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
stand.setSmall(true);
+ stand.setLegacyRightHandItemTransform(true);
stand.setInvulnerable(true);
stand.setVisible(false);
stand.setSilent(true);
@@ -510,71 +498,48 @@ public static DisplayEntityHoldingMode getStandMode(DisplayEntity stand) {
}
private static void toggleStandMode(DisplayEntity stand, String mode) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plain.equals("IV.Custom.Item")) {
- if (plain.equals("IV.Custom.Block")) {
- stand.setCustomName("IV.Custom.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
-
- }
- if (plain.equals("IV.Custom.LowBlock")) {
- stand.setCustomName("IV.Custom.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
-
- }
- if (plain.equals("IV.Custom.Tool")) {
- stand.setCustomName("IV.Custom.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plain.equals("IV.Custom.Standing")) {
- stand.setCustomName("IV.Custom.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.Custom.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.Custom.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
+ String requestedMode = canonicalHoldingMode(mode);
+ String previousMode = getStandModeRaw(stand);
+ if (requestedMode.equals(previousMode)) {
+ stand.setRightArmPose(holdingPose(requestedMode));
+ return;
}
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.Custom.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
+
+ // This is the legacy DisplayEntity adapter for the same stable-anchor
+ // semantics as WorkstationDisplayPositioning#setRenderMode. The public
+ // return type cannot be changed to Item without breaking API consumers.
+ Location location = stand.getLocation();
+ float baseYaw = location.getYaw() - (usesDiagonalYaw(previousMode) ? 45.0F : 0.0F);
+ stand.setCustomName("IV.Custom." + requestedMode);
+ stand.setRightArmPose(holdingPose(requestedMode));
+ stand.setRotation(baseYaw + (usesDiagonalYaw(requestedMode) ? 45.0F : 0.0F), location.getPitch());
+ }
+
+ private static EulerAngle holdingPose(String mode) {
+ return switch (mode.toLowerCase(java.util.Locale.ROOT)) {
+ case "block", "lowblock" -> new EulerAngle(357.9, 0.0, 0.0);
+ case "tool" -> new EulerAngle(357.99, 0.0, 300.0);
+ case "standing" -> new EulerAngle(0.0, 4.7, 4.7);
+ case "item" -> EulerAngle.ZERO;
+ default -> throw new IllegalArgumentException("Unsupported display entity holding mode: " + mode);
+ };
+ }
+
+ private static String canonicalHoldingMode(String mode) {
+ DisplayEntityHoldingMode value = DisplayEntityHoldingMode.fromName(mode);
+ if (value != null) {
+ return value.toString();
}
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.Custom.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
+ // Preserve support for the historical internal Block pose even though
+ // it was never exposed as a DisplayEntityHoldingMode enum constant.
+ if ("Block".equalsIgnoreCase(mode)) {
+ return "Block";
}
+ throw new IllegalArgumentException("Unsupported display entity holding mode: " + mode);
+ }
+
+ private static boolean usesDiagonalYaw(String mode) {
+ return "Block".equalsIgnoreCase(mode) || "LowBlock".equalsIgnoreCase(mode);
}
/**
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityActivatedEvent.java b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityActivatedEvent.java
new file mode 100644
index 00000000..4e0b16fc
--- /dev/null
+++ b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityActivatedEvent.java
@@ -0,0 +1,49 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.api.events;
+
+import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
+import org.bukkit.block.Block;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/** Called when a previously indexed tile entity of the same type becomes active again. */
+public class TileEntityActivatedEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final Block block;
+ private final TileEntityType type;
+
+ public TileEntityActivatedEvent(Block block, TileEntityType type) {
+ this.block = block;
+ this.type = type;
+ }
+
+ public Block getBlock() {
+ return block;
+ }
+
+ public TileEntityType getTileEntityType() {
+ return type;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityAddedEvent.java b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityAddedEvent.java
new file mode 100644
index 00000000..7ae55758
--- /dev/null
+++ b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityAddedEvent.java
@@ -0,0 +1,49 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.api.events;
+
+import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
+import org.bukkit.block.Block;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/** Called when a new tile entity or a different type is added to the active index. */
+public class TileEntityAddedEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final Block block;
+ private final TileEntityType type;
+
+ public TileEntityAddedEvent(Block block, TileEntityType type) {
+ this.block = block;
+ this.type = type;
+ }
+
+ public Block getBlock() {
+ return block;
+ }
+
+ public TileEntityType getTileEntityType() {
+ return type;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityDeactivatedEvent.java b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityDeactivatedEvent.java
new file mode 100644
index 00000000..b365fd0f
--- /dev/null
+++ b/common/src/main/java/com/loohp/interactionvisualizer/api/events/TileEntityDeactivatedEvent.java
@@ -0,0 +1,52 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.api.events;
+
+import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
+import org.bukkit.block.Block;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+/**
+ * Called when a tile entity leaves the nearby active index without implying
+ * that the underlying block was broken.
+ */
+public class TileEntityDeactivatedEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final Block block;
+ private final TileEntityType type;
+
+ public TileEntityDeactivatedEvent(Block block, TileEntityType type) {
+ this.block = block;
+ this.type = type;
+ }
+
+ public Block getBlock() {
+ return block;
+ }
+
+ public TileEntityType getTileEntityType() {
+ return type;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/AnvilDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/AnvilDisplay.java
index 7cbd6bdd..888cd3f9 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/AnvilDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/AnvilDisplay.java
@@ -33,14 +33,16 @@
import com.loohp.interactionvisualizer.utils.MaterialUtils;
import com.loohp.interactionvisualizer.utils.MaterialUtils.MaterialMode;
import com.loohp.interactionvisualizer.utils.VanishUtils;
+import com.loohp.interactionvisualizer.utils.WorkstationDisplayPositioning;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.Block;
+import org.bukkit.entity.Display;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -53,7 +55,6 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
@@ -99,6 +100,7 @@ public void process(Player player) {
Map map = new HashMap<>();
map.put("Player", player);
map.put("2", "N/A");
+ map.put("2Label", "N/A");
map.putAll(spawnDisplayEntitys(player, block));
openedAnvil.put(block, map);
}
@@ -110,44 +112,42 @@ public void process(Player player) {
}
ItemStack[] items = new ItemStack[] {view.getItem(0), view.getItem(1)};
- if (view.getItem(2) != null) {
- ItemStack itemstack = view.getItem(2);
- if (itemstack == null || itemstack.getType().equals(Material.AIR)) {
- itemstack = null;
+ ItemStack itemstack = view.getItem(2);
+ if (itemstack != null && itemstack.getType().equals(Material.AIR)) {
+ itemstack = null;
+ }
+ if (map.get("2") instanceof String) {
+ if (itemstack != null) {
+ Item item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ item.setItemStack(itemstack);
+ item.setVelocity(new Vector(0, 0, 0));
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ DisplayEntity label = spawnResultLabel(item.getLocation(), itemstack.getItemMeta().displayName());
+ map.put("2", item);
+ map.put("2Label", label);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ DisplayManager.updateItem(item);
+ } else {
+ map.put("2", "N/A");
}
- Item item = null;
- if (map.get("2") instanceof String) {
- if (itemstack != null) {
- item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ } else {
+ Item item = (Item) map.get("2");
+ if (itemstack != null) {
+ if (!item.getItemStack().equals(itemstack)) {
item.setItemStack(itemstack);
- item.setVelocity(new Vector(0, 0, 0));
- item.setPickupDelay(32767);
- item.setGravity(false);
- item.setCustomName(itemstack.getItemMeta().displayName());
- item.setCustomNameVisible(true);
- map.put("2", item);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
DisplayManager.updateItem(item);
- } else {
- map.put("2", "N/A");
+ DisplayEntity label = resultLabel(map, item.getLocation(), itemstack.getItemMeta().displayName());
+ DisplayManager.updateDisplay(label);
}
} else {
- item = (Item) map.get("2");
- if (itemstack != null) {
- if (!item.getItemStack().equals(itemstack)) {
- item.setItemStack(itemstack);
- item.setCustomName(itemstack.getItemMeta().displayName());
- item.setCustomNameVisible(true);
- DisplayManager.updateItem(item);
- }
- } else {
- map.put("2", "N/A");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
- }
+ map.put("2", "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
+ removeResultLabel(map);
}
}
for (int i = 0; i < 2; i++) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
+ Item stand = (Item) map.get(String.valueOf(i));
ItemStack item = items[i];
if (item == null || item.getType().equals(Material.AIR)) {
item = null;
@@ -156,19 +156,19 @@ public void process(Player player) {
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
boolean changed = materialMode != standMode(stand);
if (changed) {
- toggleStandMode(stand, materialMode.toString());
+ toggleStandMode(stand, materialMode);
}
- if (!item.equals(stand.getItemInMainHand())) {
+ if (!item.equals(stand.getItemStack())) {
changed = true;
- stand.setItemInMainHand(item);
+ stand.setItemStack(item);
}
if (changed) {
- DisplayManager.updateDisplay(stand);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getItemInMainHand().getType().equals(Material.AIR)) {
- stand.setItemInMainHand(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
}
@@ -238,35 +238,45 @@ public void onAnvil(InventoryClickEvent event) {
return;
}
- ItemStack itemstack = event.getCurrentItem();
+ ItemStack itemstack = event.getCurrentItem().clone();
Location loc = block.getLocation();
Player player = (Player) event.getWhoClicked();
+ InventoryView view = event.getView();
- DisplayEntity slot0 = (DisplayEntity) map.get("0");
- DisplayEntity slot1 = (DisplayEntity) map.get("1");
+ Item slot0 = (Item) map.get("0");
+ Item slot1 = (Item) map.get("1");
if (map.get("2") instanceof String) {
- map.put("2", new Item(block.getLocation().clone().add(0.5, 1.2, 0.5)));
+ Item result = new Item(block.getLocation().clone().add(0.5, 1.2, 0.5));
+ result.setItemStack(itemstack);
+ result.setVelocity(new Vector());
+ result.setPickupDelay(32767);
+ result.setGravity(false);
+ DisplayEntity label = spawnResultLabel(result.getLocation(), itemstack.getItemMeta().displayName());
+ map.put("2", result);
+ map.put("2Label", label);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), result);
}
Item item = (Item) map.get("2");
+ DisplayEntity resultLabel = resultLabel(map, item.getLocation(), itemstack.getItemMeta().displayName());
+ DisplayManager.updateDisplay(resultLabel);
Inventory before = Bukkit.createInventory(null, 9);
- before.setItem(0, player.getOpenInventory().getItem(0).clone());
- before.setItem(1, player.getOpenInventory().getItem(1).clone());
+ before.setItem(0, InventoryUtils.cloneItem(view.getItem(0)));
+ before.setItem(1, InventoryUtils.cloneItem(view.getItem(1)));
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!player.isOnline() || player.getOpenInventory() != view) {
+ return;
+ }
Inventory after = Bukkit.createInventory(null, 9);
- after.setItem(0, player.getOpenInventory().getItem(0).clone());
- after.setItem(1, player.getOpenInventory().getItem(1).clone());
+ after.setItem(0, InventoryUtils.cloneItem(view.getItem(0)));
+ after.setItem(1, InventoryUtils.cloneItem(view.getItem(1)));
if (InventoryUtils.compareContents(before, after)) {
return;
}
- slot0.setLocked(true);
- slot1.setLocked(true);
- item.setLocked(true);
-
openedAnvil.remove(block);
float yaw = getCardinalDirection(player);
@@ -274,8 +284,13 @@ public void onAnvil(InventoryClickEvent event) {
slot0.teleport(slot0.getLocation().add(rotateVectorAroundY(vector.clone(), 90).multiply(0.1)));
slot1.teleport(slot1.getLocation().add(rotateVectorAroundY(vector.clone(), -90).multiply(0.1)));
- DisplayManager.updateDisplay(slot0);
- DisplayManager.updateDisplay(slot1);
+ DisplayManager.updateItem(slot0);
+ DisplayManager.updateItem(slot1);
+
+ item.setItemStack(itemstack);
+ slot0.setLocked(true);
+ slot1.setLocked(true);
+ item.setLocked(true);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
for (Player each : InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY)) {
@@ -284,13 +299,13 @@ public void onAnvil(InventoryClickEvent event) {
}, 6);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- item.setItemStack(itemstack);
+ DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), resultLabel);
DisplayManager.updateItem(item);
DisplayManager.collectItem(item, player);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot0);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot0);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot1);
}, 8);
}, 10);
}, 1);
@@ -396,121 +411,94 @@ public void onCloseAnvil(InventoryCloseEvent event) {
}
}
}
+ removeResultLabel(map);
openedAnvil.remove(block);
}
- public MaterialMode standMode(DisplayEntity stand) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (plain.startsWith("IV.Anvil.")) {
- return MaterialMode.getModeFromName(plain.substring(plain.lastIndexOf(".") + 1));
+ private DisplayEntity spawnResultLabel(Location itemLocation, Component name) {
+ DisplayEntity label = createResultLabel(itemLocation, name);
+ DisplayManager.spawnDisplay(
+ InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), label);
+ return label;
+ }
+
+ private DisplayEntity resultLabel(Map map, Location itemLocation, Component name) {
+ Object value = map.get("2Label");
+ if (value instanceof DisplayEntity label) {
+ label.setCustomName(name);
+ return label;
+ }
+ DisplayEntity label = spawnResultLabel(itemLocation, name);
+ map.put("2Label", label);
+ return label;
+ }
+
+ private void removeResultLabel(Map map) {
+ Object value = map.put("2Label", "N/A");
+ if (value instanceof DisplayEntity label) {
+ DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), label);
}
- return null;
}
- public void toggleStandMode(DisplayEntity stand, String mode) {
- String plainText = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plainText.equals("IV.Anvil.Item")) {
- if (plainText.equals("IV.Anvil.Block")) {
- stand.setCustomName("IV.Anvil.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
+ static DisplayEntity createResultLabel(Location itemLocation, Component name) {
+ DisplayEntity label = new DisplayEntity(resultLabelLocation(itemLocation));
+ configureResultLabel(label, name);
+ return label;
+ }
- }
- if (plainText.equals("IV.Anvil.LowBlock")) {
- stand.setCustomName("IV.Anvil.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
+ static Location resultLabelLocation(Location itemLocation) {
+ return itemLocation.clone().add(0.0, 0.55, 0.0);
+ }
- }
- if (plainText.equals("IV.Anvil.Tool")) {
- stand.setCustomName("IV.Anvil.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plainText.equals("IV.Anvil.Standing")) {
- stand.setCustomName("IV.Anvil.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.Anvil.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.Anvil.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.Anvil.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
- }
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.Anvil.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
- }
+ static void configureResultLabel(DisplayEntity label, Component name) {
+ label.setCustomName(name);
+ label.setCustomNameVisible(true);
+ label.setBillboard(Display.Billboard.CENTER);
+ label.setDefaultBackground(true);
+ label.setTextScale(1.0F);
+ label.setUnboundedTextWidth(true);
+ label.setInterpolationDuration(0);
+ label.setTeleportDuration(0);
}
- public Map spawnDisplayEntitys(Player player, Block block) { //.add(0.68, 0.600781, 0.35)
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.600781, 0.5);
- DisplayEntity center = new DisplayEntity(loc);
+ public MaterialMode standMode(Item stand) {
+ return switch (stand.getRenderMode()) {
+ case ITEM -> MaterialMode.ITEM;
+ case BLOCK -> MaterialMode.BLOCK;
+ case LOW_BLOCK -> MaterialMode.LOWBLOCK;
+ case TOOL -> MaterialMode.TOOL;
+ case STANDING -> MaterialMode.STANDING;
+ default -> null;
+ };
+ }
+
+ public void toggleStandMode(Item stand, MaterialMode mode) {
+ WorkstationDisplayPositioning.setRenderMode(stand, mode);
+ }
+
+ public Map spawnDisplayEntitys(Player player, Block block) {
+ Map map = new HashMap<>();
float yaw = getCardinalDirection(player);
- center.setRotation(yaw, center.getLocation().getPitch());
- setStand(center);
- center.setCustomName("IV.Anvil.Center");
- Vector vector = rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.19), -100).add(center.getLocation().clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity middle = new DisplayEntity(loc.clone().add(vector));
- setStand(middle, yaw);
- DisplayEntity slot0 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Location origin = block.getLocation();
+ Item slot0 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.0);
setStand(slot0, yaw + 20);
- DisplayEntity slot1 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot1 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.0);
setStand(slot1, yaw - 20);
map.put("0", slot0);
map.put("1", slot1);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
+ public void setStand(Item stand, float yaw) {
stand.setGravity(false);
- stand.setInvulnerable(true);
- stand.setVisible(false);
stand.setSilent(true);
- stand.setSmall(true);
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.setCustomName("IV.Anvil.Item");
+ stand.setVelocity(new Vector());
+ stand.setPickupDelay(32767);
stand.setRotation(yaw, stand.getLocation().getPitch());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BannerDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BannerDisplay.java
index f5934a47..f7d70200 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BannerDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BannerDisplay.java
@@ -268,6 +268,7 @@ public Vector getDirection(BlockFace face) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BarrelDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BarrelDisplay.java
index 97225d33..0cc05236 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BarrelDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BarrelDisplay.java
@@ -207,11 +207,10 @@ public void onUseBarrel(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -308,12 +307,10 @@ public void onDragBarrel(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeaconDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeaconDisplay.java
index 40f12142..cedd3cad 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeaconDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeaconDisplay.java
@@ -331,6 +331,7 @@ public Map spawnDisplayEntitys(Block block) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeHiveDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeHiveDisplay.java
index cdb3dd15..fe5ad973 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeHiveDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeHiveDisplay.java
@@ -25,17 +25,21 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityActivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityAddedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityDeactivatedEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@@ -45,8 +49,17 @@
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockBurnEvent;
+import org.bukkit.event.block.BlockDispenseEvent;
+import org.bukkit.event.block.BlockExplodeEvent;
+import org.bukkit.event.block.BlockFadeEvent;
+import org.bukkit.event.block.BlockFromToEvent;
+import org.bukkit.event.block.BlockIgniteEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityEnterBlockEvent;
+import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
@@ -70,9 +83,12 @@ public class BeeHiveDisplay extends VisualizerRunnableDisplay implements Listene
private String filledColor = "&e";
private String noCampfireColor = "&c";
private String beeCountText = "&e{Current}&6/{Max}";
+ private final BlockUpdateScheduler blockUpdates;
public BeeHiveDisplay() {
onReload(new InteractionVisualizerReloadEvent());
+ this.blockUpdates = new BlockUpdateScheduler<>(this::nearbyBeehive,
+ this.checkingPeriod, this.gcPeriod);
}
@EventHandler
@@ -93,6 +109,9 @@ public EntryKey key() {
@Override
public ScheduledTask gc() {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return null;
+ }
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Iterator>> itr = beehiveMap.entrySet().iterator();
int count = 0;
@@ -140,6 +159,21 @@ public ScheduledTask gc() {
@Override
public ScheduledTask run() {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return legacyRun();
+ }
+ return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ int checks = this.blockUpdates.tick(Bukkit.getCurrentTick(),
+ InteractionVisualizer.blockUpdateMaxDirtyPerTick, this::updateHybridBlock);
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(checks, System.nanoTime() - start);
+ }
+ }, 0, 1);
+ }
+
+ private ScheduledTask legacyRun() {
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Set list = nearbyBeehive();
for (Block block : list) {
@@ -168,50 +202,214 @@ public ScheduledTask run() {
}
Block block = entry.getKey();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- updateBlock(block);
+ measureLegacyUpdate(block);
}, delay, block.getLocation());
}
}, 0, checkingPeriod);
}
+ private void measureLegacyUpdate(Block block) {
+ if (!PerformanceMetrics.isCollecting()) {
+ updateBlock(block);
+ return;
+ }
+ long start = System.nanoTime();
+ try {
+ updateBlock(block);
+ } finally {
+ PerformanceMetrics.blockUpdateChecks(1, System.nanoTime() - start);
+ }
+ }
+
+ private boolean updateHybridBlock(Block block) {
+ if (!TileEntityManager.getTileEntities(TileEntityType.BEEHIVE).contains(block)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (block.getType() != Material.BEEHIVE) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isActive(block.getLocation())) {
+ return false;
+ }
+ if (!beehiveMap.containsKey(block)) {
+ beehiveMap.put(block, new HashMap<>(spawnDisplayEntitys(block)));
+ }
+ updateBlock(block);
+ return false;
+ }
+
+ private void markDirty(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null
+ && block.getType() == Material.BEEHIVE && beehiveMap.containsKey(block)) {
+ blockUpdates.markDirty(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ /** Optimized aggregate-listener entry after the affected vertical column has been scanned once. */
+ public void onAffectedBeeBlock(Block block) {
+ markDirty(block);
+ }
+
+ private void markAffectedColumnDirty(Block changedBlock) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates || changedBlock == null) {
+ return;
+ }
+ for (int distance = 1; distance <= 5; distance++) {
+ markDirty(changedBlock.getRelative(BlockFace.UP, distance));
+ }
+ }
+
+ public void onAffectedBlockPlace(BlockPlaceEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlockPlaced());
+ }
+
+ public void onAffectedBlockBreak(BlockBreakEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockBurn(BlockBurnEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockFade(BlockFadeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockIgnite(BlockIgniteEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedFluidFlow(BlockFromToEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getToBlock());
+ }
+
+ public void onAffectedBlockExplode(BlockExplodeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ for (Block block : event.blockList()) {
+ markAffectedColumnDirty(block);
+ }
+ }
+
+ public void onAffectedEntityExplode(EntityExplodeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ for (Block block : event.blockList()) {
+ markAffectedColumnDirty(block);
+ }
+ }
+
+ public void onDispenserHarvest(BlockDispenseEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ Material dispensed = event.getItem().getType();
+ if (dispensed != Material.GLASS_BOTTLE && dispensed != Material.SHEARS) {
+ return;
+ }
+ BlockData data = event.getBlock().getBlockData();
+ if (data instanceof Directional directional) {
+ markDirty(event.getBlock().getRelative(directional.getFacing()));
+ }
+ }
+
+ public void onBeehiveAdded(TileEntityAddedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEEHIVE) {
+ blockUpdates.markDirty(event.getBlock(), (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ public void onBeehiveActivated(TileEntityActivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEEHIVE) {
+ blockUpdates.markDirty(event.getBlock(), (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ public void onBeehiveDeactivated(TileEntityDeactivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEEHIVE) {
+ removeTrackedDisplay(event.getBlock());
+ }
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onBeeEnterBeehive(EntityEnterBlockEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getBlock();
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBeeLeaveBeehive(EntityChangeBlockEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getBlock();
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.MONITOR)
public void onInteractBeehive(PlayerInteractEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getClickedBlock();
if (block != null) {
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBreakBeehive(TileEntityRemovedEvent event) {
- Block block = event.getBlock();
- if (!beehiveMap.containsKey(block)) {
+ removeTrackedDisplay(event.getBlock());
+ }
+
+ private void removeTrackedDisplay(Block block) {
+ blockUpdates.remove(block);
+ Map map = beehiveMap.remove(block);
+ if (map == null) {
return;
}
-
- Map map = beehiveMap.get(block);
if (map.get("0") instanceof DisplayEntity) {
DisplayEntity stand = (DisplayEntity) map.get("0");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
@@ -220,7 +418,6 @@ public void onBreakBeehive(TileEntityRemovedEvent event) {
DisplayEntity stand = (DisplayEntity) map.get("1");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
}
- beehiveMap.remove(block);
}
public void updateBlock(Block block) {
@@ -252,14 +449,10 @@ public void updateBlock(Block block) {
}
String str1 = beeCountText.replace("{Current}", beehiveState.getEntityCount() + "").replace("{Max}", beehiveState.getMaxEntities() + "");
- if (!PlainTextComponentSerializer.plainText().serialize(line0.getCustomName()).equals(str0)) {
- line0.setCustomName(str0);
- line0.setCustomNameVisible(true);
+ if (line0.updateCustomName(str0, true)) {
DisplayManager.updateDisplay(line0);
}
- if (!PlainTextComponentSerializer.plainText().serialize(line1.getCustomName()).equals(str1)) {
- line1.setCustomName(str1);
- line1.setCustomNameVisible(true);
+ if (line1.updateCustomName(str1, true)) {
DisplayManager.updateDisplay(line1);
}
}
@@ -275,19 +468,15 @@ public boolean isActive(Location loc) {
public Map spawnDisplayEntitys(Block block) {
Map map = new HashMap<>();
- Location origin = block.getLocation();
-
BlockData blockData = block.getState().getBlockData();
BlockFace facing = ((Directional) blockData).getFacing();
- Location target = block.getRelative(facing).getLocation();
- Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
- Location loc0 = block.getLocation().clone().add(direction).add(0.5, 0.25, 0.5);
+ Location loc0 = labelLocation(block.getLocation(), facing, 0.25);
loc0.setDirection(facing.getDirection());
DisplayEntity line0 = new DisplayEntity(loc0.clone());
setStand(line0);
- Location loc1 = block.getLocation().clone().add(direction).add(0.5, 0, 0.5);
+ Location loc1 = labelLocation(block.getLocation(), facing, 0.0);
loc1.setDirection(facing.getDirection());
DisplayEntity line1 = new DisplayEntity(loc1.clone());
setStand(line1);
@@ -301,6 +490,11 @@ public Map spawnDisplayEntitys(Block block) {
return map;
}
+ static Location labelLocation(Location blockLocation, BlockFace facing, double lineOffset) {
+ Vector direction = facing.getDirection().multiply(0.7);
+ return blockLocation.clone().add(direction).add(0.5, lineOffset, 0.5);
+ }
+
public void setStand(DisplayEntity stand) {
stand.setBasePlate(false);
stand.setMarker(true);
@@ -311,6 +505,7 @@ public void setStand(DisplayEntity stand) {
stand.setVisible(false);
stand.setCustomName("");
stand.setRightArmPose(EulerAngle.ZERO);
+ stand.useLegacyNameTagGeometry();
}
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeNestDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeNestDisplay.java
index 02b848a8..d707eb79 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeNestDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BeeNestDisplay.java
@@ -25,17 +25,21 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityActivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityAddedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityDeactivatedEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@@ -45,8 +49,17 @@
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockBurnEvent;
+import org.bukkit.event.block.BlockDispenseEvent;
+import org.bukkit.event.block.BlockExplodeEvent;
+import org.bukkit.event.block.BlockFadeEvent;
+import org.bukkit.event.block.BlockFromToEvent;
+import org.bukkit.event.block.BlockIgniteEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.event.entity.EntityEnterBlockEvent;
+import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
@@ -70,9 +83,12 @@ public class BeeNestDisplay extends VisualizerRunnableDisplay implements Listene
private String filledColor = "&e";
private String noCampfireColor = "&c";
private String beeCountText = "&e{Current}&6/{Max}";
+ private final BlockUpdateScheduler blockUpdates;
public BeeNestDisplay() {
onReload(new InteractionVisualizerReloadEvent());
+ this.blockUpdates = new BlockUpdateScheduler<>(this::nearbyBeenest,
+ this.checkingPeriod, this.gcPeriod);
}
@EventHandler
@@ -93,6 +109,9 @@ public EntryKey key() {
@Override
public ScheduledTask gc() {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return null;
+ }
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Iterator>> itr = beenestMap.entrySet().iterator();
int count = 0;
@@ -140,6 +159,21 @@ public ScheduledTask gc() {
@Override
public ScheduledTask run() {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return legacyRun();
+ }
+ return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ int checks = this.blockUpdates.tick(Bukkit.getCurrentTick(),
+ InteractionVisualizer.blockUpdateMaxDirtyPerTick, this::updateHybridBlock);
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(checks, System.nanoTime() - start);
+ }
+ }, 0, 1);
+ }
+
+ private ScheduledTask legacyRun() {
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Set list = nearbyBeenest();
for (Block block : list) {
@@ -168,50 +202,214 @@ public ScheduledTask run() {
}
Block block = entry.getKey();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- updateBlock(block);
+ measureLegacyUpdate(block);
}, delay, block.getLocation());
}
}, 0, checkingPeriod);
}
+ private void measureLegacyUpdate(Block block) {
+ if (!PerformanceMetrics.isCollecting()) {
+ updateBlock(block);
+ return;
+ }
+ long start = System.nanoTime();
+ try {
+ updateBlock(block);
+ } finally {
+ PerformanceMetrics.blockUpdateChecks(1, System.nanoTime() - start);
+ }
+ }
+
+ private boolean updateHybridBlock(Block block) {
+ if (!TileEntityManager.getTileEntities(TileEntityType.BEE_NEST).contains(block)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (block.getType() != Material.BEE_NEST) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isActive(block.getLocation())) {
+ return false;
+ }
+ if (!beenestMap.containsKey(block)) {
+ beenestMap.put(block, new HashMap<>(spawnDisplayEntitys(block)));
+ }
+ updateBlock(block);
+ return false;
+ }
+
+ private void markDirty(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null
+ && block.getType() == Material.BEE_NEST && beenestMap.containsKey(block)) {
+ blockUpdates.markDirty(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ /** Optimized aggregate-listener entry after the affected vertical column has been scanned once. */
+ public void onAffectedBeeBlock(Block block) {
+ markDirty(block);
+ }
+
+ private void markAffectedColumnDirty(Block changedBlock) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates || changedBlock == null) {
+ return;
+ }
+ for (int distance = 1; distance <= 5; distance++) {
+ markDirty(changedBlock.getRelative(BlockFace.UP, distance));
+ }
+ }
+
+ public void onAffectedBlockPlace(BlockPlaceEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlockPlaced());
+ }
+
+ public void onAffectedBlockBreak(BlockBreakEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockBurn(BlockBurnEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockFade(BlockFadeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedBlockIgnite(BlockIgniteEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getBlock());
+ }
+
+ public void onAffectedFluidFlow(BlockFromToEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ markAffectedColumnDirty(event.getToBlock());
+ }
+
+ public void onAffectedBlockExplode(BlockExplodeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ for (Block block : event.blockList()) {
+ markAffectedColumnDirty(block);
+ }
+ }
+
+ public void onAffectedEntityExplode(EntityExplodeEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ for (Block block : event.blockList()) {
+ markAffectedColumnDirty(block);
+ }
+ }
+
+ public void onDispenserHarvest(BlockDispenseEvent event) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ Material dispensed = event.getItem().getType();
+ if (dispensed != Material.GLASS_BOTTLE && dispensed != Material.SHEARS) {
+ return;
+ }
+ BlockData data = event.getBlock().getBlockData();
+ if (data instanceof Directional directional) {
+ markDirty(event.getBlock().getRelative(directional.getFacing()));
+ }
+ }
+
+ public void onBeenestAdded(TileEntityAddedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEE_NEST) {
+ blockUpdates.markDirty(event.getBlock(), (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ public void onBeenestActivated(TileEntityActivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEE_NEST) {
+ blockUpdates.markDirty(event.getBlock(), (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ public void onBeenestDeactivated(TileEntityDeactivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BEE_NEST) {
+ removeTrackedDisplay(event.getBlock());
+ }
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onBeeEnterBeenest(EntityEnterBlockEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getBlock();
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBeeLeaveBeenest(EntityChangeBlockEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getBlock();
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.MONITOR)
public void onInteractBeenest(PlayerInteractEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
if (event.isCancelled()) {
return;
}
Block block = event.getClickedBlock();
if (block != null) {
- Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> updateBlock(block), 1, block.getLocation());
+ Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> measureLegacyUpdate(block), 1, block.getLocation());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBreakBeenest(TileEntityRemovedEvent event) {
- Block block = event.getBlock();
- if (!beenestMap.containsKey(block)) {
+ removeTrackedDisplay(event.getBlock());
+ }
+
+ private void removeTrackedDisplay(Block block) {
+ blockUpdates.remove(block);
+ Map map = beenestMap.remove(block);
+ if (map == null) {
return;
}
-
- Map map = beenestMap.get(block);
if (map.get("0") instanceof DisplayEntity) {
DisplayEntity stand = (DisplayEntity) map.get("0");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
@@ -220,7 +418,6 @@ public void onBreakBeenest(TileEntityRemovedEvent event) {
DisplayEntity stand = (DisplayEntity) map.get("1");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
}
- beenestMap.remove(block);
}
public void updateBlock(Block block) {
@@ -252,14 +449,10 @@ public void updateBlock(Block block) {
}
String str1 = beeCountText.replace("{Current}", beehiveState.getEntityCount() + "").replace("{Max}", beehiveState.getMaxEntities() + "");
- if (!PlainTextComponentSerializer.plainText().serialize(line0.getCustomName()).equals(str0)) {
- line0.setCustomName(str0);
- line0.setCustomNameVisible(true);
+ if (line0.updateCustomName(str0, true)) {
DisplayManager.updateDisplay(line0);
}
- if (!PlainTextComponentSerializer.plainText().serialize(line1.getCustomName()).equals(str1)) {
- line1.setCustomName(str1);
- line1.setCustomNameVisible(true);
+ if (line1.updateCustomName(str1, true)) {
DisplayManager.updateDisplay(line1);
}
}
@@ -275,19 +468,15 @@ public boolean isActive(Location loc) {
public Map spawnDisplayEntitys(Block block) {
Map map = new HashMap<>();
- Location origin = block.getLocation();
-
BlockData blockData = block.getState().getBlockData();
BlockFace facing = ((Directional) blockData).getFacing();
- Location target = block.getRelative(facing).getLocation();
- Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.7);
- Location loc0 = block.getLocation().clone().add(direction).add(0.5, 0.25, 0.5);
+ Location loc0 = labelLocation(block.getLocation(), facing, 0.25);
loc0.setDirection(facing.getDirection());
DisplayEntity line0 = new DisplayEntity(loc0.clone());
setStand(line0);
- Location loc1 = block.getLocation().clone().add(direction).add(0.5, 0, 0.5);
+ Location loc1 = labelLocation(block.getLocation(), facing, 0.0);
loc1.setDirection(facing.getDirection());
DisplayEntity line1 = new DisplayEntity(loc1.clone());
setStand(line1);
@@ -301,6 +490,11 @@ public Map spawnDisplayEntitys(Block block) {
return map;
}
+ static Location labelLocation(Location blockLocation, BlockFace facing, double lineOffset) {
+ Vector direction = facing.getDirection().multiply(0.7);
+ return blockLocation.clone().add(direction).add(0.5, lineOffset, 0.5);
+ }
+
public void setStand(DisplayEntity stand) {
stand.setBasePlate(false);
stand.setMarker(true);
@@ -311,6 +505,7 @@ public void setStand(DisplayEntity stand) {
stand.setVisible(false);
stand.setCustomName("");
stand.setRightArmPose(EulerAngle.ZERO);
+ stand.useLegacyNameTagGeometry();
}
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlastFurnaceDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlastFurnaceDisplay.java
index 596d4622..a7c1c3d4 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlastFurnaceDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlastFurnaceDisplay.java
@@ -25,11 +25,15 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityActivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityAddedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityDeactivatedEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
@@ -38,7 +42,7 @@
import com.loohp.interactionvisualizer.utils.VanishUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -50,9 +54,14 @@
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.FurnaceBurnEvent;
+import org.bukkit.event.inventory.FurnaceExtractEvent;
+import org.bukkit.event.inventory.FurnaceSmeltEvent;
+import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.EulerAngle;
@@ -78,9 +87,12 @@ public class BlastFurnaceDisplay extends VisualizerRunnableDisplay implements Li
private String noFuelColor = "&c";
private int progressBarLength = 10;
private String amountPending = " &7+{Amount}";
+ private final BlockUpdateScheduler blockUpdates;
public BlastFurnaceDisplay() {
onReload(new InteractionVisualizerReloadEvent());
+ this.blockUpdates = new BlockUpdateScheduler<>(this::nearbyBlastFurnace,
+ this.checkingPeriod, this.gcPeriod);
}
@EventHandler
@@ -102,6 +114,9 @@ public EntryKey key() {
@Override
public ScheduledTask gc() {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return null;
+ }
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Iterator>> itr = blastfurnaceMap.entrySet().iterator();
int count = 0;
@@ -149,6 +164,21 @@ public ScheduledTask gc() {
@Override
public ScheduledTask run() {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return legacyRun();
+ }
+ return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ int checks = this.blockUpdates.tick(Bukkit.getCurrentTick(),
+ InteractionVisualizer.blockUpdateMaxDirtyPerTick, this::updateHybridBlock);
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(checks, System.nanoTime() - start);
+ }
+ }, 0, 1);
+ }
+
+ private ScheduledTask legacyRun() {
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Set list = nearbyBlastFurnace();
for (Block block : list) {
@@ -178,15 +208,18 @@ public ScheduledTask run() {
}
Block block = entry.getKey();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- if (!isActive(block.getLocation())) {
- return;
- }
- if (!block.getType().equals(Material.BLAST_FURNACE)) {
- return;
- }
- org.bukkit.block.BlastFurnace blastfurnace = (org.bukkit.block.BlastFurnace) block.getState();
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ try {
+ if (!isActive(block.getLocation())) {
+ return;
+ }
+ if (!block.getType().equals(Material.BLAST_FURNACE)) {
+ return;
+ }
+ org.bukkit.block.BlastFurnace blastfurnace = (org.bukkit.block.BlastFurnace) block.getState();
- {
+ {
Inventory inv = blastfurnace.getInventory();
ItemStack itemstack = inv.getItem(0);
if (itemstack != null) {
@@ -261,25 +294,18 @@ public ScheduledTask run() {
symbol = symbol.replace("{CompletedAmount}", (inv.getItem(2) == null ? 0 : inv.getItem(2).getAmount()) + "");
}
if (hasFuel(blastfurnace)) {
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals(symbol) || !stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(true);
- stand.setCustomName(symbol);
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.showProgress(stand, entry.getValue(), symbol);
} else {
symbol = noFuelColor + ChatColorUtils.stripColor(symbol);
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals(symbol) || !stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(true);
- stand.setCustomName(symbol);
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.showProgress(stand, entry.getValue(), symbol);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals("") || stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(false);
- stand.setCustomName("");
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.hideProgress(stand, entry.getValue());
+ }
+ }
+ } finally {
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(1, System.nanoTime() - start);
}
}
}, delay, block.getLocation());
@@ -287,6 +313,106 @@ public ScheduledTask run() {
}, 0, checkingPeriod);
}
+ private boolean updateHybridBlock(Block block) {
+ if (!TileEntityManager.getTileEntities(TileEntityType.BLAST_FURNACE).contains(block)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isBlastFurnace(block.getType())) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isActive(block.getLocation())) {
+ return false;
+ }
+ Map values = blastfurnaceMap.get(block);
+ if (values == null) {
+ values = new HashMap<>();
+ values.put("Item", "N/A");
+ values.putAll(spawnDisplayEntitys(block));
+ blastfurnaceMap.put(block, values);
+ }
+ org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) block.getState();
+ return FurnaceDisplayUpdater.update(furnace, values, KEY, progressBarCharacter, emptyColor,
+ filledColor, noFuelColor, progressBarLength, amountPending);
+ }
+
+ private void markDirty(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null && isBlastFurnace(block.getType())) {
+ blockUpdates.markDirty(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ private void markDirtyUnlessActive(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null && isBlastFurnace(block.getType())) {
+ blockUpdates.markDirtyUnlessActive(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ /** Optimized aggregate-listener entry after the inventory location has been resolved once. */
+ public void onBlastFurnaceInventoryChanged(Block block) {
+ markDirty(block);
+ }
+
+ private void markDirty(Inventory inventory) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ Location location;
+ try {
+ location = inventory.getLocation();
+ } catch (Exception | AbstractMethodError ignored) {
+ return;
+ }
+ if (location != null) {
+ markDirty(location.getBlock());
+ }
+ }
+
+ public void onBlastFurnaceBurn(FurnaceBurnEvent event) {
+ markDirty(event.getBlock());
+ }
+
+ public void onBlastFurnaceStartSmelt(FurnaceStartSmeltEvent event) {
+ markDirtyUnlessActive(event.getBlock());
+ }
+
+ public void onBlastFurnaceSmelt(FurnaceSmeltEvent event) {
+ markDirtyUnlessActive(event.getBlock());
+ }
+
+ public void onBlastFurnaceExtract(FurnaceExtractEvent event) {
+ markDirty(event.getBlock());
+ }
+
+ public void onBlastFurnaceMoveItem(InventoryMoveItemEvent event) {
+ markDirty(event.getSource());
+ markDirty(event.getDestination());
+ }
+
+ public void onBlastFurnaceAdded(TileEntityAddedEvent event) {
+ if (event.getTileEntityType() == TileEntityType.BLAST_FURNACE) {
+ markDirty(event.getBlock());
+ }
+ }
+
+ public void onBlastFurnaceActivated(TileEntityActivatedEvent event) {
+ if (event.getTileEntityType() == TileEntityType.BLAST_FURNACE) {
+ markDirty(event.getBlock());
+ }
+ }
+
+ public void onBlastFurnaceDeactivated(TileEntityDeactivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.BLAST_FURNACE) {
+ removeTrackedDisplay(event.getBlock());
+ }
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onBlastFurnace(InventoryClickEvent event) {
if (VanishUtils.isVanished((Player) event.getWhoClicked())) {
@@ -348,6 +474,7 @@ public void onBlastFurnace(InventoryClickEvent event) {
if (!event.getView().getTopInventory().getLocation().getBlock().getType().equals(Material.BLAST_FURNACE)) {
return;
}
+ markDirty(event.getView().getTopInventory().getLocation().getBlock());
Block block = event.getView().getTopInventory().getLocation().getBlock();
@@ -438,6 +565,7 @@ public void onDragBlastFurnace(InventoryDragEvent event) {
for (int slot : event.getRawSlots()) {
if (slot >= 0 && slot <= 2) {
+ markDirty(event.getView().getTopInventory().getLocation().getBlock());
DisplayManager.sendHandMovement(InteractionVisualizerAPI.getPlayers(), (Player) event.getWhoClicked());
break;
}
@@ -446,12 +574,15 @@ public void onDragBlastFurnace(InventoryDragEvent event) {
@EventHandler(priority = EventPriority.MONITOR)
public void onBreakBlastFurnace(TileEntityRemovedEvent event) {
- Block block = event.getBlock();
- if (!blastfurnaceMap.containsKey(block)) {
+ removeTrackedDisplay(event.getBlock());
+ }
+
+ private void removeTrackedDisplay(Block block) {
+ blockUpdates.remove(block);
+ Map map = blastfurnaceMap.remove(block);
+ if (map == null) {
return;
}
-
- Map map = blastfurnaceMap.get(block);
if (map.get("Item") instanceof Item) {
Item item = (Item) map.get("Item");
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
@@ -460,7 +591,6 @@ public void onBreakBlastFurnace(TileEntityRemovedEvent event) {
DisplayEntity stand = (DisplayEntity) map.get("Stand");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
}
- blastfurnaceMap.remove(block);
}
public boolean hasItemToCook(org.bukkit.block.BlastFurnace blastfurnace) {
@@ -490,6 +620,10 @@ public boolean isActive(Location loc) {
return PlayerLocationManager.hasPlayerNearby(loc);
}
+ private boolean isBlastFurnace(Material material) {
+ return material == Material.BLAST_FURNACE;
+ }
+
public Map spawnDisplayEntitys(Block block) {
Map map = new HashMap<>();
Location origin = block.getLocation();
@@ -512,6 +646,7 @@ public Map spawnDisplayEntitys(Block block) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlockUpdateScheduler.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlockUpdateScheduler.java
new file mode 100644
index 00000000..238ecfbe
--- /dev/null
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BlockUpdateScheduler.java
@@ -0,0 +1,306 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.blocks;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.function.Supplier;
+
+/**
+ * Main-thread scheduler for event-driven block displays.
+ *
+ *
Dirty entries are coalesced by key and become eligible on an explicit
+ * tick. Active entries are rotated through a reusable queue so each entry is
+ * checked approximately once per configured update period. Callers can absorb
+ * synchronized level signals for already-active entries into that cadence
+ * without weakening urgent interaction edges. A weakly
+ * consistent iterator performs a bounded safety audit without allocating a
+ * snapshot of every tracked block. The dirty cap supplied to {@link #tick}
+ * applies to this scheduler instance (one display type), not globally across
+ * every display.
+ */
+final class BlockUpdateScheduler {
+
+ @FunctionalInterface
+ interface Updater {
+ /**
+ * @return {@code true} while the value needs periodic active updates
+ */
+ boolean update(T value);
+ }
+
+ private final Supplier extends Collection> auditSource;
+ private final int activePeriod;
+ private final int auditPeriod;
+ private final NavigableMap> dirtyByTick;
+ private final Map dirtyDueTick;
+ private final NavigableMap> activeByTick;
+ private final Map activeDueTick;
+ private final HashSet processedThisTick;
+ private final HashSet removedValues;
+
+ private Iterator auditIterator;
+ private long nextAuditTick;
+ private int auditBudget;
+ private int activeBudget;
+ private boolean initialAudit;
+
+ BlockUpdateScheduler(Supplier extends Collection> auditSource, int activePeriod, int auditPeriod) {
+ this.auditSource = Objects.requireNonNull(auditSource, "auditSource");
+ this.activePeriod = Math.max(1, activePeriod);
+ this.auditPeriod = Math.max(1, auditPeriod);
+ this.dirtyByTick = new TreeMap<>();
+ this.dirtyDueTick = new HashMap<>();
+ this.activeByTick = new TreeMap<>();
+ this.activeDueTick = new HashMap<>();
+ this.processedThisTick = new HashSet<>();
+ this.removedValues = new HashSet<>();
+ this.nextAuditTick = Long.MIN_VALUE;
+ this.activeBudget = 0;
+ this.initialAudit = true;
+ }
+
+ void markDirty(T value, long readyTick) {
+ if (value == null) {
+ return;
+ }
+ this.removedValues.remove(value);
+ Long previousTick = this.dirtyDueTick.get(value);
+ if (previousTick != null && previousTick <= readyTick) {
+ return;
+ }
+ if (previousTick != null) {
+ LinkedHashSet previous = this.dirtyByTick.get(previousTick);
+ if (previous != null) {
+ previous.remove(value);
+ if (previous.isEmpty()) {
+ this.dirtyByTick.remove(previousTick);
+ }
+ }
+ }
+ this.dirtyDueTick.put(value, readyTick);
+ this.dirtyByTick.computeIfAbsent(readyTick, ignored -> new LinkedHashSet<>()).add(value);
+ }
+
+ /**
+ * Coalesces a non-urgent level signal into the existing active cadence.
+ * Inactive values still enter the dirty queue so the signal can bootstrap
+ * tracking. Urgent player, inventory and lifecycle edges must use
+ * {@link #markDirty(Object, long)}.
+ */
+ void markDirtyUnlessActive(T value, long readyTick) {
+ if (value != null && this.activeDueTick.containsKey(value)) {
+ return;
+ }
+ this.markDirty(value, readyTick);
+ }
+
+ void markActive(T value, long readyTick) {
+ this.removedValues.remove(value);
+ this.schedule(value, readyTick, this.activeDueTick, this.activeByTick);
+ }
+
+ void remove(T value) {
+ if (value == null) {
+ return;
+ }
+ if (this.auditIterator != null) {
+ this.removedValues.add(value);
+ }
+ this.unschedule(value, this.activeDueTick, this.activeByTick);
+ this.unschedule(value, this.dirtyDueTick, this.dirtyByTick);
+ }
+
+ int tick(long tick, int maxDirtyPerTick, Updater updater) {
+ Objects.requireNonNull(updater, "updater");
+ int dirtyBudget = Math.max(1, maxDirtyPerTick);
+ this.processedThisTick.clear();
+
+ int checks = this.drainDirty(tick, dirtyBudget, updater);
+ checks += this.drainActive(tick, updater);
+ checks += this.drainAudit(tick, dirtyBudget, updater);
+ return checks;
+ }
+
+ private int drainDirty(long tick, int budget, Updater updater) {
+ int checks = 0;
+ while (checks < budget) {
+ Map.Entry> entry = this.dirtyByTick.firstEntry();
+ if (entry == null || entry.getKey() > tick) {
+ break;
+ }
+ Iterator iterator = entry.getValue().iterator();
+ if (!iterator.hasNext()) {
+ this.dirtyByTick.pollFirstEntry();
+ continue;
+ }
+ T value = iterator.next();
+ iterator.remove();
+ this.dirtyDueTick.remove(value, entry.getKey());
+ if (entry.getValue().isEmpty()) {
+ this.dirtyByTick.pollFirstEntry();
+ }
+ if (this.updateOnce(value, tick, updater)) {
+ checks++;
+ }
+ }
+ return checks;
+ }
+
+ private int drainActive(long tick, Updater updater) {
+ if (this.activeDueTick.isEmpty()) {
+ this.activeBudget = 0;
+ return 0;
+ }
+ this.activeBudget = Math.max(this.activeBudget,
+ Math.max(1, (this.activeDueTick.size() + this.activePeriod - 1) / this.activePeriod));
+ int checks = 0;
+ while (checks < this.activeBudget) {
+ Map.Entry> entry = this.activeByTick.firstEntry();
+ if (entry == null || entry.getKey() > tick) {
+ break;
+ }
+ Iterator iterator = entry.getValue().iterator();
+ if (!iterator.hasNext()) {
+ this.activeByTick.pollFirstEntry();
+ continue;
+ }
+ T value = iterator.next();
+ iterator.remove();
+ this.activeDueTick.remove(value, entry.getKey());
+ if (entry.getValue().isEmpty()) {
+ this.activeByTick.pollFirstEntry();
+ }
+ if (this.removedValues.contains(value)) {
+ continue;
+ }
+ if (this.processedThisTick.contains(value)) {
+ this.markActive(value, tick + this.activePeriod);
+ continue;
+ }
+ boolean keepActive = this.updateAndRemember(value, updater);
+ checks++;
+ if (keepActive) {
+ this.markActive(value, tick + this.activePeriod);
+ }
+ }
+ return checks;
+ }
+
+ private int drainAudit(long tick, int initialBudget, Updater updater) {
+ if (this.auditIterator == null && tick >= this.nextAuditTick) {
+ Collection source = this.auditSource.get();
+ this.auditIterator = source.iterator();
+ this.auditBudget = this.initialAudit
+ ? initialBudget
+ : Math.max(1, (source.size() + this.auditPeriod - 1) / this.auditPeriod);
+ this.initialAudit = false;
+ this.nextAuditTick = tick + this.auditPeriod;
+ }
+ if (this.auditIterator == null) {
+ return 0;
+ }
+
+ int checks = 0;
+ int attempts = this.auditBudget;
+ while (attempts-- > 0 && this.auditIterator.hasNext()) {
+ T value = this.auditIterator.next();
+ if (this.updateOnce(value, tick, updater)) {
+ checks++;
+ }
+ }
+ if (!this.auditIterator.hasNext()) {
+ this.auditIterator = null;
+ this.removedValues.clear();
+ }
+ return checks;
+ }
+
+ private boolean updateOnce(T value, long tick, Updater updater) {
+ if (this.removedValues.contains(value)) {
+ return false;
+ }
+ if (!this.processedThisTick.add(value)) {
+ return false;
+ }
+ boolean keepActive = updater.update(value);
+ if (keepActive) {
+ this.unschedule(value, this.activeDueTick, this.activeByTick);
+ this.markActive(value, tick + this.activePeriod);
+ } else {
+ this.unschedule(value, this.activeDueTick, this.activeByTick);
+ }
+ return true;
+ }
+
+ private boolean updateAndRemember(T value, Updater updater) {
+ this.processedThisTick.add(value);
+ return updater.update(value);
+ }
+
+ private void schedule(T value, long readyTick, Map dueTicks,
+ NavigableMap> byTick) {
+ if (value == null) {
+ return;
+ }
+ Long previousTick = dueTicks.get(value);
+ if (previousTick != null && previousTick <= readyTick) {
+ return;
+ }
+ if (previousTick != null) {
+ LinkedHashSet previous = byTick.get(previousTick);
+ if (previous != null) {
+ previous.remove(value);
+ if (previous.isEmpty()) {
+ byTick.remove(previousTick);
+ }
+ }
+ }
+ dueTicks.put(value, readyTick);
+ byTick.computeIfAbsent(readyTick, ignored -> new LinkedHashSet<>()).add(value);
+ }
+
+ private void unschedule(T value, Map dueTicks,
+ NavigableMap> byTick) {
+ Long dueTick = dueTicks.remove(value);
+ if (dueTick == null) {
+ return;
+ }
+ LinkedHashSet values = byTick.get(dueTick);
+ if (values != null) {
+ values.remove(value);
+ if (values.isEmpty()) {
+ byTick.remove(dueTick);
+ }
+ }
+ }
+
+ int pendingDirtyCount() {
+ return this.dirtyDueTick.size();
+ }
+
+ int activeCount() {
+ return this.activeDueTick.size();
+ }
+
+ int invalidatedAuditValueCount() {
+ return this.removedValues.size();
+ }
+
+}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BrewingStandDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BrewingStandDisplay.java
index d113d3f8..2b4e1c85 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/BrewingStandDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/BrewingStandDisplay.java
@@ -36,7 +36,6 @@
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -219,17 +218,17 @@ public ScheduledTask run() {
if (brewingstand.getFuelLevel() == 0) {
DisplayEntity stand = (DisplayEntity) entry.getValue().get("Stand");
if (hasPotion(brewingstand)) {
- stand.setCustomNameVisible(true);
String name = noFuelColor;
for (int i = 0; i < progressBarLength; i++) {
name += progressBarCharacter;
}
- stand.setCustomName(name);
- DisplayManager.updateDisplay(stand);
+ if (stand.updateCustomName(name, true)) {
+ DisplayManager.updateDisplay(stand);
+ }
} else {
- stand.setCustomNameVisible(false);
- stand.setCustomName("");
- DisplayManager.updateDisplay(stand);
+ if (stand.updateCustomName("", false)) {
+ DisplayManager.updateDisplay(stand);
+ }
}
} else {
DisplayEntity stand = (DisplayEntity) entry.getValue().get("Stand");
@@ -252,15 +251,11 @@ public ScheduledTask run() {
for (i = progressBarLength - 1; i >= percentagescaled; i--) {
symbol += emptyColor + progressBarCharacter;
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals(symbol) || !stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(true);
- stand.setCustomName(symbol);
+ if (stand.updateCustomName(symbol, true)) {
DisplayManager.updateDisplay(stand);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals("") || stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(false);
- stand.setCustomName("");
+ if (stand.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand);
}
}
@@ -393,6 +388,11 @@ public Map spawnDisplayEntitys(Block block) { //.add(0.68
}
public void setStand(DisplayEntity stand) {
+ configureLabel(stand);
+ }
+
+ static void configureLabel(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CampfireDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CampfireDisplay.java
index 6d769a17..ee36c1e3 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CampfireDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CampfireDisplay.java
@@ -35,7 +35,6 @@
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@@ -242,15 +241,11 @@ public ScheduledTask run() {
symbol += emptyColor + progressBarCharacter;
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand1.getCustomName()).equals(symbol) || !stand1.isCustomNameVisible()) {
- stand1.setCustomNameVisible(true);
- stand1.setCustomName(symbol);
+ if (stand1.updateCustomName(symbol, true)) {
DisplayManager.updateDisplay(stand1);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand1.getCustomName()).equals("") || stand1.isCustomNameVisible()) {
- stand1.setCustomNameVisible(false);
- stand1.setCustomName("");
+ if (stand1.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand1);
}
}
@@ -275,15 +270,11 @@ public ScheduledTask run() {
symbol += emptyColor + progressBarCharacter;
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand2.getCustomName()).equals(symbol) || !stand2.isCustomNameVisible()) {
- stand2.setCustomNameVisible(true);
- stand2.setCustomName(symbol);
+ if (stand2.updateCustomName(symbol, true)) {
DisplayManager.updateDisplay(stand2);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand2.getCustomName()).equals("") || stand2.isCustomNameVisible()) {
- stand2.setCustomNameVisible(false);
- stand2.setCustomName("");
+ if (stand2.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand2);
}
}
@@ -308,15 +299,11 @@ public ScheduledTask run() {
symbol += emptyColor + progressBarCharacter;
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand3.getCustomName()).equals(symbol) || !stand3.isCustomNameVisible()) {
- stand3.setCustomNameVisible(true);
- stand3.setCustomName(symbol);
+ if (stand3.updateCustomName(symbol, true)) {
DisplayManager.updateDisplay(stand3);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand3.getCustomName()).equals("") || stand3.isCustomNameVisible()) {
- stand3.setCustomNameVisible(false);
- stand3.setCustomName("");
+ if (stand3.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand3);
}
}
@@ -341,15 +328,11 @@ public ScheduledTask run() {
symbol += emptyColor + progressBarCharacter;
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand4.getCustomName()).equals(symbol) || !stand4.isCustomNameVisible()) {
- stand4.setCustomNameVisible(true);
- stand4.setCustomName(symbol);
+ if (stand4.updateCustomName(symbol, true)) {
DisplayManager.updateDisplay(stand4);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand4.getCustomName()).equals("") || stand4.isCustomNameVisible()) {
- stand4.setCustomNameVisible(false);
- stand4.setCustomName("");
+ if (stand4.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand4);
}
}
@@ -403,7 +386,7 @@ public Map spawnDisplayEntitys(Block block) {
Location target = block.getRelative(facing).getLocation();
Vector direction = rotateVectorAroundY(target.toVector().subtract(origin.toVector()).multiply(0.44194173), 135);
- Location loc = origin.clone().add(0.5, 0.3, 0.5);
+ Location loc = labelOrigin(origin);
DisplayEntity slot1 = new DisplayEntity(loc.clone().add(direction));
setStand(slot1);
DisplayEntity slot2 = new DisplayEntity(loc.clone().add(rotateVectorAroundY(direction.clone(), 90)));
@@ -426,7 +409,16 @@ public Map spawnDisplayEntitys(Block block) {
return map;
}
+ static Location labelOrigin(Location origin) {
+ return origin.clone().add(0.5, 0.3, 0.5);
+ }
+
public void setStand(DisplayEntity stand) {
+ configureLabel(stand);
+ }
+
+ static void configureLabel(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CartographyTableDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CartographyTableDisplay.java
index 432833bd..3f34fa79 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CartographyTableDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CartographyTableDisplay.java
@@ -24,7 +24,8 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI;
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerInteractDisplay;
-import com.loohp.interactionvisualizer.entityholders.ItemFrame;
+import com.loohp.interactionvisualizer.entityholders.Item;
+import com.loohp.interactionvisualizer.entityholders.Item.RenderMode;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.utils.VanishUtils;
@@ -32,10 +33,10 @@
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
+import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
-import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@@ -98,11 +99,11 @@ public void run() {
}
}
- if (map.get("Item") instanceof ItemFrame) {
- Entity entity = (Entity) map.get("Item");
- DisplayManager.removeItemFrame(InteractionVisualizerAPI.getPlayers(), (ItemFrame) entity);
+ if (map.get("Item") instanceof Item item) {
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
openedCTable.remove(block);
+ playermap.remove((Player) map.get("Player"), block);
}
}.runTaskLater(InteractionVisualizer.plugin, delay, block.getLocation());
}
@@ -168,31 +169,37 @@ public void process(Player player) {
itemstack = output;
}
- ItemFrame item = null;
- if (!block.getRelative(BlockFace.UP).getType().isSolid()) {
- if (map.get("Item") instanceof String) {
- if (itemstack != null) {
- item = new ItemFrame(block.getRelative(BlockFace.UP).getLocation());
- item.setItem(itemstack);
- item.setFacingDirection(BlockFace.UP);
- item.setSilent(true);
- map.put("Item", item);
- DisplayManager.sendItemFrameSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), item);
- DisplayManager.updateItemFrame(item);
- } else {
- map.put("Item", "N/A");
- }
+ if (block.getRelative(BlockFace.UP).getType().isSolid()) {
+ if (map.get("Item") instanceof Item item) {
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
+ map.put("Item", "N/A");
+ }
+ return;
+ }
+
+ if (map.get("Item") instanceof String) {
+ if (itemstack != null) {
+ Location location = block.getLocation().clone().add(0.5, 1.03125, 0.5);
+ location.setPitch(-90.0F);
+ Item item = new Item(location, RenderMode.FRAME);
+ item.setItemStack(itemstack);
+ item.setSilent(true);
+ map.put("Item", item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), item);
+ DisplayManager.updateItem(item);
} else {
- item = (ItemFrame) map.get("Item");
- if (itemstack != null) {
- if (!item.getItem().equals(itemstack)) {
- item.setItem(itemstack);
- DisplayManager.updateItemFrame(item);
- }
- } else {
- map.put("Item", "N/A");
- DisplayManager.removeItemFrame(InteractionVisualizerAPI.getPlayers(), item);
+ map.put("Item", "N/A");
+ }
+ } else {
+ Item item = (Item) map.get("Item");
+ if (itemstack != null) {
+ if (!item.getItemStack().equals(itemstack)) {
+ item.setItemStack(itemstack);
+ DisplayManager.updateItem(item);
}
+ } else {
+ map.put("Item", "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
}
}
@@ -230,27 +237,21 @@ public void onDragCartographyTable(InventoryDragEvent event) {
@EventHandler
public void onCloseCartographyTable(InventoryCloseEvent event) {
- if (!playermap.containsKey((Player) event.getPlayer())) {
- return;
- }
-
- Block block = playermap.get((Player) event.getPlayer());
-
- if (!openedCTable.containsKey(block)) {
+ Player player = (Player) event.getPlayer();
+ Block block = playermap.remove(player);
+ if (block == null) {
return;
}
Map map = openedCTable.get(block);
- if (!map.get("Player").equals(event.getPlayer())) {
+ if (map == null || !map.get("Player").equals(player)) {
return;
}
- if (map.get("Item") instanceof ItemFrame) {
- ItemFrame entity = (ItemFrame) map.get("Item");
- DisplayManager.removeItemFrame(InteractionVisualizerAPI.getPlayers(), entity);
+ if (map.get("Item") instanceof Item item) {
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
openedCTable.remove(block);
- playermap.remove((Player) event.getPlayer());
}
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ChestDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ChestDisplay.java
index ccdf426e..5b039c0c 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ChestDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ChestDisplay.java
@@ -216,11 +216,10 @@ public void onUseChest(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList- ());
}
@@ -323,12 +322,10 @@ public void onDragChest(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ConduitDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ConduitDisplay.java
index 21098065..8157285d 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ConduitDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ConduitDisplay.java
@@ -271,6 +271,7 @@ public Map spawnDisplayEntitys(Block block) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CrafterDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CrafterDisplay.java
index 691b029b..9ee6cc48 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CrafterDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CrafterDisplay.java
@@ -26,16 +26,16 @@
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
-import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
+import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
import com.loohp.interactionvisualizer.utils.MaterialUtils;
+import com.loohp.interactionvisualizer.utils.WorkstationDisplayPositioning;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -51,7 +51,6 @@
import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
@@ -103,9 +102,9 @@ public ScheduledTask gc() {
if (!isActive(block.getLocation())) {
Map map = entry.getValue();
for (int i = 1; i <= 9; i++) {
- if (map.get(String.valueOf(i)) instanceof DisplayEntity) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
+ if (map.get(String.valueOf(i)) instanceof Item) {
+ Item stand = (Item) map.get(String.valueOf(i));
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), stand);
}
}
crafterMap.remove(block);
@@ -114,9 +113,9 @@ public ScheduledTask gc() {
if (!block.getType().equals(Material.CRAFTER) || getCardinalDirection(block) < 0F) {
Map map = entry.getValue();
for (int i = 1; i <= 9; i++) {
- if (map.get(String.valueOf(i)) instanceof DisplayEntity) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
+ if (map.get(String.valueOf(i)) instanceof Item) {
+ Item stand = (Item) map.get(String.valueOf(i));
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), stand);
}
}
crafterMap.remove(block);
@@ -160,7 +159,10 @@ public ScheduledTask run() {
}
public void handleUpdate(Block block, Map map) {
- Crafter crafter = (Crafter) block.getState();
+ if (block.getType() != Material.CRAFTER || crafterMap.get(block) != map
+ || !(block.getState() instanceof Crafter crafter)) {
+ return;
+ }
Inventory inventory = crafter.getInventory();
ItemStack[] items = new ItemStack[] {
@@ -176,30 +178,30 @@ public void handleUpdate(Block block, Map map) {
};
for (int i = 0; i < 9; i++) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i + 1));
- ItemStack item = items[i];
+ Item stand = (Item) map.get(String.valueOf(i + 1));
+ ItemStack itemStack = items[i];
if (crafter.isSlotDisabled(i)) {
- item = new ItemStack(Material.BARRIER);
- } else if (item == null || item.getType().equals(Material.AIR)) {
- item = null;
+ itemStack = new ItemStack(Material.BARRIER);
+ } else if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
+ itemStack = ItemStack.empty();
}
- if (item != null) {
- MaterialUtils.MaterialMode materialMode = MaterialUtils.getMaterialType(item);
- boolean changed = materialMode != standMode(stand);
+ if (!itemStack.isEmpty()) {
+ Item.RenderMode renderMode = renderMode(MaterialUtils.getMaterialType(itemStack));
+ boolean changed = renderMode != standMode(stand);
if (changed) {
- toggleStandMode(stand, materialMode.toString());
+ toggleStandMode(stand, renderMode);
}
- if (!item.equals(stand.getItemInMainHand())) {
+ if (!itemStack.equals(stand.getItemStack())) {
changed = true;
- stand.setItemInMainHand(item);
+ stand.setItemStack(itemStack);
}
if (changed) {
- DisplayManager.updateDisplay(stand);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getItemInMainHand().getType().equals(Material.AIR)) {
- stand.setItemInMainHand(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
}
@@ -278,7 +280,7 @@ public void onUseCrafter(InventoryClickEvent event) {
return;
}
- if (event.getRawSlot() >= 0 && event.getRawSlot() <= 2) {
+ if (event.getRawSlot() >= 0 && event.getRawSlot() <= 8) {
DisplayManager.sendHandMovement(InteractionVisualizerAPI.getPlayers(), (Player) event.getWhoClicked());
}
Map map = crafterMap.get(block);
@@ -296,9 +298,9 @@ public void onBreakCrafter(TileEntityRemovedEvent event) {
Map map = crafterMap.get(block);
for (int i = 1; i <= 9; i++) {
- if (map.get(String.valueOf(i)) instanceof DisplayEntity) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
+ if (map.get(String.valueOf(i)) instanceof Item) {
+ Item stand = (Item) map.get(String.valueOf(i));
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), stand);
}
}
crafterMap.remove(block);
@@ -312,108 +314,45 @@ public boolean isActive(Location loc) {
return PlayerLocationManager.hasPlayerNearby(loc);
}
- public MaterialUtils.MaterialMode standMode(DisplayEntity stand) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (plain.startsWith("IV.Crafter.")) {
- return MaterialUtils.MaterialMode.getModeFromName(plain.substring(plain.lastIndexOf(".") + 1));
- }
- return null;
+ public Item.RenderMode standMode(Item stand) {
+ return stand.getRenderMode();
}
- public void toggleStandMode(DisplayEntity stand, String mode) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plain.equals("IV.Crafter.Item")) {
- if (plain.equals("IV.Crafter.Block")) {
- stand.setCustomName("IV.Crafter.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
-
- }
- if (plain.equals("IV.Crafter.LowBlock")) {
- stand.setCustomName("IV.Crafter.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
+ private static Item.RenderMode renderMode(MaterialUtils.MaterialMode mode) {
+ return switch (mode) {
+ case ITEM -> Item.RenderMode.ITEM;
+ case BLOCK -> Item.RenderMode.BLOCK;
+ case LOWBLOCK -> Item.RenderMode.LOW_BLOCK;
+ case TOOL -> Item.RenderMode.TOOL;
+ case STANDING -> Item.RenderMode.STANDING;
+ };
+ }
- }
- if (plain.equals("IV.Crafter.Tool")) {
- stand.setCustomName("IV.Crafter.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plain.equals("IV.Crafter.Standing")) {
- stand.setCustomName("IV.Crafter.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.Crafter.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.Crafter.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.Crafter.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
- }
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.Crafter.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
- }
+ public void toggleStandMode(Item stand, Item.RenderMode mode) {
+ WorkstationDisplayPositioning.setRenderMode(stand, mode);
}
- public Map spawnDisplayEntitys(Block block) { //.add(0.68, 0.600781, 0.35)
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.600781, 0.5);
- DisplayEntity center = new DisplayEntity(loc);
+ public Map spawnDisplayEntitys(Block block) {
+ Map map = new HashMap<>();
float yaw = getCardinalDirection(block);
- center.setRotation(yaw, center.getLocation().getPitch());
- setStand(center);
- center.setCustomName("IV.Crafter.Center");
- Vector vector = rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.19), -100).add(center.getLocation().clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity slot5 = new DisplayEntity(loc.clone().add(vector));
+ Location origin = block.getLocation();
+ Item slot5 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, 0.0);
setStand(slot5, yaw);
- DisplayEntity slot2 = new DisplayEntity(slot5.getLocation().clone().add(center.getLocation().clone().getDirection().normalize().multiply(0.2)));
+ Item slot2 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, 0.2);
setStand(slot2, yaw);
- DisplayEntity slot1 = new DisplayEntity(slot2.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot1 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.2);
setStand(slot1, yaw);
- DisplayEntity slot3 = new DisplayEntity(slot2.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot3 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.2);
setStand(slot3, yaw);
- DisplayEntity slot4 = new DisplayEntity(slot5.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot4 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.0);
setStand(slot4, yaw);
- DisplayEntity slot6 = new DisplayEntity(slot5.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot6 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.0);
setStand(slot6, yaw);
- DisplayEntity slot8 = new DisplayEntity(slot5.getLocation().clone().add(center.getLocation().getDirection().clone().normalize().multiply(-0.2)));
+ Item slot8 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, -0.2);
setStand(slot8, yaw);
- DisplayEntity slot7 = new DisplayEntity(slot8.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot7 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, -0.2);
setStand(slot7, yaw);
- DisplayEntity slot9 = new DisplayEntity(slot8.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot9 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, -0.2);
setStand(slot9, yaw);
map.put("1", slot1);
@@ -426,54 +365,35 @@ public Map spawnDisplayEntitys(Block block) { //.add(0.68
map.put("8", slot8);
map.put("9", slot9);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot3);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot4);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot5);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot6);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot7);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot8);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot9);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot3);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot4);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot5);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot6);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot7);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot8);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot9);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
- stand.setGravity(false);
- stand.setSmall(true);
- stand.setInvulnerable(true);
- stand.setVisible(false);
- stand.setSilent(true);
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.setCustomName("IV.Crafter.Item");
+ public void setStand(Item stand, float yaw) {
stand.setRotation(yaw, stand.getLocation().getPitch());
}
- public void setStand(DisplayEntity stand) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
- stand.setGravity(false);
- stand.setSmall(true);
- stand.setSilent(true);
- stand.setInvulnerable(true);
- stand.setVisible(false);
- }
-
+ /**
+ * Retained for binary compatibility with integrations compiled against the
+ * historical public helper.
+ */
public Vector rotateVectorAroundY(Vector vector, double degrees) {
- double rad = Math.toRadians(degrees);
-
+ double radians = Math.toRadians(degrees);
double currentX = vector.getX();
double currentZ = vector.getZ();
-
- double cosine = Math.cos(rad);
- double sine = Math.sin(rad);
-
- return new Vector((cosine * currentX - sine * currentZ), vector.getY(), (sine * currentX + cosine * currentZ));
+ double cosine = Math.cos(radians);
+ double sine = Math.sin(radians);
+ return new Vector(cosine * currentX - sine * currentZ, vector.getY(),
+ sine * currentX + cosine * currentZ);
}
public float getCardinalDirection(Block block) {
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CraftingTableDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CraftingTableDisplay.java
index 47de2875..8eac80a2 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/CraftingTableDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/CraftingTableDisplay.java
@@ -24,7 +24,6 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI;
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerInteractDisplay;
-import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
@@ -33,10 +32,10 @@
import com.loohp.interactionvisualizer.utils.MaterialUtils;
import com.loohp.interactionvisualizer.utils.MaterialUtils.MaterialMode;
import com.loohp.interactionvisualizer.utils.VanishUtils;
+import com.loohp.interactionvisualizer.utils.WorkstationDisplayPositioning;
import com.loohp.interactionvisualizer.scheduler.ScheduledRunnable;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
@@ -56,8 +55,6 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
@@ -113,16 +110,15 @@ public void run() {
if (!(map.get(String.valueOf(i)) instanceof String)) {
Object entity = map.get(String.valueOf(i));
if (i == 5) {
- InteractionVisualizer.lightManager.deleteLight(((DisplayEntity) entity).getLocation());
+ InteractionVisualizer.lightManager.deleteLight(((Item) entity).getLocation());
}
if (entity instanceof Item) {
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), (Item) entity);
- } else if (entity instanceof DisplayEntity) {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), (DisplayEntity) entity);
}
}
}
openedBenches.remove(block);
+ playermap.remove(player, block);
}
}.runTaskLater(InteractionVisualizer.plugin, delay, block.getLocation());
}
@@ -182,68 +178,65 @@ public void process(Player player) {
view.getItem(9)
};
- if (view.getItem(0) != null) {
- ItemStack itemstack = view.getItem(0);
- if (itemstack == null || itemstack.getType().equals(Material.AIR)) {
- itemstack = null;
+ ItemStack itemstack = view.getItem(0);
+ if (itemstack != null && itemstack.getType().equals(Material.AIR)) {
+ itemstack = null;
+ }
+ if (map.get("0") instanceof String) {
+ if (itemstack != null) {
+ Item item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ item.setItemStack(itemstack);
+ item.setVelocity(new Vector(0, 0, 0));
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ map.put("0", item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ DisplayManager.updateItem(item);
+ } else {
+ map.put("0", "N/A");
}
- Item item = null;
- if (map.get("0") instanceof String) {
- if (itemstack != null) {
- item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ } else {
+ Item item = (Item) map.get("0");
+ if (itemstack != null) {
+ if (!item.getItemStack().equals(itemstack)) {
item.setItemStack(itemstack);
- item.setVelocity(new Vector(0, 0, 0));
- item.setPickupDelay(32767);
- item.setGravity(false);
- map.put("0", item);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
DisplayManager.updateItem(item);
- } else {
- map.put("0", "N/A");
}
} else {
- item = (Item) map.get("0");
- if (itemstack != null) {
- if (!item.getItemStack().equals(itemstack)) {
- item.setItemStack(itemstack);
- DisplayManager.updateItem(item);
- }
- } else {
- map.put("0", "N/A");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
- }
+ map.put("0", "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
}
for (int i = 0; i < 9; i++) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i + 1));
- ItemStack item = items[i];
- if (item == null || item.getType().equals(Material.AIR)) {
- item = null;
+ Item stand = (Item) map.get(String.valueOf(i + 1));
+ ItemStack itemStack = items[i];
+ if (itemStack == null || itemStack.getType().equals(Material.AIR)) {
+ itemStack = ItemStack.empty();
}
- if (item != null) {
- MaterialMode materialMode = MaterialUtils.getMaterialType(item);
- boolean changed = materialMode != standMode(stand);
+ if (!itemStack.isEmpty()) {
+ Item.RenderMode renderMode = renderMode(MaterialUtils.getMaterialType(itemStack));
+ boolean changed = renderMode != standMode(stand);
if (changed) {
- toggleStandMode(stand, materialMode.toString());
+ toggleStandMode(stand, renderMode);
}
- if (!item.equals(stand.getItemInMainHand())) {
+ if (!itemStack.equals(stand.getItemStack())) {
changed = true;
- stand.setItemInMainHand(item);
+ stand.setItemStack(itemStack);
}
if (changed) {
- DisplayManager.updateDisplay(stand);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getItemInMainHand().getType().equals(Material.AIR)) {
- stand.setItemInMainHand(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
}
- Location loc1 = ((DisplayEntity) map.get("5")).getLocation();
+ Location loc1 = ((Item) map.get("5")).getLocation();
InteractionVisualizer.lightManager.deleteLight(loc1);
- int skylight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromSky();
- int blocklight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromBlocks() - 1;
+ int skylight = block.getRelative(BlockFace.UP).getLightFromSky();
+ int blocklight = block.getRelative(BlockFace.UP).getLightFromBlocks() - 1;
blocklight = Math.max(blocklight, 0);
if (skylight > 0) {
InteractionVisualizer.lightManager.createLight(loc1, skylight, LightType.SKY);
@@ -307,48 +300,46 @@ public void onCraft(InventoryClickEvent event) {
ItemStack itemstack = event.getCurrentItem().clone();
Location loc = block.getLocation();
Player player = (Player) event.getWhoClicked();
+ InventoryView view = event.getView();
+ Item item;
if (map.get("0") instanceof String) {
- map.put("0", new Item(block.getLocation().clone().add(0.5, 1.2, 0.5)));
- }
- Item item = (Item) map.get("0");
- DisplayEntity slot1 = (DisplayEntity) map.get("1");
- DisplayEntity slot2 = (DisplayEntity) map.get("2");
- DisplayEntity slot3 = (DisplayEntity) map.get("3");
- DisplayEntity slot4 = (DisplayEntity) map.get("4");
- DisplayEntity slot5 = (DisplayEntity) map.get("5");
- DisplayEntity slot6 = (DisplayEntity) map.get("6");
- DisplayEntity slot7 = (DisplayEntity) map.get("7");
- DisplayEntity slot8 = (DisplayEntity) map.get("8");
- DisplayEntity slot9 = (DisplayEntity) map.get("9");
+ item = new Item(block.getLocation().clone().add(0.5, 1.2, 0.5));
+ item.setItemStack(itemstack);
+ map.put("0", item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ } else {
+ item = (Item) map.get("0");
+ }
+ Item slot1 = (Item) map.get("1");
+ Item slot2 = (Item) map.get("2");
+ Item slot3 = (Item) map.get("3");
+ Item slot4 = (Item) map.get("4");
+ Item slot5 = (Item) map.get("5");
+ Item slot6 = (Item) map.get("6");
+ Item slot7 = (Item) map.get("7");
+ Item slot8 = (Item) map.get("8");
+ Item slot9 = (Item) map.get("9");
Inventory before = Bukkit.createInventory(null, 9);
for (int i = 1; i < 10; i++) {
- before.setItem(i - 1, player.getOpenInventory().getItem(i).clone());
+ before.setItem(i - 1, InventoryUtils.cloneItem(view.getItem(i)));
}
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!player.isOnline() || player.getOpenInventory() != view) {
+ return;
+ }
Inventory after = Bukkit.createInventory(null, 9);
for (int i = 1; i < 10; i++) {
- after.setItem(i - 1, player.getOpenInventory().getItem(i).clone());
+ after.setItem(i - 1, InventoryUtils.cloneItem(view.getItem(i)));
}
if (InventoryUtils.compareContents(before, after)) {
return;
}
- item.setLocked(true);
- slot1.setLocked(true);
- slot2.setLocked(true);
- slot3.setLocked(true);
- slot4.setLocked(true);
- slot5.setLocked(true);
- slot6.setLocked(true);
- slot7.setLocked(true);
- slot8.setLocked(true);
- slot9.setLocked(true);
-
openedBenches.remove(block);
float yaw = getCardinalDirection(player);
@@ -363,15 +354,27 @@ public void onCraft(InventoryClickEvent event) {
slot8.teleport(slot8.getLocation().add(vector.clone().multiply(0.2)));
slot9.teleport(slot9.getLocation().add(rotateVectorAroundY(vector.clone(), -45).multiply(0.2828)));
- DisplayManager.updateDisplay(slot1);
- DisplayManager.updateDisplay(slot2);
- DisplayManager.updateDisplay(slot3);
- DisplayManager.updateDisplay(slot4);
- DisplayManager.updateDisplay(slot5);
- DisplayManager.updateDisplay(slot6);
- DisplayManager.updateDisplay(slot7);
- DisplayManager.updateDisplay(slot8);
- DisplayManager.updateDisplay(slot9);
+ DisplayManager.updateItem(slot1);
+ DisplayManager.updateItem(slot2);
+ DisplayManager.updateItem(slot3);
+ DisplayManager.updateItem(slot4);
+ DisplayManager.updateItem(slot5);
+ DisplayManager.updateItem(slot6);
+ DisplayManager.updateItem(slot7);
+ DisplayManager.updateItem(slot8);
+ DisplayManager.updateItem(slot9);
+
+ item.setItemStack(itemstack, true);
+ item.setLocked(true);
+ slot1.setLocked(true);
+ slot2.setLocked(true);
+ slot3.setLocked(true);
+ slot4.setLocked(true);
+ slot5.setLocked(true);
+ slot6.setLocked(true);
+ slot7.setLocked(true);
+ slot8.setLocked(true);
+ slot9.setLocked(true);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
for (Player each : InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY)) {
@@ -380,20 +383,23 @@ public void onCraft(InventoryClickEvent event) {
}, 6);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- item.setItemStack(itemstack);
DisplayManager.updateItem(item);
DisplayManager.collectItem(item, player);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot2);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot3);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot4);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot5);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot6);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot7);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot8);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot9);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot1);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot2);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot3);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot4);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot5);
+ Map current = openedBenches.get(block);
+ if (current == null || current.get("5") == slot5) {
+ InteractionVisualizer.lightManager.deleteLight(slot5.getLocation());
+ }
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot6);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot7);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot8);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot9);
}, 8);
}, 10);
}, 1);
@@ -432,18 +438,14 @@ public void onDragCraftingBench(InventoryDragEvent event) {
@EventHandler
public void onCloseCraftingBench(InventoryCloseEvent event) {
- if (!playermap.containsKey((Player) event.getPlayer())) {
- return;
- }
-
- Block block = playermap.get((Player) event.getPlayer());
-
- if (!openedBenches.containsKey(block)) {
+ Player player = (Player) event.getPlayer();
+ Block block = playermap.remove(player);
+ if (block == null) {
return;
}
Map map = openedBenches.get(block);
- if (!map.get("Player").equals(event.getPlayer())) {
+ if (map == null || !map.get("Player").equals(player)) {
return;
}
@@ -451,16 +453,13 @@ public void onCloseCraftingBench(InventoryCloseEvent event) {
if (!(map.get(String.valueOf(i)) instanceof String)) {
Object entity = map.get(String.valueOf(i));
if (entity instanceof Item) {
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), (Item) entity);
- } else if (entity instanceof DisplayEntity) {
- if (!((DisplayEntity) entity).isLocked()) {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), (DisplayEntity) entity);
- }
+ Item item = (Item) entity;
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
int finalI = i;
new ScheduledRunnable() {
public void run() {
if (finalI == 5) {
- InteractionVisualizer.lightManager.deleteLight(((DisplayEntity) entity).getLocation());
+ InteractionVisualizer.lightManager.deleteLight(item.getLocation());
}
}
}.runTaskLater(InteractionVisualizer.plugin, 20);
@@ -468,111 +467,47 @@ public void run() {
}
}
openedBenches.remove(block);
- playermap.remove((Player) event.getPlayer());
}
- public MaterialMode standMode(DisplayEntity stand) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (plain.startsWith("IV.CraftingTable.")) {
- return MaterialMode.getModeFromName(plain.substring(plain.lastIndexOf(".") + 1));
- }
- return null;
+ public Item.RenderMode standMode(Item stand) {
+ return stand.getRenderMode();
}
- public void toggleStandMode(DisplayEntity stand, String mode) {
- String plainText = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plainText.equals("IV.CraftingTable.Item")) {
- if (plainText.equals("IV.CraftingTable.Block")) {
- stand.setCustomName("IV.CraftingTable.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
-
- }
- if (plainText.equals("IV.CraftingTable.LowBlock")) {
- stand.setCustomName("IV.CraftingTable.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
+ private static Item.RenderMode renderMode(MaterialMode mode) {
+ return switch (mode) {
+ case ITEM -> Item.RenderMode.ITEM;
+ case BLOCK -> Item.RenderMode.BLOCK;
+ case LOWBLOCK -> Item.RenderMode.LOW_BLOCK;
+ case TOOL -> Item.RenderMode.TOOL;
+ case STANDING -> Item.RenderMode.STANDING;
+ };
+ }
- }
- if (plainText.equals("IV.CraftingTable.Tool")) {
- stand.setCustomName("IV.CraftingTable.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plainText.equals("IV.CraftingTable.Standing")) {
- stand.setCustomName("IV.CraftingTable.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.CraftingTable.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.CraftingTable.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.CraftingTable.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
- }
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.CraftingTable.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
- }
+ public void toggleStandMode(Item stand, Item.RenderMode mode) {
+ WorkstationDisplayPositioning.setRenderMode(stand, mode);
}
- public Map spawnDisplayEntitys(Player player, Block block) { //.add(0.68, 0.600781, 0.35)
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.600781, 0.5);
- DisplayEntity center = new DisplayEntity(loc);
+ public Map spawnDisplayEntitys(Player player, Block block) {
+ Map map = new HashMap<>();
float yaw = getCardinalDirection(player);
- center.setRotation(yaw, center.getLocation().getPitch());
- setStand(center);
- center.setCustomName("IV.CraftingTable.Center");
- Vector vector = rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.19), -100).add(center.getLocation().clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity slot5 = new DisplayEntity(loc.clone().add(vector));
+ Location origin = block.getLocation();
+ Item slot5 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, 0.0);
setStand(slot5, yaw);
- DisplayEntity slot2 = new DisplayEntity(slot5.getLocation().clone().add(center.getLocation().clone().getDirection().normalize().multiply(0.2)));
+ Item slot2 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, 0.2);
setStand(slot2, yaw);
- DisplayEntity slot1 = new DisplayEntity(slot2.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot1 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.2);
setStand(slot1, yaw);
- DisplayEntity slot3 = new DisplayEntity(slot2.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot3 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.2);
setStand(slot3, yaw);
- DisplayEntity slot4 = new DisplayEntity(slot5.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot4 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.0);
setStand(slot4, yaw);
- DisplayEntity slot6 = new DisplayEntity(slot5.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot6 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.0);
setStand(slot6, yaw);
- DisplayEntity slot8 = new DisplayEntity(slot5.getLocation().clone().add(center.getLocation().getDirection().clone().normalize().multiply(-0.2)));
+ Item slot8 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, -0.2);
setStand(slot8, yaw);
- DisplayEntity slot7 = new DisplayEntity(slot8.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Item slot7 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, -0.2);
setStand(slot7, yaw);
- DisplayEntity slot9 = new DisplayEntity(slot8.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot9 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, -0.2);
setStand(slot9, yaw);
map.put("1", slot1);
@@ -585,44 +520,23 @@ public Map spawnDisplayEntitys(Player player, Block block
map.put("8", slot8);
map.put("9", slot9);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot3);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot4);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot5);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot6);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot7);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot8);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot9);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot3);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot4);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot5);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot6);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot7);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot8);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot9);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
- stand.setGravity(false);
- stand.setSmall(true);
- stand.setInvulnerable(true);
- stand.setVisible(false);
- stand.setSilent(true);
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.setCustomName("IV.CraftingTable.Item");
+ public void setStand(Item stand, float yaw) {
stand.setRotation(yaw, stand.getLocation().getPitch());
}
- public void setStand(DisplayEntity stand) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
- stand.setGravity(false);
- stand.setSmall(true);
- stand.setSilent(true);
- stand.setInvulnerable(true);
- stand.setVisible(false);
- }
-
public Vector rotateVectorAroundY(Vector vector, double degrees) {
double rad = Math.toRadians(degrees);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DispenserDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DispenserDisplay.java
index 28efcf96..2fb5b0d6 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DispenserDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DispenserDisplay.java
@@ -204,11 +204,10 @@ public void onUseDispenser(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -302,12 +301,10 @@ public void onDragDispenser(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DoubleChestDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DoubleChestDisplay.java
index e6aeb9ae..ac67b34c 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DoubleChestDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DoubleChestDisplay.java
@@ -231,11 +231,10 @@ public void onUseDoubleChest(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -355,12 +354,10 @@ public void onDragDoubleChest(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DropperDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DropperDisplay.java
index 8d54db54..6699f35f 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/DropperDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/DropperDisplay.java
@@ -207,11 +207,10 @@ public void onUseDropper(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -308,12 +307,10 @@ public void onDragDropper(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/EnderchestDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/EnderchestDisplay.java
index baa7c26b..c4721711 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/EnderchestDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/EnderchestDisplay.java
@@ -212,11 +212,10 @@ public void onUseEnderChest(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -294,12 +293,10 @@ public void onDragEnderChest(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 1, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.15).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplay.java
index 01fa5472..8db85a5d 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplay.java
@@ -25,11 +25,15 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityActivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityAddedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityDeactivatedEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
@@ -38,7 +42,7 @@
import com.loohp.interactionvisualizer.utils.VanishUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -50,9 +54,14 @@
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.FurnaceBurnEvent;
+import org.bukkit.event.inventory.FurnaceExtractEvent;
+import org.bukkit.event.inventory.FurnaceSmeltEvent;
+import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.EulerAngle;
@@ -78,9 +87,11 @@ public class FurnaceDisplay extends VisualizerRunnableDisplay implements Listene
private String noFuelColor = "&c";
private int progressBarLength = 10;
private String amountPending = " &7+{Amount}";
+ private final BlockUpdateScheduler blockUpdates;
public FurnaceDisplay() {
onReload(new InteractionVisualizerReloadEvent());
+ this.blockUpdates = new BlockUpdateScheduler<>(this::nearbyFurnace, this.checkingPeriod, this.gcPeriod);
}
@EventHandler
@@ -102,6 +113,9 @@ public EntryKey key() {
@Override
public ScheduledTask gc() {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return null;
+ }
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Iterator>> itr = furnaceMap.entrySet().iterator();
int count = 0;
@@ -149,6 +163,21 @@ public ScheduledTask gc() {
@Override
public ScheduledTask run() {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return legacyRun();
+ }
+ return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ int checks = this.blockUpdates.tick(Bukkit.getCurrentTick(),
+ InteractionVisualizer.blockUpdateMaxDirtyPerTick, this::updateHybridBlock);
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(checks, System.nanoTime() - start);
+ }
+ }, 0, 1);
+ }
+
+ private ScheduledTask legacyRun() {
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Set list = nearbyFurnace();
for (Block block : list) {
@@ -178,16 +207,18 @@ public ScheduledTask run() {
}
Block block = entry.getKey();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ boolean collecting = PerformanceMetrics.isCollecting();
+ long start = collecting ? System.nanoTime() : 0L;
+ try {
+ if (!isActive(block.getLocation())) {
+ return;
+ }
+ if (!isFurnace(block.getType())) {
+ return;
+ }
+ org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) block.getState();
- if (!isActive(block.getLocation())) {
- return;
- }
- if (!isFurnace(block.getType())) {
- return;
- }
- org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) block.getState();
-
- {
+ {
Inventory inv = furnace.getInventory();
ItemStack itemstack = inv.getItem(0);
if (itemstack != null) {
@@ -262,25 +293,18 @@ public ScheduledTask run() {
symbol = symbol.replace("{CompletedAmount}", (inv.getItem(2) == null ? 0 : inv.getItem(2).getAmount()) + "");
}
if (hasFuel(furnace)) {
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals(symbol) || !stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(true);
- stand.setCustomName(symbol);
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.showProgress(stand, entry.getValue(), symbol);
} else {
symbol = noFuelColor + ChatColorUtils.stripColor(symbol);
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals(symbol) || !stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(true);
- stand.setCustomName(symbol);
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.showProgress(stand, entry.getValue(), symbol);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).equals("") || stand.isCustomNameVisible()) {
- stand.setCustomNameVisible(false);
- stand.setCustomName("");
- DisplayManager.updateDisplay(stand);
- }
+ FurnaceDisplayUpdater.hideProgress(stand, entry.getValue());
+ }
+ }
+ } finally {
+ if (collecting) {
+ PerformanceMetrics.blockUpdateChecks(1, System.nanoTime() - start);
}
}
}, delay, block.getLocation());
@@ -288,6 +312,106 @@ public ScheduledTask run() {
}, 0, checkingPeriod);
}
+ private boolean updateHybridBlock(Block block) {
+ if (!TileEntityManager.getTileEntities(TileEntityType.FURNACE).contains(block)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!block.getWorld().isChunkLoaded(block.getX() >> 4, block.getZ() >> 4)) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isFurnace(block.getType())) {
+ removeTrackedDisplay(block);
+ return false;
+ }
+ if (!isActive(block.getLocation())) {
+ return false;
+ }
+ Map values = furnaceMap.get(block);
+ if (values == null) {
+ values = new HashMap<>();
+ values.put("Item", "N/A");
+ values.putAll(spawnDisplayEntitys(block));
+ furnaceMap.put(block, values);
+ }
+ org.bukkit.block.Furnace furnace = (org.bukkit.block.Furnace) block.getState();
+ return FurnaceDisplayUpdater.update(furnace, values, KEY, progressBarCharacter, emptyColor,
+ filledColor, noFuelColor, progressBarLength, amountPending);
+ }
+
+ private void markDirty(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null && isFurnace(block.getType())) {
+ blockUpdates.markDirty(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ private void markDirtyUnlessActive(Block block) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates && block != null && isFurnace(block.getType())) {
+ blockUpdates.markDirtyUnlessActive(block, (long) Bukkit.getCurrentTick() + 1L);
+ }
+ }
+
+ /** Optimized aggregate-listener entry after the inventory location has been resolved once. */
+ public void onFurnaceInventoryChanged(Block block) {
+ markDirty(block);
+ }
+
+ private void markDirty(Inventory inventory) {
+ if (!InteractionVisualizer.eventDrivenBlockUpdates) {
+ return;
+ }
+ Location location;
+ try {
+ location = inventory.getLocation();
+ } catch (Exception | AbstractMethodError ignored) {
+ return;
+ }
+ if (location != null) {
+ markDirty(location.getBlock());
+ }
+ }
+
+ public void onFurnaceBurn(FurnaceBurnEvent event) {
+ markDirty(event.getBlock());
+ }
+
+ public void onFurnaceStartSmelt(FurnaceStartSmeltEvent event) {
+ markDirtyUnlessActive(event.getBlock());
+ }
+
+ public void onFurnaceSmelt(FurnaceSmeltEvent event) {
+ markDirtyUnlessActive(event.getBlock());
+ }
+
+ public void onFurnaceExtract(FurnaceExtractEvent event) {
+ markDirty(event.getBlock());
+ }
+
+ public void onFurnaceMoveItem(InventoryMoveItemEvent event) {
+ markDirty(event.getSource());
+ markDirty(event.getDestination());
+ }
+
+ public void onFurnaceAdded(TileEntityAddedEvent event) {
+ if (event.getTileEntityType() == TileEntityType.FURNACE) {
+ markDirty(event.getBlock());
+ }
+ }
+
+ public void onFurnaceActivated(TileEntityActivatedEvent event) {
+ if (event.getTileEntityType() == TileEntityType.FURNACE) {
+ markDirty(event.getBlock());
+ }
+ }
+
+ public void onFurnaceDeactivated(TileEntityDeactivatedEvent event) {
+ if (InteractionVisualizer.eventDrivenBlockUpdates
+ && event.getTileEntityType() == TileEntityType.FURNACE) {
+ removeTrackedDisplay(event.getBlock());
+ }
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onFurnace(InventoryClickEvent event) {
if (VanishUtils.isVanished((Player) event.getWhoClicked())) {
@@ -407,6 +531,7 @@ public void onUseFurnace(InventoryClickEvent event) {
if (!isFurnace(event.getView().getTopInventory().getLocation().getBlock().getType())) {
return;
}
+ markDirty(event.getView().getTopInventory().getLocation().getBlock());
if (event.getRawSlot() >= 0 && event.getRawSlot() <= 2) {
DisplayManager.sendHandMovement(InteractionVisualizerAPI.getPlayers(), (Player) event.getWhoClicked());
@@ -440,6 +565,7 @@ public void onDragFurnace(InventoryDragEvent event) {
for (int slot : event.getRawSlots()) {
if (slot >= 0 && slot <= 2) {
+ markDirty(event.getView().getTopInventory().getLocation().getBlock());
DisplayManager.sendHandMovement(InteractionVisualizerAPI.getPlayers(), (Player) event.getWhoClicked());
break;
}
@@ -448,11 +574,15 @@ public void onDragFurnace(InventoryDragEvent event) {
@EventHandler(priority = EventPriority.MONITOR)
public void onBreakFurnace(TileEntityRemovedEvent event) {
- Block block = event.getBlock();
- if (!furnaceMap.containsKey(block)) {
+ removeTrackedDisplay(event.getBlock());
+ }
+
+ private void removeTrackedDisplay(Block block) {
+ blockUpdates.remove(block);
+ Map map = furnaceMap.remove(block);
+ if (map == null) {
return;
}
- Map map = furnaceMap.get(block);
if (map.get("Item") instanceof Item) {
Item item = (Item) map.get("Item");
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
@@ -461,7 +591,6 @@ public void onBreakFurnace(TileEntityRemovedEvent event) {
DisplayEntity stand = (DisplayEntity) map.get("Stand");
DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), stand);
}
- furnaceMap.remove(block);
}
public boolean hasItemToCook(org.bukkit.block.Furnace furnace) {
@@ -513,6 +642,7 @@ public Map spawnDisplayEntitys(Block block) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplayUpdater.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplayUpdater.java
new file mode 100644
index 00000000..8a05cbf2
--- /dev/null
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/FurnaceDisplayUpdater.java
@@ -0,0 +1,156 @@
+/*
+ * This file is part of InteractionVisualizer.
+ *
+ * Copyright (C) 2026. Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+package com.loohp.interactionvisualizer.blocks;
+
+import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI;
+import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
+import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
+import com.loohp.interactionvisualizer.entityholders.Item;
+import com.loohp.interactionvisualizer.managers.DisplayManager;
+import com.loohp.interactionvisualizer.objectholders.EntryKey;
+import com.loohp.interactionvisualizer.utils.ChatColorUtils;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Material;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.util.Vector;
+
+import java.util.Map;
+
+/** Shared hybrid update path for the three Bukkit furnace variants. */
+final class FurnaceDisplayUpdater {
+
+ static final String PROGRESS_TEXT_KEY = "ProgressText";
+
+ private FurnaceDisplayUpdater() {
+ }
+
+ static boolean update(org.bukkit.block.Furnace furnace, Map values, EntryKey key,
+ String progressBarCharacter, String emptyColor, String filledColor,
+ String noFuelColor, int progressBarLength, String amountPending) {
+ Inventory inventory = furnace.getInventory();
+ ItemStack itemStack = normalize(inventory.getItem(0));
+ if (itemStack == null) {
+ itemStack = normalize(inventory.getItem(2));
+ }
+
+ if (values.get("Item") instanceof String) {
+ if (itemStack != null) {
+ Item item = new Item(furnace.getLocation().clone().add(0.5, 1.0, 0.5));
+ item.setItemStack(itemStack);
+ item.setVelocity(new Vector());
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ values.put("Item", item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, key), item);
+ } else {
+ values.put("Item", "N/A");
+ }
+ } else {
+ Item item = (Item) values.get("Item");
+ if (itemStack != null) {
+ if (!item.getItemStack().equals(itemStack)) {
+ item.setItemStack(itemStack);
+ DisplayManager.updateItem(item);
+ }
+ } else {
+ values.put("Item", "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
+ }
+ }
+
+ DisplayEntity stand = (DisplayEntity) values.get("Stand");
+ ItemStack input = normalize(inventory.getItem(0));
+ if (input == null) {
+ hideProgress(stand, values);
+ return false;
+ }
+
+ int time = furnace.getCookTime();
+ int maximum = furnace.getCookTimeTotal();
+ double scaled = scaledProgress(time, maximum, progressBarLength);
+ StringBuilder symbol = new StringBuilder(Math.max(16, progressBarLength * 3));
+ double i = 1;
+ for (; i < scaled; i++) {
+ symbol.append(filledColor).append(progressBarCharacter);
+ }
+ i--;
+ if (scaled - i > 0 && scaled - i < 0.67) {
+ symbol.append(emptyColor).append(progressBarCharacter);
+ } else if (scaled - i > 0) {
+ symbol.append(filledColor).append(progressBarCharacter);
+ }
+ for (i = progressBarLength - 1; i >= scaled; i--) {
+ symbol.append(emptyColor).append(progressBarCharacter);
+ }
+
+ int left = input.getAmount() - 1;
+ if (left > 0) {
+ symbol.append(amountPending.replace("{Amount}", Integer.toString(left)));
+ }
+ String text = symbol.toString();
+ if (text.contains("{CompletedAmount}")) {
+ ItemStack output = inventory.getItem(2);
+ text = text.replace("{CompletedAmount}", Integer.toString(output == null ? 0 : output.getAmount()));
+ }
+ if (!hasFuel(furnace)) {
+ text = noFuelColor + ChatColorUtils.stripColor(text);
+ }
+ showProgress(stand, values, text);
+ return furnace.getBurnTime() > 0 || furnace.getCookTime() > 0;
+ }
+
+ static double scaledProgress(int time, int maximum, int progressBarLength) {
+ int boundedLength = Math.max(0, progressBarLength);
+ if (maximum <= 0 || boundedLength == 0) {
+ return 0.0D;
+ }
+ double scaled = (double) time / (double) maximum * (double) boundedLength;
+ return Math.max(0.0D, Math.min((double) boundedLength, scaled));
+ }
+
+ private static ItemStack normalize(ItemStack itemStack) {
+ return itemStack == null || itemStack.getType() == Material.AIR ? null : itemStack;
+ }
+
+ private static boolean hasFuel(org.bukkit.block.Furnace furnace) {
+ if (furnace.getBurnTime() > 0) {
+ return true;
+ }
+ return normalize(furnace.getInventory().getItem(1)) != null;
+ }
+
+ static boolean shouldUpdateProgress(Map values, String text, boolean visible) {
+ return !visible || !text.equals(values.get(PROGRESS_TEXT_KEY));
+ }
+
+ static void showProgress(DisplayEntity stand, Map values, String text) {
+ if (shouldUpdateProgress(values, text, stand.isCustomNameVisible())) {
+ stand.setCustomNameVisible(true);
+ stand.setCustomName(text);
+ DisplayManager.updateDisplay(stand);
+ values.put(PROGRESS_TEXT_KEY, text);
+ }
+ }
+
+ static void hideProgress(DisplayEntity stand, Map values) {
+ if (values.containsKey(PROGRESS_TEXT_KEY)
+ || !PlainTextComponentSerializer.plainText().serialize(stand.getCustomName()).isEmpty()
+ || stand.isCustomNameVisible()) {
+ stand.setCustomNameVisible(false);
+ stand.setCustomName("");
+ DisplayManager.updateDisplay(stand);
+ values.remove(PROGRESS_TEXT_KEY);
+ }
+ }
+
+}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/GrindstoneDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/GrindstoneDisplay.java
index 37b84012..fc8f95da 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/GrindstoneDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/GrindstoneDisplay.java
@@ -33,8 +33,8 @@
import com.loohp.interactionvisualizer.utils.MaterialUtils;
import com.loohp.interactionvisualizer.utils.MaterialUtils.MaterialMode;
import com.loohp.interactionvisualizer.utils.VanishUtils;
+import com.loohp.interactionvisualizer.utils.WorkstationDisplayPositioning;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
@@ -53,7 +53,6 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
@@ -110,40 +109,37 @@ public void process(Player player) {
}
ItemStack[] items = new ItemStack[] {view.getItem(0), view.getItem(1)};
- if (view.getItem(2) != null) {
- ItemStack itemstack = view.getItem(2);
- if (itemstack == null || itemstack.getType().equals(Material.AIR)) {
- itemstack = null;
+ ItemStack itemstack = view.getItem(2);
+ if (itemstack != null && itemstack.getType().equals(Material.AIR)) {
+ itemstack = null;
+ }
+ if (map.get("2") instanceof String) {
+ if (itemstack != null) {
+ Item item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ item.setItemStack(itemstack);
+ item.setVelocity(new Vector(0, 0, 0));
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ map.put("2", item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ DisplayManager.updateItem(item);
+ } else {
+ map.put("2", "N/A");
}
- Item item = null;
- if (map.get("2") instanceof String) {
- if (itemstack != null) {
- item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ } else {
+ Item item = (Item) map.get("2");
+ if (itemstack != null) {
+ if (!item.getItemStack().equals(itemstack)) {
item.setItemStack(itemstack);
- item.setVelocity(new Vector(0, 0, 0));
- item.setPickupDelay(32767);
- item.setGravity(false);
- map.put("2", item);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
DisplayManager.updateItem(item);
- } else {
- map.put("2", "N/A");
}
} else {
- item = (Item) map.get("2");
- if (itemstack != null) {
- if (!item.getItemStack().equals(itemstack)) {
- item.setItemStack(itemstack);
- DisplayManager.updateItem(item);
- }
- } else {
- map.put("2", "N/A");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
- }
+ map.put("2", "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
}
for (int i = 0; i < 2; i++) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
+ Item stand = (Item) map.get(String.valueOf(i));
ItemStack item = items[i];
if (item == null || item.getType().equals(Material.AIR)) {
item = null;
@@ -152,19 +148,19 @@ public void process(Player player) {
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
boolean changed = materialMode != standMode(stand);
if (changed) {
- toggleStandMode(stand, materialMode.toString());
+ toggleStandMode(stand, materialMode);
}
- if (!item.equals(stand.getItemInMainHand())) {
+ if (!item.equals(stand.getItemStack())) {
changed = true;
- stand.setItemInMainHand(item);
+ stand.setItemStack(item);
}
if (changed) {
- DisplayManager.updateDisplay(stand);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getItemInMainHand().getType().equals(Material.AIR)) {
- stand.setItemInMainHand(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
}
@@ -237,31 +233,37 @@ public void onGrindstone(InventoryClickEvent event) {
ItemStack itemstack = event.getCurrentItem().clone();
Location loc = block.getLocation();
Player player = (Player) event.getWhoClicked();
+ InventoryView view = event.getView();
- DisplayEntity slot0 = (DisplayEntity) map.get("0");
- DisplayEntity slot1 = (DisplayEntity) map.get("1");
+ Item slot0 = (Item) map.get("0");
+ Item slot1 = (Item) map.get("1");
if (map.get("2") instanceof String) {
- map.put("2", new Item(block.getLocation().clone().add(0.5, 1.2, 0.5)));
+ Item result = new Item(block.getLocation().clone().add(0.5, 1.2, 0.5));
+ result.setItemStack(itemstack);
+ result.setVelocity(new Vector());
+ result.setPickupDelay(32767);
+ result.setGravity(false);
+ map.put("2", result);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), result);
}
Item item = (Item) map.get("2");
Inventory before = Bukkit.createInventory(null, 9);
- before.setItem(0, player.getOpenInventory().getItem(0).clone());
- before.setItem(1, player.getOpenInventory().getItem(1).clone());
+ before.setItem(0, InventoryUtils.cloneItem(view.getItem(0)));
+ before.setItem(1, InventoryUtils.cloneItem(view.getItem(1)));
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!player.isOnline() || player.getOpenInventory() != view) {
+ return;
+ }
Inventory after = Bukkit.createInventory(null, 9);
- after.setItem(0, player.getOpenInventory().getItem(0).clone());
- after.setItem(1, player.getOpenInventory().getItem(1).clone());
+ after.setItem(0, InventoryUtils.cloneItem(view.getItem(0)));
+ after.setItem(1, InventoryUtils.cloneItem(view.getItem(1)));
if (InventoryUtils.compareContents(before, after)) {
return;
}
- slot0.setLocked(true);
- slot1.setLocked(true);
- item.setLocked(true);
-
openedGrindstone.remove(block);
float yaw = getCardinalDirection(player);
@@ -269,8 +271,13 @@ public void onGrindstone(InventoryClickEvent event) {
slot0.teleport(slot0.getLocation().add(rotateVectorAroundY(vector.clone(), 90).multiply(0.1)));
slot1.teleport(slot1.getLocation().add(rotateVectorAroundY(vector.clone(), -90).multiply(0.1)));
- DisplayManager.updateDisplay(slot0);
- DisplayManager.updateDisplay(slot1);
+ DisplayManager.updateItem(slot0);
+ DisplayManager.updateItem(slot1);
+
+ item.setItemStack(itemstack);
+ slot0.setLocked(true);
+ slot1.setLocked(true);
+ item.setLocked(true);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
for (Player each : InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY)) {
@@ -279,13 +286,12 @@ public void onGrindstone(InventoryClickEvent event) {
}, 6);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- item.setItemStack(itemstack);
DisplayManager.updateItem(item);
DisplayManager.collectItem(item, player);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot0);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot0);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot1);
}, 8);
}, 10);
}, 1);
@@ -394,118 +400,44 @@ public void onCloseGrindstone(InventoryCloseEvent event) {
openedGrindstone.remove(block);
}
- public MaterialMode standMode(DisplayEntity stand) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (plain.startsWith("IV.Grindstone.")) {
- return MaterialMode.getModeFromName(plain.substring(plain.lastIndexOf(".") + 1));
- }
- return null;
+ public MaterialMode standMode(Item stand) {
+ return switch (stand.getRenderMode()) {
+ case ITEM -> MaterialMode.ITEM;
+ case BLOCK -> MaterialMode.BLOCK;
+ case LOW_BLOCK -> MaterialMode.LOWBLOCK;
+ case TOOL -> MaterialMode.TOOL;
+ case STANDING -> MaterialMode.STANDING;
+ default -> null;
+ };
}
- public void toggleStandMode(DisplayEntity stand, String mode) {
- String plainText = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plainText.equals("IV.Grindstone.Item")) {
- if (plainText.equals("IV.Grindstone.Block")) {
- stand.setCustomName("IV.Grindstone.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
-
- }
- if (plainText.equals("IV.Grindstone.LowBlock")) {
- stand.setCustomName("IV.Grindstone.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
-
- }
- if (plainText.equals("IV.Grindstone.Tool")) {
- stand.setCustomName("IV.Grindstone.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plainText.equals("IV.Grindstone.Standing")) {
- stand.setCustomName("IV.Grindstone.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.Grindstone.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.Grindstone.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.Grindstone.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
- }
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.Grindstone.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
- }
+ public void toggleStandMode(Item stand, MaterialMode mode) {
+ WorkstationDisplayPositioning.setRenderMode(stand, mode);
}
- public Map spawnDisplayEntitys(Player player, Block block) { //.add(0.68, 0.600781, 0.35)
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.600781, 0.5);
- DisplayEntity center = new DisplayEntity(loc);
+ public Map spawnDisplayEntitys(Player player, Block block) {
+ Map map = new HashMap<>();
float yaw = getCardinalDirection(player);
- center.setRotation(yaw, center.getLocation().getPitch());
- setStand(center);
- center.setCustomName("IV.Grindstone.Center");
- Vector vector = rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.19), -100).add(center.getLocation().clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity middle = new DisplayEntity(loc.clone().add(vector));
- setStand(middle, yaw);
- DisplayEntity slot0 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), -90)));
+ Location origin = block.getLocation();
+ Item slot0 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.2, 0.0);
setStand(slot0, yaw + 20);
- DisplayEntity slot1 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.2), 90)));
+ Item slot1 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.2, 0.0);
setStand(slot1, yaw - 20);
map.put("0", slot0);
map.put("1", slot1);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
+ public void setStand(Item stand, float yaw) {
stand.setGravity(false);
- stand.setInvulnerable(true);
stand.setSilent(true);
- stand.setVisible(false);
- stand.setSmall(true);
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.setCustomName("IV.Grindstone.Item");
+ stand.setVelocity(new Vector());
+ stand.setPickupDelay(32767);
stand.setRotation(yaw, stand.getLocation().getPitch());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/HopperDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/HopperDisplay.java
index 764d215e..9660dcd1 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/HopperDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/HopperDisplay.java
@@ -207,11 +207,10 @@ public void onUseHopper(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 0.65, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -308,12 +307,10 @@ public void onDragHopper(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 0.65, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/JukeBoxDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/JukeBoxDisplay.java
index 2dbea01b..d1cd6033 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/JukeBoxDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/JukeBoxDisplay.java
@@ -26,6 +26,7 @@
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
+import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
@@ -41,6 +42,7 @@
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
+import org.bukkit.entity.Display;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -57,6 +59,10 @@
public class JukeBoxDisplay extends VisualizerRunnableDisplay implements Listener {
public static final EntryKey KEY = new EntryKey("jukebox");
+ private static final String ITEM_KEY = "Item";
+ private static final String LABEL_KEY = "Label";
+ private static final String EMPTY_VISUAL = "N/A";
+ private static final double LABEL_Y_OFFSET = 0.55D;
public ConcurrentHashMap> jukeboxMap = new ConcurrentHashMap<>();
private int checkingPeriod = 20;
@@ -94,23 +100,21 @@ public ScheduledTask gc() {
}
Entry> entry = itr.next();
Block block = entry.getKey();
+ Map values = entry.getValue();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!ownsVisualState(jukeboxMap.get(block), values)) {
+ return;
+ }
if (!isActive(block.getLocation())) {
- Map map = entry.getValue();
- if (map.get("Item") instanceof Item) {
- Item item = (Item) map.get("Item");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
+ if (jukeboxMap.remove(block, values)) {
+ removeVisuals(values);
}
- jukeboxMap.remove(block);
return;
}
if (!block.getType().equals(Material.JUKEBOX)) {
- Map map = entry.getValue();
- if (map.get("Item") instanceof Item) {
- Item item = (Item) map.get("Item");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
+ if (jukeboxMap.remove(block, values)) {
+ removeVisuals(values);
}
- jukeboxMap.remove(block);
return;
}
}, delay, block.getLocation());
@@ -127,7 +131,8 @@ public ScheduledTask run() {
if (jukeboxMap.get(block) == null && isActive(block.getLocation())) {
if (block.getType().equals(Material.JUKEBOX)) {
HashMap map = new HashMap<>();
- map.put("Item", "N/A");
+ map.put(ITEM_KEY, EMPTY_VISUAL);
+ map.put(LABEL_KEY, EMPTY_VISUAL);
jukeboxMap.put(block, map);
}
}
@@ -147,7 +152,11 @@ public ScheduledTask run() {
delay++;
}
Block block = entry.getKey();
+ Map values = entry.getValue();
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!ownsVisualState(jukeboxMap.get(block), values)) {
+ return;
+ }
if (!isActive(block.getLocation())) {
return;
}
@@ -158,65 +167,36 @@ public ScheduledTask run() {
{
ItemStack itemstack = jukebox.getRecord() == null ? null : (jukebox.getRecord().getType().equals(Material.AIR) ? null : jukebox.getRecord().clone());
+ if (itemstack == null) {
+ removeVisuals(values);
+ return;
+ }
- if (entry.getValue().get("Item") instanceof String) {
- if (itemstack != null) {
- Item item = new Item(jukebox.getLocation().clone().add(0.5, 1.0, 0.5));
-
- String disc = jukebox.getPlaying().toString();
- Component text;
- if (showDiscName) {
- Component displayName = itemstack.getItemMeta().displayName();
- if (displayName != null) {
- text = ComponentFont.parseFont(displayName.colorIfAbsent(getColor(disc)));
- } else {
- text = Component.translatable(TranslationUtils.getRecord(disc));
- text = text.color(getColor(disc));
- }
- item.setCustomName(text);
- item.setCustomNameVisible(true);
- } else {
- item.setCustomName("");
- item.setCustomNameVisible(false);
- }
+ Item item;
+ if (values.get(ITEM_KEY) instanceof Item existing) {
+ item = existing;
+ boolean changed = suppressNativeName(item);
+ if (!item.getItemStack().equals(itemstack)) {
item.setItemStack(itemstack);
- item.setVelocity(new Vector(0, 0, 0));
- item.setPickupDelay(32767);
- item.setGravity(false);
- entry.getValue().put("Item", item);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ changed = true;
+ }
+ if (changed) {
DisplayManager.updateItem(item);
- } else {
- entry.getValue().put("Item", "N/A");
}
} else {
- Item item = (Item) entry.getValue().get("Item");
- if (itemstack != null) {
- if (!item.getItemStack().equals(itemstack)) {
- item.setItemStack(itemstack);
- String disc = jukebox.getPlaying().toString();
- Component text;
- if (showDiscName) {
- Component displayName = itemstack.getItemMeta().displayName();
- if (displayName != null) {
- text = ComponentFont.parseFont(displayName.colorIfAbsent(getColor(disc)));
- } else {
- text = Component.translatable(TranslationUtils.getRecord(disc));
- text = text.color(getColor(disc));
- }
- item.setCustomName(text);
- item.setCustomNameVisible(true);
- } else {
- item.setCustomName("");
- item.setCustomNameVisible(false);
- }
- DisplayManager.updateItem(item);
- }
- } else {
- entry.getValue().put("Item", "N/A");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
- }
+ item = new Item(jukebox.getLocation().clone().add(0.5, 1.0, 0.5));
+ item.setItemStack(itemstack);
+ item.setVelocity(new Vector(0, 0, 0));
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ suppressNativeName(item);
+ values.put(ITEM_KEY, item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ DisplayManager.updateItem(item);
}
+
+ Component text = showDiscName ? discName(itemstack, jukebox.getPlaying().toString()) : null;
+ reconcileLabel(values, item.getLocation(), text);
}
}, delay, block.getLocation());
}
@@ -230,12 +210,85 @@ public void onBreakJukeBox(TileEntityRemovedEvent event) {
return;
}
- Map map = jukeboxMap.get(block);
- if (map.get("Item") instanceof Item) {
- Item item = (Item) map.get("Item");
+ Map values = jukeboxMap.remove(block);
+ removeVisuals(values);
+ }
+
+ private Component discName(ItemStack itemstack, String disc) {
+ Component displayName = itemstack.getItemMeta().displayName();
+ if (displayName != null) {
+ return ComponentFont.parseFont(displayName.colorIfAbsent(getColor(disc)));
+ }
+ return Component.translatable(TranslationUtils.getRecord(disc)).color(getColor(disc));
+ }
+
+ private void reconcileLabel(Map values, Location itemLocation, Component text) {
+ Object current = values.get(LABEL_KEY);
+ if (text == null) {
+ if (current instanceof DisplayEntity label) {
+ DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), label);
+ }
+ values.put(LABEL_KEY, EMPTY_VISUAL);
+ return;
+ }
+
+ if (current instanceof DisplayEntity label) {
+ if (!text.equals(label.getCustomName()) || !label.isCustomNameVisible()) {
+ label.setCustomName(text);
+ label.setCustomNameVisible(true);
+ DisplayManager.updateDisplay(label);
+ }
+ return;
+ }
+
+ DisplayEntity label = new DisplayEntity(labelLocation(itemLocation));
+ configureLabel(label);
+ label.setCustomName(text);
+ values.put(LABEL_KEY, label);
+ DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), label);
+ DisplayManager.updateDisplay(label);
+ }
+
+ private void removeVisuals(Map values) {
+ if (values == null) {
+ return;
+ }
+ if (values.get(ITEM_KEY) instanceof Item item) {
DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
- jukeboxMap.remove(block);
+ if (values.get(LABEL_KEY) instanceof DisplayEntity label) {
+ DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), label);
+ }
+ values.put(ITEM_KEY, EMPTY_VISUAL);
+ values.put(LABEL_KEY, EMPTY_VISUAL);
+ }
+
+ static Location labelLocation(Location itemLocation) {
+ return itemLocation.clone().add(0.0, LABEL_Y_OFFSET, 0.0);
+ }
+
+ static void configureLabel(DisplayEntity label) {
+ label.setBillboard(Display.Billboard.CENTER);
+ label.setDefaultBackground(true);
+ label.setTextScale(1.0F);
+ label.setUnboundedTextWidth(true);
+ label.setInterpolationDuration(0);
+ label.setTeleportDuration(0);
+ label.setGravity(false);
+ label.setInvulnerable(true);
+ label.setSilent(true);
+ label.setCustomNameVisible(true);
+ }
+
+ static boolean suppressNativeName(Item item) {
+ boolean changed = item.getCustomName() != null || item.isCustomNameVisible();
+ item.setCustomName((Component) null);
+ item.setCustomNameVisible(false);
+ return changed;
+ }
+
+ static boolean ownsVisualState(Map current, Map scheduled) {
+ return current == scheduled;
}
public Set nearbyJukeBox() {
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/LecternDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/LecternDisplay.java
index c8c99272..de2c6e9a 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/LecternDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/LecternDisplay.java
@@ -35,13 +35,13 @@
import com.loohp.interactionvisualizer.utils.ChatColorUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
+import org.bukkit.entity.Display;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
@@ -49,7 +49,6 @@
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.util.EulerAngle;
-import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.Iterator;
@@ -193,25 +192,17 @@ public ScheduledTask run() {
.replace("{Author}", meta.getAuthor() == null ? "" : meta.getAuthor())
.replace("{Page}", lectern.getPage() + "");
- if (!PlainTextComponentSerializer.plainText().serialize(stand1.getCustomName()).equals(line1) || !stand1.isCustomNameVisible()) {
- stand1.setCustomNameVisible(true);
- stand1.setCustomName(line1);
+ if (stand1.updateCustomName(line1, true)) {
DisplayManager.updateDisplay(stand1);
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand2.getCustomName()).equals(line2) || !stand2.isCustomNameVisible()) {
- stand2.setCustomNameVisible(true);
- stand2.setCustomName(line2);
+ if (stand2.updateCustomName(line2, true)) {
DisplayManager.updateDisplay(stand2);
}
} else {
- if (!PlainTextComponentSerializer.plainText().serialize(stand1.getCustomName()).equals("") || stand1.isCustomNameVisible()) {
- stand1.setCustomNameVisible(false);
- stand1.setCustomName("");
+ if (stand1.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand1);
}
- if (!PlainTextComponentSerializer.plainText().serialize(stand2.getCustomName()).equals("") || stand2.isCustomNameVisible()) {
- stand2.setCustomNameVisible(false);
- stand2.setCustomName("");
+ if (stand2.updateCustomName("", false)) {
DisplayManager.updateDisplay(stand2);
}
}
@@ -254,13 +245,10 @@ public Map spawnDisplayEntitys(Block block) {
Location origin = block.getLocation();
BlockData blockData = block.getState().getBlockData();
BlockFace facing = ((Directional) blockData).getFacing();
- Location target = block.getRelative(facing).getLocation();
- Vector direction = target.toVector().subtract(origin.toVector()).multiply(0.2);
-
- Location loc = origin.clone().add(direction).add(0.5, 1.301, 0.5);
+ Location loc = firstLineLocation(origin, facing);
DisplayEntity slot1 = new DisplayEntity(loc.clone());
setStand(slot1);
- DisplayEntity slot2 = new DisplayEntity(loc.clone().add(0, -0.3, 0));
+ DisplayEntity slot2 = new DisplayEntity(secondLineLocation(origin, facing));
setStand(slot2);
map.put("1", slot1);
@@ -272,7 +260,17 @@ public Map spawnDisplayEntitys(Block block) {
return map;
}
- public void setStand(DisplayEntity stand) {
+ static Location firstLineLocation(Location origin, BlockFace facing) {
+ return origin.clone().add(facing.getDirection().multiply(0.2D)).add(0.5D, 1.301D, 0.5D);
+ }
+
+ static Location secondLineLocation(Location origin, BlockFace facing) {
+ return firstLineLocation(origin, facing).add(0.0D, -0.3D, 0.0D);
+ }
+
+ public static void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
+ stand.setBillboard(labelBillboard());
stand.setBasePlate(false);
stand.setMarker(true);
stand.setGravity(false);
@@ -284,4 +282,10 @@ public void setStand(DisplayEntity stand) {
stand.setRightArmPose(EulerAngle.ZERO);
}
+ static Display.Billboard labelBillboard() {
+ // Legacy marker-entity name tags always faced the viewer. Preserve
+ // that behavior when the label is rendered by a TextDisplay.
+ return Display.Billboard.CENTER;
+ }
+
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/LoomDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/LoomDisplay.java
index 7b6619ec..3e1d3111 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/LoomDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/LoomDisplay.java
@@ -24,11 +24,9 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI;
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerInteractDisplay;
-import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
-import com.loohp.interactionvisualizer.objectholders.LightType;
import com.loohp.interactionvisualizer.utils.InventoryUtils;
import com.loohp.interactionvisualizer.utils.LocationUtils;
import com.loohp.interactionvisualizer.utils.VanishUtils;
@@ -40,31 +38,34 @@
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
-import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
+import java.util.UUID;
public class LoomDisplay extends VisualizerInteractDisplay implements Listener {
public static final EntryKey KEY = new EntryKey("loom");
public Map> openedLooms = new HashMap<>();
+ private final Map loomBlocksByPlayer = new HashMap<>();
@Override
public EntryKey key() {
@@ -96,20 +97,15 @@ public void run() {
if (block.getType().equals(Material.LOOM)) {
Player player = (Player) map.get("Player");
if (!GameMode.SPECTATOR.equals(player.getGameMode())) {
- if (player.getOpenInventory() != null) {
- if (player.getOpenInventory().getTopInventory() != null) {
- if (player.getOpenInventory().getTopInventory().getLocation().getBlock().getType().equals(Material.LOOM)) {
- return;
- }
- }
+ Block openBlock = resolveLoomBlock(player, player.getOpenInventory());
+ if (block.equals(openBlock)) {
+ return;
}
}
}
- if (map.get("Banner") instanceof DisplayEntity) {
- DisplayEntity entity = (DisplayEntity) map.get("Banner");
- InteractionVisualizer.lightManager.deleteLight(entity.getLocation());
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), entity);
+ if (map.get("Banner") instanceof Item item) {
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
openedLooms.remove(block);
}
@@ -127,21 +123,11 @@ public void process(Player player) {
if (GameMode.SPECTATOR.equals(player.getGameMode())) {
return;
}
- if (player.getOpenInventory().getTopInventory().getLocation() == null) {
- return;
- }
- if (!LocationUtils.isLoaded(player.getOpenInventory().getTopInventory().getLocation())) {
- return;
- }
- if (player.getOpenInventory().getTopInventory().getLocation().getBlock() == null) {
- return;
- }
- if (!player.getOpenInventory().getTopInventory().getLocation().getBlock().getType().equals(Material.LOOM)) {
+ InventoryView view = player.getOpenInventory();
+ Block block = resolveLoomBlock(player, view);
+ if (block == null) {
return;
}
-
- InventoryView view = player.getOpenInventory();
- Block block = view.getTopInventory().getLocation().getBlock();
if (!openedLooms.containsKey(block)) {
Map map = new HashMap<>();
map.put("Player", player);
@@ -176,30 +162,64 @@ public void process(Player player) {
item = output;
}
- DisplayEntity stand = (DisplayEntity) map.get("Banner");
+ Item stand = (Item) map.get("Banner");
if (item != null) {
- if (!item.isSimilar(stand.getHelmet())) {
- stand.setHelmet(item);
- DisplayManager.updateDisplay(stand);
+ if (!item.isSimilar(stand.getItemStack())) {
+ stand.setItemStack(item);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getHelmet().getType().equals(Material.AIR)) {
- stand.setHelmet(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onLoomInteract(PlayerInteractEvent event) {
+ if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null
+ || event.getClickedBlock().getType() != Material.LOOM) {
+ return;
+ }
+ loomBlocksByPlayer.put(event.getPlayer().getUniqueId(), event.getClickedBlock());
+ }
- Location loc1 = ((DisplayEntity) map.get("Banner")).getLocation();
- InteractionVisualizer.lightManager.deleteLight(loc1);
- int skylight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromSky();
- int blocklight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromBlocks() - 1;
- blocklight = Math.max(blocklight, 0);
- if (skylight > 0) {
- InteractionVisualizer.lightManager.createLight(loc1, skylight, LightType.SKY);
+ private Block resolveLoomBlock(Player player, InventoryView view) {
+ if (view == null || view.getTopInventory().getType() != InventoryType.LOOM) {
+ return null;
}
- if (blocklight > 0) {
- InteractionVisualizer.lightManager.createLight(loc1, blocklight, LightType.BLOCK);
+ try {
+ Location location = view.getTopInventory().getLocation();
+ if (location != null && LocationUtils.isLoaded(location)) {
+ Block block = location.getBlock();
+ if (block.getType() == Material.LOOM) {
+ loomBlocksByPlayer.put(player.getUniqueId(), block);
+ return block;
+ }
+ }
+ } catch (Exception | AbstractMethodError ignored) {
+ // Some server versions do not expose a workstation inventory location.
}
+
+ UUID playerId = player.getUniqueId();
+ Block block = loomBlocksByPlayer.get(playerId);
+ if (block == null) {
+ return null;
+ }
+ Location blockLocation = block.getLocation();
+ if (!block.getWorld().equals(player.getWorld())
+ || blockLocation.clone().add(0.5, 0.5, 0.5).distanceSquared(player.getLocation()) > 64.0
+ || !LocationUtils.isLoaded(blockLocation) || block.getType() != Material.LOOM) {
+ loomBlocksByPlayer.remove(playerId, block);
+ return null;
+ }
+ return block;
+ }
+
+ @EventHandler
+ public void onLoomPlayerQuit(PlayerQuitEvent event) {
+ loomBlocksByPlayer.remove(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR)
@@ -247,50 +267,39 @@ public void onLoom(InventoryClickEvent event) {
}
}
- if (event.getView().getTopInventory() == null) {
- return;
- }
- try {
- if (event.getView().getTopInventory().getLocation() == null) {
- return;
- }
- } catch (Exception | AbstractMethodError e) {
- return;
- }
- if (event.getView().getTopInventory().getLocation().getBlock() == null) {
- return;
- }
- if (!event.getView().getTopInventory().getLocation().getBlock().getType().equals(Material.LOOM)) {
+ Player player = (Player) event.getWhoClicked();
+ Block block = resolveLoomBlock(player, event.getView());
+ if (block == null) {
return;
}
- Block block = event.getView().getTopInventory().getLocation().getBlock();
-
if (!openedLooms.containsKey(block)) {
return;
}
ItemStack itemstack = event.getCurrentItem().clone();
- Player player = (Player) event.getWhoClicked();
-
+ InventoryView view = player.getOpenInventory();
Inventory before = Bukkit.createInventory(null, 9);
- before.setItem(0, player.getOpenInventory().getItem(0).clone());
- before.setItem(1, player.getOpenInventory().getItem(1).clone());
- before.setItem(2, player.getOpenInventory().getItem(2).clone());
+ before.setItem(0, cloneItem(view.getItem(0)));
+ before.setItem(1, cloneItem(view.getItem(1)));
+ before.setItem(2, cloneItem(view.getItem(2)));
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!player.isOnline() || player.getOpenInventory() != view) {
+ return;
+ }
Inventory after = Bukkit.createInventory(null, 9);
- after.setItem(0, player.getOpenInventory().getItem(0).clone());
- after.setItem(1, player.getOpenInventory().getItem(1).clone());
- after.setItem(2, player.getOpenInventory().getItem(2).clone());
+ after.setItem(0, cloneItem(view.getItem(0)));
+ after.setItem(1, cloneItem(view.getItem(1)));
+ after.setItem(2, cloneItem(view.getItem(2)));
if (InventoryUtils.compareContents(before, after)) {
return;
}
Map map = openedLooms.get(block);
- if (!map.get("Player").equals(event.getWhoClicked())) {
+ if (map == null || !player.equals(map.get("Player"))) {
return;
}
@@ -303,6 +312,10 @@ public void onLoom(InventoryClickEvent event) {
}, 1);
}
+ private static ItemStack cloneItem(ItemStack item) {
+ return item == null ? null : item.clone();
+ }
+
@EventHandler(priority = EventPriority.MONITOR)
public void onUseLoom(InventoryClickEvent event) {
if (event.isCancelled()) {
@@ -311,20 +324,7 @@ public void onUseLoom(InventoryClickEvent event) {
if (GameMode.SPECTATOR.equals(event.getWhoClicked().getGameMode())) {
return;
}
- if (event.getView().getTopInventory() == null) {
- return;
- }
- try {
- if (event.getView().getTopInventory().getLocation() == null) {
- return;
- }
- } catch (Exception | AbstractMethodError e) {
- return;
- }
- if (event.getView().getTopInventory().getLocation().getBlock() == null) {
- return;
- }
- if (!event.getView().getTopInventory().getLocation().getBlock().getType().equals(Material.LOOM)) {
+ if (resolveLoomBlock((Player) event.getWhoClicked(), event.getView()) == null) {
return;
}
@@ -341,20 +341,7 @@ public void onDragLoom(InventoryDragEvent event) {
if (GameMode.SPECTATOR.equals(event.getWhoClicked().getGameMode())) {
return;
}
- if (event.getView().getTopInventory() == null) {
- return;
- }
- try {
- if (event.getView().getTopInventory().getLocation() == null) {
- return;
- }
- } catch (Exception | AbstractMethodError e) {
- return;
- }
- if (event.getView().getTopInventory().getLocation().getBlock() == null) {
- return;
- }
- if (!event.getView().getTopInventory().getLocation().getBlock().getType().equals(Material.LOOM)) {
+ if (resolveLoomBlock((Player) event.getWhoClicked(), event.getView()) == null) {
return;
}
@@ -368,22 +355,15 @@ public void onDragLoom(InventoryDragEvent event) {
@EventHandler
public void onCloseLoom(InventoryCloseEvent event) {
- if (event.getView().getTopInventory() == null) {
- return;
- }
- try {
- if (event.getView().getTopInventory().getLocation() == null) {
- return;
- }
- } catch (Exception | AbstractMethodError e) {
+ if (!(event.getPlayer() instanceof Player player)) {
return;
}
- if (event.getView().getTopInventory().getLocation().getBlock() == null) {
+ Block block = resolveLoomBlock(player, event.getView());
+ loomBlocksByPlayer.remove(player.getUniqueId());
+ if (block == null) {
return;
}
- Block block = event.getView().getTopInventory().getLocation().getBlock();
-
if (!openedLooms.containsKey(block)) {
return;
}
@@ -395,7 +375,6 @@ public void onCloseLoom(InventoryCloseEvent event) {
if (event.getView().getItem(0) != null) {
if (!event.getView().getItem(0).getType().equals(Material.AIR)) {
- Player player = (Player) event.getPlayer();
Item item = new Item(block.getLocation().clone().add(0.5, 1.5, 0.5));
item.setItemStack(event.getView().getItem(0));
item.setLocked(true);
@@ -405,41 +384,33 @@ public void onCloseLoom(InventoryCloseEvent event) {
}
}
- if (map.get("Banner") instanceof DisplayEntity) {
- DisplayEntity entity = (DisplayEntity) map.get("Banner");
- InteractionVisualizer.lightManager.deleteLight(entity.getLocation());
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), entity);
+ if (map.get("Banner") instanceof Item item) {
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
openedLooms.remove(block);
}
- public Map spawnDisplayEntitys(Player player, Block block) {
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.03, 0.5);
+ public Map spawnDisplayEntitys(Player player, Block block) {
+ Map map = new HashMap<>();
+ Location loc = block.getLocation().clone().add(0.5, 1.0, 0.5);
Location temploc = new Location(loc.getWorld(), loc.getX(), loc.getY(), loc.getZ()).setDirection(player.getLocation().getDirection().normalize().multiply(-1));
float yaw = temploc.getYaw();
- DisplayEntity banner = new DisplayEntity(loc.clone());
+ Item banner = new Item(loc.clone(), Item.RenderMode.BANNER);
setStand(banner, yaw);
map.put("Banner", banner);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), banner);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), banner);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
+ public void setStand(Item stand, float yaw) {
stand.setGravity(false);
stand.setSilent(true);
- stand.setInvulnerable(true);
- stand.setVisible(false);
- stand.setSmall(true);
- stand.setCustomName("IV.Loom.Banner");
+ stand.setVelocity(new Vector());
+ stand.setPickupDelay(32767);
stand.setRotation(yaw, stand.getLocation().getPitch());
- stand.setHeadPose(EulerAngle.ZERO);
}
public Vector rotateVectorAroundY(Vector vector, double degrees) {
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/NoteBlockDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/NoteBlockDisplay.java
index 31c6d192..e739fd91 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/NoteBlockDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/NoteBlockDisplay.java
@@ -114,8 +114,8 @@ public void onUseNoteBlock(PlayerInteractEvent event) {
return;
}
ConcurrentHashMap map = displayingNotes.get(block);
- DisplayEntity stand = map == null ? new DisplayEntity(textLocation.clone().add(0.0, -0.3, 0.0)) : (DisplayEntity) map.get("Stand");
- stand.teleport(textLocation.clone().add(0.0, -0.3, 0.0));
+ DisplayEntity stand = map == null ? new DisplayEntity(labelLocation(textLocation)) : (DisplayEntity) map.get("Stand");
+ stand.teleport(labelLocation(textLocation));
setStand(stand);
map = map == null ? new ConcurrentHashMap() : map;
@@ -147,6 +147,7 @@ public void onUseNoteBlock(PlayerInteractEvent event) {
}
public void setStand(DisplayEntity stand) {
+ stand.useLegacyNameTagStyle();
stand.setArms(true);
stand.setBasePlate(false);
stand.setMarker(true);
@@ -158,6 +159,12 @@ public void setStand(DisplayEntity stand) {
stand.setCustomNameVisible(true);
}
+ static Location labelLocation(Location clickedFaceLocation) {
+ // Keep the original marker-entity anchor. DisplayManager applies the
+ // vanilla name-tag attachment and TextDisplay origin compensation.
+ return clickedFaceLocation.clone().add(0.0, -0.3, 0.0);
+ }
+
@SuppressWarnings("DuplicateBranchesInSwitch")
public Location getFaceOffset(Block block, BlockFace face) {
Location location = block.getLocation().clone().add(0.5, 0.5, 0.5);
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ShulkerBoxDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ShulkerBoxDisplay.java
index 3cea4a34..4bc99155 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/ShulkerBoxDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/ShulkerBoxDisplay.java
@@ -208,11 +208,10 @@ public void onUseShulkerbox(InventoryClickEvent event) {
vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
}
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
@@ -304,12 +303,10 @@ public void onDragShulkerbox(InventoryDragEvent event) {
Vector offset = new Vector(0.0, 0.15, 0.0);
Vector vector = loc.clone().add(0.5, 0.5, 0.5).toVector().subtract(event.getWhoClicked().getEyeLocation().clone().add(0.0, InteractionVisualizer.playerPickupYOffset, 0.0).toVector()).multiply(0.13).add(offset);
item.setVelocity(vector);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
item.setItemStack(itemstack);
- item.setCustomName(System.currentTimeMillis() + "");
item.setPickupDelay(32767);
item.setGravity(true);
- DisplayManager.updateItem(item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
if (!link.containsKey(player)) {
link.put(player, new ArrayList
- ());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmithingTableDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmithingTableDisplay.java
index fcafbc16..464da86e 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmithingTableDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmithingTableDisplay.java
@@ -33,8 +33,8 @@
import com.loohp.interactionvisualizer.utils.MaterialUtils;
import com.loohp.interactionvisualizer.utils.MaterialUtils.MaterialMode;
import com.loohp.interactionvisualizer.utils.VanishUtils;
+import com.loohp.interactionvisualizer.utils.WorkstationDisplayPositioning;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.FluidCollisionMode;
import org.bukkit.GameMode;
@@ -56,7 +56,6 @@
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.SmithingInventory;
-import org.bukkit.util.EulerAngle;
import org.bukkit.util.Vector;
import java.util.HashMap;
@@ -120,40 +119,37 @@ public void process(Player player) {
items[i] = view.getItem(i);
}
- if (view.getItem(maxSlot) != null) {
- ItemStack itemstack = view.getItem(maxSlot);
- if (itemstack == null || itemstack.getType().equals(Material.AIR)) {
- itemstack = null;
+ ItemStack itemstack = view.getItem(maxSlot);
+ if (itemstack != null && itemstack.getType().equals(Material.AIR)) {
+ itemstack = null;
+ }
+ if (map.get(maxSlotStr) instanceof String) {
+ if (itemstack != null) {
+ Item item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ item.setItemStack(itemstack);
+ item.setVelocity(new Vector(0, 0, 0));
+ item.setPickupDelay(32767);
+ item.setGravity(false);
+ map.put(maxSlotStr, item);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
+ DisplayManager.updateItem(item);
+ } else {
+ map.put(maxSlotStr, "N/A");
}
- Item item = null;
- if (map.get(maxSlotStr) instanceof String) {
- if (itemstack != null) {
- item = new Item(loc.clone().add(0.5, 1.2, 0.5));
+ } else {
+ Item item = (Item) map.get(maxSlotStr);
+ if (itemstack != null) {
+ if (!item.getItemStack().equals(itemstack)) {
item.setItemStack(itemstack);
- item.setVelocity(new Vector(0, 0, 0));
- item.setPickupDelay(32767);
- item.setGravity(false);
- map.put(maxSlotStr, item);
- DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), item);
DisplayManager.updateItem(item);
- } else {
- map.put(maxSlotStr, "N/A");
}
} else {
- item = (Item) map.get(maxSlotStr);
- if (itemstack != null) {
- if (!item.getItemStack().equals(itemstack)) {
- item.setItemStack(itemstack);
- DisplayManager.updateItem(item);
- }
- } else {
- map.put(maxSlotStr, "N/A");
- DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
- }
+ map.put(maxSlotStr, "N/A");
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), item);
}
}
for (int i = 0; i < maxSlot; i++) {
- DisplayEntity stand = (DisplayEntity) map.get(String.valueOf(i));
+ Item stand = (Item) map.get(String.valueOf(i));
ItemStack item = items[i];
if (item == null || item.getType().equals(Material.AIR)) {
item = null;
@@ -162,27 +158,27 @@ public void process(Player player) {
MaterialMode materialMode = MaterialUtils.getMaterialType(item);
boolean changed = materialMode != standMode(stand);
if (changed) {
- toggleStandMode(stand, materialMode.toString());
+ toggleStandMode(stand, materialMode);
}
- if (!item.equals(stand.getItemInMainHand())) {
+ if (!item.equals(stand.getItemStack())) {
changed = true;
- stand.setItemInMainHand(item);
+ stand.setItemStack(item);
}
if (changed) {
- DisplayManager.updateDisplay(stand);
+ DisplayManager.updateItem(stand);
}
} else {
- if (!stand.getItemInMainHand().getType().equals(Material.AIR)) {
- stand.setItemInMainHand(new ItemStack(Material.AIR));
- DisplayManager.updateDisplay(stand);
+ if (!stand.getItemStack().isEmpty()) {
+ stand.setItemStack(ItemStack.empty());
+ DisplayManager.updateItem(stand);
}
}
}
- Location loc1 = ((DisplayEntity) map.get("0")).getLocation();
+ Location loc1 = ((Item) map.get("0")).getLocation();
InteractionVisualizer.lightManager.deleteLight(loc1);
- int skylight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromSky();
- int blocklight = loc1.getBlock().getRelative(BlockFace.UP).getLightFromBlocks() - 1;
+ int skylight = block.getRelative(BlockFace.UP).getLightFromSky();
+ int blocklight = block.getRelative(BlockFace.UP).getLightFromBlocks() - 1;
blocklight = Math.max(blocklight, 0);
if (skylight > 0) {
InteractionVisualizer.lightManager.createLight(loc1, skylight, LightType.SKY);
@@ -231,7 +227,8 @@ public void onSmithingTable(InventoryClickEvent event) {
return;
}
- Block block = event.getWhoClicked().getTargetBlockExact(7, FluidCollisionMode.NEVER);
+ Player player = (Player) event.getWhoClicked();
+ Block block = playermap.get(player);
if (!openedSTables.containsKey(block)) {
return;
@@ -242,51 +239,54 @@ public void onSmithingTable(InventoryClickEvent event) {
return;
}
- ItemStack itemstack = event.getCurrentItem();
+ ItemStack itemstack = event.getCurrentItem().clone();
Location loc = block.getLocation();
- Player player = (Player) event.getWhoClicked();
+ InventoryView view = event.getView();
- DisplayEntity slot0;
- DisplayEntity slot1;
- DisplayEntity slot2;
+ Item slot0;
+ Item slot1;
+ Item slot2;
if (maxSlot == 2) {
slot0 = null;
- slot1 = (DisplayEntity) map.get("0");
- slot2 = (DisplayEntity) map.get("1");
+ slot1 = (Item) map.get("0");
+ slot2 = (Item) map.get("1");
} else {
- slot0 = (DisplayEntity) map.get("0");
- slot1 = (DisplayEntity) map.get("1");
- slot2 =(DisplayEntity) map.get("2");
+ slot0 = (Item) map.get("0");
+ slot1 = (Item) map.get("1");
+ slot2 = (Item) map.get("2");
}
if (map.get(maxSlotStr) instanceof String) {
- map.put(maxSlotStr, new Item(block.getLocation().clone().add(0.5, 1.2, 0.5)));
+ Item result = new Item(block.getLocation().clone().add(0.5, 1.2, 0.5));
+ result.setItemStack(itemstack);
+ result.setVelocity(new Vector());
+ result.setPickupDelay(32767);
+ result.setGravity(false);
+ map.put(maxSlotStr, result);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY), result);
}
Item item = (Item) map.get(maxSlotStr);
Inventory before = Bukkit.createInventory(null, 9);
for (int i = 0; i <= maxSlot; i++) {
- before.setItem(i, player.getOpenInventory().getItem(i).clone());
+ before.setItem(i, InventoryUtils.cloneItem(view.getItem(i)));
}
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
+ if (!player.isOnline() || player.getOpenInventory() != view) {
+ return;
+ }
Inventory after = Bukkit.createInventory(null, 9);
for (int i = 0; i <= maxSlot; i++) {
- after.setItem(i, player.getOpenInventory().getItem(i).clone());
+ after.setItem(i, InventoryUtils.cloneItem(view.getItem(i)));
}
if (InventoryUtils.compareContents(before, after)) {
return;
}
- if (slot0 != null) {
- slot0.setLocked(true);
- }
- slot1.setLocked(true);
- slot2.setLocked(true);
- item.setLocked(true);
-
openedSTables.remove(block);
+ InteractionVisualizer.lightManager.deleteLight(((Item) map.get("0")).getLocation());
float yaw = getCardinalDirection(player);
Vector vector = new Location(slot1.getWorld(), slot1.getLocation().getX(), slot1.getLocation().getY(), slot1.getLocation().getZ(), yaw, 0).getDirection().normalize();
@@ -294,10 +294,18 @@ public void onSmithingTable(InventoryClickEvent event) {
slot2.teleport(slot2.getLocation().add(rotateVectorAroundY(vector.clone(), -90).multiply(0.1)));
if (slot0 != null) {
- DisplayManager.updateDisplay(slot0);
+ DisplayManager.updateItem(slot0);
+ }
+ DisplayManager.updateItem(slot1);
+ DisplayManager.updateItem(slot2);
+
+ item.setItemStack(itemstack);
+ if (slot0 != null) {
+ slot0.setLocked(true);
}
- DisplayManager.updateDisplay(slot1);
- DisplayManager.updateDisplay(slot2);
+ slot1.setLocked(true);
+ slot2.setLocked(true);
+ item.setLocked(true);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
for (Player each : InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMDROP, KEY)) {
@@ -306,16 +314,15 @@ public void onSmithingTable(InventoryClickEvent event) {
}, 6);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
- item.setItemStack(itemstack);
DisplayManager.updateItem(item);
DisplayManager.collectItem(item, player);
Scheduler.runTaskLater(InteractionVisualizer.plugin, () -> {
if (slot0 != null) {
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot0);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot0);
}
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot1);
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), slot2);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot1);
+ DisplayManager.removeItem(InteractionVisualizerAPI.getPlayers(), slot2);
}, 8);
}, 10);
}, 1);
@@ -363,18 +370,14 @@ public void onDragSmithingTable(InventoryDragEvent event) {
@EventHandler
public void onCloseSmithingTable(InventoryCloseEvent event) {
- if (!playermap.containsKey((Player) event.getPlayer())) {
- return;
- }
-
- Block block = playermap.get((Player) event.getPlayer());
-
- if (!openedSTables.containsKey(block)) {
+ Player player = (Player) event.getPlayer();
+ Block block = playermap.remove(player);
+ if (block == null) {
return;
}
Map map = openedSTables.get(block);
- if (!map.get("Player").equals(event.getPlayer())) {
+ if (map == null || !map.get("Player").equals(player)) {
return;
}
@@ -391,108 +394,36 @@ public void onCloseSmithingTable(InventoryCloseEvent event) {
}
}
- if (map.get("0") instanceof DisplayEntity) {
- DisplayEntity entity = (DisplayEntity) map.get("0");
+ if (map.get("0") instanceof Item entity) {
InteractionVisualizer.lightManager.deleteLight(entity.getLocation());
- DisplayManager.removeDisplay(InteractionVisualizerAPI.getPlayers(), entity);
}
openedSTables.remove(block);
- playermap.remove((Player) event.getPlayer());
}
- public MaterialMode standMode(DisplayEntity stand) {
- String plain = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (plain.startsWith("IV.SmithingTable.")) {
- return MaterialMode.getModeFromName(plain.substring(plain.lastIndexOf(".") + 1));
- }
- return null;
+ public MaterialMode standMode(Item stand) {
+ return switch (stand.getRenderMode()) {
+ case ITEM -> MaterialMode.ITEM;
+ case BLOCK -> MaterialMode.BLOCK;
+ case LOW_BLOCK -> MaterialMode.LOWBLOCK;
+ case TOOL -> MaterialMode.TOOL;
+ case STANDING -> MaterialMode.STANDING;
+ default -> null;
+ };
}
- public void toggleStandMode(DisplayEntity stand, String mode) {
- String plainText = PlainTextComponentSerializer.plainText().serialize(stand.getCustomName());
- if (!plainText.equals("IV.SmithingTable.Item")) {
- if (plainText.equals("IV.SmithingTable.Block")) {
- stand.setCustomName("IV.SmithingTable.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.084, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.102), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.14)));
-
- }
- if (plainText.equals("IV.SmithingTable.LowBlock")) {
- stand.setCustomName("IV.SmithingTable.Item");
- stand.setRotation(stand.getLocation().getYaw() - 45, stand.getLocation().getPitch());
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.teleport(stand.getLocation().add(0.0, -0.02, 0.0));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.09), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.15)));
-
- }
- if (plainText.equals("IV.SmithingTable.Tool")) {
- stand.setCustomName("IV.SmithingTable.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.1)));
- stand.teleport(stand.getLocation().add(0, 0.26, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- if (plainText.equals("IV.SmithingTable.Standing")) {
- stand.setCustomName("IV.SmithingTable.Item");
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(0.323), -90)));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(-0.115)));
- stand.teleport(stand.getLocation().add(0, 0.32, 0));
- stand.setRightArmPose(EulerAngle.ZERO);
- }
- }
- if (mode.equals("Block")) {
- stand.setCustomName("IV.SmithingTable.Block");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.14)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.102), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.084, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("LowBlock")) {
- stand.setCustomName("IV.SmithingTable.LowBlock");
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(0.15)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(0.09), -90)));
- stand.teleport(stand.getLocation().add(0.0, 0.02, 0.0));
- stand.setRightArmPose(new EulerAngle(357.9, 0.0, 0.0));
- stand.setRotation(stand.getLocation().getYaw() + 45, stand.getLocation().getPitch());
- }
- if (mode.equals("Tool")) {
- stand.setCustomName("IV.SmithingTable.Tool");
- stand.setRightArmPose(new EulerAngle(357.99, 0.0, 300.0));
- stand.teleport(stand.getLocation().add(0, -0.26, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().clone().getDirection().normalize().multiply(-0.1)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().clone().getDirection().normalize().multiply(-0.3), -90)));
- }
- if (mode.equals("Standing")) {
- stand.setCustomName("IV.SmithingTable.Standing");
- stand.setRightArmPose(new EulerAngle(0.0, 4.7, 4.7));
- stand.teleport(stand.getLocation().add(0, -0.32, 0));
- stand.teleport(stand.getLocation().add(stand.getLocation().getDirection().normalize().multiply(0.115)));
- stand.teleport(stand.getLocation().add(rotateVectorAroundY(stand.getLocation().getDirection().normalize().multiply(-0.323), -90)));
- }
+ public void toggleStandMode(Item stand, MaterialMode mode) {
+ WorkstationDisplayPositioning.setRenderMode(stand, mode);
}
- public Map spawnDisplayEntitys(Player player, Block block, int maxSlot) { //.add(0.68, 0.600781, 0.35)
- Map map = new HashMap<>();
- Location loc = block.getLocation().clone().add(0.5, 0.600781, 0.5);
- DisplayEntity center = new DisplayEntity(loc);
+ public Map spawnDisplayEntitys(Player player, Block block, int maxSlot) {
+ Map map = new HashMap<>();
float yaw = getCardinalDirection(player);
- center.setRotation(yaw, center.getLocation().getPitch());
- setStand(center);
- center.setCustomName("IV.SmithingTable.Center");
- Vector vector = rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.19), -100).add(center.getLocation().clone().getDirection().normalize().multiply(-0.11));
- DisplayEntity middle = new DisplayEntity(loc.clone().add(vector));
- setStand(middle, yaw);
-
- DisplayEntity slot0 = new DisplayEntity(middle.getLocation());
+ Location origin = block.getLocation();
+ Item slot0 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.0, 0.0);
setStand(slot0, yaw);
- DisplayEntity slot1 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.3), -90)));
+ Item slot1 = WorkstationDisplayPositioning.gridItem(origin, yaw, -0.3, 0.0);
setStand(slot1, yaw + 20);
- DisplayEntity slot2 = new DisplayEntity(middle.getLocation().clone().add(rotateVectorAroundY(center.getLocation().clone().getDirection().normalize().multiply(0.3), 90)));
+ Item slot2 = WorkstationDisplayPositioning.gridItem(origin, yaw, 0.3, 0.0);
setStand(slot2, yaw - 20);
if (maxSlot == 2) {
@@ -503,25 +434,19 @@ public Map spawnDisplayEntitys(Player player, Block block
map.put("1", slot1);
map.put("2", slot2);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot0);
}
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
- DisplayManager.spawnDisplay(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot1);
+ DisplayManager.sendItemSpawn(InteractionVisualizerAPI.getPlayerModuleList(Modules.ITEMSTAND, KEY), slot2);
return map;
}
- public void setStand(DisplayEntity stand, float yaw) {
- stand.setArms(true);
- stand.setBasePlate(false);
- stand.setMarker(true);
+ public void setStand(Item stand, float yaw) {
stand.setGravity(false);
- stand.setInvulnerable(true);
- stand.setVisible(false);
stand.setSilent(true);
- stand.setSmall(true);
- stand.setRightArmPose(EulerAngle.ZERO);
- stand.setCustomName("IV.SmithingTable.Item");
+ stand.setVelocity(new Vector());
+ stand.setPickupDelay(32767);
stand.setRotation(yaw, stand.getLocation().getPitch());
}
diff --git a/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmokerDisplay.java b/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmokerDisplay.java
index 0842d9e6..7b99b12a 100644
--- a/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmokerDisplay.java
+++ b/common/src/main/java/com/loohp/interactionvisualizer/blocks/SmokerDisplay.java
@@ -25,10 +25,15 @@
import com.loohp.interactionvisualizer.api.InteractionVisualizerAPI.Modules;
import com.loohp.interactionvisualizer.api.VisualizerRunnableDisplay;
import com.loohp.interactionvisualizer.api.events.InteractionVisualizerReloadEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityActivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityAddedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityDeactivatedEvent;
+import com.loohp.interactionvisualizer.api.events.TileEntityRemovedEvent;
import com.loohp.interactionvisualizer.entityholders.DisplayEntity;
import com.loohp.interactionvisualizer.entityholders.Item;
import com.loohp.interactionvisualizer.managers.DisplayManager;
import com.loohp.interactionvisualizer.managers.PlayerLocationManager;
+import com.loohp.interactionvisualizer.managers.PerformanceMetrics;
import com.loohp.interactionvisualizer.managers.TileEntityManager;
import com.loohp.interactionvisualizer.objectholders.EntryKey;
import com.loohp.interactionvisualizer.objectholders.TileEntity.TileEntityType;
@@ -37,7 +42,7 @@
import com.loohp.interactionvisualizer.utils.VanishUtils;
import com.loohp.interactionvisualizer.scheduler.ScheduledTask;
import com.loohp.interactionvisualizer.scheduler.Scheduler;
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -50,9 +55,14 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.inventory.FurnaceBurnEvent;
+import org.bukkit.event.inventory.FurnaceExtractEvent;
+import org.bukkit.event.inventory.FurnaceSmeltEvent;
+import org.bukkit.event.inventory.FurnaceStartSmeltEvent;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.EulerAngle;
@@ -78,9 +88,12 @@ public class SmokerDisplay extends VisualizerRunnableDisplay implements Listener
private String noFuelColor = "&c";
private int progressBarLength = 10;
private String amountPending = " &7+{Amount}";
+ private final BlockUpdateScheduler blockUpdates;
public SmokerDisplay() {
onReload(new InteractionVisualizerReloadEvent());
+ this.blockUpdates = new BlockUpdateScheduler<>(this::nearbySmoker,
+ this.checkingPeriod, this.gcPeriod);
}
@EventHandler
@@ -102,6 +115,9 @@ public EntryKey key() {
@Override
public ScheduledTask gc() {
+ if (InteractionVisualizer.eventDrivenBlockUpdates) {
+ return null;
+ }
return Scheduler.runTaskTimer(InteractionVisualizer.plugin, () -> {
Iterator