Compare commits

..
Author SHA1 Message Date
YoyoNow f63390915b Add glyph flight for enchant particles and blend team links
Pull Request Build / Build (pull_request) Successful in 1m57s
Entries with the enchant particle get a Glyph flight setting: float down as
usual, fly into the player or fly out of them, the latter two unlocked by the
WarGear Season placement and participation. Team link lines now start in the
drawing player's particle and blend into the partner's along the way.
2026-10-26 15:13:39 +01:00
YoyoNow db939e27d9 Turn the touches back into reward designs styled like any other
The Touch setting is gone. Its unique mechanics become reward designs that use
the entry's particle: Burst (on jumping, landing or sneaking, as a puff or a
ring), Footprints, Team link, Shimmer and Heartbeat. Each is unlocked by any
of the rewards that had it, and Burst's triggers are locked separately. The
wing beat becomes the Beat on jump wing motion, unlocked by WarGear Season
2022. December snow and the glyphs are dropped: the first is a plain particle
with a condition, the second only works with enchanting glyphs.
2026-10-26 15:08:08 +01:00
YoyoNow 36e6329f2b Redraw beating wings every tick only after a jump
The wing beat touch no longer lowers the entry's interval. Its wings are drawn
every tick during the beat and at their usual interval otherwise; the extra
redraws do not count towards the slot budget.
2026-10-26 15:02:56 +01:00
YoyoNow b2300c128e Let the wing beat touch beat the entry's own wings
On a Wings entry the touch adds a beat after each jump to the wings' motion
and draws nothing of its own; picking it redraws the wings every 2 ticks like
a motion does. Other designs keep the dragon breath wings.
2026-10-26 14:59:32 +01:00
YoyoNow 9cb4e0b7db Unlock everything for administrators
Every particle requirement passes for users with the administration
permission. Lore keeps naming the regular requirement.
2026-10-26 14:49:53 +01:00
YoyoNow 8e4730bd12 Turn reward touches into an unlockable Touch setting
Every entry gets a Touch setting next to its design, offering the 16 touches
the rewards carried, each unlocked by its reward. The reward designs that the
builder can now make are removed; only the underwater rain cloud and the
medium, hard and advanced egg hunt designs stay. Reward requirements move to
Unlocks, shared by touches, wing images and cosy smoke. The trail duration
moves to the particle row to make room in the design row.
2026-10-26 14:48:29 +01:00
YoyoNow baa278a1f5 Cap particle speed at 0.05 and give each wing shape its own icon
Speed steps by 0.005 up to 0.05; presets keep their faster particles. Number
values now show up to three decimals.
2026-10-26 14:43:44 +01:00
YoyoNow 8fd6fdf81e Unlock tournament wing images and cosy smoke for building
The MiniWarGear league, Underwater MWG, WarGear season 2022 and WarGear clash
wing images become Wings shapes, locked behind the placement that earns their
reward. Cosy campfire smoke becomes a buildable particle for Hells Bells
participants. Choice options can now carry a requirement: locked options show
it in the picker, are skipped when cycling and hide entries that use them.
2026-10-26 14:37:40 +01:00
YoyoNow 7d0f5aceae Give the rebuildable reward designs unique touches
Rewards whose look the basic designs can build now carry a touch the builder
cannot make: bursts on jumping, landing or sneaking, footprints, a line to
nearby players with the same reward, a December extra, aimed enchanting
glyphs, a heartbeat, a jump-beat wing or an orbiting shape. BodyMotion tracks
the jump, land and sneak events the touches react to.
2026-10-26 14:34:50 +01:00
YoyoNow ad9087c038 Remove the breathe wing motion
Wings keep the flap motions only; saved breathing wings load as none.
2026-10-26 13:57:59 +01:00
YoyoNow 872da5f62a Open wings with speed and let them flap or breathe
All wings stand 5 degrees open when idle and open up to 15 degrees the
faster the player moves. The wings design gets a motion setting to flap
slowly, flap quickly or breathe; picking a motion redraws the wing every
2 ticks so it looks smooth, and going back to none restores every 20.
Body rotation tracking now also tracks speed, as BodyMotion.
2026-10-26 13:55:58 +01:00
YoyoNow e830d087c9 Merge wing designs, turn wings with the body, limit leaves
Wing and Drawn wings become one Wings design whose shapes include the
images and the drawn bat, butterfly and fairy; for drawn shapes the points
per wing replace the amount. Body rotation is tracked per player the way
the client turns the body, and wings, the heart beat and body placement
follow it instead of the head. Leaves particles are for the server team
only, and typed hex colours show the closest dye.
2026-10-26 13:39:28 +01:00
YoyoNow b4661ea345 Add colour along the wing and sparkle to lobby wings
Both wing designs can colour their points from the spine to the tips
instead of changing colour over time, and a sparkle setting shows only a
share of the points each tick, which also lowers their cost. The angel
and phoenix drawn shapes are removed.
2026-10-26 13:34:23 +01:00
YoyoNow 4e5945d2bc Lower drawn wings to the shoulder blades
The drawn shapes had their shoulder joint at head height. They now sit
about 1.3 blocks above the feet at the default size.
2026-10-26 10:50:57 +01:00
YoyoNow a5b8f00c58 Add drawn wing shapes to the slot builder
A new Drawn wings design draws angel, bat, butterfly, fairy and phoenix
wings from lines and curves instead of images. The points per wing can be
set from 6 to 60, so sparse wings stay cheap enough to show often.
2026-10-26 10:48:32 +01:00
YoyoNow 8e23490b04 Settle open particle slot questions
Slots hold up to 9 entries, so the slot editor is one entry row plus the
bottom bar, and the server team may spawn 90 particles per tick. The
water and lava rings for taking part in any event return as reward
designs, and locked slots can be deleted from their read-only view.
2026-10-26 10:33:15 +01:00
YoyoNow 4c6b5e654a Quiet no-op particle setting clicks and show entry particle counts
Clicking a step past the minimum or maximum, or setting an unchanged
value, now does nothing instead of showing the budget message, and
changes that lower a slot's particles are allowed even over budget.
Entries show their particles per tick in the slot editor list, and the
entry editor's paper shows the edited entry's share.
2026-10-26 10:23:24 +01:00
YoyoNow 38a33b7298 Show the resulting value on number step buttons
Each step button on the number screen shows the value it leads to, and
marks when that value is the minimum or maximum.
2026-10-26 10:20:50 +01:00
YoyoNow 32797bd2a8 Limit lobby particle speed to 0.2
Speed went up to 1, which makes most particles shoot several blocks away.
It now goes up to 0.2; presets use at most 0.1.
2026-10-26 10:12:14 +01:00
YoyoNow 28b62bf27b Limit lobby particle spread to half a block
Spread went up to 2 blocks, which scatters a design's particles far
around the player. It now goes up to 0.5 in steps of 0.01.
2026-10-26 10:12:10 +01:00
YoyoNow db915f7be7 Keep large and lingering particles out of the slot builder
Explosion, sonic boom, sweep attack, gusts, campfire and large smoke,
dust pillar, block marker and shriek are a block or more in size or rise
for seconds and wall players in once a design spawns many of them. They
stay available to presets only.
2026-10-26 10:10:21 +01:00
YoyoNow aef26b5e9f Add the trail particle with duration and target
Trail particles fly from where they spawn to a target offset over the
chosen number of ticks, in the entry's colour. The target follows the
body like other offsets when the entry is placed relative to it.
2026-10-26 10:10:17 +01:00
YoyoNow 8009098965 Space out entry editor rows and show empty slots as firework stars
Settings in the entry editor start one column after the row headers, and
empty particle slots show a firework star instead of a gray pane.
2026-10-26 09:49:20 +01:00
YoyoNow 47f0d9858b Place lobby particle entries relative to the body
Entries get a World/Body setting. In body mode offsets, heights and wings
follow the body's facing and lie along the body while gliding or swimming,
so wings stay on the back. Rings circle the body but keep their world
angle, so they do not turn with the player. New wings default to body;
presets and rewards stay in world mode and look exactly as before.
2026-10-26 09:49:11 +01:00
YoyoNow c3eda01e20 Cycle particle settings with up to five options
Choices with up to five options cycle forward on left click and backward
on right click, listing all options in the lore. Two-option choices still
read as a toggle; longer choices keep their list.
2026-10-26 09:49:07 +01:00
YoyoNow 5e81081620 Toggle two-option particle settings on click
Choices with exactly two options switch on click like on/off settings,
and their lore lists both options with the current one highlighted.
2026-10-26 09:49:02 +01:00
YoyoNow ed70d65bc4 Size particle choice menus by their options
Lists that fit on one page get one row per nine options plus the bottom
bar, so a condition with three options opens a two row menu. Longer lists
keep the paged 6 row menu.
2026-10-26 09:48:57 +01:00
YoyoNow 2ce8799a19 Use black panes for the particle menu bottom bar
Unused bottom bar columns are black glass panes, empty places above it
are gray, including the free entry places of the slot editor.
2026-10-26 09:48:51 +01:00
YoyoNow 15036a7ba0 Allow hex colour codes for lobby particles
The colour screen gets a button to type a colour as #RRGGBB or #RGB,
next to picking one of the dye colours.
2026-10-26 09:48:46 +01:00
YoyoNow d77c014409 Translate lobby particle types and blocks
Add English and German names for all 104 selectable particle types and
the 43 blocks of the block setting.
2026-10-26 09:48:41 +01:00
YoyoNow 6b5441bb04 Offer nearly every vanilla particle in the slot builder
Add the remaining particles, including colour, colour transition, block
and item particles, which take their colours or a new block setting from
the entry. Only particles that cover the screen, show only under water or
need a target stay out.
2026-10-26 09:48:36 +01:00
YoyoNow 54d30f4a42 Fill unused particle menu columns with gray panes
Unused bottom bar columns show a gray glass pane instead of a barrier,
and the slot editor drops its use button since the particle menu
toggles slots with a click.
2026-10-26 09:48:32 +01:00
YoyoNow e41f76937e Give particle menus a shared bottom bar
Back, use, add, rename, icon, particle count and delete or reset now sit
in the same column of the last row in every particle menu, and columns a
menu does not use show a barrier.
2026-10-26 09:48:28 +01:00
YoyoNow 0c7ee052e8 Toggle particle slots with a click and label entry editor rows
A slot with entries is switched on or off with a click in the particle
menu and edited with a shift click; empty slots still open the editor.
The entry editor rows get distinct glowing header icons.
2026-10-26 09:48:25 +01:00
YoyoNow b60cdfc88d Simplify lobby particle menus
Every menu item now acts on a plain click: slots open their editor with a
use button, entries open their editor, and duplicating is gone. The entry
editor groups settings into labelled rows, numbers and colours get their
own screens instead of relying on right click or Q, and slot names show
the particles per tick used out of the maximum.
2026-10-26 09:48:20 +01:00
YoyoNow 98f04451a4 Polish lobby particle slots
Count the real particle amount of reward designs, set slot budgets to
30 particles per tick (60 for the server team), let requirements test
only against an UnlockContext, and drop requirements and messages the
old particle menu used.
2026-10-26 09:48:05 +01:00
YoyoNow c95901bc42 Add lobby particle slot menus
Replace the particle inventory with a main menu showing the 7 slots and
unlocked presets, a slot editor with a read-only view for locked slots,
and an entry editor with pickers for designs, choices, colours and icons.
Slots are limited to 10 entries and a particles-per-tick budget.
2026-10-26 09:48:00 +01:00
YoyoNow 211e2693cf Move lobby presets and rewards onto the particle slot model
Team- and user-specific particles become presets built from entries,
tournament and egg hunt particles become fixed reward designs, and the
old particle enums, ParticleEnum and ParticleData are removed. The
particle menu temporarily lists only unlocked presets.
2026-10-26 09:47:55 +01:00
YoyoNow 1980375e73 Add configurable particle slot model
Introduce parameters, particle types, designs and entries that build the
existing particle elements, store each of the 7 particle slots in its own
UserConfig key, and let LobbyPlayer select a slot next to the old enum
particles. Slots 4-5 need a team, 6-7 the server team.
2026-10-26 09:47:49 +01:00
YoyoNow ef041272bb Refactor lobby particle runtime for multi-entry particles
Tick particles from per-player cached always/move lists, drop the
per-instance listeners in Delayed and RandomParticle, and compute
particle viewers once per tick instead of once per spawned point.
2026-10-26 09:47:43 +01:00
230 changed files with 9705 additions and 5124 deletions
-29
View File
@@ -1,29 +0,0 @@
name: Setup build
description: Installs the JDKs and Gradle and configures the SteamWar Maven repository
inputs:
maven-credentials:
description: Contents of steamwar.properties
required: true
runs:
using: composite
steps:
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
# The last version becomes the default
java-version: |
8
11
17
21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Setup Maven Repository
shell: bash
env:
SW_MAVEN_CREDENTIALS: ${{ inputs.maven-credentials }}
run: |
echo "$SW_MAVEN_CREDENTIALS" > steamwar.properties
-51
View File
@@ -1,51 +0,0 @@
name: Setup SSH
description: Configures the deploy server as the SSH host "deploy" for ssh and scp
inputs:
host:
description: Deploy server host name
required: true
user:
description: Deploy server user
required: true
port:
description: Deploy server SSH port
required: false
default: '22'
key:
description: Private SSH key of the deploy user
required: true
runs:
using: composite
steps:
- name: Configure SSH
shell: bash
env:
DEPLOY_HOST: ${{ inputs.host }}
DEPLOY_USER: ${{ inputs.user }}
DEPLOY_PORT: ${{ inputs.port }}
DEPLOY_SSH_KEY: ${{ inputs.key }}
run: |
set -euo pipefail
: "${DEPLOY_HOST:?Missing DEPLOY_HOST secret}"
: "${DEPLOY_USER:?Missing DEPLOY_USER secret}"
: "${DEPLOY_SSH_KEY:?Missing DEPLOY_SSH_KEY secret}"
port="${DEPLOY_PORT:-22}"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$port" "$DEPLOY_HOST" >> ~/.ssh/known_hosts
cat >> ~/.ssh/config <<EOF
Host deploy
HostName ${DEPLOY_HOST}
User ${DEPLOY_USER}
Port ${port}
IdentityFile ~/.ssh/deploy_key
EOF
chmod 600 ~/.ssh/config
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env bash
#
# Usage: affected-projects.sh <state-dir> [base-commit]
#
# Runs the checkAffected task of every project for the changes between the base commit and HEAD
# and writes the result to $GITHUB_OUTPUT:
# projects - space separated paths of the affected projects
# tasks - space separated Gradle tasks to run (empty if nothing is affected)
#
# The projects exchange files with this script through the state directory. If it contains
# previous-dependency-versions (deploys only), projects whose resolved dependency versions changed are
# affected as well and the collected dependency-versions, deploy-artifacts and deploy-commands of all
# projects are written to it. Without a known base commit everything is affected.
set -euo pipefail
shopt -s nullglob
mkdir -p "$1"
state="$(realpath "$1")"
base="${2:-}"
deploy=false
if [[ -f "${state}/previous-dependency-versions" ]]; then
deploy=true
fi
# Whether the diff of settings.gradle.kts only touches library versions of the version catalog
catalog_only() {
! git diff -U0 --no-renames "$base" HEAD -- settings.gradle.kts \
| grep -E '^[+-]' \
| grep -vE '^(\+\+\+|---) ' \
| grep -qvE '^[+-][[:space:]]*(library\("|val [A-Za-z]+Version = "|$)'
}
# All lines the projects wrote for the given kind
collect() {
local files=("${state}/projects/$1"/*)
if (( ${#files[@]} )); then
sort "${files[@]}"
fi
}
rm -f "${state}/changed-files"
if [[ -z "$base" ]]; then
echo "No base commit, everything is affected"
elif ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
echo "Base commit ${base} is unknown, everything is affected"
else
echo "Changed files since ${base}:"
git diff --name-only --no-renames "$base" HEAD | tee "${state}/changed-files"
# Library version changes are detected through the resolved dependency versions of the projects using them
if [[ "$deploy" == "true" ]] && grep -qx 'settings.gradle.kts' "${state}/changed-files" && catalog_only; then
echo "settings.gradle.kts only changed library versions, relying on the dependency versions"
grep -vx 'settings.gradle.kts' "${state}/changed-files" > "${state}/changed-files.filtered" || true
mv "${state}/changed-files.filtered" "${state}/changed-files"
fi
fi
args=(-q --no-daemon checkAffected -PciDir="$state")
if [[ "$deploy" == "true" ]]; then
args+=(deployInfo --refresh-dependencies)
fi
./gradlew "${args[@]}"
if [[ "$deploy" == "true" ]]; then
collect dependency-versions > "${state}/dependency-versions"
collect deploy-artifacts > "${state}/deploy-artifacts"
collect deploy-commands > "${state}/deploy-commands"
fi
projects="$(collect affected | cut -d ' ' -f 1 | sort -u | paste -sd ' ' -)"
tasks="$(collect affected | cut -d ' ' -f 1 | sort -u | sed 's/$/:build/' | paste -sd ' ' -)"
echo "projects=${projects}"
echo "tasks=${tasks}"
{
echo "projects=${projects}"
echo "tasks=${tasks}"
} >> "$GITHUB_OUTPUT"
-32
View File
@@ -1,32 +0,0 @@
#!/usr/bin/env bash
#
# Usage: stage-artifacts.sh <state-dir> <output-dir> [projects]
#
# Copies the deployed files of the given projects to the output directory under their deployed name,
# skipping those identical to the deployed file, and lists the projects with copied files in
# state-dir/deployed-projects. Reads state-dir/deploy-artifacts ("<project> <build output> <deployed name>"
# per line, written by affected-projects.sh) and state-dir/deployed-hashes (sha256sum output of the deployed files).
set -euo pipefail
state="$1"
output="$2"
projects=" ${3:-} "
mkdir -p "$output"
: > "${state}/deployed-projects"
while read -r project source name; do
if [[ "$projects" != *" ${project} "* ]]; then
continue
fi
hash="$(sha256sum "$source" | cut -d ' ' -f 1)"
if grep -qxF "${hash} ${name}" "${state}/deployed-hashes"; then
echo "${name} from ${project} is unchanged"
else
echo "Staging ${name} from ${project}"
cp "$source" "${output}/${name}"
echo "$project" >> "${state}/deployed-projects"
fi
done < "${state}/deploy-artifacts"
+100 -76
View File
@@ -7,25 +7,84 @@ on:
- version/*
jobs:
deploy:
name: Build and deploy
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Setup Java 8
uses: actions/setup-java@v5
with:
fetch-depth: 0
- name: Setup build
uses: ./.gitea/actions/setup-build
distribution: 'temurin'
java-version: 8
- name: Setup Java 11
uses: actions/setup-java@v5
with:
maven-credentials: ${{ secrets.SW_MAVEN_CREDENTIALS }}
- name: Setup SSH
uses: ./.gitea/actions/setup-ssh
distribution: 'temurin'
java-version: 11
- name: Setup Java 17
uses: actions/setup-java@v5
with:
host: ${{ secrets.DEPLOY_HOST }}
user: ${{ secrets.DEPLOY_USER }}
port: ${{ secrets.DEPLOY_PORT }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
distribution: 'temurin'
java-version: 17
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Setup Maven Repository
env:
SW_MAVEN_CREDENTIALS: ${{ secrets.SW_MAVEN_CREDENTIALS }}
run: |
echo "$SW_MAVEN_CREDENTIALS" > steamwar.properties
- name: Build with Gradle
run: ./gradlew clean build --no-daemon --no-build-cache --refresh-dependencies
- name: Stage deploy artifacts
shell: bash
run: |
set -euo pipefail
rm -rf deploy
mkdir -p deploy
cp "AccessWidener/build/libs/AccessWidener-all.jar" "deploy/AccessWidener.jar"
cp "BauSystem/build/libs/BauSystem-all.jar" "deploy/BauSystem.jar"
cp "FightSystem/build/libs/FightSystem-all.jar" "deploy/FightSystem.jar"
cp "KotlinCore/build/libs/KotlinCore-all.jar" "deploy/KotlinCore.jar"
cp "TNTLeague/build/libs/TNTLeague.jar" "deploy/TNTLeague.jar"
cp "LobbySystem/build/libs/LobbySystem.jar" "deploy/LobbySystem.jar"
cp "MissileWars/build/libs/MissileWars.jar" "deploy/MissileWars.jar"
cp "Realtime/build/libs/Realtime.jar" "deploy/RealTime.jar"
cp "SchematicSystem/build/libs/SchematicSystem-all.jar" "deploy/SchematicSystem.jar"
cp "SpigotCore/build/libs/SpigotCore-all.jar" "deploy/SpigotCore.jar"
cp "Teamserver/build/libs/Teamserver.jar" "deploy/Builder.jar"
cp "TowerRun/build/libs/TowerRun.jar" "deploy/TowerRun.jar"
cp "VelocityCore/Persistent/build/libs/Persistent.jar" "deploy/PersistentVelocityCore.jar"
cp "VelocityCore/Dependencies/build/libs/Dependencies-all.jar" "deploy/DependenciesVelocityCore.jar"
cp "VelocityCore/build/libs/VelocityCore-all.jar" "deploy/VelocityCore.jar"
cp "WebsiteBackend/build/libs/WebsiteBackend-all.jar" "deploy/website-api.jar"
cp "CLI/build/distributions/sw.zip" "deploy/sw.zip"
- name: Upload deploy artifacts
uses: actions/upload-artifact@v3
with:
name: steamwar-jars
path: deploy/
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: build
steps:
- name: Download deploy artifacts
uses: actions/download-artifact@v3
with:
name: steamwar-jars
path: deploy
- name: Resolve deploy target
id: target
@@ -47,80 +106,45 @@ jobs:
exit 1
fi
- name: Read deployed state
id: deployed
- name: Upload jars with scp
shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_PATH: ${{ steps.target.outputs.path }}
run: |
set -euo pipefail
mkdir -p .ci
ssh deploy "cat '$DEPLOY_PATH/.dependency-versions' 2>/dev/null || true" > .ci/previous-dependency-versions
ssh deploy "cd '$DEPLOY_PATH' 2>/dev/null && sha256sum -- *.jar *.zip 2>/dev/null || true" > .ci/deployed-hashes
: "${DEPLOY_HOST:?Missing DEPLOY_HOST secret}"
: "${DEPLOY_USER:?Missing DEPLOY_USER secret}"
: "${DEPLOY_SSH_KEY:?Missing DEPLOY_SSH_KEY secret}"
commit="$(ssh deploy "cat '$DEPLOY_PATH/.deployed-commit' 2>/dev/null || true")"
if [[ ! "$commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "No valid deployed commit found in ${DEPLOY_PATH}"
commit=""
fi
port="${DEPLOY_PORT:-22}"
echo "Deployed commit: ${commit:-none}"
echo "commit=${commit}" >> "$GITHUB_OUTPUT"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$port" "$DEPLOY_HOST" >> ~/.ssh/known_hosts
- name: Determine affected projects
id: affected
shell: bash
run: bash .gitea/scripts/affected-projects.sh .ci "${{ steps.deployed.outputs.commit }}"
- name: Build with Gradle
if: steps.affected.outputs.tasks != ''
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '$DEPLOY_PATH'"
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '$DEPLOY_PATH/upload'"
scp -i ~/.ssh/deploy_key -P "$port" deploy/* "${DEPLOY_USER}@${DEPLOY_HOST}:$DEPLOY_PATH/upload"
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "rm -f '$DEPLOY_PATH'/*.jar '$DEPLOY_PATH'/*.zip"
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "mv '$DEPLOY_PATH'/upload/* '$DEPLOY_PATH'"
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "rm -r '$DEPLOY_PATH/upload'"
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "chmod o-w '$DEPLOY_PATH'/*"
- name: Restart Services
shell: bash
env:
TASKS: ${{ steps.affected.outputs.tasks }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
set -euo pipefail
read -ra tasks <<< "$TASKS"
./gradlew clean "${tasks[@]}" --no-daemon --no-build-cache --refresh-dependencies
# Only jars whose content differs from the deployed one, so dependents that merely compile against a change stay untouched
- name: Stage changed artifacts
shell: bash
run: bash .gitea/scripts/stage-artifacts.sh .ci .ci/jars "${{ steps.affected.outputs.projects }}"
- name: Deploy
shell: bash
env:
DEPLOY_PATH: ${{ steps.target.outputs.path }}
run: |
set -euo pipefail
ssh deploy "mkdir -p '$DEPLOY_PATH'"
if compgen -G ".ci/jars/*" > /dev/null; then
ssh deploy "rm -rf '$DEPLOY_PATH/upload' && mkdir -p '$DEPLOY_PATH/upload'"
scp .ci/jars/* "deploy:$DEPLOY_PATH/upload"
ssh deploy "mv -f '$DEPLOY_PATH'/upload/* '$DEPLOY_PATH' && rm -r '$DEPLOY_PATH/upload' && chmod o-w '$DEPLOY_PATH'/*"
else
echo "No changed artifacts to deploy"
fi
# The next run only builds what changed since this commit
scp .ci/dependency-versions "deploy:$DEPLOY_PATH/.dependency-versions"
ssh deploy "echo '$GITHUB_SHA' > '$DEPLOY_PATH/.deployed-commit'"
# The afterDeploy commands of the deploy blocks of all projects with changed artifacts
- name: Run after deploy commands
if: github.ref_name == 'main'
shell: bash
env:
DEPLOY_PATH: ${{ steps.target.outputs.path }}
run: |
set -euo pipefail
while read -r project command; do
if grep -qxF "$project" .ci/deployed-projects; then
echo "Running '${command}' for ${project}"
ssh -n deploy "cd '$DEPLOY_PATH' && ${command}"
fi
done < .ci/deploy-commands
ssh -i ~/.ssh/deploy_key -p "$DEPLOY_PORT" "${DEPLOY_USER}@${DEPLOY_HOST}" "sudo systemctl restart api.service"
ssh -i ~/.ssh/deploy_key -p "$DEPLOY_PORT" "${DEPLOY_USER}@${DEPLOY_HOST}" "unzip -o /jars/current/sw.zip -d /jars"
+25 -21
View File
@@ -14,31 +14,35 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Setup Java 8
uses: actions/setup-java@v5
with:
fetch-depth: 0
- name: Setup build
uses: ./.gitea/actions/setup-build
distribution: 'temurin'
java-version: 8
- name: Setup Java 11
uses: actions/setup-java@v5
with:
maven-credentials: ${{ secrets.SW_MAVEN_CREDENTIALS }}
- name: Determine affected projects
id: affected
shell: bash
run: |
set -euo pipefail
base_branch="$(jq -r '.pull_request.base.ref' "$GITHUB_EVENT_PATH")"
base="$(git merge-base "origin/${base_branch}" HEAD || true)"
bash .gitea/scripts/affected-projects.sh .ci "$base"
- name: Build with Gradle
if: steps.affected.outputs.tasks != ''
shell: bash
distribution: 'temurin'
java-version: 11
- name: Setup Java 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 17
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- name: Setup Maven Repository
env:
TASKS: ${{ steps.affected.outputs.tasks }}
SW_MAVEN_CREDENTIALS: ${{ secrets.SW_MAVEN_CREDENTIALS }}
run: |
set -euo pipefail
read -ra tasks <<< "$TASKS"
./gradlew "${tasks[@]}" --no-daemon
echo "$SW_MAVEN_CREDENTIALS" > steamwar.properties
- name: Build with Gradle
run: ./gradlew build --no-daemon
- name: Merge successful backport PR
shell: bash
-5
View File
@@ -20,11 +20,6 @@
plugins {
`java-library`
alias(libs.plugins.shadow)
deploy
}
deploy {
artifact(tasks.shadowJar)
}
dependencies {
@@ -36,7 +36,6 @@ dependencies {
annotationProcessor(libs.classindex)
compileOnly(project(":SpigotCore", "default"))
compileOnly(project(":KotlinCore", "default"))
implementation(project(":HiderCore"))
compileOnly(libs.axiom)
compileOnly(libs.authlib)
@@ -35,7 +35,8 @@ SCOREBOARD_LOADER = Loader
SCOREBOARD_TPS = TPS
SCOREBOARD_TPS_FROZEN = §eFrozen
SCOREBOARD_TRACE_TICKS = Ticks
SCOREBOARD_VIEW = View§8: §a{0}
SCOREBOARD_TECHHIDER = TechHider§8: §aOn
SCOREBOARD_XRAY = XRay§8: §aOn
SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam
SCOREBOARD_LOCK_SUPERVISOR = Bau Lock§8: §eSupervisor
SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server) Team
@@ -1032,11 +1033,6 @@ XRAY_HELP = §8/§exray §8- §7Toggle Xray
XRAY_GLOBAL = §cNo xray in global region
XRAY_ON = §aXray activated
XRAY_OFF = §cXray deactivated
# HULLHIDER
HULLHIDER_HELP = §8/§ehullhider §8- §7Toggle Hullhider
HULLHIDER_GLOBAL = §cNo hullhider in global region
HULLHIDER_ON = §aHullhider activated
HULLHIDER_OFF = §cHullhider deactivated
# WorldEdit
COLORREPLACE_HELP = §8//§ecolorreplace §8[§7color§8] §8[§7color§8] §8- §7Replace all blocks of one color with another
TYPEREPLACE_HELP = §8//§etypereplace §8[§7type§8] §8[§7type§8] §8- §7Replace all blocks of one type with another
@@ -35,7 +35,8 @@ SCOREBOARD_LOADER = Loader
SCOREBOARD_TPS = TPS
SCOREBOARD_TPS_FROZEN = §eEingefroren
SCOREBOARD_TRACE_TICKS = Ticks
SCOREBOARD_VIEW = Ansicht§8: §a{0}
SCOREBOARD_TECHHIDER = TechHider§8: §aAn
SCOREBOARD_XRAY = XRay§8: §aAn
SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam
SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server-) Team
SCOREBOARD_LOCK_SERVERTEAM = Bau Lock§8: §eServerteam
@@ -965,11 +966,6 @@ XRAY_HELP = §8/§exray §8- §7Xray umschalten
XRAY_GLOBAL = §cKein Xray in der globalen region
XRAY_ON = §aXray aktiviert
XRAY_OFF = §cXray deaktiviert
# HULLHIDER
HULLHIDER_HELP = §8/§ehullhider §8- §7Hullhider umschalten
HULLHIDER_GLOBAL = §cKein Hullhider in der globalen region
HULLHIDER_ON = §aHullhider aktiviert
HULLHIDER_OFF = §cHullhider deaktiviert
# WorldEdit
COLORREPLACE_HELP = §8//§ecolorreplace §8[§7color§8] §8[§7color§8] §8- §7Ersetzt eine Farbe mit einer anderen
TYPEREPLACE_HELP = §8//§etyreplace §8[§7type§8] §8[§7type§8] §8- §7Ersetzt einen Blockgruppe mit einer anderen
@@ -24,6 +24,7 @@ import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.features.gui.BauGUI;
import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin;
import de.steamwar.bausystem.features.script.lua.libs.LuaLib;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.panzern.Panzern;
import de.steamwar.bausystem.features.slaves.panzern.PanzernAlgorithm;
import de.steamwar.bausystem.features.tracer.TraceManager;
@@ -40,6 +41,7 @@ import de.steamwar.core.CRIUSleepEvent;
import de.steamwar.core.Core;
import de.steamwar.core.WorldIdentifier;
import de.steamwar.core.worldedit.SteamEdit;
import de.steamwar.core.worldedit.WorldEditRendererCUIEditor;
import de.steamwar.linkage.AbstractLinker;
import de.steamwar.linkage.SpigotLinker;
import de.steamwar.message.Message;
@@ -108,9 +110,9 @@ public class BauSystem extends JavaPlugin implements Listener {
if (any instanceof PanzernAlgorithm) {
Panzern.add((PanzernAlgorithm) any);
}
// if (any instanceof BoundingBoxLoader) {
// ((BoundingBoxLoader) any).load();
// }
if (any instanceof BoundingBoxLoader) {
((BoundingBoxLoader) any).load();
}
}
};
try {
@@ -1,177 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.hullhider;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.techhider.HiddenHull;
import de.steamwar.bausystem.features.techhider.HidingRule;
import de.steamwar.bausystem.features.techhider.TechHiderCommand;
import de.steamwar.bausystem.features.xray.XrayCommand;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.command.SWCommand;
import de.steamwar.hullhider.Hull;
import de.steamwar.hullhider.HullHider;
import de.steamwar.hullhider.HullRegion;
import de.steamwar.hullhider.Viewers;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.linkage.LinkedTimer;
import de.steamwar.techhider.ObfuscationConfig;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Hides the inside of the build area hull of a region from the players who toggle it, as the hull hider of a fight would.
*/
@Linked
public class HullHiderCommand extends SWCommand implements Listener {
public HullHiderCommand() {
super("hullhider");
}
@Linked
// Only blocks are hidden, entities stay visible
private final HullHider<Region> hullHider = new HullHider<>(false);
// Read on netty threads by the BauTechHider
private final Map<Region, HiddenHull> hulls = new ConcurrentHashMap<>();
private final Map<Region, HidingRule> hidingRules = new ConcurrentHashMap<>();
private final Map<Region, Set<Player>> hidden = new ConcurrentHashMap<>();
@LinkedInstance
public TechHiderCommand techHiderCommand;
@LinkedInstance
public XrayCommand xrayCommand;
@Register(description = "HULLHIDER_HELP")
public void toggleHider(@Validator Player player) {
Region region = Region.getRegion(player.getLocation());
if (region.getType().isGlobal()) {
BauSystem.MESSAGE.send("HULLHIDER_GLOBAL", player);
return;
}
if (!region.getGameModeConfig().Techhider.Active) {
return;
}
techHiderCommand.disable(region, player);
xrayCommand.disable(region, player);
if (hidden.getOrDefault(region, Collections.emptySet()).contains(player)) {
disable(region, player);
BauSystem.MESSAGE.sendPrefixless("HULLHIDER_OFF", player, ChatMessageType.ACTION_BAR);
} else {
enable(region, player);
BauSystem.MESSAGE.sendPrefixless("HULLHIDER_ON", player, ChatMessageType.ACTION_BAR);
}
}
private void enable(Region region, Player player) {
if (!hulls.containsKey(region)) {
Point min = region.getBuildArea().getMinPoint(true);
Point max = region.getBuildArea().getMaxPoint(true);
HullRegion hullRegion = HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ());
// The techhider of the gamemode is applied as well, its hidden blocks occlude as they are replaced by the obfuscation block
ObfuscationConfig obfuscation = ObfuscationConfig.of(region.getGameModeConfig().Techhider);
Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, obfuscation, new Viewers.VisibleByDefault());
hullHider.addHull(region, hull);
hulls.put(region, new HiddenHull(hull, obfuscation.getObfuscationState()));
hidingRules.put(region, HidingRule.of(region.getBuildArea(), obfuscation));
}
hidden.computeIfAbsent(region, rg -> ConcurrentHashMap.newKeySet()).add(player);
// Added players see the hull blocks of a hull visible by default
hullHider.addViewer(player, region);
// Block changes by WorldEdit or Axiom do not update the hull, so it is rebuilt every time somebody enables it.
// This also resends the chunks to the player.
hullHider.initialize(region);
}
/**
* Stops hiding the hull of the region from the player, removing the hull if nobody uses it anymore.
*/
public void disable(Region region, Player player) {
Set<Player> players = hidden.get(region);
if (players == null || !players.remove(player)) return;
if (players.isEmpty()) {
hidden.remove(region);
hulls.remove(region);
hidingRules.remove(region);
hullHider.removeHull(region);
} else {
hullHider.removeViewer(player, region);
hullHider.resendChunks(player, region);
}
}
public void addHidingRules(Player player, List<HidingRule> rules) {
hidden.forEach((region, players) -> {
HidingRule rule = hidingRules.get(region);
if (rule != null && players.contains(player)) {
rules.add(rule);
}
});
}
public void addHiddenHulls(Player player, List<HiddenHull> hiddenHulls) {
hidden.forEach((region, players) -> {
HiddenHull hiddenHull = hulls.get(region);
if (hiddenHull != null && players.contains(player)) {
hiddenHulls.add(hiddenHull);
}
});
}
@LinkedTimer(delay = 1, period = 1)
private void onTick() {
if (hidden.isEmpty()) return;
hullHider.sendUncoveredBlocks();
}
private World world() {
return Bukkit.getWorlds().get(0);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
for (Region region : hidden.keySet()) {
disable(region, event.getPlayer());
}
}
public boolean isActive(Region region, Player player) {
return hidden.getOrDefault(region, Collections.emptySet()).contains(player);
}
}
@@ -36,11 +36,9 @@ import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitTask;
import java.util.ArrayList;
import java.util.List;
@@ -65,14 +63,13 @@ public class Loader implements Listener, SWPlayer.Component {
private List<LoaderElement> elements = new ArrayList<>();
private int currentElement = 0;
private long waitTime = 0;
private final BukkitTask task;
public Loader(Player p) {
this.p = p;
this.recorder = new LoaderRecorder(p, elements);
Bukkit.getPluginManager().registerEvents(this, BauSystem.getInstance());
task = BauSystem.runTaskTimer(BauSystem.getInstance(), () -> {
BauSystem.runTaskTimer(BauSystem.getInstance(), () -> {
if (stage != Stage.RUNNING && stage != Stage.SINGLE) return;
if (!Permission.BUILD.hasPermission(p)) return;
if (waitTime > 0) {
@@ -139,27 +136,13 @@ public class Loader implements Listener, SWPlayer.Component {
}
public void stop() {
end();
if (getLoader(p) == this) {
SWPlayer.of(p).removeComponent(Loader.class);
}
}
// Also called when the loader is replaced by a new one
@Override
public void onUnmount(SWPlayer player) {
end();
}
private void end() {
stage = Stage.END;
if (recorder != null) {
recorder.stop();
recorder = null;
}
elements.clear();
task.cancel();
HandlerList.unregisterAll(this);
SWPlayer.of(p).removeComponent(Loader.class);
}
public boolean setTicksBetweenShots(int delay) {
@@ -24,7 +24,6 @@ import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.core.SWPlayer;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Material;
@@ -44,12 +43,13 @@ public class ObserverTracerListener implements Listener {
static Set<Player> enabled = new HashSet<>();
@LinkedTimer(delay = 15, period = 15)
private void showTracers() {
SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> {
if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return;
pair.getComponent().show();
});
public ObserverTracerListener() {
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> {
if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return;
pair.getComponent().show();
});
}, 15L, 15L);
}
@EventHandler
@@ -28,7 +28,6 @@ import de.steamwar.command.SWCommand;
import de.steamwar.entity.CRay;
import de.steamwar.entity.REntityServer;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import org.bukkit.*;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
@@ -105,16 +104,15 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
public RayVisualizerCommand() {
super("rayvisualizer");
}
@LinkedTimer(delay = 1, period = 1)
private void updateRays() {
if (server.getPlayers().isEmpty()) return;
List<TNTPrimed> primedList = WORLD.getEntitiesByClass(TNTPrimed.class)
.stream().toList();
rayData.reset();
if (primedList.isEmpty()) return;
calculateRays(primedList, primedList);
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
if (server.getPlayers().isEmpty()) return;
List<TNTPrimed> primedList = WORLD.getEntitiesByClass(TNTPrimed.class)
.stream().toList();
rayData.reset();
if (primedList.isEmpty()) return;
calculateRays(primedList, primedList);
}, 1, 1);
}
private void calculateRays(List<TNTPrimed> fromTNTs, List<TNTPrimed> toTNTs) {
@@ -27,7 +27,6 @@ import de.steamwar.bausystem.features.script.lua.libs.StorageLib;
import de.steamwar.bausystem.region.Region;
import de.steamwar.core.SWPlayer;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;
@@ -48,12 +47,13 @@ import org.luaj.vm2.LuaValue;
@Linked
public class EventListener implements Listener {
@LinkedTimer(delay = 1, period = 1)
private static void resetLastF() {
long millis = System.currentTimeMillis();
SWPlayer.allWithSingleComponent(ScriptRunner.ScriptData.class)
.filter(pair -> millis - pair.getComponent().getLastF() > 200)
.forEach(pair -> pair.getComponent().setLastF(Long.MAX_VALUE));
static {
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
long millis = System.currentTimeMillis();
SWPlayer.allWithSingleComponent(ScriptRunner.ScriptData.class)
.filter(pair -> millis - pair.getComponent().getLastF() > 200)
.forEach(pair -> pair.getComponent().setLastF(Long.MAX_VALUE));
}, 1, 1);
}
@EventHandler(priority = EventPriority.MONITOR)
@@ -44,9 +44,9 @@ import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
import de.steamwar.inventory.SWAnvilInv;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
@@ -136,10 +136,11 @@ public class SimulatorCursor implements Listener {
private static final Map<Player, Long> LAST_SNEAKS = new HashMap<>();
@LinkedTimer(delay = 1, period = 1)
private static void clearLastSneaks() {
long millis = System.currentTimeMillis();
LAST_SNEAKS.entrySet().removeIf(entry -> millis - entry.getValue() > 200);
static {
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
long millis = System.currentTimeMillis();
LAST_SNEAKS.entrySet().removeIf(entry -> millis - entry.getValue() > 200);
}, 1, 1);
}
@EventHandler(priority = EventPriority.HIGH)
@@ -19,97 +19,48 @@
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.api.Enable;
import com.sk89q.worldedit.blocks.SkullBlock;
import com.sk89q.worldedit.world.block.BaseBlock;
import de.steamwar.inventory.SWItem;
import lombok.Getter;
import lombok.ToString;
import net.minecraft.core.BlockPos;
import net.minecraft.locale.Language;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.phys.shapes.CollisionContext;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.block.data.type.Bed;
import org.bukkit.block.data.type.Door;
import org.bukkit.craftbukkit.block.CraftBlockType;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.util.CraftVoxelShape;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.VoxelShape;
import org.bukkit.block.data.type.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Random;
import java.util.function.Consumer;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@ToString
@Getter
public class BlockBoundingBox {
@Linked
public static class Loader implements Enable {
private static List<String> randomPlayerHead = new ArrayList<>();
private static Random random = new Random();
@Override
public void enable() {
long time = System.currentTimeMillis();
Registry.BLOCK.forEach(blockType -> {
Block block = CraftBlockType.bukkitToMinecraftNew(blockType);
Material material = blockType.asMaterial();
if (!material.isItem()) return;
if (!material.isBlock()) return;
if (material == Material.WATER) return;
if (material.getBlastResistance() <= 0.0) return;
if (material.getBlastResistance() > 9.0) return;
block.getStateDefinition().getPossibleStates().forEach(blockState -> {
VoxelShape voxelShape = new CraftVoxelShape(blockState.getCollisionShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO, CollisionContext.empty()));
Collection<BoundingBox> boundingBoxes = voxelShape.getBoundingBoxes();
if (boundingBoxes.isEmpty()) return;
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double minZ = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double maxY = Double.MIN_VALUE;
double maxZ = Double.MIN_VALUE;
for (BoundingBox boundingBox : boundingBoxes) {
minX = Math.min(minX, boundingBox.getMinX());
minY = Math.min(minY, boundingBox.getMinY());
minZ = Math.min(minZ, boundingBox.getMinZ());
maxX = Math.max(maxX, boundingBox.getMaxX());
maxY = Math.max(maxY, boundingBox.getMaxY());
maxZ = Math.max(maxZ, boundingBox.getMaxZ());
}
// Ignore Full Blocks
if (maxX - minX == 1 && maxY - minY == 1 && maxZ - minZ == 1) return;
BlockData blockData = CraftBlockData.createData(blockState);
if (blockData instanceof Door) return; // Doors dont seem to be 2 blocks tall?
if (blockData instanceof Bed) return; // Beds dont seem to be 2 blocks tall?
if (blockData instanceof Waterlogged waterlogged && waterlogged.isWaterlogged()) return;
String key = material.isItem()
? material.getItemTranslationKey() // "item.minecraft.diamond" / "block.minecraft.stone"
: material.getBlockTranslationKey(); // for block-only materials like WATER
new BlockBoundingBox(
blockData,
boundingBoxes.stream().map(Cuboid::new).collect(Collectors.toList()),
material,
"§f" + Language.getInstance().getOrDefault(key)
);
});
});
BauSystem.getInstance().getLogger().info("Laufbau init > " + (System.currentTimeMillis() - time) + "ms " + elements.size() + " blocks");
elements.sort(Comparator.<BlockBoundingBox>comparingDouble(value -> value.material.getBlastResistance()).thenComparing(value -> value.name));
}
static {
randomPlayerHead.add("zOnlyKroks");
randomPlayerHead.add("YoyoNow");
randomPlayerHead.add("Konstruktix");
randomPlayerHead.add("Merling03");
randomPlayerHead.add("Lord_Loading");
/*
randomPlayerHead.add(UUID.fromString("1623d4b1-b21c-41d3-93c2-eee2845b8497"));
randomPlayerHead.add(UUID.fromString("f75632be-e3ec-4069-9bec-d13ac6891177"));
randomPlayerHead.add(UUID.fromString("80382970-424b-4299-9296-d956beab9e7b"));
randomPlayerHead.add(UUID.fromString("f395ce89-096d-48e9-b796-a2d88722c22b"));
randomPlayerHead.add(UUID.fromString("db516394-d999-4b77-8d13-73cceec77dfe"));
*/
}
public static List<BlockBoundingBox> elements = new ArrayList<>();
@@ -117,32 +68,221 @@ public class BlockBoundingBox {
public BlockData blockData;
public List<Cuboid> cuboidList;
private double volume;
public Material material;
public String name;
public SWItem swItem;
public BlockBoundingBox(BlockData blockData, List<Cuboid> cuboidList, Material material, String name) {
public Consumer<BaseBlock> blockConsumer = null;
public BlockBoundingBox(BlockData blockData, List<Cuboid> cuboidList, SWItem swItem) {
this.blockData = blockData;
this.cuboidList = cuboidList;
this.material = material;
this.name = name;
this.swItem = swItem;
volume = cuboidList.stream().mapToDouble(Cuboid::volume).sum();
// Override/Ignore items with worse/better blast resistance
for (int i = 0; i < elements.size(); i++) {
BlockBoundingBox other = elements.get(i);
if (other.cuboidList.equals(cuboidList)) {
if (other.material.getBlastResistance() < material.getBlastResistance()) {
elements.remove(i);
break;
} else {
return;
}
}
}
elements.add(this);
}
public BlockBoundingBox(BlockData blockData, List<Cuboid> cuboidList, SWItem swItem, Consumer<BaseBlock> blockConsumer) {
this.blockData = blockData;
this.cuboidList = cuboidList;
this.swItem = swItem;
volume = cuboidList.stream().mapToDouble(Cuboid::volume).sum();
elements.add(this);
this.blockConsumer = blockConsumer;
}
public double volume() {
return volume;
}
static {
addPixel(Material.AIR.createBlockData(), 0, 0, 0, 0, 0, 0, null);
// addPixel(Material.COBWEB.createBlockData(), 0, 0, 0, 0, 0, 0, createItem("LAUFBAU_BLOCK_COBWEB", Material.COBWEB));
addPixel(Material.END_STONE.createBlockData(), 0, 0, 0, 16, 16, 16, null);
addPixel(Material.DIRT_PATH.createBlockData(), 0, 0, 0, 16, 15, 16, createItem("LAUFBAU_BLOCK_GRASS_PATH", Material.DIRT_PATH));
addPixel(Material.MUD.createBlockData(), 0, 0, 0, 16, 14, 16, createItem("LAUFBAU_BLOCK_SOUL_SAND", Material.SOUL_SAND));
Cocoa cocoaNorth = (Cocoa) Material.COCOA.createBlockData();
cocoaNorth.setAge(2);
cocoaNorth.setFacing(BlockFace.NORTH);
addPixel(cocoaNorth, 4, 3, 1, 8, 9, 8, createItem("LAUFBAU_BLOCK_COCOA", Material.COCOA_BEANS, "LAUFBAU_FACING_NORTH"));
Cocoa cocoaSouth = (Cocoa) Material.COCOA.createBlockData();
cocoaSouth.setAge(2);
cocoaSouth.setFacing(BlockFace.SOUTH);
addPixel(cocoaSouth, 4, 3, 7, 8, 9, 8, createItem("LAUFBAU_BLOCK_COCOA", Material.COCOA_BEANS, "LAUFBAU_FACING_SOUTH"));
Cocoa cocoaWest = (Cocoa) Material.COCOA.createBlockData();
cocoaWest.setAge(2);
cocoaWest.setFacing(BlockFace.WEST);
addPixel(cocoaWest, 1, 3, 4, 8, 9, 8, createItem("LAUFBAU_BLOCK_COCOA", Material.COCOA_BEANS, "LAUFBAU_FACING_WEST"));
Cocoa cocoaEast = (Cocoa) Material.COCOA.createBlockData();
cocoaEast.setAge(2);
cocoaEast.setFacing(BlockFace.EAST);
addPixel(cocoaEast, 7, 3, 4, 8, 9, 8, createItem("LAUFBAU_BLOCK_COCOA", Material.COCOA_BEANS, "LAUFBAU_FACING_EAST"));
TurtleEgg singleTurtleEgg = (TurtleEgg) Material.TURTLE_EGG.createBlockData();
singleTurtleEgg.setEggs(1);
singleTurtleEgg.setHatch(0);
addPixel(singleTurtleEgg, 4, 0, 3, 9, 7, 9, createItem("LAUFBAU_BLOCK_TURTLE_EGG", Material.TURTLE_EGG, "LAUFBAU_COUNT_1"));
TurtleEgg doubleTurtleEgg = (TurtleEgg) Material.TURTLE_EGG.createBlockData();
doubleTurtleEgg.setEggs(2);
doubleTurtleEgg.setHatch(0);
addPixel(doubleTurtleEgg, 1, 0, 1, 14, 7, 14, createItem("LAUFBAU_BLOCK_TURTLE_EGG", Material.TURTLE_EGG, "LAUFBAU_COUNT_2"));
addPixel(Material.CHEST.createBlockData(), 1, 0, 1, 14, 14, 14, createItem("LAUFBAU_BLOCK_CHEST", Material.CHEST));
Snow snowLayers8 = (Snow) Material.SNOW.createBlockData();
snowLayers8.setLayers(8);
addPixel(snowLayers8, 0, 0, 0, 16, 14, 16, createItem("LAUFBAU_BLOCK_SNOW", Material.SNOW, "LAUFBAU_LAYERS_8"));
Directional headNorth = (Directional) Material.PLAYER_WALL_HEAD.createBlockData();
headNorth.setFacing(BlockFace.NORTH);
addPixel(headNorth, 4, 4, 8, 8, 8, 8, createItem("LAUFBAU_BLOCK_PLAYER_WALL_HEAD", Material.PLAYER_HEAD, "LAUFBAU_FACING_NORTH"), BlockBoundingBox::randomPlayerHead);
Directional headSouth = (Directional) Material.PLAYER_WALL_HEAD.createBlockData();
headSouth.setFacing(BlockFace.SOUTH);
addPixel(headSouth, 4, 4, 0, 8, 8, 8, createItem("LAUFBAU_BLOCK_PLAYER_WALL_HEAD", Material.PLAYER_HEAD, "LAUFBAU_FACING_SOUTH"), BlockBoundingBox::randomPlayerHead);
Directional headWest = (Directional) Material.PLAYER_WALL_HEAD.createBlockData();
headWest.setFacing(BlockFace.WEST);
addPixel(headWest, 8, 4, 4, 8, 8, 8, createItem("LAUFBAU_BLOCK_PLAYER_WALL_HEAD", Material.PLAYER_HEAD, "LAUFBAU_FACING_WEST"), BlockBoundingBox::randomPlayerHead);
Directional headEast = (Directional) Material.PLAYER_WALL_HEAD.createBlockData();
headEast.setFacing(BlockFace.EAST);
addPixel(headEast, 0, 4, 4, 8, 8, 8, createItem("LAUFBAU_BLOCK_PLAYER_WALL_HEAD", Material.PLAYER_HEAD, "LAUFBAU_FACING_EAST"), BlockBoundingBox::randomPlayerHead);
Snow snowLayers7 = (Snow) Material.SNOW.createBlockData();
snowLayers7.setLayers(7);
addPixel(snowLayers7, 0, 0, 0, 16, 12, 16, createItem("LAUFBAU_BLOCK_SNOW", Material.SNOW, "LAUFBAU_LAYERS_7"));
Snow snowLayers6 = (Snow) Material.SNOW.createBlockData();
snowLayers6.setLayers(6);
addPixel(snowLayers6, 0, 0, 0, 16, 10, 16, createItem("LAUFBAU_BLOCK_SNOW", Material.SNOW, "LAUFBAU_LAYERS_6"));
addPixel(Material.STONECUTTER.createBlockData(), 0, 0, 0, 16, 9, 16, createItem("LAUFBAU_BLOCK_STONECUTTER", Material.STONECUTTER));
addPixel(Material.PLAYER_HEAD.createBlockData(), 4, 0, 4, 8, 8, 8, createItem("LAUFBAU_BLOCK_PLAYER_HEAD", Material.PLAYER_HEAD), BlockBoundingBox::randomPlayerHead);
addPixel(Material.CAKE.createBlockData(), 1, 0, 1, 14, 8, 14, createItem("LAUFBAU_BLOCK_CAKE", Material.CAKE));
Slab bottomSlab = (Slab) Material.END_STONE_BRICK_SLAB.createBlockData();
bottomSlab.setType(Slab.Type.BOTTOM);
addPixel(bottomSlab, 0, 0, 0, 16, 8, 16, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_SLAB", Material.END_STONE_BRICK_SLAB, "LAUFBAU_TYPE_BOTTOM"));
Slab topSlab = (Slab) Material.END_STONE_BRICK_SLAB.createBlockData();
topSlab.setType(Slab.Type.TOP);
addPixel(topSlab, 0, 8, 0, 16, 8, 16, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_SLAB", Material.END_STONE_BRICK_SLAB, "LAUFBAU_TYPE_TOP"));
SeaPickle quadSeaPickle = (SeaPickle) Material.SEA_PICKLE.createBlockData();
quadSeaPickle.setWaterlogged(false);
quadSeaPickle.setPickles(4);
addPixel(quadSeaPickle, 2, 0, 2, 12, 7, 12, createItem("LAUFBAU_BLOCK_SEA_PICKLE", Material.SEA_PICKLE, "LAUFBAU_COUNT_4"));
Campfire campfire = (Campfire) Material.CAMPFIRE.createBlockData();
campfire.setSignalFire(false);
campfire.setLit(false);
addPixel(campfire, 0, 0, 0, 16, 7, 16, createItem("LAUFBAU_BLOCK_CAMPFIRE", Material.CAMPFIRE));
SeaPickle trippleSeaPickle = (SeaPickle) Material.SEA_PICKLE.createBlockData();
trippleSeaPickle.setWaterlogged(false);
trippleSeaPickle.setPickles(3);
addPixel(trippleSeaPickle, 2, 0, 2, 12, 6, 12, createItem("LAUFBAU_BLOCK_SEA_PICKLE", Material.SEA_PICKLE, "LAUFBAU_COUNT_3"));
SeaPickle doubleSeaPickle = (SeaPickle) Material.SEA_PICKLE.createBlockData();
doubleSeaPickle.setWaterlogged(false);
doubleSeaPickle.setPickles(2);
addPixel(doubleSeaPickle, 3, 0, 3, 10, 6, 10, createItem("LAUFBAU_BLOCK_SEA_PICKLE", Material.SEA_PICKLE, "LAUFBAU_COUNT_2"));
SeaPickle singleSeaPickle = (SeaPickle) Material.SEA_PICKLE.createBlockData();
singleSeaPickle.setWaterlogged(false);
singleSeaPickle.setPickles(1);
addPixel(singleSeaPickle, 6, 0, 6, 4, 6, 4, createItem("LAUFBAU_BLOCK_SEA_PICKLE", Material.SEA_PICKLE, "LAUFBAU_COUNT_1"));
addPixel(Material.FLOWER_POT.createBlockData(), 5, 0, 5, 6, 6, 6, createItem("LAUFBAU_BLOCK_FLOWER_POT", Material.FLOWER_POT));
Snow snowLayers3 = (Snow) Material.SNOW.createBlockData();
snowLayers3.setLayers(3);
addPixel(snowLayers3, 0, 0, 0, 16, 4, 16, createItem("LAUFBAU_BLOCK_SNOW", Material.SNOW, "LAUFBAU_LAYERS_3"));
TrapDoor bottomTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
bottomTrapDoor.setHalf(Bisected.Half.BOTTOM);
addPixel(bottomTrapDoor, 0, 0, 0, 16, 3, 16, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_HALF_BOTTOM"));
TrapDoor topTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
topTrapDoor.setHalf(Bisected.Half.TOP);
addPixel(topTrapDoor, 0, 13, 0, 16, 3, 16, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_HALF_TOP"));
Snow snowLayers2 = (Snow) Material.SNOW.createBlockData();
snowLayers2.setLayers(2);
addPixel(snowLayers2, 0, 0, 0, 16, 2, 16, createItem("LAUFBAU_BLOCK_SNOW", Material.SNOW, "LAUFBAU_LAYERS_2"));
addPixel(Material.LILY_PAD.createBlockData(), 1, 0, 1, 14, 1.5, 14, createItem("LAUFBAU_BLOCK_LILY_PAD", Material.LILY_PAD));
addPixel(Material.WHITE_CARPET.createBlockData(), 0, 0, 0, 16, 1, 16, createItem("LAUFBAU_BLOCK_WHITE_CARPET", Material.WHITE_CARPET));
Directional endRodBottomTop = (Directional) Material.END_ROD.createBlockData();
endRodBottomTop.setFacing(BlockFace.UP);
addPixel(endRodBottomTop, 6, 0, 6, 4, 16, 4, createItem("LAUFBAU_BLOCK_END_ROD", Material.END_ROD, "LAUFBAU_FACING_UP", "LAUFBAU_FACING_DOWN"));
Directional endRodNorthSouth = (Directional) Material.END_ROD.createBlockData();
endRodNorthSouth.setFacing(BlockFace.NORTH);
addPixel(endRodNorthSouth, 6, 6, 0, 4, 4, 16, createItem("LAUFBAU_BLOCK_END_ROD", Material.END_ROD, "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
Directional endRodEastWest = (Directional) Material.END_ROD.createBlockData();
endRodEastWest.setFacing(BlockFace.EAST);
addPixel(endRodEastWest, 0, 6, 6, 16, 4, 4, createItem("LAUFBAU_BLOCK_END_ROD", Material.END_ROD, "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
Directional lightningRodBottomTop = (Directional) Material.LIGHTNING_ROD.createBlockData();
lightningRodBottomTop.setFacing(BlockFace.UP);
addPixel(lightningRodBottomTop, 6, 0, 6, 4, 16, 4, createItem("LAUFBAU_BLOCK_LIGHTNING_ROD", Material.LIGHTNING_ROD, "LAUFBAU_FACING_UP", "LAUFBAU_FACING_DOWN"));
Directional lightningRodNorthSouth = (Directional) Material.LIGHTNING_ROD.createBlockData();
lightningRodNorthSouth.setFacing(BlockFace.NORTH);
addPixel(lightningRodNorthSouth, 6, 6, 0, 4, 4, 16, createItem("LAUFBAU_BLOCK_LIGHTNING_ROD", Material.LIGHTNING_ROD, "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
Directional lightningRodEastWest = (Directional) Material.LIGHTNING_ROD.createBlockData();
lightningRodEastWest.setFacing(BlockFace.EAST);
addPixel(lightningRodEastWest, 0, 6, 6, 16, 4, 4, createItem("LAUFBAU_BLOCK_LIGHTNING_ROD", Material.LIGHTNING_ROD, "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
Waterlogged conduit = (Waterlogged) Material.CONDUIT.createBlockData();
conduit.setWaterlogged(false);
addPixel(conduit, 5, 5, 5, 6, 6, 6, createItem("LAUFBAU_BLOCK_CONDUIT", Material.CONDUIT));
TrapDoor northTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
northTrapDoor.setFacing(BlockFace.NORTH);
northTrapDoor.setOpen(true);
addPixel(northTrapDoor, 0, 0, 13, 16, 16, 3, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_FACING_NORTH", "LAUFBAU_OPEN"));
TrapDoor southTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
southTrapDoor.setFacing(BlockFace.SOUTH);
southTrapDoor.setOpen(true);
addPixel(southTrapDoor, 0, 0, 0, 16, 16, 3, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_FACING_SOUTH", "LAUFBAU_OPEN"));
TrapDoor westTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
westTrapDoor.setFacing(BlockFace.WEST);
westTrapDoor.setOpen(true);
addPixel(westTrapDoor, 13, 0, 0, 3, 16, 16, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_FACING_WEST", "LAUFBAU_OPEN"));
TrapDoor eastTrapDoor = (TrapDoor) Material.IRON_TRAPDOOR.createBlockData();
eastTrapDoor.setFacing(BlockFace.EAST);
eastTrapDoor.setOpen(true);
addPixel(eastTrapDoor, 0, 0, 0, 3, 16, 16, createItem("LAUFBAU_BLOCK_IRON_TRAPDOOR", Material.IRON_TRAPDOOR, "LAUFBAU_FACING_EAST", "LAUFBAU_OPEN"));
}
private static void addPixel(BlockData blockData, double xPixel, double yPixel, double zPixel, double dxPixel, double dyPixel, double dzPixel, SWItem swItem) {
new BlockBoundingBox(blockData, Arrays.asList(pixelCuboid(xPixel, yPixel, zPixel, dxPixel, dyPixel, dzPixel)), swItem);
}
private static void addPixel(BlockData blockData, double xPixel, double yPixel, double zPixel, double dxPixel, double dyPixel, double dzPixel, SWItem swItem, Consumer<BaseBlock> blockConsumer) {
new BlockBoundingBox(blockData, Arrays.asList(pixelCuboid(xPixel, yPixel, zPixel, dxPixel, dyPixel, dzPixel)), swItem, blockConsumer);
}
@SuppressWarnings("deprecation")
private static void randomPlayerHead(BaseBlock block) {
if (block instanceof SkullBlock) {
SkullBlock skullBlock = (SkullBlock) block;
skullBlock.setOwner(randomPlayerHead.get(random.nextInt(randomPlayerHead.size())).toString());
}
}
}
@@ -17,8 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
package de.steamwar.bausystem.features.slaves.laufbau;
public interface ParticleEnum {
ParticleData getParticle();
public interface BoundingBoxLoader {
void load();
}
@@ -22,7 +22,6 @@ package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.region.Point;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.bukkit.util.BoundingBox;
@Data
@AllArgsConstructor
@@ -34,15 +33,6 @@ public class Cuboid {
private double dy;
private double dz;
public Cuboid(BoundingBox boundingBox) {
this.x = boundingBox.getMinX();
this.y = boundingBox.getMinY();
this.z = boundingBox.getMinZ();
this.dx = boundingBox.getWidthX();
this.dy = boundingBox.getHeight();
this.dz = boundingBox.getWidthZ();
}
public boolean intersects(Cuboid cuboid) {
double minx = x - cuboid.dx;
double miny = y - cuboid.dy;
@@ -29,15 +29,11 @@ import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Laufbau {
private static final Comparator<BlockBoundingBox> BLAST_RESISTANCE = Comparator.comparingDouble(value -> -value.material.getBlastResistance());
private static final Comparator<BlockBoundingBox> VOLUME = Comparator.comparingDouble(value -> -value.volume());
private World world;
private Location pos1;
private Location pos2;
@@ -65,10 +61,26 @@ public class Laufbau {
editSession = WorldEditUtils.getEditSession(player);
elements = BlockBoundingBox.elements.stream().filter(blockBoundingBox -> {
return !LaufbauConfig.STORE.get(player).getDeactivated().contains(blockBoundingBox.getMaterial());
return !LaufbauUtils.isDeactivated(player, blockBoundingBox);
}).collect(Collectors.toList());
if (preferingBlastResistance) {
elements.sort((o1, o2) -> {
int compared = Double.compare(o1.blockData.getMaterial().getBlastResistance(), o2.blockData.getMaterial().getBlastResistance());
if (compared != 0) {
return -compared;
}
return -Double.compare(o1.volume(), o2.volume());
});
} else {
elements.sort((o1, o2) -> {
int compared = Double.compare(o1.volume(), o2.volume());
if (compared != 0) {
return -compared;
}
return -Double.compare(o1.blockData.getMaterial().getBlastResistance(), o2.blockData.getMaterial().getBlastResistance());
});
}
elements.sort(preferingBlastResistance ? BLAST_RESISTANCE.thenComparing(VOLUME) : VOLUME.thenComparing(BLAST_RESISTANCE));
active = new ProcessingTracesState(world, this::inRegion, editSession, elements, factor);
}
@@ -95,8 +107,19 @@ public class Laufbau {
}
private boolean inRegion(Vector location, int expansion) {
return location.getBlockX() >= pos1.getBlockX() - expansion && location.getBlockX() <= pos2.getBlockX() + expansion
&& location.getBlockY() >= pos1.getBlockY() - expansion && location.getBlockY() <= pos2.getBlockY() + expansion
&& location.getBlockZ() >= pos1.getBlockZ() - expansion && location.getBlockZ() <= pos2.getBlockZ() + expansion;
if (location.getBlockX() >= pos1.getBlockX() - expansion) {
if (location.getBlockY() >= pos1.getBlockY() - expansion) {
if (location.getBlockZ() >= pos1.getBlockZ() - expansion) {
if (location.getBlockX() <= pos2.getBlockX() + expansion) {
if (location.getBlockY() <= pos2.getBlockY() + expansion) {
if (location.getBlockZ() <= pos2.getBlockZ() + expansion) {
return true;
}
}
}
}
}
}
return false;
}
}
@@ -21,10 +21,10 @@ package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.features.slaves.laufbau.converter.LaufbauConfigV1ToV2Converter;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Material;
import java.util.HashSet;
import java.util.Set;
@@ -43,9 +43,14 @@ import java.util.Set;
@Setter
public class LaufbauConfig implements VersionedConfig {
public static final int VERSION = 3;
public static final int VERSION = 2;
/**
* LaufbauConfigV1 is stored as Base64 of gzipped YAPION, the current version as JSON
*/
public static final ConfigStore<LaufbauConfig> STORE = new ConfigStore<>(LaufbauConfig.class, VERSION, LaufbauConfig::new)
.legacy(s -> !s.startsWith("{"), LaufbauConfigV1::parse)
.converter(new LaufbauConfigV1ToV2Converter())
.userConfig("bausystem-laufbau")
.removeIf(config -> config.getDeactivated().isEmpty());
@@ -53,7 +58,7 @@ public class LaufbauConfig implements VersionedConfig {
protected int version = VERSION;
/**
* Materials of the deactivated {@link BlockBoundingBox}es
* Identifiers of the deactivated {@link BlockBoundingBox}es, see {@link LaufbauUtils#identifier(BlockBoundingBox)}
*/
protected Set<Material> deactivated = new HashSet<>();
protected Set<String> deactivated = new HashSet<>();
}
@@ -0,0 +1,65 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* The legacy laufbau config that was stored as Base64 of the gzipped YAPION,
* an object with the deactivated identifiers as keys.
*/
@Getter
@Setter
@Deprecated
public class LaufbauConfigV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
protected Set<String> deactivated = new HashSet<>();
public static LaufbauConfigV1 parse(String base64) {
String yapion;
try (InputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64)))) {
yapion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
LaufbauConfigV1 config = new LaufbauConfigV1();
config.deactivated.addAll(YapionReader.parse(yapion).keySet());
return config;
}
}
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.shared.Pair;
import de.steamwar.data.CMDs;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import org.bukkit.Material;
@@ -31,11 +32,13 @@ import java.util.*;
public class LaufbauSettings {
private static Set<Pair<Material, String>> groups = new LinkedHashSet<>();
private static Map<Pair<Material, String>, List<BlockBoundingBox>> groupMap = new LinkedHashMap<>();
static {
BlockBoundingBox.elements.forEach(blockBoundingBox -> {
groups.add(new Pair<>(blockBoundingBox.getMaterial(), blockBoundingBox.getName()));
if (blockBoundingBox.getSwItem() == null) return;
String displayName = blockBoundingBox.getSwItem().getItemMeta().getDisplayName();
groupMap.computeIfAbsent(new Pair<>(blockBoundingBox.getSwItem().getItemStack().getType(), displayName), k -> new ArrayList<>()).add(blockBoundingBox);
});
}
@@ -47,35 +50,96 @@ public class LaufbauSettings {
}
public void open() {
List<SWListInv.SWListEntry<Pair<Material, String>>> list = new ArrayList<>();
for (Pair<Material, String> group : groups) {
list.add(new SWListInv.SWListEntry<>(createItem(group.getKey(), group.getValue()), group));
List<SWListInv.SWListEntry<Map.Entry<Pair<Material, String>, List<BlockBoundingBox>>>> list = new ArrayList<>();
for (Map.Entry<Pair<Material, String>, List<BlockBoundingBox>> entry : groupMap.entrySet()) {
SWItem swItem = createItem(entry.getKey().getKey(), entry.getKey().getValue(), entry.getValue());
list.add(new SWListInv.SWListEntry<>(swItem, entry));
}
SWListInv<Pair<Material, String>> inv = new SWListInv<>(p, BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_GUI_NAME", p), false, list, (clickType, group) -> {
toggle(group.getKey());
SWListInv<Map.Entry<Pair<Material, String>, List<BlockBoundingBox>>> inv = new SWListInv<>(p, BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_GUI_NAME", p), false, list, (clickType, entry) -> {
if (entry.getValue().size() == 1) {
LaufbauUtils.toggle(p, entry.getValue().get(0));
open();
return;
}
if (clickType.isLeftClick()) {
open(entry.getKey());
return;
}
toggle(entry.getValue());
open();
});
inv.open();
}
public SWItem createItem(Material material, String name) {
SWItem swItem = new SWItem(material, name);
ItemMeta itemMeta = swItem.getItemMeta();
List<String> lore = new ArrayList<>();
if (itemMeta != null && itemMeta.getLore() != null) {
lore = itemMeta.getLore();
public void open(Pair<Material, String> key) {
List<BlockBoundingBox> blockBoundingBoxes = groupMap.get(key);
List<SWListInv.SWListEntry<BlockBoundingBox>> list = new ArrayList<>();
for (BlockBoundingBox bb : blockBoundingBoxes) {
SWItem swItem = LaufbauUtils.translateItem(bb.getSwItem(), p);
ItemMeta itemMeta = swItem.getItemMeta();
List<String> lore = new ArrayList<>();
if (itemMeta.getLore() != null) {
lore = itemMeta.getLore();
}
lore.add("");
lore.add(BauSystem.MESSAGE.parse(LaufbauUtils.isDeactivated(p, bb) ? "LAUFBAU_SETTINGS_INACTIVE" : "LAUFBAU_SETTINGS_ACTIVE", p));
itemMeta.setLore(lore);
swItem.setItemMeta(itemMeta);
list.add(new SWListInv.SWListEntry<>(swItem, bb));
}
lore.add("");
lore.add(BauSystem.MESSAGE.parse(LaufbauConfig.STORE.get(p).getDeactivated().contains(material) ? "LAUFBAU_SETTINGS_INACTIVE" : "LAUFBAU_SETTINGS_ACTIVE", p));
lore.add("");
lore.add(BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_TOGGLE", p));
if (itemMeta != null) itemMeta.setLore(lore);
swItem.setItemMeta(itemMeta);
return swItem;
SWListInv<BlockBoundingBox> inv = new SWListInv<>(p, BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_GUI_NAME", p), false, list, (clickType, blockBoundingBox) -> {
LaufbauUtils.toggle(p, blockBoundingBox);
open(key);
});
inv.setItem(49, new SWItem(Material.ARROW, BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_GUI_BACK", p), clickType -> {
open();
}).setCustomModelData(CMDs.BACK));
inv.open();
}
public void toggle(Material material) {
Set<Material> deactivated = LaufbauConfig.STORE.get(p).getDeactivated();
if (!deactivated.remove(material)) deactivated.add(material);
public SWItem createItem(Material material, String group, List<BlockBoundingBox> blockBoundingBoxes) {
if (blockBoundingBoxes.size() == 1) {
BlockBoundingBox bb = blockBoundingBoxes.get(0);
SWItem swItem = LaufbauUtils.translateItem(bb.getSwItem(), p);
ItemMeta itemMeta = swItem.getItemMeta();
List<String> lore = new ArrayList<>();
if (itemMeta.getLore() != null) {
lore = itemMeta.getLore();
}
lore.add("");
lore.add(BauSystem.MESSAGE.parse(LaufbauUtils.isDeactivated(p, bb) ? "LAUFBAU_SETTINGS_INACTIVE" : "LAUFBAU_SETTINGS_ACTIVE", p));
lore.add("");
lore.add(BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_TOGGLE", p));
itemMeta.setLore(lore);
swItem.setItemMeta(itemMeta);
return swItem;
} else {
long count = blockBoundingBoxes.stream().filter(bb -> LaufbauUtils.isDeactivated(p, bb)).count();
if (count == 0) {
return new SWItem(material, BauSystem.MESSAGE.parse(group, p), Arrays.asList("", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_ACTIVE", p), "", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_ADVANCED", p), BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_TOGGLE", p)), false, clickType -> {
});
}
if (count == blockBoundingBoxes.size()) {
return new SWItem(material, BauSystem.MESSAGE.parse(group, p), Arrays.asList("", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_INACTIVE", p), "", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_ADVANCED", p), BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_TOGGLE", p)), false, clickType -> {
});
}
return new SWItem(material, BauSystem.MESSAGE.parse(group, p), Arrays.asList("", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_MIXED", p, blockBoundingBoxes.size() - count, blockBoundingBoxes.size()), "", BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_ADVANCED", p), BauSystem.MESSAGE.parse("LAUFBAU_SETTINGS_TOGGLE", p)), false, clickType -> {
});
}
}
public void toggle(List<BlockBoundingBox> blockBoundingBoxes) {
long count = blockBoundingBoxes.stream().filter(bb -> LaufbauUtils.isDeactivated(p, bb)).count();
if (count != blockBoundingBoxes.size()) {
blockBoundingBoxes.forEach(bb -> {
if (!LaufbauUtils.isDeactivated(p, bb)) {
LaufbauUtils.toggle(p, bb);
}
});
} else {
blockBoundingBoxes.forEach(bb -> {
LaufbauUtils.toggle(p, bb);
});
}
}
}
@@ -0,0 +1,99 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.inventory.SWItem;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@UtilityClass
public class LaufbauUtils {
public static Cuboid pixelCuboid(double pixelX, double pixelY, double pixelZ, double pixelDX, double pixelDY, double pixelDZ) {
return new Cuboid(pixelToBlock(pixelX), pixelToBlock(pixelY), pixelToBlock(pixelZ), pixelToBlock(pixelDX), pixelToBlock(pixelDY), pixelToBlock(pixelDZ));
}
public static double pixelToBlock(double pixel) {
return 1.0 / 16 * pixel;
}
public static SWItem createItem(String name, Material material, String... lore) {
return new SWItem(material, name, Arrays.asList(lore), false, clickType -> {
});
}
public static SWItem translateItem(SWItem item, Player p) {
ItemMeta itemMeta = item.getItemMeta();
List<String> lore = new ArrayList<>();
if (itemMeta.getLore() != null) {
lore = itemMeta.getLore().stream().map(s -> BauSystem.MESSAGE.parse(s, p)).collect(Collectors.toList());
}
return new SWItem(item.getItemStack().getType(), BauSystem.MESSAGE.parse(itemMeta.getDisplayName(), p), lore, false, clickType -> {
});
}
public static String identifier(BlockBoundingBox bb) {
if (bb.getSwItem() == null) {
return null;
}
SWItem item = bb.getSwItem();
ItemMeta itemMeta = item.getItemMeta();
List<String> lore = new ArrayList<>();
if (itemMeta.getLore() != null) {
lore = itemMeta.getLore()
.stream()
.map(s -> s.substring(8))
.collect(Collectors.toList());
}
String name = itemMeta.getDisplayName().substring(14);
return name + "[" + String.join(",", lore) + "]";
}
@SneakyThrows
public static boolean isDeactivated(Player p, BlockBoundingBox bb) {
if (bb.getSwItem() == null) {
return false;
}
String identifier = identifier(bb);
return LaufbauConfig.STORE.get(p).getDeactivated().contains(identifier);
}
@SneakyThrows
public static void toggle(Player p, BlockBoundingBox bb) {
if (bb.getSwItem() == null) {
return;
}
String identifier = identifier(bb);
Set<String> deactivated = LaufbauConfig.STORE.get(p).getDeactivated();
if (!deactivated.remove(identifier)) {
deactivated.add(identifier);
}
}
}
@@ -0,0 +1,261 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.AmethystCluster;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class AmethystBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
floorSmallAmethystBud();
ceilingSmallAmethystBud();
northSmallAmethystBud();
southSmallAmethystBud();
eastSmallAmethystBud();
westSmallAmethystBud();
floorMediumAmethystBud();
ceilingMediumAmethystBud();
northMediumAmethystBud();
southMediumAmethystBud();
eastMediumAmethystBud();
westMediumAmethystBud();
floorLargeAmethystBud();
ceilingLargeAmethystBud();
northLargeAmethystBud();
southLargeAmethystBud();
eastLargeAmethystBud();
westLargeAmethystBud();
floorAmethystCluster();
ceilingAmethystCluster();
northAmethystCluster();
southAmethystCluster();
eastAmethystCluster();
westAmethystCluster();
}
private void floorSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.UP);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 0, 4, 8, 3, 8));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_UP"));
}
private void ceilingSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.DOWN);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 13, 4, 8, 3, 8));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_DOWN"));
}
private void northSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 13, 8, 8, 3));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_NORTH"));
}
private void southSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 0, 8, 8, 3));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_SOUTH"));
}
private void eastSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 4, 4, 3, 8, 8));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_EAST"));
}
private void westSmallAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.SMALL_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(13, 4, 4, 3, 8, 8));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_SMALL_AMETHYST_BUD", Material.SMALL_AMETHYST_BUD, "LAUFBAU_FACING_WEST"));
}
private void floorMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.UP);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 0, 3, 10, 4, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_UP"));
}
private void ceilingMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.DOWN);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 12, 3, 10, 4, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_DOWN"));
}
private void northMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 12, 10, 10, 4));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_NORTH"));
}
private void southMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 0, 10, 10, 4));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_SOUTH"));
}
private void eastMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 3, 3, 4, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_EAST"));
}
private void westMediumAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.MEDIUM_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(12, 3, 3, 4, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_MEDIUM_AMETHYST_BUD", Material.MEDIUM_AMETHYST_BUD, "LAUFBAU_FACING_WEST"));
}
private void floorLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.UP);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 0, 3, 10, 5, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_UP"));
}
private void ceilingLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.DOWN);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 11, 3, 10, 5, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_DOWN"));
}
private void northLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 11, 10, 10, 5));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_NORTH"));
}
private void southLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 0, 10, 10, 5));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_SOUTH"));
}
private void eastLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 3, 3, 5, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_EAST"));
}
private void westLargeAmethystBud() {
AmethystCluster cluster = (AmethystCluster) Material.LARGE_AMETHYST_BUD.createBlockData();
cluster.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(11, 3, 3, 5, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_LARGE_AMETHYST_BUD", Material.LARGE_AMETHYST_BUD, "LAUFBAU_FACING_WEST"));
}
private void floorAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.UP);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 0, 3, 10, 7, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_UP"));
}
private void ceilingAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.DOWN);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 9, 3, 10, 7, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_DOWN"));
}
private void northAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 9, 10, 10, 7));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_NORTH"));
}
private void southAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 0, 10, 10, 7));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_SOUTH"));
}
private void eastAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 3, 3, 7, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_EAST"));
}
private void westAmethystCluster() {
AmethystCluster cluster = (AmethystCluster) Material.AMETHYST_CLUSTER.createBlockData();
cluster.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(9, 3, 3, 7, 10, 10));
new BlockBoundingBox(cluster, cuboidList, createItem("LAUFBAU_BLOCK_AMETHYST_CLUSTER", Material.AMETHYST_CLUSTER, "LAUFBAU_FACING_WEST"));
}
}
@@ -0,0 +1,46 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class AzaleaBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
BlockData blockData = Material.FLOWERING_AZALEA.createBlockData();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(6, 0, 6, 4, 8, 4));
new BlockBoundingBox(blockData, cuboidList, createItem("LAUFBAU_BLOCK_AZALEA", Material.FLOWERING_AZALEA));
}
}
@@ -0,0 +1,149 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Bell;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class BellBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
ceilingBell();
floorNorthBell();
floorEastBell();
doubleWallNorthBell();
doubleWallEastBell();
singleWallNorthBell();
singleWallSouthBell();
singleWallEastBell();
singleWallWestBell();
}
public void ceilingBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.CEILING);
bell.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(7, 13, 7, 2, 3, 2));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_CEILING"));
}
public void floorNorthBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.FLOOR);
bell.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 4, 16, 16, 8));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_FLOOR", "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
}
public void floorEastBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.FLOOR);
bell.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 0, 0, 8, 16, 16));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_FLOOR", "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
}
public void doubleWallNorthBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.DOUBLE_WALL);
bell.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(7, 13, 0, 2, 2, 16));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_DOUBLE_WALL", "LAUFBAU_FACING_NORTH"));
}
public void doubleWallEastBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.DOUBLE_WALL);
bell.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(0, 13, 7, 16, 2, 2));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_DOUBLE_WALL", "LAUFBAU_FACING_EAST"));
}
public void singleWallNorthBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.SINGLE_WALL);
bell.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(7, 13, 0, 2, 2, 13));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_SINGLE_WALL", "LAUFBAU_FACING_NORTH"));
}
public void singleWallSouthBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.SINGLE_WALL);
bell.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(7, 13, 3, 2, 2, 13));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_SINGLE_WALL", "LAUFBAU_FACING_SOUTH"));
}
public void singleWallEastBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.SINGLE_WALL);
bell.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(3, 13, 7, 13, 2, 2));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_SINGLE_WALL", "LAUFBAU_FACING_EAST"));
}
public void singleWallWestBell() {
Bell bell = (Bell) Material.BELL.createBlockData();
bell.setAttachment(Bell.Attachment.SINGLE_WALL);
bell.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 4, 8, 2, 8));
cuboidList.add(pixelCuboid(5, 6, 5, 6, 8, 6));
cuboidList.add(pixelCuboid(0, 13, 7, 13, 2, 2));
new BlockBoundingBox(bell, cuboidList, createItem("LAUFBAU_BLOCK_BELL", Material.BELL, "LAUFBAU_ATTACHMENT_SINGLE_WALL", "LAUFBAU_FACING_WEST"));
}
}
@@ -0,0 +1,46 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.type.BrewingStand;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class BrewingStandBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
BrewingStand brewingStand = (BrewingStand) Material.BREWING_STAND.createBlockData();
List<Cuboid> cuboids = new ArrayList<>();
cuboids.add(pixelCuboid(1, 0, 1, 14, 2, 14));
cuboids.add(pixelCuboid(7, 2, 7, 2, 12, 2));
new BlockBoundingBox(brewingStand, cuboids, createItem("LAUFBAU_BLOCK_BREWING_STAND", Material.BREWING_STAND));
}
}
@@ -0,0 +1,62 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.Lightable;
import org.bukkit.block.data.type.Candle;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class CandleBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
caked();
single();
}
private void caked() {
Lightable candleCake = (Lightable) Material.CYAN_CANDLE_CAKE.createBlockData();
candleCake.setLit(true);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(7, 8, 7, 2, 6, 2));
new BlockBoundingBox(candleCake, cuboidList, createItem("LAUFBAU_BLOCK_CANDLE_CAKE", Material.CAKE));
}
private void single() {
Candle candle = (Candle) Material.CYAN_CANDLE.createBlockData();
candle.setCandles(1);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(7, 0, 7, 2, 6, 2));
new BlockBoundingBox(candle, cuboidList, createItem("LAUFBAU_BLOCK_CANDLE", Material.CYAN_CANDLE, "LAUFBAU_COUNT_1"));
}
}
@@ -0,0 +1,69 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Axis;
import org.bukkit.Material;
import org.bukkit.block.data.Orientable;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class ChainBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
bottomTopChain();
northSouthChain();
eastWestChain();
}
private void bottomTopChain() {
Orientable chainBottomTop = (Orientable) Material.CHAIN.createBlockData();
chainBottomTop.setAxis(Axis.Y);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(6.5, 0, 6.5, 3, 16, 3));
new BlockBoundingBox(chainBottomTop, cuboidList, createItem("LAUFBAU_BLOCK_CHAIN", Material.CHAIN, "LAUFBAU_FACING_UP", "LAUFBAU_FACING_DOWN"));
}
private void northSouthChain() {
Orientable chainBottomTop = (Orientable) Material.CHAIN.createBlockData();
chainBottomTop.setAxis(Axis.Z);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(6.5, 6.5, 0, 3, 3, 16));
new BlockBoundingBox(chainBottomTop, cuboidList, createItem("LAUFBAU_BLOCK_CHAIN", Material.CHAIN, "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
}
private void eastWestChain() {
Orientable chainBottomTop = (Orientable) Material.CHAIN.createBlockData();
chainBottomTop.setAxis(Axis.X);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 6.5, 6.5, 16, 3, 3));
new BlockBoundingBox(chainBottomTop, cuboidList, createItem("LAUFBAU_BLOCK_CHAIN", Material.CHAIN, "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
}
}
@@ -0,0 +1,89 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.MultipleFacing;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class ChorusPlantBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
for (int nx = 0; nx < 2; nx++) {
for (int ny = 0; ny < 2; ny++) {
for (int nz = 0; nz < 2; nz++) {
for (int px = 0; px < 2; px++) {
for (int py = 0; py < 2; py++) {
for (int pz = 0; pz < 2; pz++) {
MultipleFacing chorusPlant = (MultipleFacing) Material.CHORUS_PLANT.createBlockData();
List<String> lore = new ArrayList<>();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(3, 3, 3, 10, 10, 10));
if (nx == 1) {
lore.add("LAUFBAU_CONNECTION_WEST");
chorusPlant.setFace(BlockFace.WEST, true);
cuboidList.add(pixelCuboid(0, 3, 3, 3, 10, 10));
}
if (ny == 1) {
lore.add("LAUFBAU_CONNECTION_DOWN");
chorusPlant.setFace(BlockFace.DOWN, true);
cuboidList.add(pixelCuboid(3, 0, 3, 10, 3, 10));
}
if (nz == 1) {
lore.add("LAUFBAU_CONNECTION_NORTH");
chorusPlant.setFace(BlockFace.NORTH, true);
cuboidList.add(pixelCuboid(3, 3, 0, 10, 10, 3));
}
if (px == 1) {
lore.add("LAUFBAU_CONNECTION_EAST");
chorusPlant.setFace(BlockFace.EAST, true);
cuboidList.add(pixelCuboid(13, 3, 3, 3, 10, 10));
}
if (py == 1) {
lore.add("LAUFBAU_CONNECTION_UP");
chorusPlant.setFace(BlockFace.UP, true);
cuboidList.add(pixelCuboid(3, 13, 3, 10, 3, 10));
}
if (pz == 1) {
lore.add("LAUFBAU_CONNECTION_SOUTH");
chorusPlant.setFace(BlockFace.SOUTH, true);
cuboidList.add(pixelCuboid(3, 3, 13, 10, 10, 3));
}
new BlockBoundingBox(chorusPlant, cuboidList, createItem("LAUFBAU_BLOCK_CHORUS_PLANT", Material.CHORUS_PLANT, lore.toArray(new String[0])));
}
}
}
}
}
}
}
}
@@ -0,0 +1,45 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class DragonEggBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
BlockData blockData = Material.DRAGON_EGG.createBlockData();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(1, 0, 1, 14, 16, 14));
new BlockBoundingBox(blockData, cuboidList, createItem("LAUFBAU_BLOCK_DRAGON_EGG", Material.DRAGON_EGG));
}
}
@@ -0,0 +1,59 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.type.BigDripleaf;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class DripLeafBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
tiltNone();
tiltPartial();
}
private void tiltNone() {
BigDripleaf bigDripleaf = (BigDripleaf) Material.BIG_DRIPLEAF.createBlockData();
bigDripleaf.setTilt(BigDripleaf.Tilt.NONE);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 11, 0, 16, 4, 16));
new BlockBoundingBox(bigDripleaf, cuboidList, createItem("LAUFBAU_BLOCK_BIG_DRIP_LEAF", Material.BIG_DRIPLEAF, "LAUFBAU_TILT_NONE"));
}
private void tiltPartial() {
BigDripleaf bigDripleaf = (BigDripleaf) Material.BIG_DRIPLEAF.createBlockData();
bigDripleaf.setTilt(BigDripleaf.Tilt.PARTIAL);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 11, 0, 16, 2, 16));
new BlockBoundingBox(bigDripleaf, cuboidList, createItem("LAUFBAU_BLOCK_BIG_DRIP_LEAF", Material.BIG_DRIPLEAF, "LAUFBAU_TILT_PARTIAL"));
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Fence;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class FencesBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
for (int nx = 0; nx < 2; nx++) {
for (int nz = 0; nz < 2; nz++) {
for (int px = 0; px < 2; px++) {
for (int pz = 0; pz < 2; pz++) {
Fence fence = (Fence) Material.NETHER_BRICK_FENCE.createBlockData();
List<String> lore = new ArrayList<>();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(6, 0, 6, 4, 24, 4));
if (nz == 1) {
lore.add("LAUFBAU_CONNECTION_NORTH");
fence.setFace(BlockFace.NORTH, true);
cuboidList.add(pixelCuboid(6, 0, 0, 4, 24, 6));
}
if (pz == 1) {
lore.add("LAUFBAU_CONNECTION_SOUTH");
fence.setFace(BlockFace.SOUTH, true);
cuboidList.add(pixelCuboid(6, 0, 10, 4, 24, 6));
}
if (nx == 1) {
lore.add("LAUFBAU_CONNECTION_WEST");
fence.setFace(BlockFace.WEST, true);
cuboidList.add(pixelCuboid(0, 0, 6, 6, 24, 4));
}
if (px == 1) {
lore.add("LAUFBAU_CONNECTION_EAST");
fence.setFace(BlockFace.EAST, true);
cuboidList.add(pixelCuboid(10, 0, 6, 6, 24, 4));
}
new BlockBoundingBox(fence, cuboidList, createItem("LAUFBAU_BLOCK_NETHER_BRICK_FENCE", Material.NETHER_BRICK_FENCE, lore.toArray(new String[0])));
}
}
}
}
}
}
@@ -0,0 +1,157 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.FaceAttachable;
import org.bukkit.block.data.type.Grindstone;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class GrindstoneBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
floorNorthGrindstone();
floorEastGrindstone();
ceilingNorthGrindstone();
ceilingEastGrindstone();
wallNorthGrindstone();
wallSouthGrindstone();
wallEastGrindstone();
wallWestGrindstone();
}
public void floorNorthGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.FLOOR);
grindstone.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 4, 2, 8, 12, 12));
cuboidList.add(pixelCuboid(2, 0, 6, 2, 7, 4));
cuboidList.add(pixelCuboid(12, 0, 6, 2, 7, 4));
cuboidList.add(pixelCuboid(2, 7, 5, 2, 6, 6));
cuboidList.add(pixelCuboid(12, 7, 5, 2, 6, 6));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_FLOOR", "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
}
public void floorEastGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.FLOOR);
grindstone.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(2, 4, 4, 12, 12, 8));
cuboidList.add(pixelCuboid(6, 0, 2, 4, 7, 2));
cuboidList.add(pixelCuboid(6, 0, 12, 4, 7, 2));
cuboidList.add(pixelCuboid(5, 7, 2, 6, 6, 2));
cuboidList.add(pixelCuboid(5, 7, 12, 6, 6, 2));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_FLOOR", "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
}
public void ceilingNorthGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.CEILING);
grindstone.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 0, 2, 8, 12, 12));
cuboidList.add(pixelCuboid(2, 9, 6, 2, 7, 4));
cuboidList.add(pixelCuboid(12, 9, 6, 2, 7, 4));
cuboidList.add(pixelCuboid(2, 3, 5, 2, 6, 6));
cuboidList.add(pixelCuboid(12, 3, 5, 2, 6, 6));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_CEILING", "LAUFBAU_FACING_NORTH", "LAUFBAU_FACING_SOUTH"));
}
public void ceilingEastGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.CEILING);
grindstone.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(2, 0, 4, 12, 12, 8));
cuboidList.add(pixelCuboid(6, 9, 2, 4, 7, 2));
cuboidList.add(pixelCuboid(6, 9, 12, 4, 7, 2));
cuboidList.add(pixelCuboid(5, 3, 2, 6, 6, 2));
cuboidList.add(pixelCuboid(5, 3, 12, 6, 6, 2));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_CEILING", "LAUFBAU_FACING_EAST", "LAUFBAU_FACING_WEST"));
}
public void wallNorthGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.WALL);
grindstone.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 2, 0, 8, 12, 12));
cuboidList.add(pixelCuboid(2, 6, 9, 2, 4, 7));
cuboidList.add(pixelCuboid(12, 6, 9, 2, 4, 7));
cuboidList.add(pixelCuboid(2, 5, 3, 2, 6, 6));
cuboidList.add(pixelCuboid(12, 5, 3, 2, 6, 6));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_WALL", "LAUFBAU_FACING_NORTH"));
}
public void wallSouthGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.WALL);
grindstone.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 2, 4, 8, 12, 12));
cuboidList.add(pixelCuboid(2, 6, 0, 2, 4, 7));
cuboidList.add(pixelCuboid(12, 6, 0, 2, 4, 7));
cuboidList.add(pixelCuboid(2, 5, 7, 2, 6, 6));
cuboidList.add(pixelCuboid(12, 5, 7, 2, 6, 6));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_WALL", "LAUFBAU_FACING_SOUTH"));
}
public void wallEastGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.WALL);
grindstone.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 2, 4, 12, 12, 8));
cuboidList.add(pixelCuboid(0, 6, 2, 7, 4, 2));
cuboidList.add(pixelCuboid(0, 6, 12, 7, 4, 2));
cuboidList.add(pixelCuboid(7, 5, 2, 6, 6, 2));
cuboidList.add(pixelCuboid(7, 5, 12, 6, 6, 2));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_WALL", "LAUFBAU_FACING_EAST"));
}
public void wallWestGrindstone() {
Grindstone grindstone = (Grindstone) Material.GRINDSTONE.createBlockData();
grindstone.setAttachedFace(FaceAttachable.AttachedFace.WALL);
grindstone.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 2, 4, 12, 12, 8));
cuboidList.add(pixelCuboid(9, 6, 2, 7, 4, 2));
cuboidList.add(pixelCuboid(9, 6, 12, 7, 4, 2));
cuboidList.add(pixelCuboid(3, 5, 2, 6, 6, 2));
cuboidList.add(pixelCuboid(3, 5, 12, 6, 6, 2));
new BlockBoundingBox(grindstone, cuboidList, createItem("LAUFBAU_BLOCK_GRINDSTONE", Material.GRINDSTONE, "LAUFBAU_ATTACHMENT_WALL", "LAUFBAU_FACING_WEST"));
}
}
@@ -0,0 +1,97 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Hopper;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class HopperBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
downHopper();
northHopper();
southHopper();
eastHopper();
westHopper();
}
public void downHopper() {
Hopper hopper = (Hopper) Material.HOPPER.createBlockData();
hopper.setFacing(BlockFace.DOWN);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 10, 0, 16, 6, 16));
cuboidList.add(pixelCuboid(4, 4, 4, 8, 6, 8));
cuboidList.add(pixelCuboid(6, 0, 6, 4, 4, 4));
new BlockBoundingBox(hopper, cuboidList, createItem("LAUFBAU_BLOCK_HOPPER", Material.HOPPER, "LAUFBAU_CONNECTION_FLOOR"));
}
public void northHopper() {
Hopper hopper = (Hopper) Material.HOPPER.createBlockData();
hopper.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 10, 0, 16, 6, 16));
cuboidList.add(pixelCuboid(4, 4, 4, 8, 6, 8));
cuboidList.add(pixelCuboid(6, 4, 0, 4, 4, 4));
new BlockBoundingBox(hopper, cuboidList, createItem("LAUFBAU_BLOCK_HOPPER", Material.HOPPER, "LAUFBAU_CONNECTION_NORTH"));
}
public void southHopper() {
Hopper hopper = (Hopper) Material.HOPPER.createBlockData();
hopper.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 10, 0, 16, 6, 16));
cuboidList.add(pixelCuboid(4, 4, 4, 8, 6, 8));
cuboidList.add(pixelCuboid(6, 4, 12, 4, 4, 4));
new BlockBoundingBox(hopper, cuboidList, createItem("LAUFBAU_BLOCK_HOPPER", Material.HOPPER, "LAUFBAU_CONNECTION_SOUTH"));
}
public void eastHopper() {
Hopper hopper = (Hopper) Material.HOPPER.createBlockData();
hopper.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 10, 0, 16, 6, 16));
cuboidList.add(pixelCuboid(4, 4, 4, 8, 6, 8));
cuboidList.add(pixelCuboid(12, 4, 6, 4, 4, 4));
new BlockBoundingBox(hopper, cuboidList, createItem("LAUFBAU_BLOCK_HOPPER", Material.HOPPER, "LAUFBAU_CONNECTION_EAST"));
}
public void westHopper() {
Hopper hopper = (Hopper) Material.HOPPER.createBlockData();
hopper.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 10, 0, 16, 6, 16));
cuboidList.add(pixelCuboid(4, 4, 4, 8, 6, 8));
cuboidList.add(pixelCuboid(0, 4, 6, 4, 4, 4));
new BlockBoundingBox(hopper, cuboidList, createItem("LAUFBAU_BLOCK_HOPPER", Material.HOPPER, "LAUFBAU_CONNECTION_WEST"));
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Fence;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class IronBarBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
for (int nx = 0; nx < 2; nx++) {
for (int nz = 0; nz < 2; nz++) {
for (int px = 0; px < 2; px++) {
for (int pz = 0; pz < 2; pz++) {
Fence fence = (Fence) Material.IRON_BARS.createBlockData();
List<String> lore = new ArrayList<>();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(7, 0, 7, 2, 16, 2));
if (nz == 1) {
lore.add("LAUFBAU_CONNECTION_NORTH");
fence.setFace(BlockFace.NORTH, true);
cuboidList.add(pixelCuboid(7, 0, 0, 2, 16, 7));
}
if (pz == 1) {
lore.add("LAUFBAU_CONNECTION_SOUTH");
fence.setFace(BlockFace.SOUTH, true);
cuboidList.add(pixelCuboid(7, 0, 9, 2, 16, 7));
}
if (nx == 1) {
lore.add("LAUFBAU_CONNECTION_WEST");
fence.setFace(BlockFace.WEST, true);
cuboidList.add(pixelCuboid(0, 0, 7, 7, 16, 2));
}
if (px == 1) {
lore.add("LAUFBAU_CONNECTION_EAST");
fence.setFace(BlockFace.EAST, true);
cuboidList.add(pixelCuboid(9, 0, 7, 7, 16, 2));
}
new BlockBoundingBox(fence, cuboidList, createItem("LAUFBAU_BLOCK_IRON_BARS", Material.IRON_BARS, lore.toArray(new String[0])));
}
}
}
}
}
}
@@ -0,0 +1,61 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.type.Lantern;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class LanternBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
lantern();
hangingLantern();
}
public void lantern() {
Lantern lantern = (Lantern) Material.LANTERN.createBlockData();
lantern.setHanging(false);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(5, 0, 5, 6, 7, 6));
cuboidList.add(pixelCuboid(6, 7, 6, 4, 2, 4));
new BlockBoundingBox(lantern, cuboidList, createItem("LAUFBAU_BLOCK_LANTERN", Material.LANTERN));
}
public void hangingLantern() {
Lantern lantern = (Lantern) Material.LANTERN.createBlockData();
lantern.setHanging(true);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(5, 1, 5, 6, 7, 6));
cuboidList.add(pixelCuboid(6, 8, 6, 4, 2, 4));
new BlockBoundingBox(lantern, cuboidList, createItem("LAUFBAU_BLOCK_LANTERN", Material.LANTERN, "LAUFBAU_HANGING"));
}
}
@@ -0,0 +1,46 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class LecternBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
BlockData blockData = Material.LECTERN.createBlockData();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 2, 16));
cuboidList.add(pixelCuboid(4, 2, 4, 8, 12, 8));
new BlockBoundingBox(blockData, cuboidList, createItem("LAUFBAU_BLOCK_LECTERN", Material.LECTERN));
}
}
@@ -0,0 +1,343 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Stairs;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class StairBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
straightBottomNorthStair();
straightBottomSouthStair();
straightBottomEastStair();
straightBottomWestStair();
straightTopNorthStair();
straightTopSouthStair();
straightTopEastStair();
straightTopWestStair();
outerLeftBottomNorthStair();
outerLeftBottomSouthStair();
outerLeftBottomEastStair();
outerLeftBottomWestStair();
outerLeftTopNorthStair();
outerLeftTopSouthStair();
outerLeftTopEastStair();
outerLeftTopWestStair();
innerLeftBottomNorthStair();
innerLeftBottomSouthStair();
innerLeftBottomEastStair();
innerLeftBottomWestStair();
innerLeftTopNorthStair();
innerLeftTopSouthStair();
innerLeftTopEastStair();
innerLeftTopWestStair();
}
public void straightBottomNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_NORTH"));
}
public void straightBottomSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 8, 16, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_SOUTH"));
}
public void straightBottomEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 8, 0, 8, 8, 16));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_EAST"));
}
public void straightBottomWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 0, 8, 8, 16));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_WEST"));
}
public void straightTopNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_NORTH"));
}
public void straightTopSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 8, 16, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_SOUTH"));
}
public void straightTopEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 0, 0, 8, 8, 16));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_EAST"));
}
public void straightTopWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.STRAIGHT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 0, 8, 8, 16));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_STRAIGHT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_WEST"));
}
public void outerLeftBottomNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_NORTH"));
}
public void outerLeftBottomSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 8, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_SOUTH"));
}
public void outerLeftBottomEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 8, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_EAST"));
}
public void outerLeftBottomWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_WEST"));
}
public void outerLeftTopNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_NORTH"));
}
public void outerLeftTopSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 0, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_SOUTH"));
}
public void outerLeftTopEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(8, 0, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_EAST"));
}
public void outerLeftTopWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.OUTER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_OUTER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_WEST"));
}
public void innerLeftBottomNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 8));
cuboidList.add(pixelCuboid(0, 8, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_NORTH"));
}
public void innerLeftBottomSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 8, 16, 8, 8));
cuboidList.add(pixelCuboid(8, 8, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_SOUTH"));
}
public void innerLeftBottomEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 8));
cuboidList.add(pixelCuboid(8, 8, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_EAST"));
}
public void innerLeftBottomWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.BOTTOM);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 8, 8, 16, 8, 8));
cuboidList.add(pixelCuboid(0, 8, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_BOTTOM", "LAUFBAU_FACING_WEST"));
}
public void innerLeftTopNorthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.NORTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 8));
cuboidList.add(pixelCuboid(0, 0, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_NORTH"));
}
public void innerLeftTopSouthStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.SOUTH);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 8, 16, 8, 8));
cuboidList.add(pixelCuboid(8, 0, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_SOUTH"));
}
public void innerLeftTopEastStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.EAST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 0, 16, 8, 8));
cuboidList.add(pixelCuboid(8, 0, 8, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_EAST"));
}
public void innerLeftTopWestStair() {
Stairs stairs = (Stairs) Material.END_STONE_BRICK_STAIRS.createBlockData();
stairs.setShape(Stairs.Shape.INNER_LEFT);
stairs.setHalf(Stairs.Half.TOP);
stairs.setFacing(BlockFace.WEST);
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(0, 8, 0, 16, 8, 16));
cuboidList.add(pixelCuboid(0, 0, 8, 16, 8, 8));
cuboidList.add(pixelCuboid(0, 0, 0, 8, 8, 8));
new BlockBoundingBox(stairs, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_STAIRS", Material.END_STONE_BRICK_STAIRS, "LAUFBAU_SHAPE_INNER_LEFT", "LAUFBAU_HALF_TOP", "LAUFBAU_FACING_WEST"));
}
}
@@ -0,0 +1,79 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.boundingboxes;
import de.steamwar.bausystem.features.slaves.laufbau.BlockBoundingBox;
import de.steamwar.bausystem.features.slaves.laufbau.BoundingBoxLoader;
import de.steamwar.bausystem.features.slaves.laufbau.Cuboid;
import de.steamwar.linkage.Linked;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.type.Wall;
import java.util.ArrayList;
import java.util.List;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.createItem;
import static de.steamwar.bausystem.features.slaves.laufbau.LaufbauUtils.pixelCuboid;
@Linked
public class WallBoundingBox implements BoundingBoxLoader {
@Override
public void load() {
v18();
}
private void v18() {
for (int nx = 0; nx < 2; nx++) {
for (int nz = 0; nz < 2; nz++) {
for (int px = 0; px < 2; px++) {
for (int pz = 0; pz < 2; pz++) {
Wall fence = (Wall) Material.END_STONE_BRICK_WALL.createBlockData();
List<String> lore = new ArrayList<>();
List<Cuboid> cuboidList = new ArrayList<>();
cuboidList.add(pixelCuboid(4, 0, 4, 8, 24, 8));
if (nz == 1) {
lore.add("LAUFBAU_CONNECTION_NORTH");
fence.setHeight(BlockFace.NORTH, Wall.Height.LOW);
cuboidList.add(pixelCuboid(5, 0, 0, 6, 24, 4));
}
if (pz == 1) {
lore.add("LAUFBAU_CONNECTION_SOUTH");
fence.setHeight(BlockFace.SOUTH, Wall.Height.LOW);
cuboidList.add(pixelCuboid(5, 0, 12, 6, 24, 4));
}
if (nx == 1) {
lore.add("LAUFBAU_CONNECTION_WEST");
fence.setHeight(BlockFace.WEST, Wall.Height.LOW);
cuboidList.add(pixelCuboid(0, 0, 5, 4, 24, 6));
}
if (px == 1) {
lore.add("LAUFBAU_CONNECTION_EAST");
fence.setHeight(BlockFace.EAST, Wall.Height.LOW);
cuboidList.add(pixelCuboid(12, 0, 5, 4, 24, 6));
}
new BlockBoundingBox(fence, cuboidList, createItem("LAUFBAU_BLOCK_END_STONE_BRICK_WALL", Material.END_STONE_BRICK_WALL, lore.toArray(new String[0])));
}
}
}
}
}
}
@@ -17,26 +17,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import org.gradle.api.provider.SetProperty
package de.steamwar.bausystem.features.slaves.laufbau.converter;
abstract class AffectedExtension {
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfig;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfigV1;
/**
* Paths relative to the root directory whose changes affect this project, a trailing / matches everything
* below a directory. Defaults to the project directory, files of subprojects never affect this project.
*/
abstract val files: SetProperty<String>
@Deprecated
public class LaufbauConfigV1ToV2Converter implements ConfigConverter<LaufbauConfigV1, LaufbauConfig> {
/**
* Projects whose changes affect this project in addition to its project dependencies
*/
abstract val projects: SetProperty<String>
fun whenChanged(vararg paths: String) {
files.addAll(*paths)
@Override
public int version() {
return LaufbauConfigV1.VERSION;
}
fun withProject(vararg paths: String) {
projects.addAll(*paths)
@Override
public LaufbauConfig convert(LaufbauConfigV1 old) {
LaufbauConfig config = new LaufbauConfig();
config.getDeactivated().addAll(old.getDeactivated());
return config;
}
}
@@ -145,6 +145,7 @@ public class CreatingLaufState implements LaufbauState {
}
try {
BaseBlock block = BukkitAdapter.adapt(highestBlockBoundingBox.blockData).toBaseBlock();
if (highestBlockBoundingBox.blockConsumer != null) highestBlockBoundingBox.blockConsumer.accept(block);
editSession.setBlock(BukkitAdapter.asBlockVector(location), block);
} catch (MaxChangedBlocksException e) {
e.printStackTrace();
@@ -19,10 +19,11 @@
package de.steamwar.bausystem.features.team.boundary;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import org.bukkit.Bukkit;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -37,16 +38,17 @@ public class BoundaryViewer implements Listener {
public static Set<Player> viewers = new HashSet<>();
@LinkedTimer(delay = 20, period = 20)
private void showBoundaries() {
if (viewers.isEmpty()) {
return;
}
viewers.forEach(player -> {
Region region = Region.getRegion(player.getLocation());
if (region.getType().isGlobal()) return;
showRegion(region, player);
});
{
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
if (viewers.isEmpty()) {
return;
}
viewers.forEach(player -> {
Region region = Region.getRegion(player.getLocation());
if (region.getType().isGlobal()) return;
showRegion(region, player);
});
}, 20, 20);
}
private void showRegion(Region region, Player player) {
@@ -1,161 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.features.hullhider.HullHiderCommand;
import de.steamwar.bausystem.features.world.SpectatorListener;
import de.steamwar.bausystem.features.xray.XrayCommand;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.AccessPrivilegeProvider;
import de.steamwar.techhider.TechHider;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray and the hulls of /hullhider per player.
* Entities are never hidden.
*/
@Linked
public class BauTechHider implements AccessPrivilegeProvider {
private static final HidingRule[] NO_RULES = new HidingRule[0];
private static final HiddenHull[] NO_HULLS = new HiddenHull[0];
// Injected after construction, until then nothing is hidden
@LinkedInstance
public XrayCommand xrayCommand;
@LinkedInstance
public TechHiderCommand techHiderCommand;
@LinkedInstance
public SpectatorListener spectatorListener;
@LinkedInstance
public HullHiderCommand hullHiderCommand;
public BauTechHider() {
new TechHider(this);
}
@Override
public ForPlayer forPlayer(Player p) {
if (xrayCommand == null || techHiderCommand == null || spectatorListener == null || hullHiderCommand == null) {
return new BauPlayerAccess(p, NO_RULES, NO_HULLS);
}
// Region rules first: where rules overlap, the first one decides the obfuscation block
List<HidingRule> rules = new ArrayList<>();
xrayCommand.addHidingRules(p, rules);
techHiderCommand.addHidingRules(p, rules);
hullHiderCommand.addHidingRules(p, rules);
spectatorListener.addHidingRules(p, rules);
List<HiddenHull> hulls = new ArrayList<>();
hullHiderCommand.addHiddenHulls(p, hulls);
return new BauPlayerAccess(p, rules.toArray(NO_RULES), hulls.toArray(NO_HULLS));
}
@Override
public boolean isPlayerPrivilegedToPerformAction(Player p) {
return true;
}
/**
* Snapshot of the hiding rules of one player, taken once per outgoing packet on the netty thread.
*/
private record BauPlayerAccess(Player player, HidingRule[] rules, HiddenHull[] hulls) implements ForPlayer {
@Override
public Player getPlayer() {
return player;
}
@Override
public boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ) {
for (HidingRule rule : rules) {
if (rule.coversChunk(chunkX, chunkZ)) return false;
}
for (HiddenHull hull : hulls) {
if (!hull.hull().getRegion().chunkOutside(chunkX, chunkZ)) return false;
}
return true;
}
@Override
public boolean canAccessPosition(int blockX, int blockY, int blockZ) {
for (HiddenHull hull : hulls) {
if (hull.hull().isBlockHidden(player, blockX, blockY, blockZ)) return false;
}
return true;
}
@Override
public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlock(block)) return false;
}
return true;
}
@Override
public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockState(blockState)) return false;
}
return true;
}
@Override
public boolean canAccessEntity(int entityId) {
return true;
}
// Entities are not hidden, even if they spawn inside a hidden hull
@Override
public boolean canAccessEntitySpawn(int entityId, int blockX, int blockY, int blockZ) {
return true;
}
@Override
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockEntity(type)) return false;
}
return true;
}
@Override
public BlockState getObfuscation(int blockX, int blockY, int blockZ) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ)) return rule.obfuscation().getObfuscationState();
}
for (HiddenHull hull : hulls) {
if (hull.hull().getRegion().contains(blockX, blockY, blockZ)) return hull.obfuscation();
}
// Only asked for positions a rule or hull hides
return Blocks.END_STONE.defaultBlockState();
}
}
}
@@ -1,52 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.hullhider.HullRegion;
import de.steamwar.techhider.ObfuscationConfig;
/**
* Hides the blocks of an obfuscation config within an area, or everywhere if the area is null.
*/
public record HidingRule(HullRegion area, ObfuscationConfig obfuscation) {
public static HidingRule everywhere(ObfuscationConfig obfuscation) {
return new HidingRule(null, obfuscation);
}
/**
* Covers the area including its extension.
*/
public static HidingRule of(Region.Area area, ObfuscationConfig obfuscation) {
Point min = area.getMinPoint(true);
Point max = area.getMaxPoint(true);
return new HidingRule(HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()), obfuscation);
}
public boolean covers(int x, int y, int z) {
return area == null || area.contains(x, y, z);
}
public boolean coversChunk(int chunkX, int chunkZ) {
return area == null || !area.chunkOutside(chunkX, chunkZ);
}
}
@@ -21,17 +21,17 @@ package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.features.hullhider.HullHiderCommand;
import de.steamwar.bausystem.features.world.SpectatorListener;
import de.steamwar.bausystem.features.xray.XrayCommand;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.command.PreviousArguments;
import de.steamwar.command.SWCommand;
import de.steamwar.command.TypeMapper;
import de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.ObfuscationConfig;
import de.steamwar.techhider.legacy.TechHider;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@@ -41,26 +41,21 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Linked
public class TechHiderCommand extends SWCommand implements Listener {
public class TechHiderCommand extends SWCommand implements Listener, ScoreboardElement {
public TechHiderCommand() {
super("techhider", "hider", "obfuscate", "hide", "hide-tech");
}
// Read on netty threads by the BauTechHider
private Map<Region, Optional<HidingRule>> hidingRules = new ConcurrentHashMap<>();
private Map<Region, Set<Player>> hidden = new ConcurrentHashMap<>();
private Map<Region, Optional<TechHider>> techHiders = new HashMap<>();
private Map<Region, Set<Player>> hidden = new HashMap<>();
@LinkedInstance
public XrayCommand xrayCommand;
@LinkedInstance
public HullHiderCommand hullHiderCommand;
@Register(description = "TECHHIDER_HELP")
public void toggleHider(@Validator Player player) {
Region region = Region.getRegion(player.getLocation());
@@ -69,20 +64,26 @@ public class TechHiderCommand extends SWCommand implements Listener {
return;
}
Optional<HidingRule> hidingRule = hidingRules.computeIfAbsent(region, rg -> {
Optional<TechHider> techHider = techHiders.computeIfAbsent(region, rg -> {
if (!region.getGameModeConfig().Techhider.Active) {
return Optional.empty();
}
hidden.put(rg, ConcurrentHashMap.newKeySet());
return Optional.of(HidingRule.of(rg.getBuildArea(), ObfuscationConfig.of(rg.getGameModeConfig().Techhider)));
hidden.put(rg, new HashSet<>());
TechHider current = new TechHider((p, cX, cY) -> {
if (rg.getBuildArea().isChunkOutside(cX, cY)) return true;
return !hidden.get(rg).contains(p);
}, region.getGameModeConfig().Techhider.ObfuscateWith, region.getGameModeConfig().Techhider.HiddenBlocks, region.getGameModeConfig().Techhider.HiddenBlockEntities);
current.enable();
return Optional.of(current);
});
if (!hidingRule.isPresent()) {
if (!techHider.isPresent()) {
return;
}
xrayCommand.disable(region, player);
hullHiderCommand.disable(region, player);
if (hidden.get(region).contains(player)) {
hidden.get(region).remove(player);
BauSystem.MESSAGE.sendPrefixless("TECHHIDER_OFF", player, ChatMessageType.ACTION_BAR);
@@ -127,14 +128,6 @@ public class TechHiderCommand extends SWCommand implements Listener {
hidden.values().forEach(set -> set.remove(event.getPlayer()));
}
public void addHidingRules(Player player, List<HidingRule> rules) {
hidden.forEach((region, players) -> {
if (players.contains(player)) {
hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add);
}
});
}
public void disable(Region region, Player player) {
hidden.computeIfPresent(region, (rg, set) -> {
set.remove(player);
@@ -142,7 +135,19 @@ public class TechHiderCommand extends SWCommand implements Listener {
});
}
public boolean isActive(Region region, Player player) {
return hidden.getOrDefault(region, Collections.emptySet()).contains(player);
@Override
public ScoreboardGroup getGroup() {
return ScoreboardGroup.OTHER;
}
@Override
public int order() {
return 0;
}
@Override
public String get(Region region, Player p) {
if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null;
return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_TECHHIDER", p);
}
}
@@ -1,70 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.hullhider.HullHiderCommand;
import de.steamwar.bausystem.features.xray.XrayCommand;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import org.bukkit.entity.Player;
/**
* Shows which of /techhider, /xray and /hullhider is active, at most one of them can be per region.
*/
@Linked
public class ViewScoreboardElement implements ScoreboardElement {
@LinkedInstance
public TechHiderCommand techHiderCommand;
@LinkedInstance
public XrayCommand xrayCommand;
@LinkedInstance
public HullHiderCommand hullHiderCommand;
@Override
public ScoreboardGroup getGroup() {
return ScoreboardGroup.OTHER;
}
@Override
public int order() {
return 0;
}
@Override
public String get(Region region, Player p) {
String view;
if (techHiderCommand.isActive(region, p)) {
view = "TechHider";
} else if (xrayCommand.isActive(region, p)) {
view = "XRay";
} else if (hullHiderCommand.isActive(region, p)) {
view = "HullHider";
} else {
return null;
}
return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_VIEW", p, view);
}
}
@@ -50,9 +50,6 @@ import java.util.Arrays;
@Linked
public class TPSSystem implements Listener {
@Linked
private final TickManager tickManager = TickManager.impl;
public TPSSystem() {
if (TickManager.impl.canFreeze()) {
new TPSFreezeCommand();
@@ -71,6 +68,7 @@ public class TPSSystem implements Listener {
new TickDefaultCommand();
new TPSBaseCommand();
new TickBaseCommand();
Bukkit.getPluginManager().registerEvents(TickManager.impl, BauSystem.getInstance());
}
@EventHandler
@@ -23,7 +23,6 @@ import de.steamwar.bausystem.BauSystem;
import de.steamwar.core.CRIUWakeupEvent;
import de.steamwar.core.CheckpointUtils;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
@@ -39,16 +38,17 @@ public class AFKStopperListener implements Listener {
private long lastMovementTime = System.currentTimeMillis();
@LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.MINUTES)
private void checkAfk() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastMovementTime > 10 * 60000) { // 10 Minutes
for (Player p : Bukkit.getOnlinePlayers()) {
p.kickPlayer(BauSystem.MESSAGE.parse("AFK_KICK_MESSAGE", p));
public AFKStopperListener() {
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
long currentTime = System.currentTimeMillis();
if (currentTime - lastMovementTime > 10 * 60000) { // 10 Minutes
for (Player p : Bukkit.getOnlinePlayers()) {
p.kickPlayer(BauSystem.MESSAGE.parse("AFK_KICK_MESSAGE", p));
}
} else if (currentTime - lastMovementTime > 9 * 60000) {
BauSystem.MESSAGE.broadcast("AFK_WARNING_MESSAGE");
}
} else if (currentTime - lastMovementTime > 9 * 60000) {
BauSystem.MESSAGE.broadcast("AFK_WARNING_MESSAGE");
}
}, 1200, 1200); //every minute
}
@EventHandler
@@ -22,11 +22,10 @@ package de.steamwar.bausystem.features.world;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.config.BauServer;
import de.steamwar.bausystem.features.techhider.HidingRule;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.linkage.Linked;
import de.steamwar.sql.BauweltMember;
import de.steamwar.techhider.ObfuscationConfig;
import de.steamwar.techhider.legacy.TechHider;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -42,18 +41,14 @@ import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.player.*;
import org.bukkit.util.Vector;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Linked
public class SpectatorListener implements Listener {
private static final HidingRule SPECTATOR_RULE;
// Read on netty threads by the BauTechHider
private static final Set<Player> NO_TECHHIDER = ConcurrentHashMap.newKeySet();
private static final TechHider techHider;
private static final Set<Player> NO_TECHHIDER = new HashSet<>();
static {
Set<Material> materials = new HashSet<>();
@@ -104,12 +99,19 @@ public class SpectatorListener implements Listener {
materials.add(Material.WATER);
materials.remove(Material.BARRIER);
materials.remove(Material.STONE);
SPECTATOR_RULE = HidingRule.everywhere(new ObfuscationConfig(Material.END_STONE, materials, Collections.emptySet()));
techHider = new TechHider((TechHider.LocationEvaluator) (player, i, i1) -> {
return Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player);
}, Material.END_STONE, materials, new HashSet<>());
}
public void addHidingRules(Player player, List<HidingRule> rules) {
if (!(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player))) {
rules.add(SPECTATOR_RULE);
private static void enableOrDisableTechhider() {
boolean enable = Bukkit.getOnlinePlayers()
.stream()
.anyMatch(player -> !(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player)));
if (enable) {
techHider.enable();
} else {
techHider.disable();
}
}
@@ -120,6 +122,7 @@ public class SpectatorListener implements Listener {
NO_TECHHIDER.add(player);
}
resendChunks(player);
enableOrDisableTechhider();
}
private static void resendChunks(Player player) {
@@ -138,6 +141,7 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
enableOrDisableTechhider();
if (BauSystem.DEV_SERVER) return;
if (event.getPlayer().getUniqueId().equals(BauServer.getInstance().getOwner())) {
return;
@@ -159,6 +163,7 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onBauMemberUpdate(BauMemberUpdateEvent event) {
enableOrDisableTechhider();
if (!anySupervisorOnline(null) && !BauSystem.DEV_SERVER) {
Bukkit.getOnlinePlayers().forEach(player -> {
player.kickPlayer("");
@@ -175,6 +180,7 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
NO_TECHHIDER.remove(event.getPlayer());
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), SpectatorListener::enableOrDisableTechhider, 1);
if (BauSystem.DEV_SERVER) return;
if (!anySupervisorOnline(event.getPlayer())) {
Bukkit.getOnlinePlayers().forEach(player -> {
@@ -21,15 +21,14 @@ package de.steamwar.bausystem.features.xray;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.hullhider.HullHiderCommand;
import de.steamwar.bausystem.features.techhider.HidingRule;
import de.steamwar.bausystem.features.techhider.TechHiderCommand;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.command.SWCommand;
import de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.ObfuscationConfig;
import de.steamwar.techhider.legacy.TechHider;
import net.md_5.bungee.api.ChatMessageType;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.server.level.ServerPlayer;
@@ -44,27 +43,22 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
@Linked
public class XrayCommand extends SWCommand implements Listener {
public class XrayCommand extends SWCommand implements Listener, ScoreboardElement {
public XrayCommand() {
super("xray");
}
// Read on netty threads by the BauTechHider and the movement filter
private Map<Region, Optional<HidingRule>> hidingRules = new ConcurrentHashMap<>();
private Map<Region, Optional<TechHider>> techHiders = new HashMap<>();
private Map<Region, Set<Material>> xrayedBlocks = new HashMap<>();
private Map<Region, Set<Player>> hidden = new ConcurrentHashMap<>();
private Map<Region, Set<Player>> hidden = new HashMap<>();
@LinkedInstance
public TechHiderCommand techHiderCommand;
@LinkedInstance
public HullHiderCommand hullHiderCommand;
@Register(description = "XRAY_HELP")
public void toggleHider(@Validator Player player) {
Region region = Region.getRegion(player.getLocation());
@@ -73,24 +67,23 @@ public class XrayCommand extends SWCommand implements Listener {
return;
}
Optional<HidingRule> hidingRule = hidingRules.computeIfAbsent(region, rg -> {
Optional<TechHider> techHider = techHiders.computeIfAbsent(region, rg -> {
if (!region.getGameModeConfig().Techhider.Active) {
return Optional.empty();
}
hidden.put(rg, ConcurrentHashMap.newKeySet());
hidden.put(rg, new HashSet<>());
Set<Material> blocks = new HashSet<>(Arrays.asList(region.getGameModeConfig().Techhider.ObfuscateWith));
if (blocks.contains(Material.END_STONE)) blocks.add(Material.END_STONE_BRICKS);
xrayedBlocks.put(region, blocks);
return Optional.of(HidingRule.of(rg.getBuildArea(), new ObfuscationConfig(Material.STRUCTURE_VOID, blocks, Collections.emptySet())));
return Optional.of(createXray(rg, blocks));
});
if (!hidingRule.isPresent()) {
if (!techHider.isPresent()) {
return;
}
techHiderCommand.disable(region, player);
hullHiderCommand.disable(region, player);
if (hidden.get(region).contains(player)) {
hidden.get(region).remove(player);
BauSystem.MESSAGE.sendPrefixless("XRAY_OFF", player, ChatMessageType.ACTION_BAR);
@@ -146,12 +139,13 @@ public class XrayCommand extends SWCommand implements Listener {
}
}
public void addHidingRules(Player player, List<HidingRule> rules) {
hidden.forEach((region, players) -> {
if (players.contains(player)) {
hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add);
}
});
private TechHider createXray(Region rg, Set<Material> obfuscate) {
TechHider current = new TechHider((TechHider.LocationEvaluator) (p, cX, cY) -> {
if (rg.getBuildArea().isChunkOutside(cX, cY)) return true;
return !hidden.get(rg).contains(p);
}, Material.STRUCTURE_VOID, obfuscate, new HashSet<>());
current.enable();
return current;
}
@EventHandler
@@ -166,7 +160,19 @@ public class XrayCommand extends SWCommand implements Listener {
});
}
public boolean isActive(Region region, Player player) {
return hidden.getOrDefault(region, Collections.emptySet()).contains(player);
@Override
public ScoreboardGroup getGroup() {
return ScoreboardGroup.OTHER;
}
@Override
public int order() {
return 0;
}
@Override
public String get(Region region, Player p) {
if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null;
return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_XRAY", p);
}
}
-5
View File
@@ -20,11 +20,6 @@
plugins {
`java-library`
alias(libs.plugins.shadow)
deploy
}
deploy {
artifact(tasks.shadowJar)
}
tasks.build {
-6
View File
@@ -1,12 +1,6 @@
plugins {
steamwar.kotlin
application
deploy
}
deploy {
artifact(tasks.distZip, "sw.zip")
afterDeploy("unzip -o sw.zip -d /jars")
}
kotlin {
@@ -91,13 +91,9 @@ public abstract class AbstractSWCommand<T> {
if (commandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
errors.forEach(Runnable::run);
} else return;
if (!errors.isEmpty()) return;
for (SubCommand<T> help : helpCommandList) {
// Generic help must not hide the error of a more specific command
if (help.noTabComplete && !errors.isEmpty()) break;
if (help.invoke(errors::add, sender, alias, args)) return;
if (errors.isEmpty() && helpCommandList.stream().noneMatch(s -> s.invoke(errors::add, sender, alias, args))) {
errors.forEach(Runnable::run);
}
errors.forEach(Runnable::run);
} catch (CommandFrameworkException e) {
commandSystemError(sender, e);
throw e;
@@ -22,18 +22,12 @@ package de.steamwar.linkage;
import de.steamwar.linkage.api.Disable;
import de.steamwar.linkage.api.Enable;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.experimental.StandardException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -44,7 +38,6 @@ public abstract class AbstractLinker<T> {
protected final T plugin;
private Map<Class<?>, Object> instances = new HashMap<>();
private final List<Object> fieldInstances = new ArrayList<>();
protected AbstractLinker(@NonNull T plugin) {
this.plugin = plugin;
@@ -97,7 +90,6 @@ public abstract class AbstractLinker<T> {
((Enable) any).enable();
}
linkObject(any);
linkFields(any);
});
} catch (SecurityException e) {
Throwable cause = e.getCause();
@@ -125,70 +117,6 @@ public abstract class AbstractLinker<T> {
Throwable cause = e.getCause();
throw new LinkException(cause.getMessage(), cause);
}
// After the injection, as timers may run immediately on another thread
for (Object any : instances.values()) {
scheduleTimers(any);
}
for (Object any : fieldInstances) {
scheduleTimers(any);
}
}
private void scheduleTimers(Object any) throws LinkException {
for (Method method : any.getClass().getDeclaredMethods()) {
LinkedTimer timer = method.getAnnotation(LinkedTimer.class);
if (timer == null) continue;
String name = any.getClass().getName() + "." + method.getName();
if (method.getParameterCount() != 0) {
throw new LinkException("Linked timer " + name + " must not have parameters");
}
MethodHandle handle;
try {
handle = MethodHandles.privateLookupIn(method.getDeclaringClass(), MethodHandles.lookup()).unreflect(method);
} catch (IllegalAccessException e) {
throw new LinkException("Could not access linked timer " + name, e);
}
if (!Modifier.isStatic(method.getModifiers())) {
handle = handle.bindTo(any);
}
// Discards the return value
MethodHandle timerHandle = handle.asType(MethodType.methodType(void.class));
scheduleTimer(() -> runTimer(timerHandle), timer.delay(), timer.period(), timer.unit());
}
}
@SneakyThrows
private static void runTimer(MethodHandle timer) {
timer.invokeExact();
}
private void linkFields(Object any) {
for (Field field : any.getClass().getDeclaredFields()) {
if (field.getAnnotation(Linked.class) == null) continue;
Object value;
try {
field.setAccessible(true);
value = field.get(Modifier.isStatic(field.getModifiers()) ? null : any);
} catch (IllegalAccessException e) {
throw new SecurityException(e.getMessage(), e);
}
if (value == null) {
String message = "Linked field " + any.getClass().getName() + "." + field.getName() + " is not initialised";
throw new SecurityException(message, new IllegalStateException(message));
}
fieldInstances.add(value);
if (value instanceof Enable) {
((Enable) value).enable();
}
linkObject(value);
}
}
public final void unlink() {
@@ -198,12 +126,6 @@ public abstract class AbstractLinker<T> {
((Disable) any).disable();
}
});
fieldInstances.forEach(any -> {
unlinkObject(any);
if (any instanceof Disable) {
((Disable) any).disable();
}
});
}
public final void addLinkableInstance(@NonNull Object instance) {
@@ -230,14 +152,6 @@ public abstract class AbstractLinker<T> {
protected void linkObject(Object any) {
}
/**
* Runs the runnable repeatedly for a {@link LinkedTimer}.
*
* @param delay time until the first run, in unit
* @param period time between two runs, in unit
*/
protected abstract void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit);
/**
* There is no need in calling {@link Disable#disable()} ()} by this method.
*/
@@ -26,12 +26,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* On a class: the class is instantiated and linked.
* On a field of a linked class: the (already initialised) value of the field is linked as well, e.g. registered as listener.
*/
@IndexAnnotated
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface Linked {
}
@@ -1,81 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.linkage;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* Runs the annotated method of a linked class (or of the value of a {@link Linked} field) repeatedly.
* The method must not have parameters, its return value is ignored. It may be static.
* The timer is not stopped on unlink, as the server stops anyway.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LinkedTimer {
/**
* Time until the first run, in {@link #unit()}.
*/
long delay();
/**
* Time between two runs, in {@link #unit()}.
*/
long period();
Unit unit() default Unit.TICKS;
@AllArgsConstructor
enum Unit {
/**
* 1 tick = 50 ms
*/
TICKS(50, TimeUnit.MILLISECONDS),
MILLISECONDS(1, TimeUnit.MILLISECONDS),
SECONDS(1, TimeUnit.SECONDS),
MINUTES(1, TimeUnit.MINUTES),
HOURS(1, TimeUnit.HOURS);
private final long factor;
@Getter
private final TimeUnit timeUnit;
/**
* @return the value in ticks, rounded down
*/
public long toTicks(long value) {
return timeUnit.toMillis(value * factor) / 50;
}
/**
* @return the value in {@link #getTimeUnit()}
*/
public long toTimeUnit(long value) {
return value * factor;
}
}
}
@@ -515,20 +515,6 @@ public final class GameModeConfig<M, W> {
*/
public final boolean WindchargesDestroyWater;
/**
* Allows Wind Charges to leave the Region.
*
* @implSpec {@code true} by default
*/
public final boolean WindchargesCanLeaveRegion;
/**
* Limits the amount of Wind Charges in each extended region. -1 is no limit
*
* @implSpec {@code -1} by default
*/
public final int WindchargeLimit;
private ArenaConfig(YMLWrapper<M, W> loader, SchematicConfig.SizeConfig Size, List<Integer> EnterStages) {
loaded = loader.canLoad();
WaterDepth = loader.getInt("WaterDepth", 0);
@@ -549,8 +535,6 @@ public final class GameModeConfig<M, W> {
WindchargesCanCrossMiddle = loader.getBoolean("WindchargesCanCrossMiddle", false);
WindchargesInteractWithBlocks = loader.getBoolean("WindchargesInteractWithBlocks", true);
WindchargesDestroyWater = loader.getBoolean("WindchargesDestroyWater", false);
WindchargesCanLeaveRegion = loader.getBoolean("WindchargesCanLeaveRegion", true);
WindchargeLimit = loader.getInt("WindchargeLimit", -1);
}
@ToString
@@ -30,7 +30,6 @@ dependencies {
compileOnly(libs.classindex)
annotationProcessor(libs.classindex)
compileOnly(project(":SpigotCore", "default"))
implementation(project(":HiderCore"))
compileOnly(libs.netty)
compileOnly(libs.paperapi)
@@ -68,10 +68,6 @@ Arena:
WindchargesCanCrossMiddle: false # defaults to false if missing
# Allows Wind Charge interaction with Blocks.
WindchargesInteractWithBlocks: false # defaults to false if missing
# Allows Wind Charges to leave the extended region.
WindchargesCanLeaveRegion: true # defaults to true
# Limits the amount of Wind Charges in each extended region. -1 is no limit
WindchargeLimit: -1 # defaults to -1
Schematic:
# The size of the schematics
@@ -34,11 +34,9 @@ import org.bukkit.util.Vector;
import java.io.File;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class Config {
@@ -82,7 +80,7 @@ public class Config {
//event parameter
public static final EventFight EventKampf;
private static final Set<UUID> Referees;
private static final Set<Integer> Referees;
public static final int EventTeamBlueID;
public static final int EventTeamRedID;
public static final boolean BothTeamsPublic;
@@ -239,11 +237,7 @@ public class Config {
BothTeamsPublic = EventTeamRedID == 0 && EventTeamBlueID == 0;
SpectatePort = EventKampf.getSpectatePort() == null ? 0 : EventKampf.getSpectatePort();
LiveReplay = SpectatePort != 0;
Referees = Referee.get(Config.EventKampf.getEventID()).stream()
.map(SteamwarUser::byId)
.filter(Objects::nonNull)
.map(SteamwarUser::getUUID)
.collect(Collectors.toUnmodifiableSet());
Referees = Referee.get(Config.EventKampf.getEventID());
Event event = Event.byId(EventKampf.getEventID());
if (BothTeamsPublic) {
@@ -311,6 +305,6 @@ public class Config {
}
public static boolean isReferee(Player player) {
return Referees.contains(player.getUniqueId());
return Referees.contains(SteamwarUser.get(player.getUniqueId()).getId());
}
}
@@ -21,8 +21,8 @@ package de.steamwar.fightsystem;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.Core;
import de.steamwar.core.WorldIdentifier;
import de.steamwar.core.worldedit.SteamEdit;
import de.steamwar.core.worldedit.WorldEditRendererCUIEditor;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.listener.ClickAnalyzer;
@@ -32,8 +32,8 @@ import de.steamwar.fightsystem.record.GlobalRecorder;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.OneShotStateDependent;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.fightsystem.utils.FightHullHider;
import de.steamwar.fightsystem.utils.FightUI;
import de.steamwar.fightsystem.utils.HullHider;
import de.steamwar.fightsystem.utils.SWSound;
import de.steamwar.fightsystem.utils.TechHiderWrapper;
import de.steamwar.linkage.AbstractLinker;
@@ -58,7 +58,7 @@ public class FightSystem extends JavaPlugin {
private FightTeam lastWinner;
private String lastWinreason;
private TechHiderWrapper techHider;
private FightHullHider hullHider;
private HullHider hullHider;
private SpigotLinker spigotLinker;
@@ -79,11 +79,6 @@ public class FightSystem extends JavaPlugin {
Core.setServerName("Replay");
}
String arenaMap = System.getProperty("arenaMap");
if (arenaMap != null) {
WorldIdentifier.setArena(arenaMap);
}
message = new Message("de.steamwar.fightsystem.FightSystem", FightSystem.class.getClassLoader());
spigotLinker = new SpigotLinker(this, message) {
@@ -116,9 +111,9 @@ public class FightSystem extends JavaPlugin {
new OneShotStateDependent(ArenaMode.Test, FightState.All, SteamEdit::init);
Config.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, ArenaMode.AntiTest.contains(Config.mode));
hullHider = new FightHullHider();
hullHider = new HullHider();
techHider = new TechHiderWrapper(hullHider);
hullHider.fillAll(true);
FightSystem.getHullHider().getHullMap().values().forEach(hull -> hull.fill(true));
FileSource.startReplay();
@@ -170,7 +165,7 @@ public class FightSystem extends JavaPlugin {
return plugin.techHider;
}
public static FightHullHider getHullHider() {
public static HullHider getHullHider() {
return plugin.hullHider;
}
@@ -56,11 +56,11 @@ public class TechhiderbugCommand extends FightSWCommand {
writer.append('\n');
writer.append("HullHider regions: ");
FightSystem.getHullHider().forEachHull((t, h) -> writer.append(t.getName()).append(':').append(h.getRegion().toString()).append(' '));
FightSystem.getHullHider().getHullMap().forEach((t, h) -> writer.append(t.getName()).append(':').append(h.getRegion().toString()).append(' '));
writer.append('\n');
writer.append("Hidden regions: ");
FightSystem.getTechHider().getHiddenRegion().forEach((p, r) -> writer.append(p.getName()).append(':').append(Arrays.toString(r)).append(' '));
FightSystem.getTechHider().getHiddenRegion().forEach((p, r) -> writer.append(p.getName()).append(':').append(r.toString()).append(' '));
writer.append('\n');
writer.append("TinyProtocol: ");
@@ -69,8 +69,8 @@ public class FightPlayer {
public void setOut() {
isOut = true;
stopEnternCountdown(); // before the event, so listeners already see canEntern() == false
Bukkit.getPluginManager().callEvent(new TeamDeathEvent(this));
stopEnternCountdown();
ifAI(AI::stop);
}
@@ -84,7 +84,7 @@ public class FightWorld extends StateDependent {
entities.forEach(Entity::remove);
entities.clear();
FightSystem.getHullHider().fillAll(true);
FightSystem.getHullHider().getHullMap().values().forEach(hull -> hull.fill(true));
Config.ArenaRegion.forEachChunk((x, z) -> {
for (Player p : Bukkit.getOnlinePlayers()) {
@@ -108,7 +108,7 @@ public class WaterRemover implements Listener {
//checks for water and removes it, if present
boolean result = true;
Material type = b.getType();
if (type == Material.WATER || type == Material.LAVA || type == Material.BUBBLE_COLUMN) {
if (type == Material.WATER || type == Material.LAVA) {
b.setType(Material.AIR);
} else {
BlockData data = b.getBlockData();
@@ -22,71 +22,31 @@ package de.steamwar.fightsystem.listener;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentTask;
import de.steamwar.fightsystem.utils.Region;
import de.steamwar.linkage.Linked;
import net.minecraft.world.entity.projectile.windcharge.WindCharge;
import org.bukkit.Location;
@Linked
public class WindchargeStopper {
public WindchargeStopper() {
canCrossMiddle = Config.GameModeConfig.Arena.WindchargesCanCrossMiddle;
canLeaveRegion = Config.GameModeConfig.Arena.WindchargesCanLeaveRegion;
limit = Config.GameModeConfig.Arena.WindchargeLimit;
boolean enabled = limit >= 0 || !canCrossMiddle || !canLeaveRegion;
new StateDependentTask(enabled, FightState.Running, this::run, 1, 1);
new StateDependentTask(!Config.GameModeConfig.Arena.WindchargesCanCrossMiddle, FightState.Running, this::run, 1, 1);
}
private static final int middleLine = Config.SpecSpawn.getBlockZ();
private static final Class<?> windChargeClass = WindCharge.class;
private final boolean canCrossMiddle;
private final boolean canLeaveRegion;
private final int limit;
private int blueCount;
private int redCount;
private void run() {
redCount = 0;
blueCount = 0;
Recording.iterateOverEntities(windChargeClass::isInstance, entity -> {
Location nextlocation = entity.getLocation().add(entity.getVelocity());
Location location = entity.getLocation();
Location nextlocation = location.clone().add(entity.getVelocity());
if (!canCrossMiddle) {
boolean passedMiddle = nextlocation.getBlockZ() >= middleLine && location.getBlockZ() <= middleLine ||
nextlocation.getBlockZ() <= middleLine && location.getBlockZ() >= middleLine;
if (passedMiddle) {
entity.remove();
return;
}
}
boolean passedMiddle = nextlocation.getBlockZ() >= middleLine && location.getBlockZ() <= middleLine ||
nextlocation.getBlockZ() <= middleLine && location.getBlockZ() >= middleLine;
Region region = Config.BlueExtendRegion.inRegion(location) ? Config.BlueExtendRegion
: Config.RedExtendRegion.inRegion(location) ? Config.RedExtendRegion
: null;
if (!canLeaveRegion) {
if (region == null) {
entity.remove();
return;
}
if (!region.inRegion(nextlocation)) {
entity.remove();
return;
}
}
if (limit >= 0 && region != null) {
if (region == Config.BlueExtendRegion) {
if (++blueCount > limit) entity.remove();
} else {
if (++redCount > limit) entity.remove();
}
if (passedMiddle) {
entity.remove();
}
});
}
@@ -20,7 +20,6 @@
package de.steamwar.fightsystem.record;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import de.steamwar.core.BlockIds;
import de.steamwar.core.worldedit.WorldEditWrapper;
import de.steamwar.entity.REntity;
import de.steamwar.entity.REntityServer;
@@ -41,6 +40,7 @@ import de.steamwar.fightsystem.utils.TechHiderWrapper;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.Team;
import de.steamwar.techhider.BlockIds;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
@@ -51,7 +51,7 @@ import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.*;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@@ -473,7 +473,7 @@ public class PacketProcessor implements Listener {
level.removeBlockEntity(pos);
level.setBlock(pos, blockData, blockState1);
level.getChunkSource().blockChanged(pos);
FightSystem.getHullHider().blockUpdate(Config.world.getBlockAt(x, y, z), CraftBlockData.fromData(Block.stateById(blockState)));
FightSystem.getHullHider().blockUpdate(Config.world.getBlockAt(x, y, z), CraftMagicNumbers.getMaterial(Block.stateById(blockState)).getItemType());
});
}
@@ -1,147 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.utils;
import de.steamwar.entity.REntity;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.events.TeamDeathEvent;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.listener.Recording;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.fightsystem.states.StateDependentTask;
import de.steamwar.hullhider.Hull;
import de.steamwar.hullhider.HullHider;
import de.steamwar.hullhider.Viewers;
import de.steamwar.techhider.ObfuscationConfig;
import lombok.Getter;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Objects;
import java.util.function.BiConsumer;
/**
* Fight specific part of the hull hider: one hull per team schematic region, and who may view which hull.
*/
public class FightHullHider {
private final HullHider<FightTeam> hullHider = new HullHider<>(true);
// Shared with the TechHiderWrapper
@Getter
private final ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider);
public FightHullHider() {
if (!TechHiderWrapper.ENABLED) return;
Fight.teams().forEach(team -> {
Region region = team.getSchemRegion();
hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), obfuscation, new Viewers.HiddenByDefault()));
});
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener());
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, hullHider);
new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1);
}
public void initialize(FightTeam team) {
if (!TechHiderWrapper.ENABLED) return;
hullHider.initialize(team);
}
public void fill(FightTeam team, boolean visible) {
if (!TechHiderWrapper.ENABLED) return;
hullHider.fill(team, visible);
}
public void fillAll(boolean visible) {
hullHider.fillAll(visible);
}
public void forEachHull(BiConsumer<FightTeam, Hull> consumer) {
hullHider.forEach(consumer);
}
// Registered in every state: players must be hidden from the hulls by default, no matter when they join.
private class MembershipListener implements Listener {
@EventHandler(priority = EventPriority.HIGH)
public void onJoin(PlayerJoinEvent e) {
updatePlayer(e.getPlayer());
}
@EventHandler
public void onLeave(PlayerQuitEvent e) {
hullHider.forgetViewer(e.getPlayer());
}
// A dead player loses the right to see the enemy hull again
@EventHandler
public void onTeamDeath(TeamDeathEvent e) {
e.getFightPlayer().ifPlayer(player -> {
updatePlayer(player);
FightSystem.getTechHider().reloadChunks(player, Fight.getOpposite(e.getFightPlayer().getTeam()).getExtendRegion(), Region.EMPTY);
});
}
}
public void updatePlayer(Player player) {
if (!TechHiderWrapper.ENABLED) return;
FightTeam team = Fight.getPlayerTeam(player);
FightPlayer fp = Fight.getFightPlayer(player);
hullHider.forEach((hullTeam, hull) -> {
if (Config.isReferee(player) || hullTeam == team || (fp != null && fp.canEntern())) {
hullHider.addViewer(player, hullTeam);
} else {
hullHider.removeViewer(player, hullTeam);
}
});
}
public void blockUpdate(Block block, BlockData changedData) {
hullHider.blockUpdate(block, changedData);
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
return hullHider.isBlockHidden(player, x, y, z);
}
private void onTick() {
Recording.iterateOverEntities(Objects::nonNull, hullHider::checkEntity);
hullHider.sendUncoveredBlocks();
}
public void updateREntity(REntity e) {
hullHider.updateREntity(e);
}
public void despawnREntity(REntity e) {
hullHider.despawnREntity(e);
}
}
@@ -0,0 +1,386 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.utils;
import com.comphenix.tinyprotocol.TinyProtocol;
import com.mojang.authlib.GameProfile;
import de.steamwar.core.ProtocolWrapper;
import de.steamwar.entity.REntity;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.FightTeam;
import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.stream.IntStream;
public class Hull {
private static final IntVector[] NO_BRANCHES = new IntVector[0];
private static boolean isOccluding(Material material) {
return material.isOccluding() || Config.GameModeConfig.Techhider.HiddenBlocks.contains(material);
}
@Getter
private final Region region;
private final boolean groundVisible;
private final IntVector[] directions;
private final BitSet occluding;
private final BitSet visibility;
private final HashMap<IntVector, BitSet> visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block
private final HashSet<IntVector> uncoveredSurface = new HashSet<>();
private final HashSet<Player> players = new HashSet<>();
private final HashSet<Entity> entities = new HashSet<>();
private final HashSet<REntity> rentities = new HashSet<>();
public Hull(FightTeam team) {
this.region = team.getSchemRegion();
this.groundVisible = region.getMinY() != Config.PlayerRegion.getMinY();
this.occluding = new BitSet(region.volume());
this.visibility = new BitSet(region.volume());
IntVector primaryDirection = new IntVector(0, 0, team.isBlue() == (Config.BlueToRedZ > 0) ? -1 : 1);
directions = IntStream.range(0, 27)
.mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1))
.filter(v -> v.sqLength() == 1 || v.x * primaryDirection.x + v.y * primaryDirection.y + v.z * primaryDirection.z == 1) // Not pointing away from primary direction
.toArray(IntVector[]::new);
for (IntVector direction : directions) {
visibilityDirections.put(direction, new BitSet(region.volume()));
}
}
public boolean blockPrecise(Player player, int chunkX, int chunkY, int chunkZ) {
return players.contains(player) && !region.chunkSectionOutside(chunkX, chunkY, chunkZ);
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
return region.inRegion(x, y, z) && players.contains(player) && !visibility.get(((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()));
}
public boolean isLocationHidden(Player player, Location location) {
return players.contains(player) && region.inRegion(location) && !visibility.get(new IntVector(location).toId(region));
}
public void addPlayer(Player player) {
if (players.add(player)) {
for (Entity entity : entities) {
untrackEntity(player, entity);
}
}
}
public void removePlayer(Player player, boolean activeRemoval) {
if (players.remove(player) && activeRemoval) {
for (Entity entity : entities) {
trackEntity(player, entity);
}
// techhider triggers block change sending
}
}
public void checkEntity(Entity entity) {
Location location = entity.getLocation();
if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
if (entities.add(entity)) {
for (Player player : players) {
untrackEntity(player, entity);
}
}
} else {
if (entities.remove(entity)) {
for (Player player : players) {
trackEntity(player, entity);
}
}
}
}
public void trackEntity(Player player, Entity entity) {
if (entity instanceof Player) {
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.REMOVE, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE));
}
player.showEntity(FightSystem.getPlugin(), entity);
}
public void untrackEntity(Player player, Entity entity) {
player.hideEntity(FightSystem.getPlugin(), entity);
if (entity instanceof Player) {
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE));
}
}
public void removeEntity(Entity entity) {
entities.remove(entity);
}
public void checkREntity(REntity entity) {
Location location = new Location(Config.world, entity.getX(), entity.getY(), entity.getZ());
if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) {
if (rentities.add(entity)) entity.hide(true);
} else {
if (rentities.remove(entity)) entity.hide(false);
}
}
public void removeREntity(REntity entity) {
rentities.remove(entity);
}
public void fill(boolean visible) {
visibility.set(0, visibility.size(), visible);
occluding.set(0, occluding.size(), !visible);
uncoveredSurface.clear();
for (BitSet directionalVisibility : visibilityDirections.values()) {
directionalVisibility.set(0, directionalVisibility.size(), visible);
}
}
public void initialize() {
visibility.clear();
occluding.clear();
uncoveredSurface.clear();
for (BitSet directionalVisibility : visibilityDirections.values()) {
directionalVisibility.clear();
}
long start = System.currentTimeMillis();
region.forEach((x, y, z) -> {
IntVector block = new IntVector(x, y, z);
if (isOccluding(Config.world.getBlockAt(x, y, z).getType())) {
occluding.set(block.toId(region));
}
});
forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction));
FightSystem.getPlugin().getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality());
}
public void updateBlockVisibility(Block b, Material changedType) {
IntVector root = new IntVector(b.getX(), b.getY(), b.getZ());
if (root.notInRegion(region)) return;
int id = root.toId(region);
if (!occluding.get(id) || isOccluding(changedType)) return;
occluding.clear(id);
if (!visibility.get(id)) return;
List<IntVector> uncovered = new ArrayList<>();
for (Map.Entry<IntVector, BitSet> directionalVisibility : visibilityDirections.entrySet()) {
if (directionalVisibility.getValue().get(id)) {
directionalVisibility.getValue().clear(id);
uncoverBlocks(uncovered, root, directionalVisibility.getKey());
}
}
uncoveredSurface.addAll(new HashSet<>(uncovered));
uncoveredSurface.remove(root);
}
public void sendUncoveredBlocks() {
Map<IntVector, List<IntVector>> sectionWise = new HashMap<>();
for (IntVector uncovered : uncoveredSurface) {
sectionWise.computeIfAbsent(new IntVector(uncovered.x >> 4, uncovered.y >> 4, uncovered.z >> 4), section -> new ArrayList<>()).add(uncovered);
}
uncoveredSurface.clear();
for (Map.Entry<IntVector, List<IntVector>> entry : sectionWise.entrySet()) {
Object result;
List<IntVector> changes = entry.getValue();
Object[] blockdata = new Object[changes.size()];
for (int i = 0; i < blockdata.length; i++) {
IntVector change = changes.get(i);
blockdata[i] = ((CraftBlockData) Config.world.getBlockData(change.getX(), change.getY(), change.getZ())).getState();
}
if (changes.size() > 1) {
IntVector section = changes.get(0);
section = new IntVector(section.getX() >> 4, section.getY() >> 4, section.getZ() >> 4);
int xOffset = 16 * section.getX();
int yOffset = 16 * section.getY();
int zOffset = 16 * section.getZ();
short[] pos = new short[changes.size()];
for (int i = 0; i < changes.size(); i++) {
IntVector change = changes.get(i);
pos[i] = (short) (((change.getX() - xOffset) << 8) + ((change.getZ() - zOffset) << 4) + (change.getY() - yOffset));
}
result = new ClientboundSectionBlocksUpdatePacket(SectionPos.of(section.getX(), section.getY(), section.getZ()), new Short2ObjectArrayMap<>(pos, blockdata, blockdata.length));
} else {
IntVector pos = changes.get(0);
result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]);
}
Object packet = result;
players.forEach(player -> TinyProtocol.instance.sendPacket(player, packet));
}
}
private void forEachBorder(BiConsumer<IntVector, IntVector> f) {
for (int x = region.getMinX(); x < region.getMaxX(); x++) {
for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
if (groundVisible) {
f.accept(new IntVector(x, region.getMinY(), z), new IntVector(0, 1, 0));
}
f.accept(new IntVector(x, region.getMaxY() - 1, z), new IntVector(0, -1, 0));
}
}
for (int x = region.getMinX(); x < region.getMaxX(); x++) {
for (int y = region.getMinY(); y < region.getMaxY(); y++) {
f.accept(new IntVector(x, y, region.getMinZ()), new IntVector(0, 0, 1));
f.accept(new IntVector(x, y, region.getMaxZ() - 1), new IntVector(0, 0, -1));
}
}
for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
for (int y = region.getMinY(); y < region.getMaxY(); y++) {
f.accept(new IntVector(region.getMinX(), y, z), new IntVector(1, 0, 0));
f.accept(new IntVector(region.getMaxX() - 1, y, z), new IntVector(-1, 0, 0));
}
}
}
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction) {
uncoverBlocks(
uncovered, block, direction, visibilityDirections.get(direction),
direction.sqLength() == 1
? Arrays.stream(directions)
.filter(v -> v.sqLength() >= 2 && v.x * direction.x + v.y * direction.y + v.z * direction.z == 1)
.toArray(IntVector[]::new)
: NO_BRANCHES
);
}
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
if (block.notInRegion(region)) return;
int id = block.toId(region);
if (directionalVisibility.get(id)) return;
directionalVisibility.set(id);
if (!visibility.get(id)) {
visibility.set(id);
uncovered.add(block);
}
if (occluding.get(id)) return;
uncoverBlocks(uncovered, block.add(direction), direction, directionalVisibility, branchDirections);
for (IntVector branchDirection : branchDirections) {
uncoverBlocks(uncovered, block.add(branchDirection), branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES);
}
}
@Getter
@AllArgsConstructor
public static class IntVector {
private final int x;
private final int y;
private final int z;
public IntVector(Location location) {
this.x = location.getBlockX();
this.y = location.getBlockY();
this.z = location.getBlockZ();
}
public boolean notInRegion(Region region) {
return !region.inRegion(x, y, z);
}
public int toId(Region region) {
return ((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX());
}
public IntVector add(int x, int y, int z) {
return new IntVector(this.x + x, this.y + y, this.z + z);
}
public IntVector add(IntVector v) {
return add(v.x, v.y, v.z);
}
public int sqLength() {
return x * x + y * y + z * z;
}
@Override
public int hashCode() {
return y << 24 ^ x << 12 ^ z;
}
@Override
public boolean equals(Object o) {
if (o == null || this.getClass() != o.getClass()) {
return false;
}
IntVector v = (IntVector) o;
return x == v.x && y == v.y && z == v.z;
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
}
private static class NullList<E> extends AbstractList<E> {
@Override
public void add(int index, E element) {
// Straight to /dev/null!
}
@Override
public E get(int index) {
return null;
}
@Override
public int size() {
return 0;
}
}
}
@@ -0,0 +1,171 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.fightsystem.utils;
import de.steamwar.entity.REntity;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.listener.Recording;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.fightsystem.states.StateDependentTask;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.block.Block;
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.BlockPhysicsEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class HullHider implements Listener {
@Getter
private final Map<FightTeam, Hull> hullMap = new HashMap<>();
private final Hull[] hulls;
public HullHider() {
if (!TechHiderWrapper.ENABLED) {
hulls = new Hull[0];
return;
}
Fight.teams().forEach(team -> hullMap.put(team, new Hull(team)));
hulls = hullMap.values().toArray(new Hull[0]);
new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, this);
new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1);
}
public void initialize(FightTeam team) {
if (!TechHiderWrapper.ENABLED) return;
hullMap.get(team).initialize();
}
public void fill(FightTeam team, boolean visible) {
if (!TechHiderWrapper.ENABLED) return;
hullMap.get(team).fill(visible);
}
@EventHandler(priority = EventPriority.HIGH)
public void onJoin(PlayerJoinEvent e) {
updatePlayer(e.getPlayer());
}
@EventHandler
public void onLeave(PlayerQuitEvent e) {
removePlayer(e.getPlayer(), false);
}
public void updatePlayer(Player player) {
if (!TechHiderWrapper.ENABLED) return;
FightTeam team = Fight.getPlayerTeam(player);
FightPlayer fp = Fight.getFightPlayer(player);
for (Map.Entry<FightTeam, Hull> hull : hullMap.entrySet()) {
if (Config.isReferee(player) || hull.getKey() == team || (fp != null && fp.canEntern())) {
hull.getValue().removePlayer(player, true);
} else {
hull.getValue().addPlayer(player);
}
}
}
private void removePlayer(Player player, boolean activeRemoval) {
for (Hull hull : hulls) {
hull.removePlayer(player, activeRemoval);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockPhysic(BlockPhysicsEvent e) {
if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) {
blockUpdate(e.getBlock(), e.getChangedType());
}
}
public void blockUpdate(Block block, Material changedType) {
for (Hull hull : hulls) {
hull.updateBlockVisibility(block, changedType);
}
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
if (!TechHiderWrapper.ENABLED) return false;
for (Hull hull : hulls) {
if (hull.isBlockHidden(player, x, y, z)) {
return true;
}
}
return false;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSpawn(EntitySpawnEvent e) {
for (Hull hull : hulls) {
hull.checkEntity(e.getEntity());
}
}
private void onTick() {
Recording.iterateOverEntities(Objects::nonNull, entity -> {
for (Hull hull : hulls) {
hull.checkEntity(entity);
}
});
for (Hull hull : hulls) {
hull.sendUncoveredBlocks();
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDeath(EntityDeathEvent e) {
for (Hull hull : hulls) {
hull.removeEntity(e.getEntity());
}
}
public void updateREntity(REntity e) {
for (Hull hull : hulls) {
hull.checkREntity(e);
}
}
public void despawnREntity(REntity e) {
for (Hull hull : hulls) {
hull.removeREntity(e);
}
}
}
@@ -19,8 +19,7 @@
package de.steamwar.fightsystem.utils;
import de.steamwar.core.ProtocolUtils;
import de.steamwar.hullhider.HullRegion;
import de.steamwar.techhider.ProtocolUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Location;
@@ -156,10 +155,6 @@ public class Region {
return in2dRegion(x, z) && minY <= y && y < maxY;
}
public HullRegion toHullRegion() {
return new HullRegion(minX, minY, minZ, maxX, maxY, maxZ);
}
public Region intersection(Region other) {
int x = Math.max(minX, other.minX);
int y = Math.max(minY, other.minY);
@@ -25,40 +25,46 @@ import de.steamwar.fightsystem.events.BoardingEvent;
import de.steamwar.fightsystem.events.TeamLeaveEvent;
import de.steamwar.fightsystem.events.TeamSpawnEvent;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightPlayer;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependent;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.techhider.AccessPrivilegeProvider;
import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer;
import de.steamwar.techhider.ObfuscationConfig;
import de.steamwar.techhider.TechHider;
import lombok.Getter;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TechHiderWrapper extends StateDependent implements Listener {
public static final boolean ENABLED = !Config.OnlyPublicSchematics && !Config.test() && Config.GameModeConfig.Techhider.Active;
private static final Region[] NO_REGIONS = new Region[0];
@Getter
private final ConcurrentHashMap<Player, Region[]> hiddenRegion = new ConcurrentHashMap<>();
private final FightHullHider hullHider;
private final ConcurrentHashMap<Player, Region> hiddenRegion = new ConcurrentHashMap<>();
private final HullHider hullHider;
public TechHiderWrapper(FightHullHider hullHider) {
public TechHiderWrapper(HullHider hullHider) {
super(ENABLED, FightState.All);
this.hullHider = hullHider;
@@ -69,91 +75,64 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@Override
public void enable() {
new TechHider(newProvider(hullHider.getObfuscation()));
}
Set<BlockState> blockStatesToObfuscate = getBlockStatesToHideFromMaterials(Config.GameModeConfig.Techhider.HiddenBlocks);
private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) {
return new AccessPrivilegeProvider() {
Set<Block> blocksToObfuscate = Config.GameModeConfig.Techhider.HiddenBlocks.stream()
.map(CraftMagicNumbers::getBlock)
.collect(Collectors.toUnmodifiableSet());
Set<BlockEntityType<?>> blockEntityTypeToObfuscate = Config.GameModeConfig.Techhider.HiddenBlockEntities.stream()
.map(id -> {
ResourceLocation loc = ResourceLocation.parse(id);
return BuiltInRegistries.BLOCK_ENTITY_TYPE.get(loc).orElse(null);
})
.filter(Objects::nonNull)
.map(Holder.Reference::value)
.collect(Collectors.toUnmodifiableSet());
new TechHider(CraftMagicNumbers.getBlock(Config.GameModeConfig.Techhider.ObfuscateWith), new AccessPrivilegeProvider() {
@Override
public ForPlayer forPlayer(Player p) {
return new FightPlayerAccess(p, getHiddenRegions(p), obfuscation);
public boolean isEveryonePrivilegedToAccessAllDataWithinChunk(int chunkX, int chunkZ) {
return Fight.teams().stream().map(FightTeam::getExtendRegion).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
}
@Override
public boolean isPlayerPrivilegedToAccessPosition(Player p, int blockX, int blockY, int blockZ) {
return !hullHider.isBlockHidden(p, blockX, blockY, blockZ);
}
@Override
public boolean isPlayerPrivilegedToAccessBlock(Player p, int blockX, int blockY, int blockZ, Block block) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block);
}
@Override
public boolean isPlayerPrivilegedToAccessBlockState(Player p, int blockX, int blockY, int blockZ, BlockState blockState) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState);
}
// TODO will require entity tracking on the netty thread to prevent future race conditions
@Override
public boolean isPlayerPrivilegedToAccessEntity(Player p, int entityId) {
net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) p.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId);
if (nmsEntity != null) {
return !hullHider.isBlockHidden(p, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ());
} else {
return true;
}
}
@Override
public boolean isPlayerPrivilegedToAccessBlockEntity(Player p, int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !getHiddenRegion(p).inRegion(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type);
}
@Override
public boolean isPlayerPrivilegedToPerformAction(Player p) {
return p.getGameMode() != GameMode.SPECTATOR;
}
};
}
/**
* Snapshot of what one player may access, taken once per outgoing packet on the netty thread.
*/
private class FightPlayerAccess implements ForPlayer {
private final Player player;
private final Region[] hiddenRegions;
private final ObfuscationConfig obfuscation;
private FightPlayerAccess(Player player, Region[] hiddenRegions, ObfuscationConfig obfuscation) {
this.player = player;
this.hiddenRegions = hiddenRegions;
this.obfuscation = obfuscation;
}
private boolean isHidden(int blockX, int blockY, int blockZ) {
for (Region region : hiddenRegions) {
if (region.inRegion(blockX, blockY, blockZ)) return true;
}
return false;
}
@Override
public Player getPlayer() {
return player;
}
@Override
public boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ) {
// Hulls the player may not see lie within the hidden regions, everything outside of them is not critical
return Arrays.stream(hiddenRegions).allMatch(region -> region.chunkOutside(chunkX, chunkZ));
}
@Override
public boolean canAccessPosition(int blockX, int blockY, int blockZ) {
return !hullHider.isBlockHidden(player, blockX, blockY, blockZ);
}
@Override
public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) {
return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlock(block);
}
@Override
public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) {
return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockState(blockState);
}
// TODO will require entity tracking on the netty thread to prevent future race conditions
@Override
public boolean canAccessEntity(int entityId) {
net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) player.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId);
if (nmsEntity != null) {
return !hullHider.isBlockHidden(player, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ());
} else {
return true;
}
}
@Override
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockEntity(type);
}
@Override
public BlockState getObfuscation(int blockX, int blockY, int blockZ) {
return obfuscation.getObfuscationState();
}
});
}
@Override
@@ -163,19 +142,19 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@EventHandler
public void teamJoin(TeamSpawnEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
}
@EventHandler
public void boarding(BoardingEvent e) {
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegions(player)));
e.getFightPlayer().ifPlayer(player -> hiddenRegion.put(player, getHiddenRegion(player)));
}
@EventHandler
public void teamLeave(TeamLeaveEvent e) {
e.getFightPlayer().ifPlayer(player -> {
if (player.isOnline()) {
hiddenRegion.put(player, getHiddenRegions(player));
hiddenRegion.put(player, getHiddenRegion(player));
}
});
}
@@ -196,18 +175,37 @@ public class TechHiderWrapper extends StateDependent implements Listener {
});
}
private Region[] getHiddenRegions(Player player) {
if (Config.isReferee(player)) return NO_REGIONS;
private Region getHiddenRegion(Player player) {
if (Config.isReferee(player)) return Region.EMPTY;
// Single lookup: runs on netty threads, the player can leave the team at any time
FightPlayer fightPlayer = Fight.getFightPlayer(player);
if (fightPlayer == null) {
// Spectators: both teams, the rest of the arena contains nothing critical
return Fight.teams().stream().map(FightTeam::getExtendRegion).toArray(Region[]::new);
FightTeam team = Fight.getPlayerTeam(player);
if (team == null) return Config.ArenaRegion;
if (team.canPlayerEntern(player)) return Region.EMPTY;
return Fight.getOpposite(team).getExtendRegion();
}
private Stream<BlockState> getWaterloggedBlockStates() {
FluidState waterFluidState = Fluids.WATER.getSource(false);
return BuiltInRegistries.BLOCK.stream()
.map((block) -> block.getStateDefinition().getPossibleStates())
.flatMap(Collection::stream)
.filter((blockState -> blockState.getFluidState() == waterFluidState));
}
private Set<BlockState> getBlockStatesToHideFromMaterials(Set<Material> materials) {
Stream<BlockState> allStatesFromMaterials = materials.stream()
.map(CraftMagicNumbers::getBlock)
.map((block) -> block.getStateDefinition().getPossibleStates())
.flatMap(Collection::stream);
if(materials.contains(Material.WATER)) {
return Stream.concat(allStatesFromMaterials, getWaterloggedBlockStates()).collect(Collectors.toUnmodifiableSet());
}
else {
return allStatesFromMaterials.collect(Collectors.toUnmodifiableSet());
}
if (fightPlayer.canEntern()) return NO_REGIONS;
return new Region[]{Fight.getOpposite(fightPlayer.getTeam()).getExtendRegion()};
}
}
-5
View File
@@ -20,11 +20,6 @@
plugins {
`java-library`
alias(libs.plugins.shadow)
deploy
}
deploy {
artifact(tasks.shadowJar)
}
tasks.build {
@@ -1,628 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.hullhider;
import com.comphenix.tinyprotocol.TinyProtocol;
import com.mojang.authlib.GameProfile;
import de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.core.ProtocolWrapper;
import de.steamwar.entity.REntity;
import de.steamwar.techhider.ObfuscationConfig;
import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.SectionPos;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.EmptyBlockGetter;
import net.minecraft.world.level.block.DoorBlock;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.craftbukkit.block.data.CraftBlockData;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.IntStream;
public class Hull {
private static final IntVector[] NO_BRANCHES = new IntVector[0];
private static final int ALL_FACES = 0b111111;
private static final byte UNKNOWN_FACES = -1;
private final Plugin plugin;
private final World world;
@Getter
private final HullRegion region;
private final boolean groundVisible;
private final ObfuscationConfig obfuscation;
private final IntVector[] directions;
private final byte[] fullFaces; // The faces of each block that are fully covered, see face()
private byte[] fullFacesByState; // Cache, per hull as the hidden blocks differ
private final BitSet visibility;
private final HashMap<IntVector, BitSet> visibilityDirections = new HashMap<>(); // The blocks the rays of each direction got inside
private final HashMap<IntVector, IntVector[]> branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into
private final StepBeside[] stepsBeside; // The steps that can lead into or past a block, see uncoverBeside()
private final HashSet<IntVector> uncoveredSurface = new HashSet<>();
private final Viewers viewers;
private final HashSet<Entity> entities = new HashSet<>();
private final HashSet<REntity> rentities = new HashSet<>();
/**
* @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground)
* @param obfuscation the techhider applied to the hull, the blocks it hides occlude as they are replaced by an occluding block
* @param viewers which players may see the hull contents, a new instance for every hull
*/
public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, ObfuscationConfig obfuscation, Viewers viewers) {
this.plugin = plugin;
this.world = world;
this.region = region;
this.groundVisible = groundVisible;
this.obfuscation = obfuscation;
this.viewers = viewers;
this.fullFaces = new byte[region.volume()];
this.visibility = new BitSet(region.volume());
directions = IntStream.range(0, 27)
.mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1))
.toArray(IntVector[]::new);
for (IntVector direction : directions) {
visibilityDirections.put(direction, new BitSet(region.volume()));
branches.put(direction, direction.sqLength() == 1
? Arrays.stream(directions).filter(v -> v.sqLength() >= 2 && dot(v, direction) == 1).toArray(IntVector[]::new)
: NO_BRANCHES);
}
stepsBeside = computeStepsBeside();
}
/**
* @return the bit of the face of a block in the single-axis direction
*/
private static int face(int dx, int dy, int dz) {
if (dx != 0) return dx > 0 ? 1 : 1 << 1;
if (dy != 0) return dy > 0 ? 1 << 2 : 1 << 3;
return dz > 0 ? 1 << 4 : 1 << 5;
}
private int fullFacesOf(BlockState state) {
if (fullFacesByState == null) {
fullFacesByState = new byte[net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY.size()];
Arrays.fill(fullFacesByState, UNKNOWN_FACES);
}
int id = net.minecraft.world.level.block.Block.getId(state);
if (fullFacesByState[id] == UNKNOWN_FACES) {
fullFacesByState[id] = (byte) computeFullFaces(state);
}
return fullFacesByState[id];
}
// The faces that hide what is behind them, mostly as the client culls them
private int computeFullFaces(BlockState state) {
// Hidden blocks are replaced by an occluding block, the same decision as the techhider's
if (obfuscation.hidesBlock(state.getBlock()) || obfuscation.hidesBlockState(state)) return ALL_FACES;
VoxelShape shape;
if (state.canOcclude()) {
shape = state.getOcclusionShape();
} else if (state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) {
// Their panel covers a face, although the client does not cull the faces behind them
shape = state.getShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO);
} else {
return 0;
}
int faces = 0;
for (Direction direction : Direction.values()) {
if (net.minecraft.world.level.block.Block.isFaceFull(shape, direction)) {
faces |= face(direction.getStepX(), direction.getStepY(), direction.getStepZ());
}
}
return faces;
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
return region.contains(x, y, z) && !isViewer(player) && !visibility.get(region.toIndex(x, y, z));
}
public boolean isViewer(Player player) {
return player != null && viewers.isViewer(player);
}
/**
* Adds the player to the {@link Viewers}, which decide whether the player may see the hull contents afterwards.
*/
public void addViewer(Player player) {
boolean wasViewer = isViewer(player);
viewers.addViewer(player);
updateEntities(player, wasViewer);
}
/**
* Removes the player from the {@link Viewers}, which decide whether the player may see the hull contents afterwards.
*/
public void removeViewer(Player player) {
boolean wasViewer = isViewer(player);
viewers.removeViewer(player);
updateEntities(player, wasViewer);
}
private void updateEntities(Player player, boolean wasViewer) {
if (isViewer(player)) {
if (!wasViewer) {
for (Entity entity : entities) {
trackEntity(player, entity);
}
}
} else {
// Also hides the entities from players that joined after they were hidden
for (Entity entity : entities) {
if (!player.equals(entity)) untrackEntity(player, entity);
}
}
// techhider triggers block change sending
}
/**
* Resets the player to the default viewer state, e.g. when leaving.
*/
public void forgetViewer(Player player) {
viewers.removeViewer(player);
}
public void checkEntity(Entity entity) {
Location location = entity.getLocation();
if (region.contains(location) && !visibility.get(new IntVector(location).toId(region))) {
if (entities.add(entity)) {
for (Player player : Bukkit.getOnlinePlayers()) {
if (!isViewer(player) && !player.equals(entity)) untrackEntity(player, entity);
}
}
} else {
if (entities.remove(entity)) {
for (Player player : Bukkit.getOnlinePlayers()) {
if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity);
}
}
}
}
public void trackEntity(Player player, Entity entity) {
if (entity instanceof Player) {
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.REMOVE, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE));
}
player.showEntity(plugin, entity);
}
public void untrackEntity(Player player, Entity entity) {
player.hideEntity(plugin, entity);
if (entity instanceof Player) {
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE));
}
}
public void removeEntity(Entity entity) {
entities.remove(entity);
}
public void checkREntity(REntity entity) {
Location location = new Location(world, entity.getX(), entity.getY(), entity.getZ());
if (region.contains(location) && !visibility.get(new IntVector(location).toId(region))) {
if (rentities.add(entity)) entity.hide(true);
} else {
if (rentities.remove(entity)) entity.hide(false);
}
}
public void removeREntity(REntity entity) {
rentities.remove(entity);
}
/**
* Shows all entities hidden by this hull again, used when the hull is removed.
*/
public void reveal() {
for (Player player : Bukkit.getOnlinePlayers()) {
if (isViewer(player)) continue;
for (Entity entity : entities) {
if (!player.equals(entity)) trackEntity(player, entity);
}
}
entities.clear();
for (REntity entity : rentities) {
entity.hide(false);
}
rentities.clear();
}
/**
* Resends the chunks of this hull the player has loaded, so they are encoded with the current visibility.
*/
public void resendChunks(Player player) {
if (player.getWorld() != world) return;
region.forEachChunk((chunkX, chunkZ) -> {
if (player.isChunkSent(Chunk.getChunkKey(chunkX, chunkZ))) {
CraftbukkitWrapper.sendChunk(player, chunkX, chunkZ);
}
});
}
public void fill(boolean visible) {
visibility.set(0, visibility.size(), visible);
Arrays.fill(fullFaces, (byte) (visible ? 0 : ALL_FACES));
uncoveredSurface.clear();
for (BitSet directionalVisibility : visibilityDirections.values()) {
directionalVisibility.set(0, directionalVisibility.size(), visible);
}
}
public void initialize() {
visibility.clear();
uncoveredSurface.clear();
for (BitSet directionalVisibility : visibilityDirections.values()) {
directionalVisibility.clear();
}
long start = System.currentTimeMillis();
ServerLevel level = ((CraftWorld) world).getHandle();
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
region.forEach((x, y, z) -> fullFaces[region.toIndex(x, y, z)] = (byte) fullFacesOf(level.getBlockState(pos.set(x, y, z))));
forEachBorder((root, direction) -> startRay(new NullList<>(), root, direction));
plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality());
}
public void updateBlockVisibility(Block b, BlockData changedData) {
IntVector root = new IntVector(b.getX(), b.getY(), b.getZ());
if (root.notInRegion(region)) return;
int id = root.toId(region);
// Faces only open up, the hull never hides anything again
int faces = fullFaces[id] & fullFacesOf(((CraftBlockData) changedData).getState());
if (faces == fullFaces[id]) return;
fullFaces[id] = (byte) faces;
List<IntVector> uncovered = new ArrayList<>();
// Rays inside the block that could not leave it
for (Map.Entry<IntVector, BitSet> directionalVisibility : visibilityDirections.entrySet()) {
if (directionalVisibility.getValue().get(id)) {
directionalVisibility.getValue().clear(id);
uncoverBlocks(uncovered, root, directionalVisibility.getKey());
}
}
// Rays that could not get into the block or past it
uncoverBeside(uncovered, root);
forEachBorderStart(root, direction -> startRay(uncovered, root, direction));
uncoveredSurface.addAll(uncovered);
uncoveredSurface.remove(root);
}
/**
* Continues the rays inside the blocks in front of the block, or of which a diagonal ray moves past the block.
*/
private void uncoverBeside(List<IntVector> uncovered, IntVector root) {
for (StepBeside step : stepsBeside) {
int x = root.x - step.part.x;
int y = root.y - step.part.y;
int z = root.z - step.part.z;
if (!region.contains(x, y, z)) continue;
int id = region.toIndex(x, y, z);
for (BitSet ray : step.rays) {
if (ray.get(id)) {
uncoverNext(uncovered, new IntVector(x, y, z), step.direction, step.directionalVisibility, step.branchDirections);
break;
}
}
}
}
/**
* The block lies in front of or beside the start of a step if it is the start moved along some or all axes of the direction.
* The rays of the direction and the rays along a single axis branching into it take the step.
*/
private StepBeside[] computeStepsBeside() {
List<StepBeside> steps = new ArrayList<>();
for (IntVector direction : directions) {
if (direction.sqLength() == 0) continue;
BitSet[] rays = Arrays.stream(directions)
.filter(ray -> ray.equals(direction) || ray.sqLength() == 1 && direction.sqLength() > 1 && dot(ray, direction) == 1)
.map(visibilityDirections::get)
.toArray(BitSet[]::new);
for (IntVector part : directions) {
if (part.sqLength() > 0 && isPartOf(part, direction)) {
steps.add(new StepBeside(direction, part, visibilityDirections.get(direction), branches.get(direction), rays));
}
}
}
return steps.toArray(new StepBeside[0]);
}
private record StepBeside(IntVector direction, IntVector part, BitSet directionalVisibility, IntVector[] branchDirections, BitSet[] rays) {
}
private static boolean isPartOf(IntVector part, IntVector direction) {
return (part.x == 0 || part.x == direction.x)
&& (part.y == 0 || part.y == direction.y)
&& (part.z == 0 || part.z == direction.z);
}
private static int dot(IntVector a, IntVector b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
public void sendUncoveredBlocks() {
Map<IntVector, List<IntVector>> sectionWise = new HashMap<>();
for (IntVector uncovered : uncoveredSurface) {
sectionWise.computeIfAbsent(new IntVector(uncovered.x >> 4, uncovered.y >> 4, uncovered.z >> 4), section -> new ArrayList<>()).add(uncovered);
}
uncoveredSurface.clear();
for (Map.Entry<IntVector, List<IntVector>> entry : sectionWise.entrySet()) {
Object result;
List<IntVector> changes = entry.getValue();
Object[] blockdata = new Object[changes.size()];
for (int i = 0; i < blockdata.length; i++) {
IntVector change = changes.get(i);
blockdata[i] = ((CraftBlockData) world.getBlockData(change.getX(), change.getY(), change.getZ())).getState();
}
if (changes.size() > 1) {
IntVector section = changes.get(0);
section = new IntVector(section.getX() >> 4, section.getY() >> 4, section.getZ() >> 4);
int xOffset = 16 * section.getX();
int yOffset = 16 * section.getY();
int zOffset = 16 * section.getZ();
short[] pos = new short[changes.size()];
for (int i = 0; i < changes.size(); i++) {
IntVector change = changes.get(i);
pos[i] = (short) (((change.getX() - xOffset) << 8) + ((change.getZ() - zOffset) << 4) + (change.getY() - yOffset));
}
result = new ClientboundSectionBlocksUpdatePacket(SectionPos.of(section.getX(), section.getY(), section.getZ()), new Short2ObjectArrayMap<>(pos, blockdata, blockdata.length));
} else {
IntVector pos = changes.get(0);
result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]);
}
Object packet = result;
Bukkit.getOnlinePlayers().stream().filter(player -> !isViewer(player)).forEach(player -> TinyProtocol.instance.sendPacket(player, packet));
}
}
// The rays forEachBorder starts at the block
private void forEachBorderStart(IntVector block, Consumer<IntVector> f) {
if (groundVisible && block.y == region.getMinY()) f.accept(new IntVector(0, 1, 0));
if (block.y == region.getMaxY() - 1) f.accept(new IntVector(0, -1, 0));
if (block.z == region.getMinZ()) f.accept(new IntVector(0, 0, 1));
if (block.z == region.getMaxZ() - 1) f.accept(new IntVector(0, 0, -1));
if (block.x == region.getMinX()) f.accept(new IntVector(1, 0, 0));
if (block.x == region.getMaxX() - 1) f.accept(new IntVector(-1, 0, 0));
}
private void forEachBorder(BiConsumer<IntVector, IntVector> f) {
for (int x = region.getMinX(); x < region.getMaxX(); x++) {
for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
if (groundVisible) {
f.accept(new IntVector(x, region.getMinY(), z), new IntVector(0, 1, 0));
}
f.accept(new IntVector(x, region.getMaxY() - 1, z), new IntVector(0, -1, 0));
}
}
for (int x = region.getMinX(); x < region.getMaxX(); x++) {
for (int y = region.getMinY(); y < region.getMaxY(); y++) {
f.accept(new IntVector(x, y, region.getMinZ()), new IntVector(0, 0, 1));
f.accept(new IntVector(x, y, region.getMaxZ() - 1), new IntVector(0, 0, -1));
}
}
for (int z = region.getMinZ(); z < region.getMaxZ(); z++) {
for (int y = region.getMinY(); y < region.getMaxY(); y++) {
f.accept(new IntVector(region.getMinX(), y, z), new IntVector(1, 0, 0));
f.accept(new IntVector(region.getMaxX() - 1, y, z), new IntVector(-1, 0, 0));
}
}
}
/**
* Starts a ray at the border of the region, coming from outside.
*/
private void startRay(List<IntVector> uncovered, IntVector block, IntVector direction) {
if (faceFull(block.x, block.y, block.z, -direction.x, -direction.y, -direction.z)) {
reveal(uncovered, block, block.toId(region));
} else {
uncoverBlocks(uncovered, block, direction);
}
}
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction) {
uncoverBlocks(uncovered, block, direction, visibilityDirections.get(direction), branches.get(direction));
}
/**
* Continues a ray that got inside the block.
*/
private void uncoverBlocks(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
if (block.notInRegion(region)) return;
int id = block.toId(region);
if (directionalVisibility.get(id)) return;
directionalVisibility.set(id);
reveal(uncovered, block, id);
uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections);
for (IntVector branchDirection : branchDirections) {
uncoverNext(uncovered, block, branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES);
}
}
private void uncoverNext(List<IntVector> uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) {
int x = block.x + direction.x;
int y = block.y + direction.y;
int z = block.z + direction.z;
// The cheap checks first: most blocks were already reached from this direction
if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z))) return;
if (canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, true)) {
uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections);
return;
}
// The ray might still reach the face of the next block, mostly it is visible already
int id = region.toIndex(x, y, z);
if (!visibility.get(id) && canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, false)) {
reveal(uncovered, new IntVector(x, y, z), id);
}
}
private void reveal(List<IntVector> uncovered, IntVector block, int id) {
if (!visibility.get(id)) {
visibility.set(id);
uncovered.add(block);
}
}
/**
* A ray moves along the single axes of its direction one after another, in any order that does not pass a full face.
* So a diagonal ray does not pass between blocks that only touch at an edge or corner.
*
* @param enter whether the ray has to get inside the target block, otherwise reaching its face suffices
*/
private boolean canMove(int x, int y, int z, int dx, int dy, int dz, boolean enter) {
if (dx == 0 && dy == 0 && dz == 0) return true;
// The blocks in between have to be entered
return (dx != 0 && canCross(x, y, z, dx, 0, 0, enter || dy != 0 || dz != 0) && canMove(x + dx, y, z, 0, dy, dz, enter))
|| (dy != 0 && canCross(x, y, z, 0, dy, 0, enter || dx != 0 || dz != 0) && canMove(x, y + dy, z, dx, 0, dz, enter))
|| (dz != 0 && canCross(x, y, z, 0, 0, dz, enter || dx != 0 || dy != 0) && canMove(x, y, z + dz, dx, dy, 0, enter));
}
/**
* @return whether a ray inside the block can leave it to the neighbouring block in the single-axis direction (and enter it)
*/
private boolean canCross(int x, int y, int z, int dx, int dy, int dz, boolean enter) {
return !faceFull(x, y, z, dx, dy, dz) && (!enter || !faceFull(x + dx, y + dy, z + dz, -dx, -dy, -dz));
}
private boolean faceFull(int x, int y, int z, int dx, int dy, int dz) {
return (fullFaces[region.toIndex(x, y, z)] & face(dx, dy, dz)) != 0;
}
@Getter
@AllArgsConstructor
public static class IntVector {
private final int x;
private final int y;
private final int z;
public IntVector(Location location) {
this.x = location.getBlockX();
this.y = location.getBlockY();
this.z = location.getBlockZ();
}
public boolean notInRegion(HullRegion region) {
return !region.contains(x, y, z);
}
public int toId(HullRegion region) {
return region.toIndex(x, y, z);
}
public IntVector add(int x, int y, int z) {
return new IntVector(this.x + x, this.y + y, this.z + z);
}
public IntVector add(IntVector v) {
return add(v.x, v.y, v.z);
}
public int sqLength() {
return x * x + y * y + z * z;
}
@Override
public int hashCode() {
return y << 24 ^ x << 12 ^ z;
}
@Override
public boolean equals(Object o) {
if (o == null || this.getClass() != o.getClass()) {
return false;
}
IntVector v = (IntVector) o;
return x == v.x && y == v.y && z == v.z;
}
@Override
public String toString() {
return x + "," + y + "," + z;
}
}
private static class NullList<E> extends AbstractList<E> {
@Override
public void add(int index, E element) {
// Straight to /dev/null!
}
@Override
public E get(int index) {
return null;
}
@Override
public int size() {
return 0;
}
}
}
@@ -1,211 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.hullhider;
import de.steamwar.entity.REntity;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
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.BlockPhysicsEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntitySpawnEvent;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
/**
* Manages one {@link Hull} per key (e.g. a team or a region).
* Which players may view which hull, when hulls are initialised and when this listener is registered is up to the user.
*/
public class HullHider<K> implements Listener {
private final boolean hideEntities;
private final Map<K, Hull> hullMap = new LinkedHashMap<>();
// Read on netty threads, replaced on every change
private volatile Hull[] hulls = new Hull[0];
/**
* @param hideEntities whether entities and REntities inside the hidden part of a hull are hidden from non-viewers as well
*/
public HullHider(boolean hideEntities) {
this.hideEntities = hideEntities;
}
/**
* The hull hides everything from non-viewers until it is initialised.
*/
public void addHull(K key, Hull hull) {
hullMap.put(key, hull);
hulls = hullMap.values().toArray(new Hull[0]);
}
/**
* Stops hiding the hull: shows the entities it hid and resends its chunks to everybody who could not view it.
*/
public void removeHull(K key) {
Hull hull = hullMap.remove(key);
if (hull == null) return;
hulls = hullMap.values().toArray(new Hull[0]);
hull.reveal();
for (Player player : Bukkit.getOnlinePlayers()) {
if (!hull.isViewer(player)) {
hull.resendChunks(player);
}
}
}
public Hull getHull(K key) {
return hullMap.get(key);
}
public void forEach(BiConsumer<K, Hull> consumer) {
hullMap.forEach(consumer);
}
public void initialize(K key) {
Hull hull = hullMap.get(key);
hull.initialize();
// Chunks encoded while the hull was filled as hidden contain obfuscated blocks that are visible now, but no block update is generated for them
for (Player player : Bukkit.getOnlinePlayers()) {
if (!hull.isViewer(player)) {
hull.resendChunks(player);
}
}
}
/**
* Has to be called after the player became a viewer of the hull or stopped being one.
*/
public void resendChunks(Player player, K key) {
hullMap.get(key).resendChunks(player);
}
public void fill(K key, boolean visible) {
hullMap.get(key).fill(visible);
}
public void fillAll(boolean visible) {
for (Hull hull : hulls) {
hull.fill(visible);
}
}
/**
* @see Viewers
*/
public void addViewer(Player player, K key) {
hullMap.get(key).addViewer(player);
}
/**
* @see Viewers
*/
public void removeViewer(Player player, K key) {
hullMap.get(key).removeViewer(player);
}
public void forgetViewer(Player player) {
for (Hull hull : hulls) {
hull.forgetViewer(player);
}
}
public boolean isBlockHidden(Player player, int x, int y, int z) {
for (Hull hull : hulls) {
if (hull.isBlockHidden(player, x, y, z)) {
return true;
}
}
return false;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBlockPhysic(BlockPhysicsEvent e) {
if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) {
blockUpdate(e.getBlock(), e.getChangedBlockData());
}
}
public void blockUpdate(Block block, BlockData changedData) {
for (Hull hull : hulls) {
hull.updateBlockVisibility(block, changedData);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onSpawn(EntitySpawnEvent e) {
checkEntity(e.getEntity());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onDeath(EntityDeathEvent e) {
if (!hideEntities) return;
for (Hull hull : hulls) {
hull.removeEntity(e.getEntity());
}
}
/**
* Has to be called every tick for every entity, as entities can move into or out of the hidden part of a hull.
*/
public void checkEntity(Entity entity) {
if (!hideEntities) return;
for (Hull hull : hulls) {
hull.checkEntity(entity);
}
}
/**
* Has to be called every tick after the entities have been checked.
*/
public void sendUncoveredBlocks() {
for (Hull hull : hulls) {
hull.sendUncoveredBlocks();
}
}
public void updateREntity(REntity e) {
if (!hideEntities) return;
for (Hull hull : hulls) {
hull.checkREntity(e);
}
}
public void despawnREntity(REntity e) {
if (!hideEntities) return;
for (Hull hull : hulls) {
hull.removeREntity(e);
}
}
}
@@ -1,113 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.hullhider;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Location;
/**
* Immutable block box a {@link Hull} covers. The minimum is inclusive, the maximum exclusive.
*/
@Getter
@AllArgsConstructor
public final class HullRegion {
public static HullRegion fromSize(int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ) {
return new HullRegion(minX, minY, minZ, minX + sizeX, minY + sizeY, minZ + sizeZ);
}
public static HullRegion fromInclusive(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
return new HullRegion(minX, minY, minZ, maxX + 1, maxY + 1, maxZ + 1);
}
private final int minX;
private final int minY;
private final int minZ;
private final int maxX;
private final int maxY;
private final int maxZ;
public int getSizeX() {
return maxX - minX;
}
public int getSizeY() {
return maxY - minY;
}
public int getSizeZ() {
return maxZ - minZ;
}
public int volume() {
return getSizeX() * getSizeY() * getSizeZ();
}
public boolean contains(int x, int y, int z) {
return minX <= x && x < maxX && minY <= y && y < maxY && minZ <= z && z < maxZ;
}
public boolean contains(Location location) {
return contains(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
public boolean chunkOutside(int chunkX, int chunkZ) {
return chunkX < (minX >> 4) || chunkX > ((maxX - 1) >> 4) || chunkZ < (minZ >> 4) || chunkZ > ((maxZ - 1) >> 4);
}
/**
* @return the index of the block within a bitset of {@link #volume()} bits, only valid for contained blocks
*/
public int toIndex(int x, int y, int z) {
return ((y - minY) * getSizeZ() + (z - minZ)) * getSizeX() + (x - minX);
}
public void forEach(BlockConsumer executor) {
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
executor.accept(x, y, z);
}
}
}
}
public void forEachChunk(ChunkConsumer executor) {
for (int chunkX = minX >> 4; chunkX <= (maxX - 1) >> 4; chunkX++) {
for (int chunkZ = minZ >> 4; chunkZ <= (maxZ - 1) >> 4; chunkZ++) {
executor.accept(chunkX, chunkZ);
}
}
}
public interface BlockConsumer {
void accept(int x, int y, int z);
}
public interface ChunkConsumer {
void accept(int chunkX, int chunkZ);
}
@Override
public String toString() {
return minX + "," + minY + "," + minZ + "->" + maxX + "," + maxY + "," + maxZ;
}
}
@@ -1,75 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.hullhider;
import org.bukkit.entity.Player;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* The players of a {@link Hull}. Whether an added player may see the hull contents or sees the hull blocks instead is decided by {@link #isViewer(Player)}.
* Stores the players, so every hull needs its own instance.
*/
public abstract sealed class Viewers {
// Concurrent as it is read on netty threads
protected final Set<UUID> players = ConcurrentHashMap.newKeySet();
/**
* @return whether the player may see the hull contents instead of the hull blocks
*/
public abstract boolean isViewer(Player player);
/**
* @return whether the player was not added before
*/
public boolean addViewer(Player player) {
return players.add(player.getUniqueId());
}
/**
* @return whether the player was added before
*/
public boolean removeViewer(Player player) {
return players.remove(player.getUniqueId());
}
/**
* Hidden from everybody, only added players may see the hull contents.
*/
public static final class HiddenByDefault extends Viewers {
@Override
public boolean isViewer(Player player) {
return players.contains(player.getUniqueId());
}
}
/**
* Visible to everybody, only added players see the hull blocks instead of the hull contents.
*/
public static final class VisibleByDefault extends Viewers {
@Override
public boolean isViewer(Player player) {
return !players.contains(player.getUniqueId());
}
}
}
@@ -1,69 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.techhider;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
public interface AccessPrivilegeProvider {
/**
* Resolves everything that can change concurrently about this player once.
* The result is used for all decisions of one outgoing packet (including all packets of a bundle).
*/
ForPlayer forPlayer(Player p);
boolean isPlayerPrivilegedToPerformAction(Player p);
/**
* What a single player may access, valid for the processing of one outgoing packet.
*/
interface ForPlayer {
Player getPlayer();
/**
* @return true if nothing within this chunk is restricted for this player, so the chunk may be sent unmodified
*/
boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ);
boolean canAccessPosition(int blockX, int blockY, int blockZ);
boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block);
boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState);
boolean canAccessEntity(int entityId);
/**
* @return whether the player may see the entity spawning at this position
*/
default boolean canAccessEntitySpawn(int entityId, int blockX, int blockY, int blockZ) {
return canAccessEntity(entityId) && canAccessPosition(blockX, blockY, blockZ);
}
boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type);
/**
* @return the block state sent instead of a block this player may not access at this position
*/
BlockState getObfuscation(int blockX, int blockY, int blockZ);
}
}
@@ -1,115 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.techhider;
import de.steamwar.sql.GameModeConfig;
import lombok.Getter;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.material.Fluids;
import org.bukkit.Material;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Which blocks, block states and block entities to hide, and the block to replace them with.
*/
@Getter
public final class ObfuscationConfig {
public static ObfuscationConfig of(GameModeConfig.TechhiderConfig<Material> config) {
return new ObfuscationConfig(config.ObfuscateWith, config.HiddenBlocks, config.HiddenBlockEntities);
}
private final Block obfuscateWith;
private final BlockState obfuscationState;
private final Set<Block> blocks;
private final Set<BlockState> blockStates;
private final Set<BlockEntityType<?>> blockEntityTypes;
/**
* @param hiddenBlocks "water" also hides all waterlogged block states
* @param hiddenBlockEntities block entity type ids, e.g. "minecraft:sign"
*/
public ObfuscationConfig(Material obfuscateWith, Set<Material> hiddenBlocks, Set<String> hiddenBlockEntities) {
this.obfuscateWith = CraftMagicNumbers.getBlock(obfuscateWith);
this.obfuscationState = this.obfuscateWith.defaultBlockState();
this.blocks = hiddenBlocks.stream()
.map(CraftMagicNumbers::getBlock)
.collect(Collectors.toUnmodifiableSet());
this.blockStates = getBlockStatesToHideFromMaterials(hiddenBlocks);
this.blockEntityTypes = hiddenBlockEntities.stream()
.map(id -> {
ResourceLocation loc = ResourceLocation.parse(id);
return BuiltInRegistries.BLOCK_ENTITY_TYPE.get(loc).orElse(null);
})
.filter(Objects::nonNull)
.map(Holder.Reference::value)
.collect(Collectors.toUnmodifiableSet());
}
public boolean hidesBlock(Block block) {
return blocks.contains(block);
}
public boolean hidesBlockState(BlockState blockState) {
return blockStates.contains(blockState);
}
public boolean hidesBlockEntity(BlockEntityType<?> type) {
return blockEntityTypes.contains(type);
}
private static Stream<BlockState> getWaterloggedBlockStates() {
FluidState waterFluidState = Fluids.WATER.getSource(false);
return BuiltInRegistries.BLOCK.stream()
.map((block) -> block.getStateDefinition().getPossibleStates())
.flatMap(Collection::stream)
.filter((blockState -> blockState.getFluidState() == waterFluidState));
}
private static Set<BlockState> getBlockStatesToHideFromMaterials(Set<Material> materials) {
Stream<BlockState> allStatesFromMaterials = materials.stream()
.map(CraftMagicNumbers::getBlock)
.map((block) -> block.getStateDefinition().getPossibleStates())
.flatMap(Collection::stream);
if(materials.contains(Material.WATER)) {
return Stream.concat(allStatesFromMaterials, getWaterloggedBlockStates()).collect(Collectors.toUnmodifiableSet());
}
else {
return allStatesFromMaterials.collect(Collectors.toUnmodifiableSet());
}
}
}
-28
View File
@@ -1,28 +0,0 @@
accessWidener v2 named
# For TechHider
accessible field net/minecraft/network/protocol/game/ClientboundEntityEventPacket entityId I
accessible field net/minecraft/network/protocol/game/ClientboundMoveEntityPacket entityId I
accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket sectionPos Lnet/minecraft/core/SectionPos;
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState;
# For ChunkHider
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData;
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData;
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List;
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List;
accessible class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo type Lnet/minecraft/world/level/block/entity/BlockEntityType;
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I
accessible method net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo <init> (IILnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V
-5
View File
@@ -22,11 +22,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
steamwar.kotlin
alias(libs.plugins.shadow)
deploy
}
deploy {
artifact(tasks.shadowJar)
}
tasks.build {
-5
View File
@@ -20,11 +20,6 @@
plugins {
steamwar.java
widener
deploy
}
deploy {
artifact(tasks.jar)
}
dependencies {
@@ -49,76 +49,341 @@ PORTAL_NO_WORLDEDIT_SELECTION = §cNo WorldEdit selection
PARTICLE_INVENTORY = §0Particle
PARTICLE_DESELECT = §8No particle
PARTICLE_SHOW_HAS_ALL = §8You have all particles
PARTICLE_SHOW_ALL = §aShow all
PARTICLE_SHOW_UNLOCKED = §cShow unlocked
PARTICLE_UNLOCKED_BY = §eUnlocked by
PARTICLE_UNLOCKED_BY_TEAM = §fJoin a team
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Easter Egg Hunt)
PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Easter Egg Hunt)
PARTICLE_UNLOCKED_BY_EVENT = §fEvent participation
PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServer Team
PARTICLE_UNLOCKED_BY_EVENT = §fEvent participation
PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. or 3. Place
PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0}
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFind 45 Easter Eggs
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fFind all Easter Eggs with difficulty {0}
PARTICLE_ATTRIBUTE = §eAttributes§7:
PARTICLE_ATTRIBUTE_CIRCLE = §8-§f Ring
PARTICLE_ATTRIBUTE_BI_CIRCLE = §8-§f Double ring
PARTICLE_ATTRIBUTE_TRI_CIRCLE = §8-§f Tripple ring
PARTICLE_ATTRIBUTE_CLOUD = §8-§f Cloud
PARTICLE_ATTRIBUTE_TICK = §8-§f Always active
PARTICLE_ATTRIBUTE_NON_FLOOR = §8-§f In air
PARTICLE_ATTRIBUTE_FLOOR = §8-§f On ground
PARTICLE_ATTRIBUTE_NON_FLYING = §8-§f Not flying
PARTICLE_ATTRIBUTE_FLYING = §8-§f Flying
PARTICLE_ATTRIBUTE_WING = §8-§f Wings
PARTICLE_ATTRIBUTE_SNEAKING = §8-§f Sneaking
PARTICLE_ATTRIBUTE_NON_MOVING = §8-§f Not moving
PARTICLE_ATTRIBUTE_SEPARATOR = §f
PARTICLE_SELECT = §eClick to select
PARTICLE_LOCKED = §c§lLocked
PARTICLE_DEACTIVATED = §cThere are too many players online, particles are deactivated
PARTICLE_SNEEZE = §aSneeze
PARTICLE_SMOKE = §7Smoke
PARTICLE_FIRE = §cFire
PARTICLE_WATER = §bWater
PARTICLE_HEART = §cHearts
PARTICLE_NOTES = §eNotes
PARTICLE_NAUTILUS = §aNautilus
PARTICLE_SNOWBALL = §fSnowball
PARTICLE_EFFECT = §5Effect
PARTICLE_CAMPFIRE = §7Smoke
PARTICLE_MAGIC = §5Magic
PARTICLE_ANGRY = §4Angry
PARTICLE_SLIME = §aSlime
PARTICLE_MOB = §7Mob
PARTICLE_SQUID = §8Squid
PARTICLE_BUBBLE = §aBubbles
PARTICLE_HONEY = §6Honey
PARTICLE_NECTAR = §6Nectar
PARTICLE_FIREWORK = §7Firework
PARTICLE_DRAGON_BREATH = §5Dragon breath
PARTICLE_DAMAGE = §5Damage
PARTICLE_DOLPHIN = §dDolphin
PARTICLE_WITCH = §5Witch
PARTICLE_ENCHANTING = §eEnchantment
PARTICLE_HAPPY = §2Joy
PARTICLE_FLAME = §7Flames
PARTICLE_END_ROD = §fEnd Rod
PARTICLE_CLOUD = §7Cloud
PARTICLE_TOTEM = §aTotem
PARTICLE_TOWN = §5Town
PARTICLE_ENCHANTING_CIRCLE = §fEnchanted
PARTICLE_WATER_FIRE = §bWater§7/§cFire
PARTICLE_MAGIC_ENCHANTING = §5Magic/Enchantment
PARTICLE_WINGS_EVIL = §5Purple wings
PARTICLE_BACK = §7Back
PARTICLE_SLOT_OPEN = §eClick to open
PARTICLE_SLOT_CLICK_USE = §eClick to use
PARTICLE_SLOT_CLICK_STOP = §eClick to stop using
PARTICLE_SLOT_SHIFT_EDIT = §eShift click to edit
PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SHIFT_TO_DELETE = §7Shift click to delete
PARTICLE_SECTION_DESIGN = §6§lDesign
PARTICLE_SECTION_PARTICLE = §6§lParticle
PARTICLE_SECTION_SHAPE = §6§lShape
PARTICLE_SECTION_TIMING = §6§lTiming and position
PARTICLE_SECTION_CONDITIONS = §6§lConditions
PARTICLE_FIXED_LOOK = §7This design always looks the same
PARTICLE_VALUE_CLICK = §eClick to change
PARTICLE_VALUE_TOGGLE = §eClick to toggle
PARTICLE_VALUE_CYCLE = §eLeft click§8: §7next, §eright click§8: §7previous
PARTICLE_VALUE_SELECTED = §aSelected
PARTICLE_VALUE_RESET = §eReset to {0}
PARTICLE_NUMBER_RANGE = §7Range§8: §e{0} §8- §e{1}
PARTICLE_NUMBER_TYPE = §eClick to type a value
PARTICLE_NUMBER_RESULT = §7→ §e{0}
PARTICLE_NUMBER_AT_MAX = §7→ §e{0} §8(max)
PARTICLE_NUMBER_AT_MIN = §7→ §e{0} §8(min)
PARTICLE_COLOR_EDITOR = §0Colours
PARTICLE_COLOR_REMOVE = §eClick to remove
PARTICLE_COLOR_ADD = §aAdd colour
PARTICLE_COLOR_HEX = §eEnter hex code
PARTICLE_COLOR_HEX_EXAMPLE = §7For example #FF8800
PARTICLE_COLOR_HEX_TITLE = Hex colour
PARTICLE_COLOR_HEX_INVALID = §cThat is not a hex colour like #FF8800
PARTICLE_COLOR_RANDOM = §eUse random colours
PARTICLE_SLOT_EMPTY = §7Empty slot
PARTICLE_SLOT_CREATE = §eClick to create
PARTICLE_SLOT_ENTRIES = §7{0} entries
PARTICLE_SLOT_ACTIVE = §aActive
PARTICLE_SLOT_VIEW = §eClick to view
PARTICLE_SLOT_ADD_ENTRY = §aAdd entry
PARTICLE_SLOT_MAX_ENTRIES = §cThis slot is full
PARTICLE_SLOT_OVER_BUDGET = §cThat would spawn too many particles for this slot
PARTICLE_SLOT_RENAME = §eRename
PARTICLE_SLOT_RENAME_TITLE = Slot name
PARTICLE_SLOT_ICON = §eChange icon
PARTICLE_SLOT_ICON_TITLE = §0Slot icon
PARTICLE_SLOT_DELETE = §cDelete slot
PARTICLE_SLOT_COST = §7Particles per tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Particle§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign locked, not shown
PARTICLE_ENTRY_COST = §7Particles per tick§8: §e{0}
PARTICLE_ENTRY_COST_SHARE = §7This entry§8: §e{0}
PARTICLE_ENTRY_EDIT = §eClick to edit
PARTICLE_ENTRY_DELETE = §cDelete entry
PARTICLE_DESIGN_PICKER = §0Choose design
PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Choose colour
PARTICLE_VALUE = §7Value§8: {0}
PARTICLE_VALUE_RANDOM = §7Random colours
PARTICLE_VALUE_YES = §aYes
PARTICLE_VALUE_NO = §cNo
PARTICLE_DESIGN_SIMPLE = §fSimple
PARTICLE_DESIGN_CIRCLE = §fCircle
PARTICLE_DESIGN_WING = §fWings
PARTICLE_DESIGN_CLOUD = §fCloud
PARTICLE_DESIGN_BURST = §fBurst
PARTICLE_DESIGN_FOOTPRINTS = §fFootprints
PARTICLE_DESIGN_TEAM_LINK = §fTeam link
PARTICLE_DESIGN_PULSE_SHIMMER = §fShimmer
PARTICLE_DESIGN_HEART_BEAT = §fHeartbeat
PARTICLE_PARAM_PARTICLE = §fParticle
PARTICLE_PARAM_GLYPH_FLIGHT = §fGlyph flight
PARTICLE_PARAM_BURST_TRIGGER = §fWhen
PARTICLE_PARAM_BURST_SHAPE = §fShape
PARTICLE_PARAM_BURST_RADIUS = §fRing radius
PARTICLE_PARAM_BURST_FALL = §fMinimum fall height
PARTICLE_PARAM_FOOTPRINT_LIFETIME = §fPrint lifetime (ticks)
PARTICLE_PARAM_FOOTPRINT_STEP = §fDistance between prints
PARTICLE_PARAM_LINK_SPACING = §fDistance between particles
PARTICLE_PARAM_PULSE_SPEED = §fPulse duration (ticks)
PARTICLE_PARAM_PULSE_ROTATION = §fRotation
PARTICLE_PARAM_HEART_BEAT_SPEED = §fBeat duration (ticks)
PARTICLE_PARAM_COUNT = §fAmount
PARTICLE_PARAM_SPREAD_X = §fSpread X
PARTICLE_PARAM_SPREAD_Y = §fSpread Y
PARTICLE_PARAM_SPREAD_Z = §fSpread Z
PARTICLE_PARAM_SPEED = §fSpeed
PARTICLE_PARAM_COLORS = §fColours
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fTrail duration (ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fTrail target X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fTrail target Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fTrail target Z
PARTICLE_PARAM_OFFSET_X = §fOffset X
PARTICLE_PARAM_OFFSET_Y = §fOffset Y
PARTICLE_PARAM_OFFSET_Z = §fOffset Z
PARTICLE_PARAM_INTERVAL = §fEvery n ticks
PARTICLE_PARAM_PLACEMENT = §fRelative to
PARTICLE_PARAM_TICK_MODE = §fShown
PARTICLE_PARAM_MOVEMENT = §fMovement
PARTICLE_PARAM_GLIDING = §fElytra
PARTICLE_PARAM_GROUND = §fGround
PARTICLE_PARAM_SNEAK = §fSneaking
PARTICLE_PARAM_VISIBILITY = §fVisible to
PARTICLE_PARAM_RADIUS = §fRadius
PARTICLE_PARAM_CIRCLE_SPEED = §fRotation speed
PARTICLE_PARAM_PHASE = §fStart angle
PARTICLE_PARAM_RISE = §fUp and down height
PARTICLE_PARAM_RISE_SPEED = §fUp and down speed
PARTICLE_PARAM_BOUNCE = §fBounce back
PARTICLE_PARAM_INVERTED = §fStart at the top
PARTICLE_PARAM_WING_IMAGE = §fWing shape
PARTICLE_PARAM_WING_SIZE = §fWing size
PARTICLE_PARAM_WING_DETAIL = §fPoints per wing
PARTICLE_PARAM_WING_COLORING = §fColour mode
PARTICLE_PARAM_WING_SPARKLE = §fSparkle (points shown, %)
PARTICLE_PARAM_WING_MOTION = §fMotion
PARTICLE_CHOICE_TICKMODE_MOVE = §fWhile moving
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fAlways
PARTICLE_CHOICE_PLACEMENT_WORLD = §fWorld (X east, Y up, Z south)
PARTICLE_CHOICE_PLACEMENT_BODY = §fBody (X right, Y up, Z forward)
PARTICLE_CHOICE_MOVEMENT_ANY = §fAny movement
PARTICLE_CHOICE_MOVEMENT_MOVING = §fOnly while moving
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fOnly while standing still
PARTICLE_CHOICE_GLIDING_ANY = §fWith or without elytra
PARTICLE_CHOICE_GLIDING_GLIDING = §fOnly while gliding
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNot while gliding
PARTICLE_CHOICE_GROUND_ANY = §fOn ground and in air
PARTICLE_CHOICE_GROUND_ON_GROUND = §fOnly on ground
PARTICLE_CHOICE_GROUND_IN_AIR = §fOnly in air
PARTICLE_CHOICE_SNEAK_ANY = §fSneaking or not
PARTICLE_CHOICE_SNEAK_SNEAKING = §fOnly while sneaking
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNot while sneaking
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fEveryone
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fOnly me
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fOnly others
PARTICLE_CHOICE_IMAGE_SIMPLE = §fSimple
PARTICLE_CHOICE_IMAGE_COMPLEX = §fComplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSword
PARTICLE_CHOICE_IMAGE_BAT = §fBat
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fButterfly
PARTICLE_CHOICE_IMAGE_FAIRY = §fFairy
PARTICLE_CHOICE_IMAGE_MWGL = §fMiniWarGear League
PARTICLE_CHOICE_IMAGE_SW = §fSteamWar
PARTICLE_CHOICE_IMAGE_WGS = §fWarGear Season
PARTICLE_CHOICE_IMAGE_SWORD_CROSSED = §fCrossed Swords
PARTICLE_CHOICE_COLORING_OVER_TIME = §fChange over time
PARTICLE_CHOICE_COLORING_ALONG_WING = §fFrom the back to the tips
PARTICLE_CHOICE_MOTION_NONE = §fNone
PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fFlap slowly
PARTICLE_CHOICE_MOTION_FLAP_FAST = §fFlap quickly
PARTICLE_CHOICE_MOTION_BEAT_ON_JUMP = §fBeat on jumping
PARTICLE_CHOICE_TRIGGER_JUMP = §fOn jumping
PARTICLE_CHOICE_TRIGGER_LAND = §fOn landing
PARTICLE_CHOICE_TRIGGER_SNEAK = §fOn sneaking
PARTICLE_CHOICE_BURSTSHAPE_PUFF = §fPuff
PARTICLE_CHOICE_BURSTSHAPE_RING = §fRing
PARTICLE_CHOICE_GLYPHFLIGHT_NORMAL = §fFloat down
PARTICLE_CHOICE_GLYPHFLIGHT_INTO_PLAYER = §fInto the player
PARTICLE_CHOICE_GLYPHFLIGHT_OUT_OF_PLAYER = §fOut of the player
# Particle types
PARTICLE_TYPE_DUST = §fDust
PARTICLE_TYPE_DUST_COLOR_TRANSITION = §fColour fading dust
PARTICLE_TYPE_ENTITY_EFFECT = §fPotion swirl
PARTICLE_TYPE_TINTED_LEAVES = §fTinted leaves
PARTICLE_TYPE_BLOCK = §fBlock breaking
PARTICLE_TYPE_BLOCK_CRUMBLE = §fBlock crumbling
PARTICLE_TYPE_BLOCK_MARKER = §fBlock marker
PARTICLE_TYPE_DUST_PILLAR = §fDust pillar
PARTICLE_TYPE_FALLING_DUST = §fFalling dust
PARTICLE_TYPE_ITEM = §fItem breaking
PARTICLE_TYPE_TRAIL = §fTrail
PARTICLE_TYPE_FLAME = §fFlame
PARTICLE_TYPE_SMALL_FLAME = §fSmall flame
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSoul fire flame
PARTICLE_TYPE_SOUL = §fSoul
PARTICLE_TYPE_LAVA = §fLava pop
PARTICLE_TYPE_SMOKE = §fSmoke
PARTICLE_TYPE_LARGE_SMOKE = §fLarge smoke
PARTICLE_TYPE_WHITE_SMOKE = §fWhite smoke
PARTICLE_TYPE_CAMPFIRE_COSY_SMOKE = §fCampfire smoke
PARTICLE_TYPE_CAMPFIRE_SIGNAL_SMOKE = §fSignal smoke
PARTICLE_TYPE_ASH = §fAsh
PARTICLE_TYPE_WHITE_ASH = §fWhite ash
PARTICLE_TYPE_POOF = §fPoof
PARTICLE_TYPE_CLOUD = §fCloud
PARTICLE_TYPE_SNEEZE = §fSneeze
PARTICLE_TYPE_SPIT = §fSpit
PARTICLE_TYPE_EXPLOSION = §fExplosion
PARTICLE_TYPE_SONIC_BOOM = §fSonic boom
PARTICLE_TYPE_SWEEP_ATTACK = §fSweep attack
PARTICLE_TYPE_GUST = §fGust
PARTICLE_TYPE_SMALL_GUST = §fSmall gust
PARTICLE_TYPE_DRIPPING_WATER = §fDripping water
PARTICLE_TYPE_FALLING_WATER = §fFalling water
PARTICLE_TYPE_DRIPPING_LAVA = §fDripping lava
PARTICLE_TYPE_FALLING_LAVA = §fFalling lava
PARTICLE_TYPE_LANDING_LAVA = §fLanding lava
PARTICLE_TYPE_DRIPPING_HONEY = §fDripping honey
PARTICLE_TYPE_FALLING_HONEY = §fFalling honey
PARTICLE_TYPE_LANDING_HONEY = §fLanding honey
PARTICLE_TYPE_FALLING_NECTAR = §fFalling nectar
PARTICLE_TYPE_DRIPPING_OBSIDIAN_TEAR = §fDripping obsidian tear
PARTICLE_TYPE_FALLING_OBSIDIAN_TEAR = §fFalling obsidian tear
PARTICLE_TYPE_LANDING_OBSIDIAN_TEAR = §fLanding obsidian tear
PARTICLE_TYPE_DRIPPING_DRIPSTONE_WATER = §fDripstone water
PARTICLE_TYPE_FALLING_DRIPSTONE_WATER = §fFalling dripstone water
PARTICLE_TYPE_DRIPPING_DRIPSTONE_LAVA = §fDripstone lava
PARTICLE_TYPE_FALLING_DRIPSTONE_LAVA = §fFalling dripstone lava
PARTICLE_TYPE_RAIN = §fRain
PARTICLE_TYPE_SPLASH = §fSplash
PARTICLE_TYPE_FISHING = §fFishing wake
PARTICLE_TYPE_UNDERWATER = §fUnderwater
PARTICLE_TYPE_BUBBLE_POP = §fBubble pop
PARTICLE_TYPE_DOLPHIN = §fDolphin trail
PARTICLE_TYPE_NAUTILUS = §fNautilus
PARTICLE_TYPE_SQUID_INK = §fSquid ink
PARTICLE_TYPE_GLOW_SQUID_INK = §fGlow squid ink
PARTICLE_TYPE_GLOW = §fGlow
PARTICLE_TYPE_FALLING_SPORE_BLOSSOM = §fFalling spore blossom
PARTICLE_TYPE_SPORE_BLOSSOM_AIR = §fFloating spores
PARTICLE_TYPE_CHERRY_LEAVES = §fCherry leaves
PARTICLE_TYPE_PALE_OAK_LEAVES = §fPale oak leaves
PARTICLE_TYPE_CRIMSON_SPORE = §fCrimson spores
PARTICLE_TYPE_WARPED_SPORE = §fWarped spores
PARTICLE_TYPE_MYCELIUM = §fMycelium
PARTICLE_TYPE_COMPOSTER = §fComposter
PARTICLE_TYPE_FIREFLY = §fFirefly
PARTICLE_TYPE_SNOWFLAKE = §fSnowflake
PARTICLE_TYPE_ITEM_SNOWBALL = §fSnowball
PARTICLE_TYPE_ITEM_SLIME = §fSlime
PARTICLE_TYPE_ITEM_COBWEB = §fCobweb
PARTICLE_TYPE_EGG_CRACK = §fEgg crack
PARTICLE_TYPE_DUST_PLUME = §fDust plume
PARTICLE_TYPE_INFESTED = §fInfested
PARTICLE_TYPE_HEART = §fHeart
PARTICLE_TYPE_NOTE = §fNote
PARTICLE_TYPE_HAPPY_VILLAGER = §fHappy villager
PARTICLE_TYPE_ANGRY_VILLAGER = §fAngry villager
PARTICLE_TYPE_DAMAGE_INDICATOR = §fDamage
PARTICLE_TYPE_CRIT = §fCritical hit
PARTICLE_TYPE_ENCHANTED_HIT = §fEnchanted hit
PARTICLE_TYPE_ENCHANT = §fEnchanting glyphs
PARTICLE_TYPE_EFFECT = §fPotion effect
PARTICLE_TYPE_INSTANT_EFFECT = §fInstant effect
PARTICLE_TYPE_WITCH = §fWitch magic
PARTICLE_TYPE_END_ROD = §fEnd rod
PARTICLE_TYPE_PORTAL = §fPortal
PARTICLE_TYPE_REVERSE_PORTAL = §fReverse portal
PARTICLE_TYPE_DRAGON_BREATH = §fDragon breath
PARTICLE_TYPE_FIREWORK = §fFirework spark
PARTICLE_TYPE_TOTEM_OF_UNDYING = §fTotem
PARTICLE_TYPE_ELECTRIC_SPARK = §fElectric spark
PARTICLE_TYPE_SCRAPE = §fScrape
PARTICLE_TYPE_WAX_ON = §fWax on
PARTICLE_TYPE_WAX_OFF = §fWax off
PARTICLE_TYPE_SCULK_SOUL = §fSculk soul
PARTICLE_TYPE_SCULK_CHARGE = §fSculk charge
PARTICLE_TYPE_SCULK_CHARGE_POP = §fSculk charge pop
PARTICLE_TYPE_SHRIEK = §fShriek
PARTICLE_TYPE_RAID_OMEN = §fRaid omen
PARTICLE_TYPE_TRIAL_OMEN = §fTrial omen
PARTICLE_TYPE_OMINOUS_SPAWNING = §fOminous spawning
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION = §fTrial spawner flame
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION_OMINOUS = §fOminous trial spawner flame
PARTICLE_TYPE_VAULT_CONNECTION = §fVault connection
# Particle blocks
PARTICLE_CHOICE_BLOCK_STONE = §fStone
PARTICLE_CHOICE_BLOCK_COBBLESTONE = §fCobblestone
PARTICLE_CHOICE_BLOCK_DIRT = §fDirt
PARTICLE_CHOICE_BLOCK_GRASS_BLOCK = §fGrass block
PARTICLE_CHOICE_BLOCK_SAND = §fSand
PARTICLE_CHOICE_BLOCK_RED_SAND = §fRed sand
PARTICLE_CHOICE_BLOCK_GRAVEL = §fGravel
PARTICLE_CHOICE_BLOCK_OAK_PLANKS = §fOak planks
PARTICLE_CHOICE_BLOCK_OAK_LEAVES = §fOak leaves
PARTICLE_CHOICE_BLOCK_CHERRY_LEAVES = §fCherry leaves
PARTICLE_CHOICE_BLOCK_SNOW_BLOCK = §fSnow block
PARTICLE_CHOICE_BLOCK_ICE = §fIce
PARTICLE_CHOICE_BLOCK_GLASS = §fGlass
PARTICLE_CHOICE_BLOCK_TNT = §fTNT
PARTICLE_CHOICE_BLOCK_PUMPKIN = §fPumpkin
PARTICLE_CHOICE_BLOCK_MELON = §fMelon
PARTICLE_CHOICE_BLOCK_HONEY_BLOCK = §fHoney block
PARTICLE_CHOICE_BLOCK_SLIME_BLOCK = §fSlime block
PARTICLE_CHOICE_BLOCK_NETHERRACK = §fNetherrack
PARTICLE_CHOICE_BLOCK_SOUL_SAND = §fSoul sand
PARTICLE_CHOICE_BLOCK_MAGMA_BLOCK = §fMagma block
PARTICLE_CHOICE_BLOCK_END_STONE = §fEnd stone
PARTICLE_CHOICE_BLOCK_OBSIDIAN = §fObsidian
PARTICLE_CHOICE_BLOCK_PRISMARINE = §fPrismarine
PARTICLE_CHOICE_BLOCK_SCULK = §fSculk
PARTICLE_CHOICE_BLOCK_AMETHYST_BLOCK = §fAmethyst block
PARTICLE_CHOICE_BLOCK_COAL_BLOCK = §fBlock of coal
PARTICLE_CHOICE_BLOCK_IRON_BLOCK = §fBlock of iron
PARTICLE_CHOICE_BLOCK_GOLD_BLOCK = §fBlock of gold
PARTICLE_CHOICE_BLOCK_REDSTONE_BLOCK = §fBlock of redstone
PARTICLE_CHOICE_BLOCK_LAPIS_BLOCK = §fBlock of lapis lazuli
PARTICLE_CHOICE_BLOCK_EMERALD_BLOCK = §fBlock of emerald
PARTICLE_CHOICE_BLOCK_DIAMOND_BLOCK = §fBlock of diamond
PARTICLE_CHOICE_BLOCK_WHITE_WOOL = §fWhite wool
PARTICLE_CHOICE_BLOCK_RED_WOOL = §fRed wool
PARTICLE_CHOICE_BLOCK_ORANGE_WOOL = §fOrange wool
PARTICLE_CHOICE_BLOCK_YELLOW_WOOL = §fYellow wool
PARTICLE_CHOICE_BLOCK_LIME_WOOL = §fLime wool
PARTICLE_CHOICE_BLOCK_LIGHT_BLUE_WOOL = §fLight blue wool
PARTICLE_CHOICE_BLOCK_BLUE_WOOL = §fBlue wool
PARTICLE_CHOICE_BLOCK_PURPLE_WOOL = §fPurple wool
PARTICLE_CHOICE_BLOCK_PINK_WOOL = §fPink wool
PARTICLE_CHOICE_BLOCK_BLACK_WOOL = §fBlack wool
PARTICLE_PLAYER_HAYLIM_AURA = §fHaylim\'s Aura
PARTICLE_PLAYER_1063 = §7Circling Circle
@@ -136,34 +401,12 @@ PARTICLE_TEAM_PULSE_AURA_3 = §fPulse Aura §fEnchanted
PARTICLE_TEAM_PULSE_LOGO = §fPulse Logo
PARTICLE_TEAM_PULSE_HEART_BEAT = §cHeart Beat
PARTICLE_RAINCLOUD_NORMAL = §fRaincloud
PARTICLE_RAINCLOUD_RED = §fLava cloud
PARTICLE_RAINCLOUD_YELLOW = §fNectar cloud
PARTICLE_RAINCLOUD_ORANGE = §fHoney cloud
PARTICLE_RAINCLOUD_GREEN = §fSpore Blossom cloud
PARTICLE_RAINCLOUD_PURPLE = §fObsidian Tears cloud
PARTICLE_RAINCLOUD_NAUTILUS = §fNautilus cloud
PARTICLE_RAINCLOUD_ENCHANTMENT = §fEnchantment cloud
PARTICLE_RAINCLOUD_SLIME = §fSlime cloud
PARTICLE_RAINCLOUD_HAIL = §fHail cloud
PARTICLE_RAINCLOUD_SNOW = §fSnow cloud
PARTICLE_RAINCLOUD_TOTEM = §fTotem cloud
PARTICLE_EVENT_ENCHANTING = §cEnchantment
PARTICLE_EVENT_CLOUD = §fClouds
PARTICLE_EVENT_SMOKE = §7Smoke
PARTICLE_EVENT_WATER = §bWater
PARTICLE_EVENT_WINGS = §fWings
PARTICLE_EVENT_RAIN_CLOUD = §fRandom Raincloud
PARTICLE_EVENT_WGS = §fWGS
PARTICLE_EVENT_WARGEARCLASH = §fClash
PARTICLE_EGG_HUNT_EASY = §aGreenery
PARTICLE_EGG_HUNT_MEDIUM = §eHide and Seek
PARTICLE_EGG_HUNT_HARD = §cOrbit
PARTICLE_EGG_HUNT_EXTREME = §5Purple rain
PARTICLE_EGG_HUNT_ADVANCED = §5Lift off
PARTICLE_EGG_HUNT_HALF = §fThe hunt is on
JUMP_AND_RUN_PROGRESS = §e{0}§8/§f{1} §c{2} §7{3}
JUMP_AND_RUN_CANCEL = {0}
@@ -49,109 +49,353 @@ PORTAL_NO_WORLDEDIT_SELECTION = §cKeine WorldEdit Selection
PARTICLE_INVENTORY = §0Partikel
PARTICLE_DESELECT = §8Keine Partikel
PARTICLE_SHOW_HAS_ALL = §8Du hast alle Partikel
PARTICLE_SHOW_ALL = §aZeige alle
PARTICLE_SHOW_UNLOCKED = §cZeige freigeschaltete
PARTICLE_UNLOCKED_BY = §eFreigeschaltet durch
PARTICLE_UNLOCKED_BY_TEAM = §fTeambeitritt
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM = §fTeam {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_TEAM_EASTER = §fTeam {0} (Oster-Eierer-Suche)
PARTICLE_UNLOCKED_BY_SPECIFIC_USER = §fUser {0}
PARTICLE_UNLOCKED_BY_SPECIFIC_USER_EASTER = §fUser {0} (Oster-Eierer-Suche)
PARTICLE_UNLOCKED_BY_EVENT = §fEventteilnahme
PARTICLE_UNLOCKED_BY_SERVER_TEAM = §fServerteam
PARTICLE_UNLOCKED_BY_EVENT = §fEventteilnahme
PARTICLE_UNLOCKED_BY_EVENT_PLACEMENT = §f{0} 1., 2. oder 3. Platz
PARTICLE_UNLOCKED_BY_EVENT_PARTICIPATION = §f{0}
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_HALF = §fFinde 45 Oster-Eierer
PARTICLE_UNLOCKED_BY_EASTER_EGG_HUNT_DIFFICULTY = §fAlle Eierer mit Schwierigkeit {0} finden
PARTICLE_ATTRIBUTE_CIRCLE = §8-§f Ring
PARTICLE_ATTRIBUTE_BI_CIRCLE = §8-§f Doppelring
PARTICLE_ATTRIBUTE_TRI_CIRCLE = §8-§f Dreifachring
PARTICLE_ATTRIBUTE_CLOUD = §8-§f Wolke
PARTICLE_ATTRIBUTE_TICK = §8-§f Immer aktiv
PARTICLE_ATTRIBUTE_NON_FLOOR = §8-§f In der Luft
PARTICLE_ATTRIBUTE_FLOOR = §8-§f Auf dem Boden
PARTICLE_ATTRIBUTE_NON_FLYING = §8-§f Nicht am Fliegen
PARTICLE_ATTRIBUTE_FLYING = §8-§f Am Fliegen
PARTICLE_ATTRIBUTE_WING = §8-§f Flügel
PARTICLE_ATTRIBUTE_SNEAKING = §8-§f Ducken
PARTICLE_ATTRIBUTE_NON_MOVING = §8-§f Beim Stehen
PARTICLE_ATTRIBUTE = §eAttribute§7:
PARTICLE_SELECT = §eZum Auswählen klicken
PARTICLE_LOCKED = §c§lGesperrt
PARTICLE_DEACTIVATED = §cEs sind zu viele Spieler online! Partikel sind deaktiviert.
PARTICLE_SNEEZE = §aSneeze
PARTICLE_SMOKE = §7Rauch
PARTICLE_FIRE = §cFeuer
PARTICLE_WATER = §bWasser
PARTICLE_HEART = §cHerzen
PARTICLE_NOTES = §eNoten
PARTICLE_NAUTILUS = §aNautilus
PARTICLE_SNOWBALL = §fSchneeball
PARTICLE_EFFECT = §5Effekt
PARTICLE_CAMPFIRE = §7Rauch
PARTICLE_MAGIC = §5Magie
PARTICLE_ANGRY = §4Wut
PARTICLE_SLIME = §aSchleim
PARTICLE_MOB = §7Mob
PARTICLE_SQUID = §8Tintenfisch
PARTICLE_BUBBLE = §aBlasen
PARTICLE_HONEY = §6Honig
PARTICLE_NECTAR = §6Nektar
PARTICLE_FIREWORK = §7Feuerwerk
PARTICLE_DRAGON_BREATH = §5Drachenatem
PARTICLE_DAMAGE = §5Schaden
PARTICLE_DOLPHIN = §dDelphin
PARTICLE_WITCH = §5Hexe
PARTICLE_ENCHANTING = §eZauber
PARTICLE_HAPPY = §2Freude
PARTICLE_FLAME = §7Flammen
PARTICLE_END_ROD = §fEnd Rod
PARTICLE_CLOUD = §7Wolke
PARTICLE_TOTEM = §aTotem
PARTICLE_TOWN = §5Town
PARTICLE_ENCHANTING_CIRCLE = §fVerzaubert
PARTICLE_WATER_FIRE = §bWasser§7/§cFeuer
PARTICLE_MAGIC_ENCHANTING = §5Magie§7/§eZauber
PARTICLE_WINGS_EVIL = §5Lila Flügel
PARTICLE_BACK = §7Zurück
PARTICLE_SLOT_OPEN = §eZum Öffnen klicken
PARTICLE_SLOT_CLICK_USE = §eZum Verwenden klicken
PARTICLE_SLOT_CLICK_STOP = §eZum Beenden klicken
PARTICLE_SLOT_SHIFT_EDIT = §eZum Bearbeiten Shift-Klick
PARTICLE_SLOT_NAME = §f{0} §8(§e{1}§8/§7{2}§8)
PARTICLE_SLOT_TITLE = §0{0} §8({1}/{2})
PARTICLE_SHIFT_TO_DELETE = §7Zum Löschen Shift-Klick
PARTICLE_SECTION_DESIGN = §6§lDesign
PARTICLE_SECTION_PARTICLE = §6§lPartikel
PARTICLE_SECTION_SHAPE = §6§lForm
PARTICLE_SECTION_TIMING = §6§lZeit und Position
PARTICLE_SECTION_CONDITIONS = §6§lBedingungen
PARTICLE_FIXED_LOOK = §7Dieses Design sieht immer gleich aus
PARTICLE_VALUE_CLICK = §eZum Ändern klicken
PARTICLE_VALUE_TOGGLE = §eZum Umschalten klicken
PARTICLE_VALUE_CYCLE = §eLinksklick§8: §7weiter, §eRechtsklick§8: §7zurück
PARTICLE_VALUE_SELECTED = §aAusgewählt
PARTICLE_VALUE_RESET = §eAuf {0} zurücksetzen
PARTICLE_NUMBER_RANGE = §7Bereich§8: §e{0} §8- §e{1}
PARTICLE_NUMBER_TYPE = §eZum Eingeben klicken
PARTICLE_NUMBER_RESULT = §7→ §e{0}
PARTICLE_NUMBER_AT_MAX = §7→ §e{0} §8(Max)
PARTICLE_NUMBER_AT_MIN = §7→ §e{0} §8(Min)
PARTICLE_COLOR_EDITOR = §0Farben
PARTICLE_COLOR_REMOVE = §eZum Entfernen klicken
PARTICLE_COLOR_ADD = §aFarbe hinzufügen
PARTICLE_COLOR_HEX = §eHex-Code eingeben
PARTICLE_COLOR_HEX_EXAMPLE = §7Zum Beispiel #FF8800
PARTICLE_COLOR_HEX_TITLE = Hex-Farbe
PARTICLE_COLOR_HEX_INVALID = §cDas ist keine Hex-Farbe wie #FF8800
PARTICLE_COLOR_RANDOM = §eZufällige Farben verwenden
PARTICLE_SLOT_EMPTY = §7Leerer Slot
PARTICLE_SLOT_CREATE = §eZum Erstellen klicken
PARTICLE_SLOT_ENTRIES = §7{0} Einträge
PARTICLE_SLOT_ACTIVE = §aAktiv
PARTICLE_SLOT_VIEW = §eZum Ansehen klicken
PARTICLE_SLOT_ADD_ENTRY = §aEintrag hinzufügen
PARTICLE_SLOT_MAX_ENTRIES = §cDieser Slot ist voll
PARTICLE_SLOT_OVER_BUDGET = §cDamit würde dieser Slot zu viele Partikel erzeugen
PARTICLE_SLOT_RENAME = §eUmbenennen
PARTICLE_SLOT_RENAME_TITLE = Slotname
PARTICLE_SLOT_ICON = §eSymbol ändern
PARTICLE_SLOT_ICON_TITLE = §0Slotsymbol
PARTICLE_SLOT_DELETE = §cSlot löschen
PARTICLE_SLOT_COST = §7Partikel pro Tick§8: §e{0}§8/§7{1}
PARTICLE_ENTRY_EDITOR = §0{0}
PARTICLE_ENTRY_DESIGN = §7Design§8: {0}
PARTICLE_ENTRY_PARTICLE = §7Partikel§8: {0}
PARTICLE_ENTRY_LOCKED = §cDesign gesperrt, wird nicht angezeigt
PARTICLE_ENTRY_COST = §7Partikel pro Tick§8: §e{0}
PARTICLE_ENTRY_COST_SHARE = §7Dieser Eintrag§8: §e{0}
PARTICLE_ENTRY_EDIT = §eZum Bearbeiten klicken
PARTICLE_ENTRY_DELETE = §cEintrag löschen
PARTICLE_DESIGN_PICKER = §0Design wählen
PARTICLE_CHOICE_PICKER = §0{0}
PARTICLE_COLOR_PICKER = §0Farbe wählen
PARTICLE_VALUE = §7Wert§8: {0}
PARTICLE_VALUE_RANDOM = §7Zufällige Farben
PARTICLE_VALUE_YES = §aJa
PARTICLE_VALUE_NO = §cNein
PARTICLE_DESIGN_SIMPLE = §fEinfach
PARTICLE_DESIGN_CIRCLE = §fKreis
PARTICLE_DESIGN_WING = §fFlügel
PARTICLE_DESIGN_CLOUD = §fWolke
PARTICLE_DESIGN_BURST = §fStoß
PARTICLE_DESIGN_FOOTPRINTS = §fFußspuren
PARTICLE_DESIGN_TEAM_LINK = §fTeamband
PARTICLE_DESIGN_PULSE_SHIMMER = §fSchimmer
PARTICLE_DESIGN_HEART_BEAT = §fHerzschlag
PARTICLE_PARAM_PARTICLE = §fPartikel
PARTICLE_PARAM_GLYPH_FLIGHT = §fGlyphenflug
PARTICLE_PARAM_BURST_TRIGGER = §fWann
PARTICLE_PARAM_BURST_SHAPE = §fForm
PARTICLE_PARAM_BURST_RADIUS = §fRingradius
PARTICLE_PARAM_BURST_FALL = §fMindestfallhöhe
PARTICLE_PARAM_FOOTPRINT_LIFETIME = §fSpurdauer (Ticks)
PARTICLE_PARAM_FOOTPRINT_STEP = §fAbstand der Spuren
PARTICLE_PARAM_LINK_SPACING = §fAbstand der Partikel
PARTICLE_PARAM_PULSE_SPEED = §fPulsdauer (Ticks)
PARTICLE_PARAM_PULSE_ROTATION = §fDrehung
PARTICLE_PARAM_HEART_BEAT_SPEED = §fSchlagdauer (Ticks)
PARTICLE_PARAM_COUNT = §fAnzahl
PARTICLE_PARAM_SPREAD_X = §fStreuung X
PARTICLE_PARAM_SPREAD_Y = §fStreuung Y
PARTICLE_PARAM_SPREAD_Z = §fStreuung Z
PARTICLE_PARAM_SPEED = §fGeschwindigkeit
PARTICLE_PARAM_COLORS = §fFarben
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fSpurdauer (Ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fSpurziel X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fSpurziel Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fSpurziel Z
PARTICLE_PARAM_OFFSET_X = §fVersatz X
PARTICLE_PARAM_OFFSET_Y = §fVersatz Y
PARTICLE_PARAM_OFFSET_Z = §fVersatz Z
PARTICLE_PARAM_INTERVAL = §fAlle n Ticks
PARTICLE_PARAM_PLACEMENT = §fAusgerichtet an
PARTICLE_PARAM_TICK_MODE = §fAnzeige
PARTICLE_PARAM_MOVEMENT = §fBewegung
PARTICLE_PARAM_GLIDING = §fElytra
PARTICLE_PARAM_GROUND = §fBoden
PARTICLE_PARAM_SNEAK = §fDucken
PARTICLE_PARAM_VISIBILITY = §fSichtbar für
PARTICLE_PARAM_RADIUS = §fRadius
PARTICLE_PARAM_CIRCLE_SPEED = §fDrehgeschwindigkeit
PARTICLE_PARAM_PHASE = §fStartwinkel
PARTICLE_PARAM_RISE = §fAuf und ab Höhe
PARTICLE_PARAM_RISE_SPEED = §fAuf und ab Geschwindigkeit
PARTICLE_PARAM_BOUNCE = §fZurückspringen
PARTICLE_PARAM_INVERTED = §fOben beginnen
PARTICLE_PARAM_WING_IMAGE = §fFlügelform
PARTICLE_PARAM_WING_SIZE = §fFlügelgröße
PARTICLE_PARAM_WING_DETAIL = §fPunkte pro Flügel
PARTICLE_PARAM_WING_COLORING = §fFarbmodus
PARTICLE_PARAM_WING_SPARKLE = §fFunkeln (sichtbare Punkte, %)
PARTICLE_PARAM_WING_MOTION = §fBewegung
PARTICLE_CHOICE_TICKMODE_MOVE = §fBeim Bewegen
PARTICLE_CHOICE_TICKMODE_ALWAYS = §fImmer
PARTICLE_CHOICE_PLACEMENT_WORLD = §fWelt (X Osten, Y oben, Z Süden)
PARTICLE_CHOICE_PLACEMENT_BODY = §fKörper (X rechts, Y oben, Z vorne)
PARTICLE_CHOICE_MOVEMENT_ANY = §fJede Bewegung
PARTICLE_CHOICE_MOVEMENT_MOVING = §fNur beim Bewegen
PARTICLE_CHOICE_MOVEMENT_STANDING_STILL = §fNur beim Stehen
PARTICLE_CHOICE_GLIDING_ANY = §fMit oder ohne Elytra
PARTICLE_CHOICE_GLIDING_GLIDING = §fNur beim Gleiten
PARTICLE_CHOICE_GLIDING_NOT_GLIDING = §fNicht beim Gleiten
PARTICLE_CHOICE_GROUND_ANY = §fAm Boden und in der Luft
PARTICLE_CHOICE_GROUND_ON_GROUND = §fNur am Boden
PARTICLE_CHOICE_GROUND_IN_AIR = §fNur in der Luft
PARTICLE_CHOICE_SNEAK_ANY = §fDucken egal
PARTICLE_CHOICE_SNEAK_SNEAKING = §fNur beim Ducken
PARTICLE_CHOICE_SNEAK_NOT_SNEAKING = §fNicht beim Ducken
PARTICLE_CHOICE_VISIBILITY_EVERYONE = §fAlle
PARTICLE_CHOICE_VISIBILITY_ONLY_SELF = §fNur ich
PARTICLE_CHOICE_VISIBILITY_ONLY_OTHERS = §fNur andere
PARTICLE_CHOICE_IMAGE_SIMPLE = §fEinfach
PARTICLE_CHOICE_IMAGE_COMPLEX = §fKomplex
PARTICLE_CHOICE_IMAGE_SWORD = §fSchwert
PARTICLE_CHOICE_IMAGE_BAT = §fFledermaus
PARTICLE_CHOICE_IMAGE_BUTTERFLY = §fSchmetterling
PARTICLE_CHOICE_IMAGE_FAIRY = §fFee
PARTICLE_CHOICE_IMAGE_MWGL = §fMiniWarGear Liga
PARTICLE_CHOICE_IMAGE_SW = §fSteamWar
PARTICLE_CHOICE_IMAGE_WGS = §fWarGear Saison
PARTICLE_CHOICE_IMAGE_SWORD_CROSSED = §fGekreuzte Schwerter
PARTICLE_CHOICE_COLORING_OVER_TIME = §fWechseln mit der Zeit
PARTICLE_CHOICE_COLORING_ALONG_WING = §fVom Rücken zu den Spitzen
PARTICLE_CHOICE_MOTION_NONE = §fKeine
PARTICLE_CHOICE_MOTION_FLAP_SLOW = §fLangsam schlagen
PARTICLE_CHOICE_MOTION_FLAP_FAST = §fSchnell schlagen
PARTICLE_CHOICE_MOTION_BEAT_ON_JUMP = §fSchlag beim Springen
PARTICLE_CHOICE_TRIGGER_JUMP = §fBeim Springen
PARTICLE_CHOICE_TRIGGER_LAND = §fBeim Landen
PARTICLE_CHOICE_TRIGGER_SNEAK = §fBeim Schleichen
PARTICLE_CHOICE_BURSTSHAPE_PUFF = §fWolke
PARTICLE_CHOICE_BURSTSHAPE_RING = §fRing
PARTICLE_CHOICE_GLYPHFLIGHT_NORMAL = §fHerabschweben
PARTICLE_CHOICE_GLYPHFLIGHT_INTO_PLAYER = §fIn den Spieler
PARTICLE_CHOICE_GLYPHFLIGHT_OUT_OF_PLAYER = §fAus dem Spieler
# Particle types
PARTICLE_TYPE_DUST = §fStaub
PARTICLE_TYPE_DUST_COLOR_TRANSITION = §fFarbverlaufsstaub
PARTICLE_TYPE_ENTITY_EFFECT = §fTrankwirbel
PARTICLE_TYPE_TINTED_LEAVES = §fGefärbte Blätter
PARTICLE_TYPE_BLOCK = §fBlocksplitter
PARTICLE_TYPE_BLOCK_CRUMBLE = §fBröckelnder Block
PARTICLE_TYPE_BLOCK_MARKER = §fBlockmarkierung
PARTICLE_TYPE_DUST_PILLAR = §fStaubsäule
PARTICLE_TYPE_FALLING_DUST = §fFallender Staub
PARTICLE_TYPE_ITEM = §fGegenstandssplitter
PARTICLE_TYPE_TRAIL = §fSpur
PARTICLE_TYPE_FLAME = §fFlamme
PARTICLE_TYPE_SMALL_FLAME = §fKleine Flamme
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSeelenfeuerflamme
PARTICLE_TYPE_SOUL = §fSeele
PARTICLE_TYPE_LAVA = §fLavafunken
PARTICLE_TYPE_SMOKE = §fRauch
PARTICLE_TYPE_LARGE_SMOKE = §fGroßer Rauch
PARTICLE_TYPE_WHITE_SMOKE = §fWeißer Rauch
PARTICLE_TYPE_CAMPFIRE_COSY_SMOKE = §fLagerfeuerrauch
PARTICLE_TYPE_CAMPFIRE_SIGNAL_SMOKE = §fSignalrauch
PARTICLE_TYPE_ASH = §fAsche
PARTICLE_TYPE_WHITE_ASH = §fWeiße Asche
PARTICLE_TYPE_POOF = §fPuff
PARTICLE_TYPE_CLOUD = §fWolke
PARTICLE_TYPE_SNEEZE = §fNiesen
PARTICLE_TYPE_SPIT = §fSpucke
PARTICLE_TYPE_EXPLOSION = §fExplosion
PARTICLE_TYPE_SONIC_BOOM = §fSchallwelle
PARTICLE_TYPE_SWEEP_ATTACK = §fSchwungangriff
PARTICLE_TYPE_GUST = §fWindstoß
PARTICLE_TYPE_SMALL_GUST = §fKleiner Windstoß
PARTICLE_TYPE_DRIPPING_WATER = §fTropfendes Wasser
PARTICLE_TYPE_FALLING_WATER = §fFallendes Wasser
PARTICLE_TYPE_DRIPPING_LAVA = §fTropfende Lava
PARTICLE_TYPE_FALLING_LAVA = §fFallende Lava
PARTICLE_TYPE_LANDING_LAVA = §fLandende Lava
PARTICLE_TYPE_DRIPPING_HONEY = §fTropfender Honig
PARTICLE_TYPE_FALLING_HONEY = §fFallender Honig
PARTICLE_TYPE_LANDING_HONEY = §fLandender Honig
PARTICLE_TYPE_FALLING_NECTAR = §fFallender Nektar
PARTICLE_TYPE_DRIPPING_OBSIDIAN_TEAR = §fTropfende Obsidianträne
PARTICLE_TYPE_FALLING_OBSIDIAN_TEAR = §fFallende Obsidianträne
PARTICLE_TYPE_LANDING_OBSIDIAN_TEAR = §fLandende Obsidianträne
PARTICLE_TYPE_DRIPPING_DRIPSTONE_WATER = §fTropfsteinwasser
PARTICLE_TYPE_FALLING_DRIPSTONE_WATER = §fFallendes Tropfsteinwasser
PARTICLE_TYPE_DRIPPING_DRIPSTONE_LAVA = §fTropfsteinlava
PARTICLE_TYPE_FALLING_DRIPSTONE_LAVA = §fFallende Tropfsteinlava
PARTICLE_TYPE_RAIN = §fRegen
PARTICLE_TYPE_SPLASH = §fSpritzer
PARTICLE_TYPE_FISHING = §fAngelspur
PARTICLE_TYPE_UNDERWATER = §fUnterwasser
PARTICLE_TYPE_BUBBLE_POP = §fPlatzende Blase
PARTICLE_TYPE_DOLPHIN = §fDelfinspur
PARTICLE_TYPE_NAUTILUS = §fNautilus
PARTICLE_TYPE_SQUID_INK = §fTintenfischtinte
PARTICLE_TYPE_GLOW_SQUID_INK = §fLeuchttintenfischtinte
PARTICLE_TYPE_GLOW = §fLeuchten
PARTICLE_TYPE_FALLING_SPORE_BLOSSOM = §fFallende Sporenblüte
PARTICLE_TYPE_SPORE_BLOSSOM_AIR = §fSchwebende Sporen
PARTICLE_TYPE_CHERRY_LEAVES = §fKirschblätter
PARTICLE_TYPE_PALE_OAK_LEAVES = §fBlasseichenblätter
PARTICLE_TYPE_CRIMSON_SPORE = §fKarmesinsporen
PARTICLE_TYPE_WARPED_SPORE = §fWirrsporen
PARTICLE_TYPE_MYCELIUM = §fMyzel
PARTICLE_TYPE_COMPOSTER = §fKomposter
PARTICLE_TYPE_FIREFLY = §fGlühwürmchen
PARTICLE_TYPE_SNOWFLAKE = §fSchneeflocke
PARTICLE_TYPE_ITEM_SNOWBALL = §fSchneeball
PARTICLE_TYPE_ITEM_SLIME = §fSchleim
PARTICLE_TYPE_ITEM_COBWEB = §fSpinnennetz
PARTICLE_TYPE_EGG_CRACK = §fEierschale
PARTICLE_TYPE_DUST_PLUME = §fStaubwolke
PARTICLE_TYPE_INFESTED = §fBefallen
PARTICLE_TYPE_HEART = §fHerz
PARTICLE_TYPE_NOTE = §fNote
PARTICLE_TYPE_HAPPY_VILLAGER = §fFröhlicher Dorfbewohner
PARTICLE_TYPE_ANGRY_VILLAGER = §fWütender Dorfbewohner
PARTICLE_TYPE_DAMAGE_INDICATOR = §fSchaden
PARTICLE_TYPE_CRIT = §fVolltreffer
PARTICLE_TYPE_ENCHANTED_HIT = §fVerzauberter Treffer
PARTICLE_TYPE_ENCHANT = §fZauberzeichen
PARTICLE_TYPE_EFFECT = §fTrankeffekt
PARTICLE_TYPE_INSTANT_EFFECT = §fSoforteffekt
PARTICLE_TYPE_WITCH = §fHexenmagie
PARTICLE_TYPE_END_ROD = §fEndstab
PARTICLE_TYPE_PORTAL = §fPortal
PARTICLE_TYPE_REVERSE_PORTAL = §fUmgekehrtes Portal
PARTICLE_TYPE_DRAGON_BREATH = §fDrachenatem
PARTICLE_TYPE_FIREWORK = §fFeuerwerksfunke
PARTICLE_TYPE_TOTEM_OF_UNDYING = §fTotem
PARTICLE_TYPE_ELECTRIC_SPARK = §fElektrischer Funke
PARTICLE_TYPE_SCRAPE = §fAbkratzen
PARTICLE_TYPE_WAX_ON = §fWachsen
PARTICLE_TYPE_WAX_OFF = §fEntwachsen
PARTICLE_TYPE_SCULK_SOUL = §fSculk-Seele
PARTICLE_TYPE_SCULK_CHARGE = §fSculk-Ladung
PARTICLE_TYPE_SCULK_CHARGE_POP = §fSculk-Entladung
PARTICLE_TYPE_SHRIEK = §fKreischen
PARTICLE_TYPE_RAID_OMEN = §fÜberfall-Omen
PARTICLE_TYPE_TRIAL_OMEN = §fPrüfungs-Omen
PARTICLE_TYPE_OMINOUS_SPAWNING = §fUnheilvolles Erscheinen
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION = §fPrüfungsspawner-Flamme
PARTICLE_TYPE_TRIAL_SPAWNER_DETECTION_OMINOUS = §fUnheilvolle Prüfungsspawner-Flamme
PARTICLE_TYPE_VAULT_CONNECTION = §fTresorverbindung
# Particle blocks
PARTICLE_CHOICE_BLOCK_STONE = §fStein
PARTICLE_CHOICE_BLOCK_COBBLESTONE = §fBruchstein
PARTICLE_CHOICE_BLOCK_DIRT = §fErde
PARTICLE_CHOICE_BLOCK_GRASS_BLOCK = §fGrasblock
PARTICLE_CHOICE_BLOCK_SAND = §fSand
PARTICLE_CHOICE_BLOCK_RED_SAND = §fRoter Sand
PARTICLE_CHOICE_BLOCK_GRAVEL = §fKies
PARTICLE_CHOICE_BLOCK_OAK_PLANKS = §fEichenholzbretter
PARTICLE_CHOICE_BLOCK_OAK_LEAVES = §fEichenlaub
PARTICLE_CHOICE_BLOCK_CHERRY_LEAVES = §fKirschlaub
PARTICLE_CHOICE_BLOCK_SNOW_BLOCK = §fSchneeblock
PARTICLE_CHOICE_BLOCK_ICE = §fEis
PARTICLE_CHOICE_BLOCK_GLASS = §fGlas
PARTICLE_CHOICE_BLOCK_TNT = §fTNT
PARTICLE_CHOICE_BLOCK_PUMPKIN = §fKürbis
PARTICLE_CHOICE_BLOCK_MELON = §fMelone
PARTICLE_CHOICE_BLOCK_HONEY_BLOCK = §fHonigblock
PARTICLE_CHOICE_BLOCK_SLIME_BLOCK = §fSchleimblock
PARTICLE_CHOICE_BLOCK_NETHERRACK = §fNetherrack
PARTICLE_CHOICE_BLOCK_SOUL_SAND = §fSeelensand
PARTICLE_CHOICE_BLOCK_MAGMA_BLOCK = §fMagmablock
PARTICLE_CHOICE_BLOCK_END_STONE = §fEndstein
PARTICLE_CHOICE_BLOCK_OBSIDIAN = §fObsidian
PARTICLE_CHOICE_BLOCK_PRISMARINE = §fPrismarin
PARTICLE_CHOICE_BLOCK_SCULK = §fSculk
PARTICLE_CHOICE_BLOCK_AMETHYST_BLOCK = §fAmethystblock
PARTICLE_CHOICE_BLOCK_COAL_BLOCK = §fKohleblock
PARTICLE_CHOICE_BLOCK_IRON_BLOCK = §fEisenblock
PARTICLE_CHOICE_BLOCK_GOLD_BLOCK = §fGoldblock
PARTICLE_CHOICE_BLOCK_REDSTONE_BLOCK = §fRedstone-Block
PARTICLE_CHOICE_BLOCK_LAPIS_BLOCK = §fLapislazuliblock
PARTICLE_CHOICE_BLOCK_EMERALD_BLOCK = §fSmaragdblock
PARTICLE_CHOICE_BLOCK_DIAMOND_BLOCK = §fDiamantblock
PARTICLE_CHOICE_BLOCK_WHITE_WOOL = §fWeiße Wolle
PARTICLE_CHOICE_BLOCK_RED_WOOL = §fRote Wolle
PARTICLE_CHOICE_BLOCK_ORANGE_WOOL = §fOrange Wolle
PARTICLE_CHOICE_BLOCK_YELLOW_WOOL = §fGelbe Wolle
PARTICLE_CHOICE_BLOCK_LIME_WOOL = §fHellgrüne Wolle
PARTICLE_CHOICE_BLOCK_LIGHT_BLUE_WOOL = §fHellblaue Wolle
PARTICLE_CHOICE_BLOCK_BLUE_WOOL = §fBlaue Wolle
PARTICLE_CHOICE_BLOCK_PURPLE_WOOL = §fViolette Wolle
PARTICLE_CHOICE_BLOCK_PINK_WOOL = §fRosa Wolle
PARTICLE_CHOICE_BLOCK_BLACK_WOOL = §fSchwarze Wolle
PARTICLE_PLAYER_1063 = §7Kreisende Kreise
PARTICLE_PLAYER_10697_AURA = §7Rauchgranate
PARTICLE_TEAM_PULSE_HEART_BEAT = §cHerzschlag
PARTICLE_RAINCLOUD_NORMAL = §fRegenwolke
PARTICLE_RAINCLOUD_RED = §fLavawolke
PARTICLE_RAINCLOUD_YELLOW = §fNektarwolke
PARTICLE_RAINCLOUD_ORANGE = §fHonigwolke
PARTICLE_RAINCLOUD_GREEN = §fSporenwolke
PARTICLE_RAINCLOUD_PURPLE = §fObsidanwolke
PARTICLE_RAINCLOUD_NAUTILUS = §fNautiluswolke
PARTICLE_RAINCLOUD_ENCHANTMENT = §fVerzauberungswolke
PARTICLE_RAINCLOUD_SLIME = §fSchleimwolke
PARTICLE_RAINCLOUD_HAIL = §fHagelwolke
PARTICLE_RAINCLOUD_SNOW = §fSchneewolke
PARTICLE_RAINCLOUD_TOTEM = §fTotemwolke
PARTICLE_EVENT_ENCHANTING = §cVerzaubert
PARTICLE_EVENT_CLOUD = §fWolken
PARTICLE_EVENT_SMOKE = §7Rauch
PARTICLE_EVENT_WATER = §bWasser
PARTICLE_EVENT_WINGS = §fFlügel
PARTICLE_EVENT_RAIN_CLOUD = §fRegenwolke
PARTICLE_EVENT_WGS = §fWGS
PARTICLE_EVENT_WARGEARCLASH = §fClash
PARTICLE_EGG_HUNT_EASY = §aGrünplfanzen
PARTICLE_EGG_HUNT_MEDIUM = §eVersteckspiel
PARTICLE_EGG_HUNT_HARD = §cOrbit
PARTICLE_EGG_HUNT_EXTREME = §5Lila regen
PARTICLE_EGG_HUNT_ADVANCED = §5Abheben
PARTICLE_EGG_HUNT_HALF = §fDie Jagd ist eröffnet
JUMP_AND_RUN_FINISHED = §aBeendet in {0} mit {1} Fails
JUMP_AND_RUN_PERSONAL_BEST = §aNice! Du hast deinen Rekord um {0} verbessert!
@@ -25,6 +25,7 @@ import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.listener.PlayerSpawn;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
@@ -46,6 +47,7 @@ public class ModifyCommand extends SWCommand implements Listener {
public ModifyCommand() {
super("modify");
Bukkit.getPluginManager().registerEvents(this, LobbySystem.getInstance());
}
@Register
@@ -20,7 +20,6 @@
package de.steamwar.lobby.jumpandrun;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.listener.PlayerSpawn;
import de.steamwar.lobby.util.LeaderboardManager;
@@ -61,51 +60,52 @@ public class JumpAndRun implements Listener {
private static final LeaderboardManager LEADERBOARD = new LeaderboardManager(LobbySystem.getEntityServer(false), JUMP_AND_RUN_CONFIG, new Location(Bukkit.getWorlds().get(0), 2338.5, 42.5, 1231.5));
@LinkedTimer(delay = 1, period = 1)
private void updatePlayers() {
Set<Player> toReset = new HashSet<>();
CURRENT_POS.forEach((player, index) -> {
if (System.currentTimeMillis() - CLICKED.getOrDefault(player, 0L) > 500) {
CLICKED.remove(player);
CLICKED_COUNT.remove(player);
} else {
CLICKED_COUNT.put(player, CLICKED_COUNT.getOrDefault(player, -1) + 1);
}
Location location = player.getLocation();
Vector point = points.get(index);
if (index < points.size() - 1) {
Vector nextPoint = points.get(index + 1);
double y = Math.min(point.getY(), nextPoint.getY()) - 2;
if (location.getY() < y) {
location.setX(point.getX());
location.setY(point.getY());
location.setZ(point.getZ());
player.teleport(location);
player.playSound(location, Sound.ENTITY_ENDERMAN_TELEPORT, 1, 1);
FAILS.put(player, FAILS.getOrDefault(player, 0) + 1);
{
Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> {
Set<Player> toReset = new HashSet<>();
CURRENT_POS.forEach((player, index) -> {
if (System.currentTimeMillis() - CLICKED.getOrDefault(player, 0L) > 500) {
CLICKED.remove(player);
CLICKED_COUNT.remove(player);
} else {
CLICKED_COUNT.put(player, CLICKED_COUNT.getOrDefault(player, -1) + 1);
}
}
int count = CLICKED_COUNT.getOrDefault(player, -1);
if (count >= 0) {
if (count > 20) {
toReset.add(player);
return;
Location location = player.getLocation();
Vector point = points.get(index);
if (index < points.size() - 1) {
Vector nextPoint = points.get(index + 1);
double y = Math.min(point.getY(), nextPoint.getY()) - 2;
if (location.getY() < y) {
location.setX(point.getX());
location.setY(point.getY());
location.setZ(point.getZ());
player.teleport(location);
player.playSound(location, Sound.ENTITY_ENDERMAN_TELEPORT, 1, 1);
FAILS.put(player, FAILS.getOrDefault(player, 0) + 1);
}
}
count = Math.min(count / 2, 30);
LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_CANCEL", player, ChatMessageType.ACTION_BAR, "§e" + BAR_EMPTY.substring(0, count) + "§7" + BAR_EMPTY.substring(count));
} else {
long time = System.currentTimeMillis() - START.get(player);
SimpleDateFormat format = new SimpleDateFormat(LobbySystem.getMessage().parse("JUMP_AND_RUN_TIME", player), Locale.ROOT);
String parsed = format.format(new Date(time));
LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_PROGRESS", player, ChatMessageType.ACTION_BAR, index + 1, points.size(), FAILS.get(player), parsed);
}
});
toReset.forEach(player -> {
reset(player);
player.teleport(Bukkit.getWorlds().get(0).getSpawnLocation().clone().add(0.5, 0, 0.5));
});
int count = CLICKED_COUNT.getOrDefault(player, -1);
if (count >= 0) {
if (count > 20) {
toReset.add(player);
return;
}
count = Math.min(count / 2, 30);
LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_CANCEL", player, ChatMessageType.ACTION_BAR, "§e" + BAR_EMPTY.substring(0, count) + "§7" + BAR_EMPTY.substring(count));
} else {
long time = System.currentTimeMillis() - START.get(player);
SimpleDateFormat format = new SimpleDateFormat(LobbySystem.getMessage().parse("JUMP_AND_RUN_TIME", player), Locale.ROOT);
String parsed = format.format(new Date(time));
LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_PROGRESS", player, ChatMessageType.ACTION_BAR, index + 1, points.size(), FAILS.get(player), parsed);
}
});
toReset.forEach(player -> {
reset(player);
player.teleport(Bukkit.getWorlds().get(0).getSpawnLocation().clone().add(0.5, 0, 0.5));
});
}, 1, 1);
}
public static boolean isPlayerInJumpAndRun(Player player) {
@@ -20,7 +20,7 @@
package de.steamwar.lobby.listener;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import de.steamwar.lobby.LobbySystem;
import org.bukkit.*;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.type.Stairs;
@@ -34,6 +34,7 @@ import org.bukkit.event.block.Action;
import org.bukkit.event.entity.EntityDismountEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scheduler.BukkitRunnable;
import java.util.HashSet;
import java.util.Set;
@@ -45,11 +46,15 @@ public class PlayerSeatListener implements Listener {
private Set<Location> seats = new HashSet<>();
@LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.MINUTES)
private static void keepArrows() {
for (Arrow arrow : world.getEntitiesByClass(Arrow.class)) {
arrow.setTicksLived(1);
}
static {
new BukkitRunnable() {
@Override
public void run() {
for (Arrow arrow : world.getEntitiesByClass(Arrow.class)) {
arrow.setTicksLived(1);
}
}
}.runTaskTimer(LobbySystem.getInstance(), 20 * 60, 20 * 60);
}
@EventHandler
@@ -0,0 +1,95 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.entity.Pose;
import org.bukkit.util.Vector;
// The player's body as a coordinate system: origin at the feet, up towards the head, forward out of the chest.
// Standing it turns with the body (as BodyMotion tracks it); gliding or swimming the body lies along the look direction.
@Getter
public class BodyFrame {
private static final Vector WORLD_UP = new Vector(0, 1, 0);
private static final double HALF_BODY = 0.9;
private final Location origin;
private final Vector right;
private final Vector up;
private final Vector forward;
// Reference directions of rings around the body; they do not turn with the body
private final Vector ringStart;
private final Vector ringSide;
private BodyFrame(Location origin, Vector right, Vector up, Vector forward) {
this.origin = origin;
this.right = right;
this.up = up;
this.forward = forward;
// World east projected onto the plane around the body, so a standing ring matches the world ring
Vector start = new Vector(1, 0, 0).subtract(up.clone().multiply(up.getX()));
if (start.lengthSquared() < 1e-6) {
start = new Vector(0, 0, 1).subtract(up.clone().multiply(up.getZ()));
}
this.ringStart = start.normalize();
this.ringSide = up.clone().crossProduct(ringStart);
}
public static BodyFrame of(Player player) {
Location location = player.getLocation();
Pose pose = player.getPose();
if (pose == Pose.FALL_FLYING || pose == Pose.SWIMMING) {
Vector up = location.getDirection().normalize();
Vector right = up.clone().crossProduct(WORLD_UP);
if (right.lengthSquared() < 1e-6) {
right = horizontal(BodyMotion.yaw(player)).crossProduct(WORLD_UP);
}
right.normalize();
Vector forward = up.clone().crossProduct(right).normalize();
// The lying body is centred in its short hitbox, its feet lie behind the centre
Location origin = location.clone().add(0, player.getHeight() / 2, 0).subtract(up.clone().multiply(HALF_BODY));
return new BodyFrame(origin, right, up, forward);
}
Vector forward = horizontal(BodyMotion.yaw(player));
Vector right = forward.clone().crossProduct(WORLD_UP).normalize();
return new BodyFrame(location, right, WORLD_UP.clone(), forward);
}
private static Vector horizontal(float yaw) {
double radians = Math.toRadians(yaw);
return new Vector(-Math.sin(radians), 0, Math.cos(radians));
}
public Vector local(double right, double up, double forward) {
return this.right.clone().multiply(right)
.add(this.up.clone().multiply(up))
.add(this.forward.clone().multiply(forward));
}
// A point on a ring around the up axis; angle 0 and the turning direction match the world rings
public Vector ring(double distance, double angle) {
return ringStart.clone().multiply(Math.cos(angle) * distance).add(ringSide.clone().multiply(Math.sin(angle) * distance));
}
}
@@ -0,0 +1,165 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import lombok.experimental.UtilityClass;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.Map;
import java.util.WeakHashMap;
// How a player's body moves: the direction it faces, worked out the way the Minecraft client turns the body, because
// the server's own body rotation for players does not follow what other players see, and how fast it moves.
// Walking turns the body towards the walking direction; standing still it only turns once the head looks more than
// 50 degrees to the side. It also notices jumps, landings and starting to sneak, which reward touches react to.
@UtilityClass
public class BodyMotion {
private final float MAX_HEAD_TURN = 50;
private final double MOVING = 0.0025;
private final Map<Player, State> states = new WeakHashMap<>();
// Counts server ticks, so events can be matched to the tick they happened in
private long tick;
private static class State {
private float body;
private double x;
private double y;
private double z;
// Blocks per tick, smoothed so single steps do not jerk the wings
private double speed;
private boolean onGround = true;
private boolean sneaking;
// Highest point of the current time in the air
private double peakY;
private long jumpTick = Long.MIN_VALUE;
private long landTick = Long.MIN_VALUE;
private double landFall;
private long sneakTick = Long.MIN_VALUE;
}
// Called once per server tick before the players are updated
public void nextTick() {
tick++;
}
// Called once per server tick for every online player
@SuppressWarnings("deprecation")
public void tick(Player player) {
Location location = player.getLocation();
State state = states.get(player);
if (state == null) {
state = new State();
state.body = location.getYaw();
state.x = location.getX();
state.y = location.getY();
state.z = location.getZ();
states.put(player, state);
return;
}
float head = location.getYaw();
double dx = location.getX() - state.x;
double dy = location.getY() - state.y;
double dz = location.getZ() - state.z;
state.x = location.getX();
state.y = location.getY();
state.z = location.getZ();
state.speed = state.speed * 0.8 + Math.sqrt(dx * dx + dy * dy + dz * dz) * 0.2;
boolean onGround = player.isOnGround();
if (state.onGround && !onGround && dy > 0) {
state.jumpTick = tick;
}
if (!onGround) {
state.peakY = state.onGround ? location.getY() : Math.max(state.peakY, location.getY());
} else if (!state.onGround) {
state.landTick = tick;
state.landFall = state.peakY - location.getY();
}
state.onGround = onGround;
if (player.isSneaking() && !state.sneaking) {
state.sneakTick = tick;
}
state.sneaking = player.isSneaking();
float target = state.body;
if (dx * dx + dz * dz > MOVING) {
target = (float) Math.toDegrees(Math.atan2(dz, dx)) - 90;
// Walking backwards keeps the body facing forwards
float away = Math.abs(wrap(head) - target);
if (away > 95 && away < 265) {
target -= 180;
}
}
state.body += wrap(target - state.body) * 0.3f;
float headTurn = wrap(head - state.body);
if (Math.abs(headTurn) > MAX_HEAD_TURN) {
state.body += headTurn - Math.signum(headTurn) * MAX_HEAD_TURN;
}
}
// The tracked body direction in degrees, or the head direction for players not tracked yet
public float yaw(Player player) {
State state = states.get(player);
return state == null ? player.getLocation().getYaw() : state.body;
}
// Smoothed speed in blocks per tick; sprinting is about 0.28, fast elytra flight well above 1
public double speed(Player player) {
State state = states.get(player);
return state == null ? 0 : state.speed;
}
public long currentTick() {
return tick;
}
public boolean jumped(Player player) {
State state = states.get(player);
return state != null && state.jumpTick == tick;
}
// Ticks since the last jump, or a large number if there was none
public long ticksSinceJump(Player player) {
State state = states.get(player);
return state == null || state.jumpTick == Long.MIN_VALUE ? Long.MAX_VALUE : tick - state.jumpTick;
}
// How far the player fell if they landed this tick, otherwise -1
public double landed(Player player) {
State state = states.get(player);
return state != null && state.landTick == tick ? state.landFall : -1;
}
public boolean startedSneaking(Player player) {
State state = states.get(player);
return state != null && state.sneakTick == tick;
}
private float wrap(float degrees) {
float wrapped = degrees % 360;
if (wrapped >= 180) wrapped -= 360;
if (wrapped < -180) wrapped += 360;
return wrapped;
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import de.steamwar.lobby.particle.slot.ParticleEntry;
import de.steamwar.lobby.particle.slot.ParticleSet;
import java.util.ArrayList;
import java.util.List;
public class CompiledSet {
private final List<ParticleElement> always = new ArrayList<>();
private final List<ParticleElement> move = new ArrayList<>();
public static CompiledSet of(ParticleElement... particleElements) {
CompiledSet compiledSet = new CompiledSet();
for (ParticleElement particleElement : particleElements) {
compiledSet.add(particleElement);
}
return compiledSet;
}
// Entries whose design or particle type is not unlocked are skipped, not removed
public static CompiledSet of(ParticleSet particleSet, UnlockContext context) {
CompiledSet compiledSet = new CompiledSet();
for (ParticleEntry entry : particleSet.getEntries()) {
if (entry.isUnlocked(context)) {
compiledSet.add(entry.build());
}
}
return compiledSet;
}
public void add(ParticleElement particleElement) {
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
always.add(particleElement);
} else {
move.add(particleElement);
}
}
public boolean hasAlways() {
return !always.isEmpty();
}
public boolean hasMove() {
return !move.isEmpty();
}
public void tickAlways(ParticleTickData particleTickData) {
always.forEach(particleElement -> particleElement.tick(particleTickData));
}
public void tickMove(ParticleTickData particleTickData) {
move.forEach(particleElement -> particleElement.tick(particleTickData));
}
}
@@ -0,0 +1,160 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// Wing shapes drawn from lines and curves instead of images, so their number of points can be chosen.
// Coordinates use the same units as the wing images: x is sideways from the spine, y upwards, about 11 by 13.
public enum DrawnWings {
// An arm bone along the top, fingers spreading from the wrist and skin hanging between them
BAT {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
lines.add(curve(p(0.5, 7), p(3, 9.5), p(6, 11), p(11, 12.5)));
lines.add(curve(p(6, 11), p(7, 4)));
lines.add(curve(p(6, 11), p(9, 6)));
lines.add(curve(p(6, 11), p(10.8, 9)));
lines.add(curve(p(0.5, 5), p(3.5, 4.6), p(5.5, 3.4), p(7, 4), p(7.6, 5.4), p(9, 6), p(9.6, 7.6), p(10.8, 9), p(11.2, 10.8), p(11, 12.5)));
return lines;
}
},
// The butterfly curve r = e^cos(t) - 2 cos(4t), which already holds both wings; its small loops around the body are left out
BUTTERFLY {
@Override
List<List<Vector>> rightWing() {
List<List<Vector>> lines = new ArrayList<>();
List<Vector> current = new ArrayList<>();
for (int i = 0; i <= 400; i++) {
double t = Math.PI * i / 400;
double r = Math.exp(Math.cos(t)) - 2 * Math.cos(4 * t);
if (Math.abs(r) < 0.9) {
if (current.size() > 1) lines.add(current);
current = new ArrayList<>();
continue;
}
current.add(p(Math.sin(t) * r * 3, Math.cos(t) * r * 2 + 6));
}
if (current.size() > 1) lines.add(current);
return lines;
}
},
// Two thin rounded pairs, a long one above and a short one below
FAIRY {
@Override
List<List<Vector>> rightWing() {
return List.of(ellipse(5.2, 9.3, 5, 1.9, 35), ellipse(3.8, 4.6, 3.6, 1.5, -30));
}
},
;
// The right wing as lines; the left wing mirrors it
abstract List<List<Vector>> rightWing();
private static final Map<String, WingDesign> CACHE = new HashMap<>();
// The shapes are drawn with the shoulder joint at y = 7; moving it to 3.3 puts it at the shoulder blades
// (about 1.3 blocks above the feet at the default size) instead of at head height
private static final double SHOULDER_SHIFT = 7 - 3.3;
// Both wings with the given number of points per wing, spread evenly along the lines
public WingDesign withDetail(int pointsPerWing) {
return CACHE.computeIfAbsent(name() + "@" + pointsPerWing, key -> {
List<Vector> right = sample(rightWing(), pointsPerWing);
right.forEach(point -> point.setY(point.getY() - SHOULDER_SHIFT));
List<Vector> both = new ArrayList<>(right);
right.forEach(point -> both.add(new Vector(-point.getX(), point.getY(), 0)));
return new WingDesign.WingDesignImpl(both.toArray(new Vector[0]));
});
}
private static List<Vector> sample(List<List<Vector>> lines, int count) {
double total = lines.stream().mapToDouble(DrawnWings::length).sum();
List<Vector> points = new ArrayList<>();
for (int i = 0; i < count; i++) {
double distance = total * (i + 0.5) / count;
for (List<Vector> line : lines) {
double length = length(line);
if (distance <= length) {
points.add(at(line, distance));
break;
}
distance -= length;
}
}
return points;
}
private static double length(List<Vector> line) {
double length = 0;
for (int i = 1; i < line.size(); i++) length += line.get(i).distance(line.get(i - 1));
return length;
}
private static Vector at(List<Vector> line, double distance) {
for (int i = 1; i < line.size(); i++) {
double segment = line.get(i).distance(line.get(i - 1));
if (distance <= segment) {
return line.get(i - 1).clone().add(line.get(i).clone().subtract(line.get(i - 1)).multiply(segment == 0 ? 0 : distance / segment));
}
distance -= segment;
}
return line.get(line.size() - 1).clone();
}
private static Vector p(double x, double y) {
return new Vector(x, y, 0);
}
// A smooth curve through the given points (Catmull-Rom), as many short segments
private static List<Vector> curve(Vector... points) {
List<Vector> result = new ArrayList<>();
for (int i = 0; i < points.length - 1; i++) {
Vector p0 = points[Math.max(0, i - 1)], p1 = points[i], p2 = points[i + 1], p3 = points[Math.min(points.length - 1, i + 2)];
for (int step = 0; step < 16; step++) {
double t = step / 16.0, t2 = t * t, t3 = t2 * t;
result.add(p1.clone().multiply(2)
.add(p2.clone().subtract(p0).multiply(t))
.add(p0.clone().multiply(2).subtract(p1.clone().multiply(5)).add(p2.clone().multiply(4)).subtract(p3).multiply(t2))
.add(p1.clone().multiply(3).subtract(p0).subtract(p2.clone().multiply(3)).add(p3).multiply(t3))
.multiply(0.5));
}
}
result.add(points[points.length - 1].clone());
return result;
}
private static List<Vector> ellipse(double centerX, double centerY, double radiusX, double radiusY, double tiltDegrees) {
double tilt = Math.toRadians(tiltDegrees);
List<Vector> result = new ArrayList<>();
for (int i = 0; i <= 64; i++) {
double a = 2 * Math.PI * i / 64;
double x = Math.cos(a) * radiusX, y = Math.sin(a) * radiusY;
result.add(p(centerX + x * Math.cos(tilt) - y * Math.sin(tilt), centerY + x * Math.sin(tilt) + y * Math.cos(tilt)));
}
return result;
}
}
@@ -1,96 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import de.steamwar.inventory.SWItem;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.sql.SteamwarUser;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class ParticleData {
private final Material material;
private final String name;
private final List<String> attributes = new ArrayList<>();
@Getter
private final ParticleRequirement requirement;
@Getter
private final ParticleElement particleElement;
public ParticleData(Material material, String name, ParticleElement particleElement) {
this.material = material;
this.name = name;
this.requirement = ParticleRequirement.NO_REQUIRMENT;
this.particleElement = particleElement;
particleElement.aggregateAttributes(this);
}
public ParticleData(Material material, String name, ParticleRequirement requirement, ParticleElement particleElement) {
this.material = material;
this.name = name;
this.requirement = requirement;
this.particleElement = particleElement;
particleElement.aggregateAttributes(this);
}
public ParticleData add(String attribute) {
attributes.add(attribute);
return this;
}
public SWItem toSWItem(Player player, SteamwarUser user, Set<Integer> eventTeilnahme, String eggHuntConfig) {
String translatedName;
try {
translatedName = LobbySystem.getMessage().parse(name, player);
} catch (Exception e) {
translatedName = name;
}
List<String> lore = new ArrayList<>();
lore.add("");
if (!attributes.isEmpty()) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_ATTRIBUTE", player));
attributes.forEach(attribute -> lore.add(LobbySystem.getMessage().parse(attribute, player)));
lore.add("");
}
String unlockedBy = requirement.getRequirementName(player);
if (unlockedBy != null) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY", player));
lore.add(unlockedBy);
lore.add("");
}
if (requirement.test(user, eventTeilnahme, eggHuntConfig)) {
lore.add(LobbySystem.getMessage().parse("PARTICLE_SELECT", player));
} else {
lore.add(LobbySystem.getMessage().parse("PARTICLE_LOCKED", player));
}
return new SWItem(material, translatedName, lore, false, clickType -> {
});
}
}
@@ -19,9 +19,7 @@
package de.steamwar.lobby.particle;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
@@ -47,23 +45,18 @@ public interface ParticleElement {
return new Particle.DustOptions(Color.fromRGB(r, g, b), (float) (RANDOM.nextFloat() / 2 + 0.5));
}
default void display(Location location, Player root, boolean onlySelf, boolean onlyOther, Consumer<Player> consumer) {
if (onlySelf) {
default void display(ParticleTickData particleTickData, Consumer<Player> consumer) {
Player root = particleTickData.getPlayer();
if (particleTickData.isOnlySelf()) {
consumer.accept(root);
return;
}
Bukkit.getOnlinePlayers().forEach(player -> {
if (onlyOther && player == root) {
return;
for (Player player : particleTickData.getViewers()) {
if (particleTickData.isOnlyOthers() && player == root) {
continue;
}
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
consumer.accept(player);
}
});
}
default void aggregateAttributes(ParticleData particleData) {
consumer.accept(player);
}
}
default ParticleTickType tickType() {
@@ -1,145 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.lobby.particle;
import de.steamwar.inventory.SWItem;
import de.steamwar.inventory.SWListInv;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.particle.particles.*;
import de.steamwar.lobby.particle.particles.custom.CustomEasterParticle;
import de.steamwar.lobby.particle.particles.custom.CustomPlayerParticle;
import de.steamwar.lobby.particle.particles.custom.CustomTeamParticle;
import de.steamwar.lobby.special.easter.EggHunt;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.Event;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.TeamTeilnahme;
import de.steamwar.sql.UserConfig;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@UtilityClass
public class ParticleInventory {
private final Class<?>[] PARTICLES = {
PlayerParticle.class,
CustomPlayerParticle.class,
TeamParticle.class,
CustomTeamParticle.class,
RainCloudParticle.class,
ServerTeamParticle.class,
EventParticle.class,
EventParticlePlacement.class,
EventParticleParticipation.class,
EasterParticle.class,
CustomEasterParticle.class,
};
public String convertToString(ParticleEnum particleEnum) {
return particleEnum.getClass().getSimpleName() + "@" + ((Enum<?>) particleEnum).name();
}
public ParticleEnum convertFromString(String string) {
String[] split = string.split("@");
Class<?> clazz = null;
for (Class<?> aClass : PARTICLES) {
if (aClass.getSimpleName().equals(split[0])) {
clazz = aClass;
break;
}
}
if (clazz != null) {
try {
return (ParticleEnum) Enum.valueOf((Class<? extends Enum>) clazz, split[1]);
} catch (Exception e) {
return null;
}
}
return null;
}
public void openInventory(Player player, boolean onlyUnlocked) {
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
Set<Integer> events = user.getTeam() == 0 ? new HashSet<>() : TeamTeilnahme.getEvents(user.getTeam()).stream().map(Event::getEventID).collect(Collectors.toSet());
String eggHuntConfig = UserConfig.getConfig(user.getId(), EggHunt.EGG_HUNT_CONFIG_KEY);
AtomicBoolean hasOneLocked = new AtomicBoolean(false);
List<SWListInv.SWListEntry<ParticleEnum>> particleList = new ArrayList<>();
for (Class<?> clazz : PARTICLES) {
addParticles(particleList, (ParticleEnum[]) clazz.getEnumConstants(), player, onlyUnlocked, hasOneLocked, user, events, eggHuntConfig);
}
for (int i = 0; i < particleList.size() % 45; i++) {
particleList.add(new SWListInv.SWListEntry<>(new SWItem(Material.BLACK_STAINED_GLASS_PANE, " "), null));
}
SWListInv<ParticleEnum> particleSWListInv = new SWListInv<>(player, LobbySystem.getMessage().parse("PARTICLE_INVENTORY", player), false, particleList, (clickType, particle) -> {
if (particle == null) return;
lobbyPlayer.setParticle(particle);
player.closeInventory();
if (ParticleListener.disabled(player)) {
LobbySystem.getMessage().send("PARTICLE_DEACTIVATED", player);
}
});
particleSWListInv.setItem(48, Material.BARRIER, LobbySystem.getMessage().parse("PARTICLE_DESELECT", player), new ArrayList<>(), false, clickType -> {
lobbyPlayer.setParticle(null);
});
if (!hasOneLocked.get()) {
particleSWListInv.setItem(50, Material.GRAY_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_HAS_ALL", player), new ArrayList<>(), false, clickType -> {
});
} else if (onlyUnlocked) {
particleSWListInv.setItem(50, Material.LIME_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_ALL", player), new ArrayList<>(), false, clickType -> {
openInventory(player, false);
});
} else {
particleSWListInv.setItem(50, Material.RED_BANNER, LobbySystem.getMessage().parse("PARTICLE_SHOW_UNLOCKED", player), new ArrayList<>(), false, clickType -> {
openInventory(player, true);
});
}
particleSWListInv.open();
}
private void addParticles(List<SWListInv.SWListEntry<ParticleEnum>> particleList, ParticleEnum[] particleEnums, Player player, boolean onlyUnlocked, AtomicBoolean hasOneLocked, SteamwarUser user, Set<Integer> events, String eggHuntConfig) {
for (ParticleEnum particle : particleEnums) {
ParticleData particleData = particle.getParticle();
SWItem swItem = particleData.toSWItem(player, user, events, eggHuntConfig);
if (particleData.getRequirement().test(user, events, eggHuntConfig)) {
particleList.add(new SWListInv.SWListEntry<>(swItem, particle));
} else {
hasOneLocked.set(true);
if (!onlyUnlocked) {
particleList.add(new SWListInv.SWListEntry<>(swItem, null));
}
}
}
for (int i = 0; i < particleList.size() % 9; i++) {
particleList.add(new SWListInv.SWListEntry<>(new SWItem(Material.BLACK_STAINED_GLASS_PANE, " "), null));
}
}
}
@@ -20,23 +20,28 @@
package de.steamwar.lobby.particle;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedTimer;
import de.steamwar.lobby.LobbySystem;
import de.steamwar.lobby.jumpandrun.JumpAndRun;
import de.steamwar.lobby.listener.PlayerSpawn;
import de.steamwar.lobby.particle.gui.ParticleMenu;
import de.steamwar.lobby.util.LobbyPlayer;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Linked
@@ -49,24 +54,22 @@ public class ParticleListener implements Listener {
private static Map<Player, Integer> movingPlayers = new HashMap<>();
@LinkedTimer(delay = 0, period = 1)
private void tickParticles() {
movingPlayers.replaceAll((player, integer) -> integer - 1);
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
deg += 0.1;
if (deg > 360) deg = 0;
Bukkit.getOnlinePlayers().forEach(player -> {
if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
ParticleEnum particle = lobbyPlayer.getParticle();
if (particle == null) return;
ParticleData particleData = particle.getParticle();
ParticleElement particleElement = particleData.getParticleElement();
if (particleElement.tickType() == ParticleTickType.ALWAYS) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player)));
}
});
public ParticleListener() {
Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> {
movingPlayers.replaceAll((player, integer) -> integer - 1);
movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0);
deg += 0.1;
if (deg > 360) deg = 0;
BodyMotion.nextTick();
Bukkit.getOnlinePlayers().forEach(BodyMotion::tick);
Bukkit.getOnlinePlayers().forEach(player -> {
if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
if (compiledSet == null || !compiledSet.hasAlways()) return;
compiledSet.tickAlways(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player), viewers(player)));
});
}, 0, 1);
}
@EventHandler(priority = EventPriority.NORMAL)
@@ -76,7 +79,7 @@ public class ParticleListener implements Listener {
if (!PlayerSpawn.PARTICLE.equals(event.getItem())) return;
event.setCancelled(true);
ParticleInventory.openInventory(player, true);
ParticleMenu.open(player);
}
@EventHandler
@@ -84,20 +87,23 @@ public class ParticleListener implements Listener {
Player player = event.getPlayer();
if (disabled(player)) return;
if (JumpAndRun.isPlayerInJumpAndRun(player)) return;
LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId());
ParticleEnum particle = lobbyPlayer.getParticle();
if (particle == null) return;
CompiledSet compiledSet = LobbyPlayer.getLobbyPlayer(player.getUniqueId()).getCompiledSet();
if (compiledSet == null) return;
ParticleData particleData = particle.getParticle();
ParticleElement particleElement = particleData.getParticleElement();
if (particleElement.tickType() == ParticleTickType.MOVE) {
particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, true));
if (compiledSet.hasMove()) {
compiledSet.tickMove(new ParticleTickData(player.getWorld(), player, deg, true, viewers(player)));
}
if (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getY() != event.getTo().getY() || event.getFrom().getZ() != event.getTo().getZ()) {
movingPlayers.put(player, 5);
}
}
// Ranks may have changed since the last session, which can lock or unlock the active slot
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
LobbyPlayer.getLobbyPlayer(event.getPlayer()).recompile();
}
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
movingPlayers.remove(event.getPlayer());
@@ -110,6 +116,18 @@ public class ParticleListener implements Listener {
}
}
private static List<Player> viewers(Player root) {
Location location = root.getLocation();
List<Player> viewers = new ArrayList<>();
for (Player player : root.getWorld().getPlayers()) {
int viewDistance = player.getClientViewDistance() * 16;
if (location.distanceSquared(player.getLocation()) <= viewDistance * viewDistance) {
viewers.add(player);
}
}
return viewers;
}
public static boolean disabled(Player player) {
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
if (Bukkit.getOnlinePlayers().size() > SERVER_TEAM_MAX_SIZE) return true;
@@ -39,7 +39,13 @@ public interface ParticleRequirement {
String getRequirementName(Player player);
boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig);
// Whether the player meets this requirement
boolean matches(UnlockContext context);
// Administrators pass every requirement; lore still names the regular requirement
default boolean test(UnlockContext context) {
return context.getUser().hasPerm(UserPerm.ADMINISTRATION) || matches(context);
}
ParticleRequirement SERVER_TEAM = new ParticleRequirement() {
@Override
@@ -48,8 +54,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.hasPerm(UserPerm.TEAM);
public boolean matches(UnlockContext context) {
return context.getUser().hasPerm(UserPerm.TEAM);
}
};
@@ -60,21 +66,23 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
public boolean matches(UnlockContext context) {
return true;
}
};
ParticleRequirement HAS_TEAM = new ParticleRequirement() {
// Does not also pass for the server team
ParticleRequirement TEAM_MEMBER = new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
return LobbySystem.getMessage().parse("PARTICLE_UNLOCKED_BY_TEAM", player);
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getTeam() != 0;
public boolean matches(UnlockContext context) {
return context.getUser().getTeam() != 0;
}
}.or(SERVER_TEAM);
};
ParticleRequirement EVENT_PARTICIPATION = new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
@@ -82,8 +90,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return !eventTeilname.isEmpty();
public boolean matches(UnlockContext context) {
return !context.getEvents().isEmpty();
}
}.or(SERVER_TEAM);
ParticleRequirement EGG_HUNT_EASY = _easterEggHuntDifficulty(EggDifficulty.EASY);
@@ -98,7 +106,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
public boolean matches(UnlockContext context) {
String eggHuntConfig = context.getEggHuntConfig();
if (eggHuntConfig == null) return false;
int count = 0;
for (int i = 0; i < eggHuntConfig.length(); i++) {
@@ -120,8 +129,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getTeam() == teamId;
public boolean matches(UnlockContext context) {
return context.getUser().getTeam() == teamId;
}
}.or(SERVER_TEAM);
}
@@ -135,8 +144,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return eventTeilname.contains(eventId);
public boolean matches(UnlockContext context) {
return context.getEvents().contains(eventId);
}
}.or(SERVER_TEAM);
}
@@ -152,9 +161,9 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
if (!eventTeilname.contains(eventId)) return false;
return teams.contains(user.getTeam());
public boolean matches(UnlockContext context) {
if (!teams.contains(context.getUser().getTeam())) return false;
return context.getEvents().contains(eventId);
}
}.or(SERVER_TEAM);
}
@@ -181,7 +190,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
public boolean matches(UnlockContext context) {
String eggHuntConfig = context.getEggHuntConfig();
if (eggHuntConfig == null) return false;
return eggs.stream().allMatch(id -> eggHuntConfig.length() > id && eggHuntConfig.charAt(id) == '1');
}
@@ -197,8 +207,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getId() == userId;
public boolean matches(UnlockContext context) {
return context.getUser().getId() == userId;
}
}.or(SERVER_TEAM);
}
@@ -212,8 +222,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getId() == userId;
public boolean matches(UnlockContext context) {
return context.getUser().getId() == userId;
}
}.or(SERVER_TEAM);
}
@@ -228,12 +238,31 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return user.getTeam() == teamId;
public boolean matches(UnlockContext context) {
return context.getUser().getTeam() == teamId;
}
}.or(SERVER_TEAM);
}
// Met by meeting any of the requirements; the lore lists them one per line
static ParticleRequirement anyOf(ParticleRequirement... requirements) {
return new ParticleRequirement() {
@Override
public String getRequirementName(Player player) {
return java.util.Arrays.stream(requirements).map(requirement -> requirement.getRequirementName(player)).filter(Objects::nonNull).collect(Collectors.joining("\n"));
}
@Override
public boolean matches(UnlockContext context) {
for (ParticleRequirement requirement : requirements) {
if (requirement.test(context)) return true;
}
return false;
}
};
}
// The alternative is checked first, as it is usually the cheap server team check
default ParticleRequirement or(ParticleRequirement particleRequirement) {
return new ParticleRequirement() {
@Override
@@ -242,8 +271,8 @@ public interface ParticleRequirement {
}
@Override
public boolean test(SteamwarUser user, Set<Integer> eventTeilname, String eggHuntConfig) {
return ParticleRequirement.this.test(user, eventTeilname, eggHuntConfig) || particleRequirement.test(user, eventTeilname, eggHuntConfig);
public boolean matches(UnlockContext context) {
return particleRequirement.test(context) || ParticleRequirement.this.test(context);
}
};
}
@@ -26,6 +26,8 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.util.List;
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@@ -36,8 +38,13 @@ public class ParticleTickData {
private Location location;
private final double deg;
private final boolean isMoving;
private final List<Player> viewers;
private boolean onlySelf = false;
private boolean onlyOthers = false;
// Set for entries placed relative to the body, null for world placement
private BodyFrame frame;
// Where a point sits along its shape, from 0 to 1; when set, gradients colour by it instead of by time
private Double shade;
public ParticleTickData withLocation(Location location) {
ParticleTickData particleTickData = copy();
@@ -45,6 +52,24 @@ public class ParticleTickData {
return particleTickData;
}
public ParticleTickData withFrame(BodyFrame frame) {
ParticleTickData particleTickData = copy();
particleTickData.frame = frame;
particleTickData.location = frame.getOrigin().clone();
return particleTickData;
}
public ParticleTickData withShade(double shade) {
ParticleTickData particleTickData = copy();
particleTickData.shade = shade;
return particleTickData;
}
// The gradient position for this tick: along the shape when a shade is set, otherwise moving with time
public double gradientPosition() {
return shade != null ? Math.min(shade, 0.9999) * 360 : (deg * 10) % 360;
}
public ParticleTickData onlySelf() {
ParticleTickData particleTickData = copy();
particleTickData.onlySelf = true;
@@ -65,6 +90,6 @@ public class ParticleTickData {
}
public ParticleTickData copy() {
return new ParticleTickData(world, player, location, deg, isMoving, onlySelf, onlyOthers);
return new ParticleTickData(world, player, location, deg, isMoving, viewers, onlySelf, onlyOthers, frame, shade);
}
}

Some files were not shown because too many files have changed in this diff Show More