Compare commits

..
Author SHA1 Message Date
YoyoNow f63390915b Add glyph flight for enchant particles and blend team links
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
YoyoNow bfbf98c1d6 Fix GUI.schemDialog for overriden Public list 2026-10-23 11:05:28 +02:00
YoyoNow c4b46e4431 Add override for Publics in GameModeConfig.Schematic 2026-10-21 08:22:05 +02:00
YoyoNow c238b9bdf2 Improve CheckCommand 2026-10-21 08:02:05 +02:00
YoyoNow fec4007120 Merge pull request 'Fixed Replay not starting for Normal Users' (#510) from 12312/SteamWar:main into main
Reviewed-on: SteamWar/SteamWar#510
2026-10-20 19:37:39 +02:00
12312 11798c3ec0 Fixed Replay not starting for Normal Users
Fixed Replay not starting for Normal Users.
2026-10-19 22:19:33 +02:00
YoyoNow 9935ab785d Fix NoClipCommand 2026-10-19 11:21:15 +02:00
YoyoNow 8e3e71dd96 CLine: render as a single display shown by view distance
The line no longer needs start, middle and end parts that were moved
around the player. It is one display that is visible for a player as soon
as any chunk the line passes through is in their view distance.
REntity#canView now receives the chunk positions of player and entity.
2026-10-19 10:41:34 +02:00
YoyoNow 3fe2e006b9 REntityServer: use per-entity view range instead of chunk view distance
Entities now decide per player whether they are visible. REntity defaults
to the player view radius, RDisplay derives it from its viewRange (1 = 64
blocks) capped at the client view distance, so values like Float.MAX_VALUE
stay cheap. Changing the viewRange re-evaluates the viewers. Fixes #488
2026-10-19 10:40:03 +02:00
YoyoNow 9d5fcc07e8 Only notify the owner's dev server about Bauwelt member changes
Dev servers are registered per owner (lowercase username), so there is
no need to wake every dev server.
2026-10-19 10:25:42 +02:00
YoyoNow 0c3e63d521 Restore BauweltMember cache and notify all dev servers on member changes
getBauMember never read its cache after the Exposed migration, so every
scoreboard render hit the DB (Too many connections, #479). Cache by
(bauwelt, member) incl. negative results, evict on addMember/remove, and
send BaumemberUpdatePacket to the Bauserver and all dev servers (also on
addmember). BauMemberUpdate clears the cache itself to not depend on
handler order.
2026-10-19 10:24:41 +02:00
YoyoNow 27d260a8d2 Delete CRIU checkpoint on /bau delete
/bau delete only removed the world folder. The checkpoint of the build
server stayed in place, so the next start restored the old JVM state
against a freshly copied world (superflat world, plugins inactive).
Extract ServerStarter#checkpointDir and delete the checkpoint together
with the world.
Fixes #438
2026-10-19 10:14:10 +02:00
YoyoNow 37f017b84c Improve action bar loading bar for server start 2026-10-19 09:59:02 +02:00
YoyoNow 21d0a4359d Update .gitignore 2026-10-19 09:31:38 +02:00
YoyoNow 8318d408b6 Fix REntity relative move rotation in 1.21
Pass yaw before pitch to ClientboundMoveEntityPacket.Rot/PosRot and stop
converting the already byte-encoded rotation a second time.
2026-10-19 09:27:27 +02:00
YoyoNow d14ddc4fab Fix lag from PistonCalculator at high TPS
Bundle piston order recalculations into one run every 200ms (wall clock,
independent of TPS) instead of one full recalculation and entity rebuild
per piston/place/break event. Tracked pistons that get moved are removed
from the visualization instead of being moved along.

Fixes #227
2026-10-19 09:19:41 +02:00
YoyoNow 5cc5529533 Reenable ReplayCommand 2026-10-19 09:07:08 +02:00
YoyoNow 380693a2b7 Fix REntity position desync and velocity in 1.21 replays
Send ClientboundEntityPositionSyncPacket instead of the teleport packet so
relative moves keep working afterwards, and pass rotation in degrees.
Pass the velocity unscaled to Vec3 instead of pre-multiplying by 8000.
Fixes #247
2026-10-19 09:03:52 +02:00
YoyoNow 69aef9dfb6 Fix ClassCastException for replay player equipment
PacketProcessor passes Bukkit EquipmentSlots to REntity#setItem, which cast
them to the NMS EquipmentSlot. Take the Bukkit slot and map it to the NMS
slot when building the equipment packet.
2026-10-19 08:38:42 +02:00
YoyoNow 097df865e5 Resend replay player tab entry when skin arrives
RPlayer requests the skin asynchronously, so the first tab list packet is
usually sent without textures and viewers already in range kept the default
skin. Re-send despawn/delist/list/spawn to the current viewers once the
skin response is received.
2026-10-19 08:21:52 +02:00
YoyoNow 230f8c521a Use PistonStructureResolver in PistonCalculator
Numbering now comes directly from the vanilla resolver instead of a
custom breadth-first search, so the order matches the real move order
(including backtracking through slime/honey chains). The display of
more than 12 blocks is removed; a blocked piston is shown in red.

Refs: #158
2026-10-18 20:09:10 +02:00
YoyoNow 253a975c73 Show no player hover for players of the public team 2026-10-18 20:01:24 +02:00
YoyoNow 12dd46176b Suggest /team info SW when clicking the SW prefix 2026-10-18 20:01:19 +02:00
YoyoNow 2111d72387 Suggest /team info when clicking a team prefix
Clicking the team prefix in the global/local chat or an event team
prefix in the fight chat puts /team info <shorthand> into the chat
input; the hover explains the click.
2026-10-18 20:01:15 +02:00
YoyoNow 3aa471c3ab Read global chat on fight servers after /chat global join 2026-10-16 11:29:02 +02:00
YoyoNow 47557d7bb8 Keep global chat readable after writing into it on fight servers
Writing /global <text> on a fight server shows the global chat until
the player leaves the server or runs /chat global leave, without
changing the default channel.
2026-10-16 11:17:30 +02:00
YoyoNow 62850b2e11 Let moderators read all fight chats automatically
/chat arenas join|leave lets moderators read the fight and team chats
of every arena and event spectate server; the choice is saved across
relogs. /chat <arena> leave excludes a single arena for the session.
2026-10-16 11:09:52 +02:00
YoyoNow 7f47d687d3 Hide /chat msg from players without moderation permission 2026-10-16 11:09:45 +02:00
YoyoNow 362e1cebff Name the fight channel Fight
The server name is already shown in the second line of the hover.
2026-10-16 11:09:41 +02:00
YoyoNow 39cacbd701 Show team on hover for @name mentions 2026-10-16 11:09:36 +02:00
YoyoNow 503261ce6d Name team chat channels TeamChat and ServerTeamChat 2026-10-16 11:09:30 +02:00
YoyoNow 8a86ebde83 Make DM names hoverable and channel icons clickable
- Both names in direct messages show their team on hover
- Clicking a channel icon suggests the command to write a single
  message into that channel
2026-10-16 11:09:24 +02:00
YoyoNow be915770b1 Show server name in local channel hover 2026-10-16 09:46:22 +02:00
YoyoNow 70c4225827 Merge pull request 'Chat channels' (#506) from feature/chat-channels into main
Reviewed-on: SteamWar/SteamWar#506
2026-10-16 09:19:54 +02:00
YoyoNow 904a850a3f Remove duplicated chat channel logic
- Shared send-or-switch helper for /global, /local, /tc and /stc
- Shared helpers for "is on server" and the player's current local
  channel
- Read fight infos from TablistManager instead of keeping a second map
- /chat resolves fight servers (arenas and spectate servers) by name in
  one place
- Reuse the global and participant formats instead of identical keys
2026-10-16 09:17:01 +02:00
YoyoNow 4c75879915 Keep spectators in fight chat and treat spectate servers as fight servers
- '+' from a spectator on a fight server stays in the fight chat
- Event spectate servers (static servers with a spectate port) use a
  local fight chat by default and hide the global chat like arenas
2026-10-16 09:07:58 +02:00
YoyoNow 65cc6119b0 Show default channel notice with its icon
The notice on join, on every default change and for /chat is now
"<icon> is your default channel" in green without the SteamWar prefix;
hovering the icon shows the channel name.
2026-10-16 09:00:41 +02:00
YoyoNow 975d42c17f Add team hovers to fight chat and make DMs non-default
- Names in the fight chat show the sender's team on hover like
  everywhere else; the Blue/Red team prefix is only hoverable in event
  fights, where it is the actual team
- Server team members using the SW prefix show the SteamWar team
- Direct messages can no longer be the default channel; /msg and /r
  require a message again
2026-10-16 08:59:26 +02:00
YoyoNow 317cb0adf8 Add channel icons and hovers to chat messages
Every chat message now starts with a fixed icon of its channel
(🌍 Global, ⌂ Local, ⚔ fight, ⚑ fight team, 👥 TC, ⚙ STC, ✉ DM),
highlighted yellow if it is the receiver's default channel. Hovering
the icon shows the channel name, hovering the sender shows their team
name and shorthand.
2026-10-16 08:48:30 +02:00
YoyoNow 4266a7f35b Merge pull request 'fix: Remove Help Text for /team server' (#499) from 12312/SteamWar:main into main
Reviewed-on: SteamWar/SteamWar#499
2026-10-15 20:25:47 +02:00
YoyoNow 26d5ff7aad Merge pull request 'Fix SmartPlace reversing debug stick' (#505) from fix/smartplace-debugstick into main
Reviewed-on: SteamWar/SteamWar#505
2026-10-15 20:09:49 +02:00
YoyoNow b21ff415a8 Show channel tags as plain gray prefix
Messages from a channel that is not the receiver's default are now
prefixed with the plain channel name in dark gray instead of brackets.
Direct messages get no tag, as their format already shows sender and
receiver.
2026-10-15 20:06:58 +02:00
YoyoNow 9fe05b59e4 Rework chat into Velocity-managed channels
Chat is now handled entirely by VelocityCore through channels with
separate readers and writers (Global, Local/Fight, Blue/Red fight team,
Team, ServerTeam, Alert and 1:1 direct messages). Each player has a
saved default channel; messages from other channels are tagged with
the channel name, and every default change is announced.

- /global, /local, /tc, /stc, /msg and /r switch the default channel
  when called without a message
- /chat <channel> switches, /chat <channel> join|leave toggles reading
  (saved); moderators can join arenas by name and write with
  /chat <channel> msg <text>
- '+' sends from global to local and from local/fight to the fight team
- Referees write yellow in the fight chat
- Subservers no longer receive chat; arenas get a record-only
  ChatRecordPacket so replays keep their chat
- Remove the chat listeners of SpigotCore, FightSystem, MissileWars,
  TowerRun and TNTLeague

Closes #460, #129
2026-10-15 19:55:25 +02:00
YoyoNow 2302f9aafb Fix SmartPlace reversing debug stick 2026-10-15 18:14:19 +02:00
YoyoNow d727982ce0 Fix AutoChecker inventory rules
Count every inventory block towards exactly one Schematic.Limited rule:
filled blocks towards a rule whose Content allows all their items, empty
blocks towards a rule without Content (unlimited if there is none). Each
rule's Amount now caps how many blocks it covers, instead of taking the
highest Amount of all rules for that block type.

Inventory blocks whose items fit no rule are now checked and reported,
items from different rules in one block are reported as mixed, and the
auto-fix trims or removes them accordingly.

Remove the hardcoded wargearseason26 wind charge handling and its 2048
limit, wind charges are configured via Limited rules now.

Log a warning for Limited rules that match no blocks, e.g. a misspelled
'Materials' key.

Fixes #458, #481, #502
2026-10-15 17:53:06 +02:00
YoyoNow 2b42f0d13c Remove YAPION from BauSystem_Main module 2026-09-29 14:26:57 +02:00
211 changed files with 10208 additions and 3453 deletions
+6
View File
@@ -7,8 +7,14 @@
!gradle/wrapper/gradle-wrapper.jar
steamwar.properties
# Kotlin
.kotlin
# IntelliJ IDEA
*.iml
.idea
!.idea/copyright
!.idea/runConfigurations
# VSCode
bin/
+1 -1
View File
@@ -47,7 +47,7 @@ dependencies {
compileOnly(libs.netty)
implementation(libs.luaj)
implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar"))
// implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar"))
}
widener {
@@ -20,8 +20,7 @@
package de.steamwar.bausystem;
import de.steamwar.bausystem.config.BauServer;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.bausystem.configplayer.ConfigConverter;
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;
@@ -36,7 +35,6 @@ import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.RegionSystem;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.bausystem.utils.TickManager;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.command.AbstractValidator;
import de.steamwar.command.SWCommandUtils;
import de.steamwar.core.CRIUSleepEvent;
@@ -112,9 +110,6 @@ public class BauSystem extends JavaPlugin implements Listener {
if (any instanceof PanzernAlgorithm) {
Panzern.add((PanzernAlgorithm) any);
}
if (any instanceof ConfigConverter) {
Config.addConfigConverter((ConfigConverter) any);
}
if (any instanceof BoundingBoxLoader) {
((BoundingBoxLoader) any).load();
}
@@ -160,9 +155,8 @@ public class BauSystem extends JavaPlugin implements Listener {
@Override
public void onDisable() {
linker.unlink();
WorldData.write();
RegionSystem.INSTANCE.save();
Config.getInstance().saveAll();
ConfigStore.saveAll();
}
private <T extends CommandSender> AbstractValidator<T, ?> validator(Permission permission) {
@@ -0,0 +1,47 @@
/*
* 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.config;
/**
* Converts a config of one version into the next version, e.g. ConfigV1 to
* ConfigV2. The converter for the second newest version converts into the
* current version. Converters are registered in the {@link ConfigStore}
* of the config.
*
* @param <F> the config version to convert from
* @param <T> the config version to convert to
*/
public interface ConfigConverter<F extends VersionedConfig, T extends VersionedConfig> {
/**
* @return the version this converter converts from
*/
int version();
/**
* Converts the config to the next version. If anything goes wrong do not
* silently exit this method, throw an Exception. The {@link ConfigStore}
* will deal with it.
*
* @param config the config to convert
* @return the converted config
*/
T convert(F config);
}
@@ -0,0 +1,364 @@
/*
* 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.config;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.steamwar.sql.UserConfig;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* Loads, converts and saves a versioned config stored as JSON. Older versions
* are converted with the registered {@link ConfigConverter}s, legacy formats
* that are not JSON can be read with {@link #legacy(Predicate, Function)}.
* <p>
* A store can be bound to where the config is stored:
* <ul>
* <li>{@link #userConfig(String)}: one config per player in the {@link UserConfig}, use {@link #get(Player)}</li>
* <li>{@link #file(File)}: one config in a file, use {@link #get()}</li>
* <li>unbound: use {@link #load(File)} and {@link #save(File, VersionedConfig)}</li>
* </ul>
*
* @param <T> the current config version
*/
public class ConfigStore<T extends VersionedConfig> {
private static final List<ConfigStore<?>> STORES = new ArrayList<>();
private final Class<T> type;
private final int version;
private final Supplier<T> defaultConfig;
private final Map<Integer, Class<? extends VersionedConfig>> jsonVersions = new HashMap<>();
private final Map<Integer, ConfigConverter<?, ?>> converters = new HashMap<>();
private final List<LegacyFormat> legacyFormats = new ArrayList<>();
private Gson gson = new Gson();
private Integer unversionedVersion = null;
private String userConfigKey = null;
private Predicate<T> removeIf = config -> false;
private final Map<UUID, T> playerConfigs = new ConcurrentHashMap<>();
private File file = null;
private File legacyFile = null;
private Function<String, ? extends VersionedConfig> legacyFileReader = null;
private T fileConfig = null;
private record LegacyFormat(Predicate<String> matches, Function<String, ? extends VersionedConfig> reader) {
}
public ConfigStore(Class<T> type, int version, Supplier<T> defaultConfig) {
this.type = type;
this.version = version;
this.defaultConfig = defaultConfig;
jsonVersions.put(version, type);
STORES.add(this);
}
public ConfigStore<T> gson(Gson gson) {
this.gson = gson;
return this;
}
/**
* Registers an older config version that is stored as JSON.
*/
public ConfigStore<T> jsonVersion(int version, Class<? extends VersionedConfig> type) {
jsonVersions.put(version, type);
return this;
}
public ConfigStore<T> converter(ConfigConverter<?, ?> converter) {
if (converters.putIfAbsent(converter.version(), converter) != null) {
throw new IllegalStateException("Duplicate converter for version " + converter.version() + " of " + type.getSimpleName());
}
return this;
}
/**
* Registers a legacy format that is not JSON, stored strings matching the predicate are read by the reader.
*/
public ConfigStore<T> legacy(Predicate<String> matches, Function<String, ? extends VersionedConfig> reader) {
legacyFormats.add(new LegacyFormat(matches, reader));
return this;
}
/**
* The version of stored JSON without a version field.
*/
public ConfigStore<T> unversioned(int version) {
this.unversionedVersion = version;
return this;
}
/**
* Binds this store to the {@link UserConfig} with the given key, one config per player.
*/
public ConfigStore<T> userConfig(String key) {
this.userConfigKey = key;
return this;
}
/**
* Removes the config from the {@link UserConfig} instead of saving it, if the predicate matches.
*/
public ConfigStore<T> removeIf(Predicate<T> removeIf) {
this.removeIf = removeIf;
return this;
}
/**
* Binds this store to a single file.
*/
public ConfigStore<T> file(File file) {
this.file = file;
return this;
}
/**
* Legacy file to read, if the file of this store does not exist yet.
*/
public ConfigStore<T> legacyFile(File legacyFile, Function<String, ? extends VersionedConfig> reader) {
this.legacyFile = legacyFile;
this.legacyFileReader = reader;
return this;
}
/**
* Reads a stored config and converts it to the current version.
*
* @throws RuntimeException if the config could not be read or converted
*/
public T read(String stored) {
for (LegacyFormat legacyFormat : legacyFormats) {
if (legacyFormat.matches().test(stored)) {
return update(legacyFormat.reader().apply(stored));
}
}
JsonObject json = JsonParser.parseString(stored).getAsJsonObject();
int storedVersion;
if (json.has("version")) {
storedVersion = json.get("version").getAsInt();
} else if (unversionedVersion != null) {
storedVersion = unversionedVersion;
json.addProperty("version", storedVersion);
} else {
throw new IllegalStateException("Missing version of " + type.getSimpleName());
}
Class<? extends VersionedConfig> storedType = jsonVersions.get(storedVersion);
if (storedType == null) {
throw new IllegalStateException("Unknown version " + storedVersion + " of " + type.getSimpleName());
}
return update(gson.fromJson(json, storedType));
}
public String write(T config) {
return gson.toJson(config);
}
/**
* Converts a config to the current version.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public T update(VersionedConfig config) {
while (config.getVersion() < version) {
ConfigConverter converter = converters.get(config.getVersion());
if (converter == null) {
throw new IllegalStateException("No converter found for version " + config.getVersion() + " of " + type.getSimpleName());
}
VersionedConfig converted = converter.convert(config);
if (converted.getVersion() <= config.getVersion()) {
throw new IllegalStateException("Version did not increase after conversion of version " + config.getVersion() + " of " + type.getSimpleName());
}
config = converted;
}
if (!type.isInstance(config)) {
throw new IllegalStateException("Version " + config.getVersion() + " is not the current version of " + type.getSimpleName());
}
return type.cast(config);
}
/**
* @return the config stored in the file, or the default config if the file is missing or empty
* @throws RuntimeException if the config could not be read or converted
*/
public T load(File file) throws IOException {
if (file.length() == 0) {
return defaultConfig.get();
}
return read(Files.readString(file.toPath(), StandardCharsets.UTF_8));
}
public void save(File file, T config) throws IOException {
Files.writeString(file.toPath(), write(config), StandardCharsets.UTF_8);
}
/**
* Get the config of a player, loads it from the {@link UserConfig} and converts it if necessary.
*/
public T get(Player player) {
requireUserConfig();
return playerConfigs.computeIfAbsent(player.getUniqueId(), this::loadPlayer);
}
/**
* Save the config of a player, this does not unload the config.
*/
public void save(Player player) {
requireUserConfig();
T config = playerConfigs.get(player.getUniqueId());
if (config != null) {
savePlayer(player.getUniqueId(), config);
}
}
/**
* Save and unload the config of a player.
*/
public void unload(Player player) {
if (userConfigKey == null) return;
T config = playerConfigs.remove(player.getUniqueId());
if (config != null) {
savePlayer(player.getUniqueId(), config);
}
}
private T loadPlayer(UUID uuid) {
String stored = UserConfig.getConfig(uuid, userConfigKey);
if (stored == null) {
return defaultConfig.get();
}
try {
return read(stored);
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not load " + type.getSimpleName() + " of " + uuid, e);
return defaultConfig.get();
}
}
private void savePlayer(UUID uuid, T config) {
if (removeIf.test(config)) {
UserConfig.removePlayerConfig(uuid, userConfigKey);
} else {
UserConfig.updatePlayerConfig(uuid, userConfigKey, write(config));
}
}
private void requireUserConfig() {
if (userConfigKey == null) {
throw new IllegalStateException(type.getSimpleName() + " is not stored in the UserConfig");
}
}
/**
* Get the config of the file of this store, loads it and converts it if necessary.
*/
public T get() {
requireFile();
if (fileConfig == null) {
fileConfig = loadFile();
save();
}
return fileConfig;
}
/**
* Save the config to the file of this store, if it was loaded.
*/
public void save() {
requireFile();
if (fileConfig == null) return;
try {
save(file, fileConfig);
} catch (IOException e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not write " + file, e);
}
}
private T loadFile() {
try {
if (file.length() != 0) {
return load(file);
}
if (legacyFile != null && legacyFile.length() != 0) {
return update(legacyFileReader.apply(Files.readString(legacyFile.toPath(), StandardCharsets.UTF_8)));
}
return defaultConfig.get();
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not load " + file + ", using default " + type.getSimpleName(), e);
backupBrokenFile();
return defaultConfig.get();
}
}
private void backupBrokenFile() {
if (file.length() == 0) return;
try {
Files.copy(file.toPath(), new File(file.getParentFile(), file.getName() + ".broken").toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not backup broken " + file, e);
}
}
private void requireFile() {
if (file == null) {
throw new IllegalStateException(type.getSimpleName() + " is not bound to a file");
}
}
static void unloadAll(Player player) {
STORES.forEach(store -> store.unload(player));
}
/**
* Saves every loaded config and unloads the configs of the players.
*/
public static void saveAll() {
for (ConfigStore<?> store : STORES) {
if (store.userConfigKey != null) {
store.playerConfigs.forEach((uuid, config) -> saveUnchecked(store, uuid, config));
store.playerConfigs.clear();
}
if (store.file != null) {
store.save();
}
}
}
@SuppressWarnings("unchecked")
private static <T extends VersionedConfig> void saveUnchecked(ConfigStore<T> store, UUID uuid, Object config) {
store.savePlayer(uuid, (T) config);
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* 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
@@ -17,21 +17,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.core.events;
package de.steamwar.bausystem.config;
import de.steamwar.core.Core;
import de.steamwar.core.CRIUSleepEvent;
import de.steamwar.linkage.Linked;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerQuitEvent;
/**
* Saves and unloads the configs of the {@link ConfigStore}s bound to the UserConfig
* when a player quits, configs are loaded on first use.
*/
@Linked
public class ChattingEvent implements Listener {
public class ConfigStoreListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onChat(AsyncPlayerChatEvent event) {
event.setCancelled(true);
Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage());
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
ConfigStore.unloadAll(event.getPlayer());
}
@EventHandler
public void onCRIUSleep(CRIUSleepEvent event) {
ConfigStore.saveAll();
}
}
@@ -0,0 +1,29 @@
/*
* 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.config;
/**
* Common interface of every version of a config stored by a {@link ConfigStore},
* the current version (e.g. Config) and every older version (ConfigV1, ConfigV2, ...).
*/
public interface VersionedConfig {
int getVersion();
}
@@ -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.bausystem.config.legacy;
import lombok.experimental.UtilityClass;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Deserializes {@link ItemStack}s that were serialized by YAPION, read with {@link YapionReader}.
*/
@UtilityClass
@Deprecated
public class YapionItemDeserializer {
private static final String TYPE_IDENTIFIER = "@type";
public ItemStack deserializeItem(Object yapion) {
if (yapion == null) return null;
Object result = deserialize(yapion);
if (result instanceof ItemStack itemStack) return itemStack;
throw new IllegalArgumentException("Not an ItemStack: " + result);
}
private Object deserialize(Object yapion) {
if (yapion instanceof List<?> list) {
List<Object> result = new ArrayList<>();
list.forEach(o -> result.add(deserialize(o)));
return result;
}
if (yapion instanceof YapionReader.YapionMap map) {
Map<Object, Object> result = new LinkedHashMap<>();
map.entries().forEach((key, value) -> result.put(deserialize(key), deserialize(value)));
return result;
}
if (yapion instanceof Map<?, ?> object) {
return deserializeObject(object);
}
return yapion;
}
private Object deserializeObject(Map<?, ?> object) {
Map<String, Object> data = new LinkedHashMap<>();
object.forEach((key, value) -> {
if (!TYPE_IDENTIFIER.equals(key)) data.put((String) key, deserialize(value));
});
if (!(object.get(TYPE_IDENTIFIER) instanceof String type)) {
return data;
}
Class<?> javaType = javaUtilClass(type);
if (javaType != null && Collection.class.isAssignableFrom(javaType)) {
return data.getOrDefault("values", new ArrayList<>());
}
if (javaType != null && Map.class.isAssignableFrom(javaType)) {
return data.containsKey("values") ? data.get("values") : data;
}
data.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, type.equals("org.bukkit.inventory.meta.ItemMeta") ? "ItemMeta" : type);
return ConfigurationSerialization.deserializeObject(data);
}
private Class<?> javaUtilClass(String type) {
if (!type.startsWith("java.util.")) return null;
try {
return Class.forName(type);
} catch (ClassNotFoundException e) {
return null;
}
}
}
@@ -0,0 +1,323 @@
/*
* 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.config.legacy;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Minimal reader for the legacy YAPION format, only used to migrate old data.
* <p>
* The result is built from plain java objects:
* <ul>
* <li>YAPION objects {@code {key(value)}} become a {@link LinkedHashMap} of String to value</li>
* <li>YAPION arrays {@code [a,b]} become a {@link List}</li>
* <li>YAPION maps {@code <(a):(b)>} become a {@link YapionMap}</li>
* <li>YAPION values {@code (value)} become null, Boolean, Number or String</li>
* </ul>
*/
@Deprecated
public class YapionReader {
private static final Pattern INTEGER = Pattern.compile("[-+]?\\d+");
private static final Pattern DECIMAL = Pattern.compile("[-+]?(\\d+\\.\\d*|\\.\\d+|\\d+)([eE][-+]?\\d+)?");
public record YapionMap(Map<Object, Object> entries) {
}
public static Map<String, Object> parse(String yapion) {
YapionReader reader = new YapionReader(yapion);
reader.skipWhitespace();
Map<String, Object> result = reader.readObject();
reader.skipWhitespace();
if (reader.index != yapion.length()) {
throw reader.error("Unexpected trailing content");
}
return result;
}
public static double getDouble(Map<?, ?> object, String key, double defaultValue) {
return object.get(key) instanceof Number value ? value.doubleValue() : defaultValue;
}
public static int getInt(Map<?, ?> object, String key, int defaultValue) {
return object.get(key) instanceof Number value ? value.intValue() : defaultValue;
}
public static boolean getBoolean(Map<?, ?> object, String key, boolean defaultValue) {
return object.get(key) instanceof Boolean value ? value : defaultValue;
}
public static String getString(Map<?, ?> object, String key, String defaultValue) {
return object.get(key) instanceof String value ? value : defaultValue;
}
/**
* @return the entries of the array that are YAPION objects
*/
public static List<Map<?, ?>> getObjects(Map<?, ?> object, String key) {
List<Map<?, ?>> objects = new ArrayList<>();
if (object.get(key) instanceof List<?> array) {
array.forEach(o -> {
if (o instanceof Map<?, ?> map) objects.add(map);
});
}
return objects;
}
private final String input;
private int index = 0;
private YapionReader(String input) {
this.input = input;
}
private Object readAny() {
return switch (peek()) {
case '{' -> readObject();
case '[' -> readArray();
case '<' -> readMap();
case '(' -> readValue();
default -> throw error("Expected '{', '[', '<' or '('");
};
}
private Map<String, Object> readObject() {
expect('{');
Map<String, Object> object = new LinkedHashMap<>();
while (true) {
skipWhitespace();
if (peek() == '}') {
index++;
return object;
}
StringBuilder key = new StringBuilder();
while (true) {
char c = peek();
if (c == '(' || c == '{' || c == '[' || c == '<') break;
if (c == '}') throw error("Key without value");
key.append(readChar());
}
object.put(key.toString(), readAny());
}
}
private List<Object> readArray() {
expect('[');
List<Object> array = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
index++;
return array;
}
while (true) {
int start = index;
skipWhitespace();
char c = peek();
if (c == '{' || c == '[' || c == '<' || c == '(') {
array.add(readAny());
skipWhitespace();
} else {
// Whitespace is part of plain values
index = start;
StringBuilder value = new StringBuilder();
while (peek() != ',' && peek() != ']') {
value.append(readChar());
}
array.add(typed(value.toString()));
}
char separator = input.charAt(index++);
if (separator == ']') return array;
if (separator != ',') throw error("Expected ',' or ']'");
}
}
private YapionMap readMap() {
expect('<');
Map<Object, Object> map = new LinkedHashMap<>();
while (true) {
skipWhitespace();
if (peek() == '>') {
index++;
return new YapionMap(map);
}
Object key = readAny();
skipWhitespace();
expect(':');
skipWhitespace();
map.put(key, readAny());
}
}
/**
* YAPION does not escape parentheses in values, balanced parentheses are part of the value.
* A backslash only escapes a backslash, a unicode sequence or a control character, otherwise it is kept.
*/
private Object readValue() {
expect('(');
StringBuilder value = new StringBuilder();
int depth = 0;
while (true) {
char c = peek();
if (c == '\\') {
value.append(readValueEscape());
continue;
}
index++;
if (c == '(') {
depth++;
} else if (c == ')') {
if (depth == 0) break;
depth--;
}
value.append(c);
}
return typed(value.toString());
}
private String readValueEscape() {
index++;
if (index >= input.length()) return "\\";
char escaped = input.charAt(index);
switch (escaped) {
case '\\', '(', ')':
index++;
return String.valueOf(escaped);
case 'n':
index++;
return "\n";
case 't':
index++;
return "\t";
case 'r':
index++;
return "\r";
case 'u':
if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) {
char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16);
index += 5;
return String.valueOf(unicode);
}
return "\\";
default:
return "\\";
}
}
/**
* Reads one character of a key or plain array value, resolving backslash escapes.
* A backslash that does not escape anything is kept, as the old player config
* collapsed escaped backslashes into a single one.
*/
private char readChar() {
char c = input.charAt(index++);
if (c != '\\') return c;
if (index >= input.length()) throw error("Unterminated escape");
char escaped = input.charAt(index);
switch (escaped) {
case 'n':
index++;
return '\n';
case 't':
index++;
return '\t';
case 'r':
index++;
return '\r';
case 'u':
if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) {
char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16);
index += 5;
return unicode;
}
return '\\';
case '\\', ',', ' ', '(', ')', '{', '}', '[', ']', '<', '>', '"', ':', '@':
index++;
return escaped;
default:
return '\\';
}
}
private static Object typed(String value) {
if (value.equals("null")) return null;
if (value.equals("true")) return true;
if (value.equals("false")) return false;
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
try {
if (INTEGER.matcher(value).matches()) {
long l = Long.parseLong(value);
if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return (int) l;
return l;
}
if (value.length() > 1) {
String number = value.substring(0, value.length() - 1);
switch (value.charAt(value.length() - 1)) {
case 'L':
if (INTEGER.matcher(number).matches()) return Long.parseLong(number);
break;
case 'S':
if (INTEGER.matcher(number).matches()) return Short.parseShort(number);
break;
case 'B':
if (INTEGER.matcher(number).matches()) return Byte.parseByte(number);
break;
case 'F':
if (DECIMAL.matcher(number).matches()) return Float.parseFloat(number);
break;
case 'D':
if (DECIMAL.matcher(number).matches()) return Double.parseDouble(number);
break;
default:
break;
}
}
if (DECIMAL.matcher(value).matches()) return Double.parseDouble(value);
} catch (NumberFormatException e) {
// Not a number, fall through to String
}
return value;
}
private void skipWhitespace() {
while (index < input.length() && Character.isWhitespace(input.charAt(index))) {
index++;
}
}
private char peek() {
if (index >= input.length()) throw error("Unexpected end of input");
return input.charAt(index);
}
private void expect(char c) {
if (peek() != c) throw error("Expected '" + c + "'");
index++;
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " at index " + index);
}
}
@@ -0,0 +1,63 @@
/*
* 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.config.serializer;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import java.io.IOException;
import java.util.Base64;
import java.util.logging.Level;
/**
* Stores an {@link ItemStack} as Base64 of {@link ItemStack#serializeAsBytes()}, which
* contains the data version and gets upgraded on newer Minecraft versions.
* Empty items are stored as null, items that can't be read are deserialized as null.
*/
public class ItemStackTypeAdapter extends TypeAdapter<ItemStack> {
@Override
public void write(JsonWriter out, ItemStack itemStack) throws IOException {
if (itemStack == null || itemStack.isEmpty()) {
out.nullValue();
return;
}
out.value(Base64.getEncoder().encodeToString(itemStack.serializeAsBytes()));
}
@Override
public ItemStack read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String value = in.nextString();
try {
return ItemStack.deserializeBytes(Base64.getDecoder().decode(value));
} catch (Exception e) {
Bukkit.getLogger().log(Level.WARNING, "Could not deserialize ItemStack", e);
return null;
}
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* 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
@@ -19,129 +19,77 @@
package de.steamwar.bausystem.configplayer;
import de.steamwar.bausystem.configplayer.serializer.ConfigurationSerializableSerializer;
import de.steamwar.linkage.Linked;
import de.steamwar.sql.UserConfig;
import com.google.gson.GsonBuilder;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.serializer.ItemStackTypeAdapter;
import de.steamwar.bausystem.configplayer.converter.ConfigV1ToV2Converter;
import de.steamwar.bausystem.features.hotbar.DefaultHotbar;
import lombok.AccessLevel;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import yapion.hierarchy.output.StringOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import yapion.serializing.SerializeManager;
import lombok.Setter;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
@Linked
public class Config implements Listener {
/**
* The current player config. It is stored as JSON, fields missing in the
* stored JSON keep the default value of their initializer, so adding a new
* field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to ConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION},
* write a {@link ConfigConverter} from the copy to this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class Config implements VersionedConfig {
static {
SerializeManager.add(new ConfigurationSerializableSerializer());
}
public static final int VERSION = 2;
@Getter
private static Config instance;
public static final ConfigStore<Config> STORE = new ConfigStore<>(Config.class, VERSION, Config::new)
.gson(new GsonBuilder().registerTypeHierarchyAdapter(ItemStack.class, new ItemStackTypeAdapter()).create())
.legacy(s -> s.startsWith("{@version(1)"), ConfigV1::parse)
.converter(new ConfigV1ToV2Converter())
.userConfig("bausystem");
{
instance = this;
}
private final Map<UUID, YAPIONObject> playerConfigurations = new HashMap<>();
private static final Map<Integer, ConfigConverter> CONFIG_CONVERTER_MAP = new HashMap<>();
public static void addConfigConverter(ConfigConverter configConverter) {
CONFIG_CONVERTER_MAP.putIfAbsent(configConverter.version(), configConverter);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
get(event.getPlayer());
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
save(event.getPlayer());
playerConfigurations.remove(event.getPlayer().getUniqueId());
}
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Get a PlayerConfig, optionally loads it from the DataBase and migrates it if necessary.
*
* @param player the player from whom to get the config.
* @return the config object
* BauGui item id to inventory slot
*/
public YAPIONObject get(Player player) {
UUID uuid = player.getUniqueId();
if (!playerConfigurations.containsKey(uuid)) {
String s = UserConfig.getConfig(uuid, "bausystem");
YAPIONObject yapionObject;
if (s == null) {
yapionObject = ConfigCreator.createDefaultConfig();
} else {
yapionObject = YAPIONParser.parse(s);
}
yapionObject = update(yapionObject);
playerConfigurations.put(uuid, yapionObject);
return yapionObject;
}
return playerConfigurations.get(uuid);
}
public void saveAll() {
playerConfigurations.forEach((uuid, yapionObject) -> {
String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\+", "\\");
UserConfig.updatePlayerConfig(uuid, "bausystem", string);
});
playerConfigurations.clear();
}
protected Map<Integer, Integer> bauGui = defaultBauGui();
protected int bauGuiSize = 45;
/**
* Save a PlayerConfig, this does not remove the key value mapping from the map.
*
* @param player the player to save the config.
* Slots 0-8 are the hotbar, slots 9-12 the armor contents
*/
public void save(Player player) {
UUID uuid = player.getUniqueId();
if (playerConfigurations.containsKey(uuid)) {
YAPIONObject yapionObject = playerConfigurations.get(uuid);
String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\\\+", "\\\\");
UserConfig.updatePlayerConfig(uuid, "bausystem", string);
}
}
protected ItemStack[] hotbar = DefaultHotbar.defaultHotbar();
private YAPIONObject update(YAPIONObject yapionObject) {
int version = yapionObject.getPlainValue("@version");
while (version < ConfigCreator.currentVersion) {
ConfigConverter configConverter = CONFIG_CONVERTER_MAP.getOrDefault(version, null);
if (configConverter == null) {
Bukkit.getLogger().log(Level.SEVERE, "No updater found for version " + version);
return ConfigCreator.createDefaultConfig();
}
try {
configConverter.update(yapionObject);
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e);
return ConfigCreator.createDefaultConfig();
}
int newVersion = yapionObject.getPlainValue("@version");
if (version == newVersion) {
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was the same after conversion");
return ConfigCreator.createDefaultConfig();
}
if (newVersion < version) {
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was earlier after conversion");
return ConfigCreator.createDefaultConfig();
}
version = newVersion;
}
return yapionObject;
protected boolean nightVision = false;
protected boolean tntClickDetails = true;
protected boolean detonatorAutostart = false;
protected boolean inventoryFill = false;
protected boolean rayVisualizer = false;
protected boolean smartPlace = false;
protected boolean blockCounter = false;
protected boolean depthMessage = true;
public static Map<Integer, Integer> defaultBauGui() {
// 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ?
// 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16
// 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11
// 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18
// 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ?
Map<Integer, Integer> bauGui = new HashMap<>();
bauGui.put(10, 2); bauGui.put(3, 3); bauGui.put(7, 4); bauGui.put(17, 5); bauGui.put(15, 6);
bauGui.put(5, 9); bauGui.put(4, 18); bauGui.put(6, 27);
bauGui.put(16, 17); bauGui.put(11, 26); bauGui.put(18, 35);
bauGui.put(19, 20); bauGui.put(21, 21); bauGui.put(9, 22); bauGui.put(0, 23); bauGui.put(1, 24);
bauGui.put(23, 37); bauGui.put(20, 38); bauGui.put(8, 39); bauGui.put(22, 40); bauGui.put(26, 41); bauGui.put(12, 42); bauGui.put(14, 43);
return bauGui;
}
}
@@ -1,56 +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.bausystem.configplayer;
import yapion.hierarchy.types.YAPIONObject;
/**
* A new {@link ConfigConverter} should be written when you remove anything
* from the Config or modify any mayor part. When you move anything from
* any key to any other key you should write a new {@link ConfigConverter}.
* For adding any new key you should be able to get the default without
* having it in the Config. Anything you need to change you should also
* change the {@link ConfigCreator} accordingly, to produce the new Config.
*/
public interface ConfigConverter {
/**
* This describes the version this Converter can convert from. The version
* it should convert to is the version 1 above this number. But this is not
* a necessity. In the config Object as parameter given in {@link #update(YAPIONObject)}
* you should update the <b>@version</b> variable in the root object to the
* new version this converter produced.
*
* @return the version number
*/
int version();
/**
* This method should update everything needed to go from a lower config
* version to a higher. It should update the <b>@version</b> variable
* accordingly. Anything else is up the implementation. If anything goes wrong
* do not silently exit this method, throw an Exception. The updater Code will
* deal with it. Never leave the inputted object in a corrupted state.
*
* @param config the config object to update
*/
void update(YAPIONObject config);
}
@@ -1,61 +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.bausystem.configplayer;
import de.steamwar.bausystem.features.hotbar.DefaultHotbar;
import lombok.experimental.UtilityClass;
import yapion.hierarchy.types.YAPIONObject;
@UtilityClass
public class ConfigCreator {
public static final int currentVersion = 1;
public YAPIONObject createDefaultConfig() {
YAPIONObject yapionObject = new YAPIONObject();
// This call should never be touched
yapionObject.add("@version", currentVersion);
// Any initialising goes into here
yapionObject.add("baugui", defaultBauGui());
// Default Hotbar Gui
yapionObject.add("hotbar", DefaultHotbar.defaultHotbar());
return yapionObject;
}
private YAPIONObject defaultBauGui() {
YAPIONObject yapionObject = new YAPIONObject();
// 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ?
// 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16
// 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11
// 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18
// 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ?
yapionObject.add("10", 2).add("3", 3).add("7", 4).add("17", 5).add("15", 6);
yapionObject.add("5", 9).add("4", 18).add("6", 27);
yapionObject.add("16", 17).add("11", 26).add("18", 35);
yapionObject.add("19", 20).add("21", 21).add("9", 22).add("0", 23).add("1", 24);
yapionObject.add("23", 37).add("20", 38).add("8", 39).add("22", 40).add("26", 41).add("12", 42).add("14", 43);
yapionObject.add("size", 45);
return yapionObject;
}
}
@@ -0,0 +1,104 @@
/*
* 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.configplayer;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The legacy config that was stored in the YAPION format.
*/
@Getter
@Setter
public class ConfigV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* BauGui item id to inventory slot
*/
protected Map<Integer, Integer> bauGui = new HashMap<>();
protected int bauGuiSize = 45;
/**
* Minecraft version to the hotbar serialized by YAPION, read with {@link YapionReader}.
* Use {@link de.steamwar.bausystem.config.legacy.YapionItemDeserializer} to get the items.
*/
protected Map<Integer, List<?>> hotbars = new HashMap<>();
protected boolean nightVision = false;
protected boolean tntClickDetails = true;
protected boolean detonatorAutostart = false;
protected boolean inventoryFill = false;
protected boolean rayVisualizer = false;
protected boolean smartPlace = false;
protected boolean blockCounter = false;
protected boolean depthMessage = true;
public static ConfigV1 parse(String yapion) {
Map<String, Object> object = YapionReader.parse(yapion);
ConfigV1 config = new ConfigV1();
config.version = (Integer) object.get("@version");
if (object.get("baugui") instanceof Map<?, ?> bauGui) {
bauGui.forEach((key, value) -> {
if (key.equals("size")) {
config.bauGuiSize = (Integer) value;
} else {
config.bauGui.put(Integer.parseInt((String) key), (Integer) value);
}
});
}
object.forEach((key, value) -> {
if (!(value instanceof List<?> hotbar)) return;
if (key.equals("hotbar")) {
// Hotbars were stored without version prior to 1.19
config.hotbars.putIfAbsent(19, hotbar);
} else if (key.startsWith("hotbar-")) {
config.hotbars.put(Integer.parseInt(key.substring("hotbar-".length())), hotbar);
}
});
config.nightVision = getBoolean(object, "nightvision", config.nightVision);
config.tntClickDetails = getBoolean(object, "tnt_click_details", config.tntClickDetails);
config.detonatorAutostart = getBoolean(object, "detonator-autostart", config.detonatorAutostart);
config.inventoryFill = getBoolean(object, "inventoryfill", config.inventoryFill);
config.rayVisualizer = getBoolean(object, "rayvisualizer", config.rayVisualizer);
config.smartPlace = getBoolean(object, "smartPlace", config.smartPlace);
config.blockCounter = getBoolean(object, "blockCounter", config.blockCounter);
config.depthMessage = getBoolean(object, "depth_message", config.depthMessage);
return config;
}
private static boolean getBoolean(Map<String, Object> object, String key, boolean defaultValue) {
return object.get(key) instanceof Boolean value ? value : defaultValue;
}
}
@@ -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.bausystem.configplayer.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.config.legacy.YapionItemDeserializer;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.bausystem.configplayer.ConfigV1;
import de.steamwar.core.Core;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
@Deprecated
public class ConfigV1ToV2Converter implements ConfigConverter<ConfigV1, Config> {
@Override
public int version() {
return ConfigV1.VERSION;
}
@Override
public Config convert(ConfigV1 old) {
Config config = new Config();
config.setBauGui(new HashMap<>(old.getBauGui()));
config.setBauGuiSize(old.getBauGuiSize());
List<?> hotbar = old.getHotbars().get(Core.getVersion());
if (hotbar != null) {
config.setHotbar(convertHotbar(hotbar));
}
config.setNightVision(old.isNightVision());
config.setTntClickDetails(old.isTntClickDetails());
config.setDetonatorAutostart(old.isDetonatorAutostart());
config.setInventoryFill(old.isInventoryFill());
config.setRayVisualizer(old.isRayVisualizer());
config.setSmartPlace(old.isSmartPlace());
config.setBlockCounter(old.isBlockCounter());
config.setDepthMessage(old.isDepthMessage());
return config;
}
private ItemStack[] convertHotbar(List<?> hotbar) {
ItemStack[] items = new ItemStack[13];
for (int i = 0; i < Math.min(hotbar.size(), items.length); i++) {
try {
items[i] = YapionItemDeserializer.deserializeItem(hotbar.get(i));
} catch (Exception e) {
Bukkit.getLogger().log(Level.WARNING, "Could not convert hotbar item " + i, e);
}
}
return items;
}
}
@@ -1,90 +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.bausystem.configplayer.serializer;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import yapion.hierarchy.api.groups.YAPIONAnyType;
import yapion.hierarchy.types.YAPIONObject;
import yapion.serializing.api.SerializerObject;
import yapion.serializing.data.DeserializeData;
import yapion.serializing.data.SerializeData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import static yapion.utils.IdentifierUtils.TYPE_IDENTIFIER;
public class ConfigurationSerializableSerializer extends SerializerObject<ConfigurationSerializable> {
@Override
public Class<ConfigurationSerializable> type() {
return ConfigurationSerializable.class;
}
@Override
public boolean isInterface() {
return true;
}
@Override
public YAPIONObject serialize(SerializeData<ConfigurationSerializable> serializeData) {
YAPIONObject yapionObject = new YAPIONObject();
yapionObject.add(TYPE_IDENTIFIER, serializeData.object.getClass().getTypeName());
if (serializeData.object instanceof ItemStack) {
yapionObject.add(TYPE_IDENTIFIER, ItemStack.class.getTypeName());
}
if (serializeData.object instanceof ItemMeta) {
yapionObject.add(TYPE_IDENTIFIER, ItemMeta.class.getTypeName());
}
Map<String, Object> serializeDataMap = serializeData.object.serialize();
serializeDataMap.forEach((s, o) -> {
YAPIONAnyType yapionAnyType = serializeData.serialize(o);
if (yapionAnyType instanceof YAPIONObject) {
YAPIONObject object = (YAPIONObject) yapionAnyType;
if (object.containsKey(TYPE_IDENTIFIER) && object.getPlainValue(TYPE_IDENTIFIER).equals("com.google.common.collect.RegularImmutableList")) {
object.put(TYPE_IDENTIFIER, ArrayList.class.getTypeName());
}
}
yapionObject.add(s, yapionAnyType);
});
return yapionObject;
}
@Override
public ConfigurationSerializable deserialize(DeserializeData<YAPIONObject> deserializeData) {
Map<String, Object> deserializeDataMap = new HashMap<>();
deserializeData.object.forEach((s, yapionAnyType) -> {
if (s.equals(TYPE_IDENTIFIER)) {
if (yapionAnyType.toString().equals("(org.bukkit.inventory.meta.ItemMeta)")) {
deserializeDataMap.put("==", "ItemMeta");
} else {
deserializeDataMap.put("==", deserializeData.deserialize(yapionAnyType));
}
return;
}
deserializeDataMap.put(s, deserializeData.deserialize(yapionAnyType));
});
return ConfigurationSerialization.deserializeObject(deserializeDataMap);
}
}
@@ -64,7 +64,7 @@ public class Depth {
dimensions.setZ(Math.abs(dimensions.getZ()));
RegionUtils.message(region, player -> {
if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) {
if (Config.STORE.get(player).isDepthMessage()) {
player.spigot().sendMessage(getMessage(player, dimensions.getBlockX() + 1, dimensions.getBlockY() + 1, dimensions.getBlockZ() + 1, tntCount));
}
});
@@ -34,11 +34,11 @@ public class DepthCommand extends SWCommand {
@Register
public void toggle(Player player) {
if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) {
Config.getInstance().get(player).put("depth_message", false);
if (Config.STORE.get(player).isDepthMessage()) {
Config.STORE.get(player).setDepthMessage(false);
BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player);
} else {
Config.getInstance().get(player).put("depth_message", true);
Config.STORE.get(player).setDepthMessage(true);
BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player);
}
}
@@ -95,7 +95,7 @@ public class Detonator {
public static void activateDetonator(DetonatorStorage detonator) {
Player p = detonator.getPlayer();
if (Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false)) {
if (Config.STORE.get(p).isDetonatorAutostart()) {
AutostartListener.instance.activate(p);
}
@@ -76,8 +76,8 @@ public class DetonatorCommand extends SWCommand {
@Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART")
public void toggleAutostartTimer(Player p) {
boolean current = Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false);
Config.getInstance().get(p).put("detonator-autostart", !current);
boolean current = Config.STORE.get(p).isDetonatorAutostart();
Config.STORE.get(p).setDetonatorAutostart(!current);
if (!current) {
BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p);
} else {
@@ -28,7 +28,6 @@ import lombok.Setter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*;
@@ -52,15 +51,15 @@ public class BauGuiMapping {
}
@Getter
private final YAPIONObject object;
private final Config config;
@Getter
private final Player owner;
@Getter
@Setter
private boolean saved;
protected BauGuiMapping(YAPIONObject object, Player p) {
this.object = object;
protected BauGuiMapping(Config config, Player p) {
this.config = config;
this.owner = p;
fromUUID.put(p.getUniqueId(), this);
mappings.add(this);
@@ -70,19 +69,18 @@ public class BauGuiMapping {
BauGuiMapping mapping = fromUUID.get(p.getUniqueId());
if (mapping == null) {
YAPIONObject yapionObject = Config.getInstance().get(p);
mapping = new BauGuiMapping(yapionObject.getObject("baugui"), p);
mapping = new BauGuiMapping(Config.STORE.get(p), p);
}
return mapping;
}
public boolean isShown(int id) {
return object.getPlainValueOrDefault(Integer.toString(id), -1) >= 0;
return config.getBauGui().getOrDefault(id, -1) >= 0;
}
public int getSlot(int id) {
return object.getPlainValue(Integer.toString(id));
return config.getBauGui().get(id);
}
public Map<Integer, Integer> getMapping() {
@@ -94,7 +92,7 @@ public class BauGuiMapping {
}
public int getSize() {
return object.getPlainValueOrDefault("size", 45);
return config.getBauGuiSize();
}
public void setSize(int size) {
@@ -102,7 +100,7 @@ public class BauGuiMapping {
return;
}
object.add("size", size);
config.setBauGuiSize(size);
}
private void tick() {
@@ -111,16 +109,17 @@ public class BauGuiMapping {
public void save() {
if (saved) return;
YAPIONObject config = Config.getInstance().get(owner);
config.add("baugui", object);
Config.getInstance().save(owner);
Config current = Config.STORE.get(owner);
current.setBauGui(config.getBauGui());
current.setBauGuiSize(config.getBauGuiSize());
Config.STORE.save(owner);
saved = true;
}
private Map<Integer, Integer> internalReadMap() {
Map<Integer, Integer> map = new HashMap<>();
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) {
Integer value = object.getPlainValueOrDefault(e.getKey().toString(), -1);
Integer value = config.getBauGui().getOrDefault(e.getKey(), -1);
if (value == -1) continue;
map.put(e.getKey(), value);
}
@@ -129,10 +128,10 @@ public class BauGuiMapping {
private void internalWriteMap(Map<Integer, Integer> map) {
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) {
object.remove(e.getKey().toString());
config.getBauGui().remove(e.getKey());
Integer value = map.getOrDefault(e.getKey(), -1);
if (value == -1) continue;
object.add(e.getKey().toString(), value);
config.getBauGui().put(e.getKey(), value);
}
}
}
@@ -20,39 +20,23 @@
package de.steamwar.bausystem.features.hotbar;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.core.Core;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
import yapion.parser.YAPIONParser;
import yapion.serializing.YAPIONDeserializer;
import yapion.serializing.YAPIONSerializer;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
@UtilityClass
public class DefaultHotbar {
private final YAPIONArray DEFAULT_HOTBAR = YAPIONParser.parse("{[{@type(org.bukkit.inventory.ItemStack)v(2230)type(WOODEN_AXE)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(WorldEdit Wand)lore{@type(java.util.ArrayList)values[Left click: select pos #1,Right click: select pos #2]}}},{@type(org.bukkit.inventory.ItemStack)v(2230)type(COMPASS)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(Navigation Wand)lore{@type(java.util.ArrayList)values[Left click: jump to location,Right click: pass through walls]}}},null,null,null,null,null,null,{@type(org.bukkit.inventory.ItemStack)v(2230)type(NETHER_STAR)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(§eBau GUI)}},null,null,{@type(org.bukkit.inventory.ItemStack)v(3117)type(ELYTRA)},null]}").getArray("");
public void updateHotbar(Player p) {
ItemStack[] hotbar = new ItemStack[13];
System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9);
System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4);
YAPIONArray yapionArray = new YAPIONArray();
for (ItemStack itemStack : hotbar) {
if (itemStack != null) {
yapionArray.add(YAPIONSerializer.serialize(itemStack));
} else {
yapionArray.add(new YAPIONValue<>(null));
}
}
Config.getInstance().get(p).add("hotbar-" + Core.getVersion(), yapionArray);
Config.getInstance().save(p);
Config.STORE.get(p).setHotbar(copy(hotbar));
Config.STORE.save(p);
}
public void setHotbar(Player p) {
@@ -66,28 +50,34 @@ public class DefaultHotbar {
}
public ItemStack[] getItems(Player p) {
Config.getInstance().get(p).remap("hotbar", "hotbar-19");
YAPIONArray yapionArray = Config.getInstance().get(p).getYAPIONArrayOrSetDefault("hotbar-" + Core.getVersion(), defaultHotbar());
return copy(Config.STORE.get(p).getHotbar());
}
public ItemStack[] defaultHotbar() {
ItemStack[] hotbar = new ItemStack[13];
Set<Integer> invalid = new HashSet<>();
yapionArray.forEach((integer, yapionAnyType) -> {
if (yapionAnyType instanceof YAPIONValue) {
hotbar[integer] = null;
} else {
try {
hotbar[integer] = YAPIONDeserializer.deserialize((YAPIONObject) yapionAnyType);
} catch (Exception e) {
invalid.add(integer);
hotbar[integer] = null;
}
}
});
invalid.forEach(i -> yapionArray.set(i, new YAPIONValue<>(null)));
if (!invalid.isEmpty()) Config.getInstance().save(p);
hotbar[0] = item(Material.WOODEN_AXE, "WorldEdit Wand", "Left click: select pos #1", "Right click: select pos #2");
hotbar[1] = item(Material.COMPASS, "Navigation Wand", "Left click: jump to location", "Right click: pass through walls");
hotbar[8] = item(Material.NETHER_STAR, "§eBau GUI");
hotbar[11] = new ItemStack(Material.ELYTRA);
return hotbar;
}
public YAPIONArray defaultHotbar() {
return DEFAULT_HOTBAR;
private ItemStack item(Material material, String name, String... lore) {
ItemStack itemStack = new ItemStack(material);
ItemMeta meta = itemStack.getItemMeta();
meta.setDisplayName(name);
if (lore.length > 0) {
meta.setLore(List.of(lore));
}
itemStack.setItemMeta(meta);
return itemStack;
}
private ItemStack[] copy(ItemStack[] hotbar) {
ItemStack[] copy = new ItemStack[13];
for (int i = 0; i < Math.min(hotbar.length, copy.length); i++) {
copy[i] = hotbar[i] == null ? null : hotbar[i].clone();
}
return copy;
}
}
@@ -49,7 +49,7 @@ public class InventoryFillBauGuiItem extends BauGuiItem {
@Override
public ItemStack getItem(Player player) {
String loreKey = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false) ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE";
String loreKey = Config.STORE.get(player).isInventoryFill() ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE";
return new SWItem(Material.HOPPER, BauSystem.MESSAGE.parse("OTHER_ITEMS_INVENTORY_FILL_NAME", player), Collections.singletonList(BauSystem.MESSAGE.parse(loreKey, player)), false, clickType -> {
}).getItemStack();
}
@@ -40,7 +40,7 @@ public class InventoryFiller implements Listener {
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return;
if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return;
if (!event.getPlayer().isSneaking()) return;
Block block = event.getPlayer().getTargetBlockExact(5);
if (block == null) return;
@@ -62,7 +62,7 @@ public class InventoryFiller implements Listener {
@EventHandler
public void onPlayerItemHeld(PlayerItemHeldEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return;
if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return;
if (!event.getPlayer().isSneaking()) return;
ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR) return;
@@ -68,8 +68,8 @@ public class InventoryFillerCommand extends SWCommand {
@Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"})
public void toggle(Player player) {
boolean inventoryFill = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false);
Config.getInstance().get(player).put("inventoryfill", !inventoryFill);
boolean inventoryFill = Config.STORE.get(player).isInventoryFill();
Config.STORE.get(player).setInventoryFill(!inventoryFill);
if (!inventoryFill) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player));
BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player);
@@ -167,7 +167,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
boolean rayvisualizer = Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("rayvisualizer", false);
boolean rayvisualizer = Config.STORE.get(event.getPlayer()).isRayVisualizer();
if (rayvisualizer) server.addPlayer(event.getPlayer());
}
@@ -181,7 +181,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (Permission.BUILD.hasPermission(player)) {
boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer();
if (rayvisualizer) server.addPlayer(player);
} else {
server.removePlayer(player);
@@ -192,8 +192,8 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@Register
public void toggle(@Validator Player player) {
boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer);
boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer();
Config.STORE.get(player).setRayVisualizer(!rayvisualizer);
if (!rayvisualizer) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player));
server.addPlayer(player);
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.inventory.InvCallback;
import de.steamwar.inventory.SWItem;
import lombok.Getter;
@@ -28,7 +29,6 @@ import lombok.Setter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.Comparator;
@@ -121,9 +121,9 @@ public abstract class SimulatorElement<T extends SimulatorPhase> {
public abstract String getType();
public void saveExtra(YAPIONObject elementObject) {
public void saveExtra(SimulatorConfig.Element element) {
}
public void loadExtra(YAPIONObject elementObject) {
public void loadExtra(SimulatorConfig.Element element) {
}
}
@@ -20,10 +20,10 @@
package de.steamwar.bausystem.features.simulator.data;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -38,9 +38,9 @@ public abstract class SimulatorPhase {
public abstract void toSimulatorActions(Vector position, BiConsumer<Integer, SimulatorAction> tickStart, BiConsumer<Integer, SimulatorAction> tickEnd);
public void saveExtra(YAPIONObject phaseObject) {
public void saveExtra(SimulatorConfig.Phase phase) {
}
public void loadExtra(YAPIONObject phaseObject) {
public void loadExtra(SimulatorConfig.Phase phase) {
}
}
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.observer;
import de.steamwar.bausystem.features.simulator.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -31,7 +32,6 @@ import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.type.Observer;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -70,12 +70,12 @@ public final class ObserverPhase extends SimulatorPhase {
}
@Override
public void saveExtra(YAPIONObject phaseObject) {
phaseObject.add("orientation", orientation.name());
public void saveExtra(SimulatorConfig.Phase phase) {
phase.setOrientation(orientation);
}
@Override
public void loadExtra(YAPIONObject phaseObject) {
orientation = BlockFace.valueOf(phaseObject.getPlainValue("orientation"));
public void loadExtra(SimulatorConfig.Phase phase) {
if (phase.getOrientation() != null) orientation = phase.getOrientation();
}
}
@@ -24,13 +24,13 @@ import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.inventory.InvCallback;
import de.steamwar.inventory.SWItem;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
public final class TNTElement extends SimulatorElement<TNTPhase> {
@@ -100,14 +100,14 @@ public final class TNTElement extends SimulatorElement<TNTPhase> {
}
@Override
public void saveExtra(YAPIONObject elementObject) {
elementObject.add("alignmentX", alignment.getX());
elementObject.add("alignmentZ", alignment.getZ());
public void saveExtra(SimulatorConfig.Element element) {
element.setAlignmentX(alignment.getX());
element.setAlignmentZ(alignment.getZ());
}
@Override
public void loadExtra(YAPIONObject elementObject) {
alignment.setX(elementObject.getDoubleOrSetDefault("alignmentX", 0));
alignment.setZ(elementObject.getDoubleOrSetDefault("alignmentZ", 0));
public void loadExtra(SimulatorConfig.Element element) {
alignment.setX(element.getAlignmentX() != null ? element.getAlignmentX() : 0);
alignment.setZ(element.getAlignmentZ() != null ? element.getAlignmentZ() : 0);
}
}
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.tnt;
import de.steamwar.bausystem.features.simulator.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.FreezeMode;
@@ -31,7 +32,6 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -83,18 +83,18 @@ public final class TNTPhase extends SimulatorPhase {
}
@Override
public void saveExtra(YAPIONObject phaseObject) {
phaseObject.add("count", count);
phaseObject.add("xJump", xJump);
phaseObject.add("yJump", yJump);
phaseObject.add("zJump", zJump);
public void saveExtra(SimulatorConfig.Phase phase) {
phase.setCount(count);
phase.setXJump(xJump);
phase.setYJump(yJump);
phase.setZJump(zJump);
}
@Override
public void loadExtra(YAPIONObject phaseObject) {
count = phaseObject.getPlainValue("count");
xJump = phaseObject.getPlainValue("xJump");
yJump = phaseObject.getPlainValue("yJump");
zJump = phaseObject.getPlainValue("zJump");
public void loadExtra(SimulatorConfig.Phase phase) {
if (phase.getCount() != null) count = phase.getCount();
if (phase.getXJump() != null) xJump = phase.getXJump();
if (phase.getYJump() != null) yJump = phase.getYJump();
if (phase.getZJump() != null) zJump = phase.getZJump();
}
}
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage;
import com.google.gson.JsonParseException;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
@@ -29,12 +30,7 @@ import de.steamwar.bausystem.features.simulator.data.redstone.RedstoneElement;
import de.steamwar.bausystem.features.simulator.data.redstone.RedstonePhase;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
@@ -51,44 +47,41 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
SimulatorConfig config;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
config = SimulatorConfig.STORE.load(file);
} catch (JsonParseException | IllegalStateException | IOException e) {
return Optional.empty();
}
String name = file.getName().substring(0, file.getName().length() - ".sim".length());
Simulator simulator = new Simulator(name);
loadSimulator(yapionObject, simulator);
loadSimulator(config, simulator);
return Optional.of(Collections.singletonList(simulator));
}
private void loadSimulator(YAPIONObject simulatorObject, Simulator simulator) {
simulator.setMaterial(Material.valueOf(simulatorObject.getPlainValue("material")));
simulator.setAutoTrace(simulatorObject.getPlainValue("autoTrace"));
simulator.setAutoTestblock(simulatorObject.getPlainValueOrDefault("autoTestblock", false));
private void loadSimulator(SimulatorConfig config, Simulator simulator) {
if (config.getMaterial() != null) simulator.setMaterial(config.getMaterial());
simulator.setAutoTrace(config.isAutoTrace());
simulator.setAutoTestblock(config.isAutoTestblock());
YAPIONArray groups = simulatorObject.getArray("groups");
groups.streamObject().forEach(groupObject -> {
config.getGroups().forEach(groupConfig -> {
SimulatorGroup simulatorGroup = new SimulatorGroup();
loadGroup(groupObject, simulatorGroup);
loadGroup(groupConfig, simulatorGroup);
simulator.add(simulatorGroup);
});
}
private void loadGroup(YAPIONObject groupObject, SimulatorGroup group) {
group.setMaterial(Material.valueOf(groupObject.getPlainValue("material")));
group.setDisabled(groupObject.getPlainValue("disabled"));
private void loadGroup(SimulatorConfig.Group groupConfig, SimulatorGroup group) {
if (groupConfig.getMaterial() != null) group.setMaterial(groupConfig.getMaterial());
group.setDisabled(groupConfig.isDisabled());
YAPIONArray elements = groupObject.getArray("elements");
elements.streamObject().forEach(elementObject -> {
groupConfig.getElements().forEach(elementConfig -> {
SimulatorElement<?> element;
Supplier<SimulatorPhase> phaseConstructor;
Vector position = new Vector(elementObject.getDouble("x"), elementObject.getDouble("y"), elementObject.getDouble("z"));
String type = elementObject.getPlainValue("type");
switch (type) {
Vector position = new Vector(elementConfig.getX(), elementConfig.getY(), elementConfig.getZ());
switch (String.valueOf(elementConfig.getType())) {
case "TNT":
element = new TNTElement(position);
phaseConstructor = TNTPhase::new;
@@ -109,28 +102,27 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
if (element == null) {
return;
}
loadElement(elementObject, element, phaseConstructor);
loadElement(elementConfig, element, phaseConstructor);
group.add(element);
});
}
private void loadElement(YAPIONObject elementObject, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) {
element.setMaterial(Material.valueOf(elementObject.getPlainValue("material")));
element.setDisabled(elementObject.getPlainValue("disabled"));
element.loadExtra(elementObject);
private void loadElement(SimulatorConfig.Element elementConfig, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) {
if (elementConfig.getMaterial() != null) element.setMaterial(elementConfig.getMaterial());
element.setDisabled(elementConfig.isDisabled());
element.loadExtra(elementConfig);
YAPIONArray phases = elementObject.getArray("phases");
phases.streamObject().forEach(phaseObject -> {
elementConfig.getPhases().forEach(phaseConfig -> {
SimulatorPhase phase = phaseConstructor.get();
loadPhase(phaseObject, phase);
loadPhase(phaseConfig, phase);
((SimulatorElement) element).add(phase);
});
}
private void loadPhase(YAPIONObject phaseObject, SimulatorPhase phase) {
phase.setTickOffset(phaseObject.getPlainValue("tickOffset"));
phase.setLifetime(phaseObject.getPlainValue("lifetime"));
phase.setOrder(phaseObject.getPlainValue("order"));
phase.loadExtra(phaseObject);
private void loadPhase(SimulatorConfig.Phase phaseConfig, SimulatorPhase phase) {
phase.setTickOffset(phaseConfig.getTickOffset());
phase.setLifetime(phaseConfig.getLifetime());
phase.setOrder(phaseConfig.getOrder());
phase.loadExtra(phaseConfig);
}
}
@@ -0,0 +1,117 @@
/*
* 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.simulator.storage;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import java.util.ArrayList;
import java.util.List;
/**
* The current stored form of a simulator, saved as JSON in a .sim file. Files
* saved before the version was stored have no version and are version 1.
* Fields missing in the stored JSON keep the default value of their initializer,
* so adding a new field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to SimulatorConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to
* this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class SimulatorConfig implements VersionedConfig {
public static final int VERSION = 1;
/**
* Files saved before the version was stored are version 1
*/
public static final ConfigStore<SimulatorConfig> STORE = new ConfigStore<>(SimulatorConfig.class, VERSION, SimulatorConfig::new)
.unversioned(1);
@Setter(AccessLevel.NONE)
protected int version = VERSION;
protected Material material = Material.BARREL;
protected boolean autoTrace = false;
protected boolean autoTestblock = false;
protected List<Group> groups = new ArrayList<>();
@Getter
@Setter
public static class Group {
protected Material material = Material.CHEST;
protected boolean disabled = false;
protected List<Element> elements = new ArrayList<>();
}
@Getter
@Setter
public static class Element {
/**
* One of "TNT", "Redstone" or "Observer"
*/
protected String type;
protected Material material;
protected boolean disabled = false;
protected double x;
protected double y;
protected double z;
/**
* Only used by TNT
*/
protected Double alignmentX;
protected Double alignmentZ;
protected List<Phase> phases = new ArrayList<>();
}
@Getter
@Setter
public static class Phase {
protected int tickOffset;
protected int lifetime;
protected int order;
/**
* Only used by TNT
*/
protected Integer count;
protected Boolean xJump;
protected Boolean yJump;
protected Boolean zJump;
/**
* Only used by Observer
*/
protected BlockFace orientation;
}
}
@@ -19,24 +19,30 @@
package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.config.legacy.YapionReader;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.config.legacy.YapionReader.*;
/**
* Loads the legacy .simulator files stored as YAPION, they are deleted after loading
* and saved in the current format by the {@link SimulatorSaver}.
*/
@Deprecated
public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
@Override
@@ -45,20 +51,19 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
Map<String, Object> yapionObject;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IllegalArgumentException | IOException e) {
return Optional.empty();
}
String name = file.getName().substring(0, file.getName().length() - ".simulator".length());
Simulator simulator = new Simulator(name);
simulator.setMaterial(Material.valueOf(yapionObject.getStringOrDefault("material", "BARREL")));
simulator.setMaterial(Material.valueOf(getString(yapionObject, "material", "BARREL")));
YAPIONArray tntElements = yapionObject.getArrayOrDefault("tntElements", new YAPIONArray());
for (YAPIONObject element : tntElements.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> element : getObjects(yapionObject, "tntElements")) {
if (element.containsKey("elements")) {
simulator.add(loadGroup(element));
} else {
@@ -70,27 +75,26 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.of(Collections.singletonList(simulator));
}
private SimulatorGroup loadGroup(YAPIONObject element) {
double x = element.getDoubleOrDefault("x", 0);
double y = element.getDoubleOrDefault("y", 0);
double z = element.getDoubleOrDefault("z", 0);
int tickOffset = element.getIntOrDefault("tickOffset", 0);
private SimulatorGroup loadGroup(Map<?, ?> element) {
double x = getDouble(element, "x", 0);
double y = getDouble(element, "y", 0);
double z = getDouble(element, "z", 0);
int tickOffset = getInt(element, "tickOffset", 0);
SimulatorGroup simulatorGroup = new SimulatorGroup();
simulatorGroup.setDisabled(element.getBooleanOrDefault("disabled", false));
simulatorGroup.setDisabled(getBoolean(element, "disabled", false));
YAPIONArray elements = element.getArrayOrDefault("elements", new YAPIONArray());
for (YAPIONObject e : elements.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> e : getObjects(element, "elements")) {
simulatorGroup.add(loadElement(e, x, y, z, tickOffset));
}
return simulatorGroup;
}
private TNTElement loadElement(YAPIONObject element, double dx, double dy, double dz, int dTickOffset) {
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x") + dx, element.getDouble("y") + dy, element.getDouble("z") + dz));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
private TNTElement loadElement(Map<?, ?> element, double dx, double dy, double dz, int dTickOffset) {
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x") + dx, requireDouble(element, "y") + dy, requireDouble(element, "z") + dz));
tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
TNTPhase tntPhase = loadPhase(element);
tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset);
@@ -99,12 +103,12 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return tntElement;
}
private SimulatorGroup loadElement(YAPIONObject element) {
private SimulatorGroup loadElement(Map<?, ?> element) {
SimulatorGroup simulatorGroup = new SimulatorGroup();
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x"), element.getDouble("y"), element.getDouble("z")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x"), requireDouble(element, "y"), requireDouble(element, "z")));
tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement);
tntElement.add(loadPhase(element));
@@ -112,15 +116,15 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return simulatorGroup;
}
private TNTPhase loadPhase(YAPIONObject element) {
private TNTPhase loadPhase(Map<?, ?> element) {
TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false));
switch (element.getStringOrDefault("order", "REPEATER")) {
tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(getBoolean(element, "zVelocity", false));
switch (getString(element, "order", "REPEATER")) {
case "REPEATER":
tntPhase.setOrder(0);
break;
@@ -133,4 +137,8 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
}
return tntPhase;
}
private double requireDouble(Map<?, ?> element, String key) {
return ((Number) element.get(key)).doubleValue();
}
}
@@ -21,9 +21,6 @@ package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import lombok.experimental.UtilityClass;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import java.io.File;
import java.io.IOException;
@@ -36,48 +33,42 @@ public class SimulatorSaver {
directory.mkdirs();
}
YAPIONObject simulatorObject = new YAPIONObject();
simulatorObject.add("material", simulator.getMaterial().name());
simulatorObject.add("autoTrace", simulator.isAutoTrace());
simulatorObject.add("autoTestblock", simulator.isAutoTestblock());
SimulatorConfig config = new SimulatorConfig();
config.setMaterial(simulator.getMaterial());
config.setAutoTrace(simulator.isAutoTrace());
config.setAutoTestblock(simulator.isAutoTestblock());
YAPIONArray groups = new YAPIONArray();
simulator.getGroups().forEach(group -> {
YAPIONObject groupObject = new YAPIONObject();
groupObject.add("material", group.getMaterial().name());
groupObject.add("disabled", group.isDisabled());
SimulatorConfig.Group groupConfig = new SimulatorConfig.Group();
groupConfig.setMaterial(group.getMaterial());
groupConfig.setDisabled(group.isDisabled());
YAPIONArray elements = new YAPIONArray();
group.getElements().forEach(element -> {
YAPIONObject elementObject = new YAPIONObject();
elementObject.add("type", element.getType());
elementObject.add("material", element.getMaterial().name());
elementObject.add("disabled", element.isDisabled());
elementObject.add("x", element.getPosition().getX());
elementObject.add("y", element.getPosition().getY());
elementObject.add("z", element.getPosition().getZ());
element.saveExtra(elementObject);
SimulatorConfig.Element elementConfig = new SimulatorConfig.Element();
elementConfig.setType(element.getType());
elementConfig.setMaterial(element.getMaterial());
elementConfig.setDisabled(element.isDisabled());
elementConfig.setX(element.getPosition().getX());
elementConfig.setY(element.getPosition().getY());
elementConfig.setZ(element.getPosition().getZ());
element.saveExtra(elementConfig);
YAPIONArray phases = new YAPIONArray();
element.getPhases().forEach(phase -> {
YAPIONObject phaseObject = new YAPIONObject();
phaseObject.add("tickOffset", phase.getTickOffset());
phaseObject.add("lifetime", phase.getLifetime());
phaseObject.add("order", phase.getOrder());
phase.saveExtra(phaseObject);
phases.add(phaseObject);
SimulatorConfig.Phase phaseConfig = new SimulatorConfig.Phase();
phaseConfig.setTickOffset(phase.getTickOffset());
phaseConfig.setLifetime(phase.getLifetime());
phaseConfig.setOrder(phase.getOrder());
phase.saveExtra(phaseConfig);
elementConfig.getPhases().add(phaseConfig);
});
elementObject.add("phases", phases);
elements.add(elementObject);
groupConfig.getElements().add(elementConfig);
});
groupObject.add("elements", elements);
groups.add(groupObject);
config.getGroups().add(groupConfig);
});
simulatorObject.add("groups", groups);
File file = new File(directory, simulator.getName() + ".sim");
try {
simulatorObject.toJSONLossy(new FileOutput(file)).close();
SimulatorConfig.STORE.save(file, config);
} catch (IOException e) {
// Ignore
}
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.config.legacy.YapionReader;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
@@ -26,18 +27,23 @@ import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.config.legacy.YapionReader.*;
/**
* Loads the legacy per player .yapion files containing multiple simulators, they are
* deleted after loading and saved in the current format by the {@link SimulatorSaver}.
*/
@Deprecated
public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
@Override
@@ -46,10 +52,10 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
Map<String, Object> yapionObject;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IllegalArgumentException | IOException e) {
return Optional.empty();
}
@@ -57,28 +63,28 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name));
List<Simulator> simulators = new ArrayList<>();
for (String s : yapionObject.getKeys()) {
for (String s : yapionObject.keySet()) {
String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s);
YAPIONArray content = yapionObject.getArray(s);
List<Map<?, ?>> content = getObjects(yapionObject, s);
if (content.isEmpty()) continue;
Simulator simulator = new Simulator(newName);
for (YAPIONObject element : content.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> element : content) {
SimulatorGroup simulatorGroup = new SimulatorGroup();
simulator.add(simulatorGroup);
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("positionX"), element.getDouble("positionY"), element.getDouble("positionZ")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); // IDK if this existed back then?
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "positionX"), requireDouble(element, "positionY"), requireDouble(element, "positionZ")));
tntElement.setDisabled(getBoolean(element, "disabled", false)); // IDK if this existed back then?
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement);
TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false));
tntPhase.setOrder(element.getBooleanOrDefault("comparator", false) ? 2 : 0);
tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(getBoolean(element, "zVelocity", false));
tntPhase.setOrder(getBoolean(element, "comparator", false) ? 2 : 0);
tntElement.add(tntPhase);
}
simulators.add(simulator);
@@ -87,4 +93,8 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
file.delete();
return Optional.of(simulators);
}
private double requireDouble(Map<?, ?> element, String key) {
return ((Number) element.get(key)).doubleValue();
}
}
@@ -0,0 +1,64 @@
/*
* 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.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 java.util.HashSet;
import java.util.Set;
/**
* The current laufbau config of a player. It is stored as JSON, fields missing
* in the stored JSON keep the default value of their initializer, so adding a
* new field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to LaufbauConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to
* this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class LaufbauConfig implements VersionedConfig {
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());
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Identifiers of the deactivated {@link BlockBoundingBox}es, see {@link LaufbauUtils#identifier(BlockBoundingBox)}
*/
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;
}
}
@@ -20,63 +20,22 @@
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.core.SWPlayer;
import de.steamwar.inventory.SWItem;
import de.steamwar.sql.UserConfig;
import lombok.Cleanup;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.ItemMeta;
import yapion.hierarchy.output.StreamOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import yapion.parser.options.StreamOptions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@UtilityClass
public class LaufbauUtils {
public static class LaufbauComponent implements SWPlayer.Component {
private YAPIONObject yapionObject = new YAPIONObject();
@Override
@SneakyThrows
public void onMount(SWPlayer player) {
String config = UserConfig.getConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau");
if (config == null) {
return;
}
byte[] bytes = Base64.getDecoder().decode(config);
@Cleanup GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes));
yapionObject = new YAPIONParser(gzipInputStream, new StreamOptions().stopOnStreamEnd(true)).parse().result();
}
@Override
@SneakyThrows
public void onUnmount(SWPlayer player) {
if (yapionObject.isEmpty()) {
UserConfig.removePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau");
return;
}
@Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@Cleanup GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
yapionObject.toYAPION(new StreamOutput(gzipOutputStream)).close();
UserConfig.updatePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau", Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
}
}
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));
}
@@ -123,7 +82,7 @@ public class LaufbauUtils {
return false;
}
String identifier = identifier(bb);
return SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject.containsKey(identifier);
return LaufbauConfig.STORE.get(p).getDeactivated().contains(identifier);
}
@SneakyThrows
@@ -132,11 +91,9 @@ public class LaufbauUtils {
return;
}
String identifier = identifier(bb);
YAPIONObject yapionObject = SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject;
if (yapionObject.containsKey(identifier)) {
yapionObject.remove(identifier);
} else {
yapionObject.put(identifier, new YAPIONObject());
Set<String> deactivated = LaufbauConfig.STORE.get(p).getDeactivated();
if (!deactivated.remove(identifier)) {
deactivated.add(identifier);
}
}
}
@@ -0,0 +1,40 @@
/*
* 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.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfig;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfigV1;
@Deprecated
public class LaufbauConfigV1ToV2Converter implements ConfigConverter<LaufbauConfigV1, LaufbauConfig> {
@Override
public int version() {
return LaufbauConfigV1.VERSION;
}
@Override
public LaufbauConfig convert(LaufbauConfigV1 old) {
LaufbauConfig config = new LaufbauConfig();
config.getDeactivated().addAll(old.getDeactivated());
return config;
}
}
@@ -35,8 +35,8 @@ public class SmartPlaceCommand extends SWCommand {
@Register(description = {"SMART_PLACE_HELP", "SMART_PLACE_INFO"})
public void genericToggle(Player p) {
boolean smartPlace = Config.getInstance().get(p).getPlainValueOrDefault("smartPlace", false);
Config.getInstance().get(p).put("smartPlace", !smartPlace);
boolean smartPlace = Config.STORE.get(p).isSmartPlace();
Config.STORE.get(p).setSmartPlace(!smartPlace);
if (!smartPlace) {
SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p));
BauSystem.MESSAGE.send("SMART_PLACE_INFO", p);
@@ -25,6 +25,7 @@ import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket;
import net.minecraft.world.InteractionHand;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
@@ -87,7 +88,9 @@ public class SmartPlaceListener implements Listener {
public SmartPlaceListener() {
TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> {
if (!Permission.BUILD.hasPermission(player)) return packet;
if (!Config.getInstance().get(player).getPlainValueOrDefault("smartPlace", false)) return packet;
if (!Config.STORE.get(player).isSmartPlace()) return packet;
ItemStack usedItem = packet.getHand() == InteractionHand.MAIN_HAND ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand();
if (usedItem.getType() == Material.DEBUG_STICK) return packet;
RayTraceResult rayTraceResult = player.rayTraceBlocks(6);
Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null;
BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null;
@@ -127,7 +130,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return;
if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return;
WAS_EXECUTED.add(event.getPlayer());
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return;
@@ -146,7 +149,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return;
if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return;
if (!SMART_PLACING.contains(event.getPlayer())) {
if (CONTAINERS.contains(event.getBlockAgainst().getType())) {
SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup();
@@ -35,18 +35,15 @@ public class BlockCounter {
public final Map<Region, BlockCount> blockCountMap = new HashMap<>();
public boolean isActive(final Player p) {
if (!Config.getInstance().get(p).containsKey("blockCounter")) {
return false;
}
return Config.getInstance().get(p).getPlainValue("blockCounter");
return Config.STORE.get(p).isBlockCounter();
}
public void enable(final Player p) {
Config.getInstance().get(p).put("blockCounter", true);
Config.STORE.get(p).setBlockCounter(true);
}
public void disable(final Player p) {
Config.getInstance().get(p).put("blockCounter", false);
Config.STORE.get(p).setBlockCounter(false);
}
public String getMessage(Player player, int count, int tntCount, long tick, long lastTick) {
@@ -36,13 +36,10 @@ import org.bukkit.entity.Player;
import org.bukkit.util.Transformation;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import yapion.hierarchy.types.YAPIONValue;
import java.util.List;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.features.util.TNTClickListener.TNT_CLICK_DETAILS;
/**
* Wrapper for the rendering of a record bundle
*/
@@ -102,7 +99,7 @@ public class TraceEntity extends RBlockDisplay {
* @param player the player the message should be printed for
*/
public void printIntoChat(Player player) {
if (!Config.getInstance().get(player).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
if (!Config.STORE.get(player).isTntClickDetails()) {
return;
}
TNTPoint representative = records.get(0);
@@ -30,7 +30,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import yapion.hierarchy.types.YAPIONObject;
@Linked
public class NightVisionCommand extends SWCommand implements Listener {
@@ -41,15 +40,15 @@ public class NightVisionCommand extends SWCommand implements Listener {
@Register(description = "NIGHT_VISION_HELP")
public void genericCommand(Player p) {
YAPIONObject yapionObject = Config.getInstance().get(p);
boolean value = !yapionObject.getBooleanOrDefault("nightvision", false);
yapionObject.put("nightvision", value);
Config config = Config.STORE.get(p);
boolean value = !config.isNightVision();
config.setNightVision(value);
setNightVision(p, value);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
setNightVision(event.getPlayer(), Config.getInstance().get(event.getPlayer()).getBooleanOrDefault("nightvision", false));
setNightVision(event.getPlayer(), Config.STORE.get(event.getPlayer()).isNightVision());
}
private void setNightVision(Player p, boolean value) {
@@ -22,123 +22,71 @@ package de.steamwar.bausystem.features.util;
import com.comphenix.tinyprotocol.TinyProtocol;
import com.mojang.authlib.GameProfile;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.tpslimit.TPSUtils;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.command.SWCommand;
import de.steamwar.core.ProtocolWrapper;
import de.steamwar.core.SWPlayer;
import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.*;
import net.minecraft.world.entity.player.Abilities;
import net.minecraft.world.level.GameType;
import org.bukkit.Bukkit;
import io.papermc.paper.event.player.PlayerFailMoveEvent;
import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
import org.bukkit.GameMode;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;
import java.util.function.BiFunction;
@Linked
public class NoClipCommand extends SWCommand implements Listener {
public static class NoClipData implements SWPlayer.Component {
private long lastTick = -1;
@Override
public void onUnmount(SWPlayer player) {
player.getPlayer().setGameMode(GameMode.CREATIVE);
public void onUnmount(SWPlayer swPlayer) {
Player player = swPlayer.getPlayer();
showClientGameMode(player, GameMode.CREATIVE, GameMode.CREATIVE);
}
}
public NoClipCommand() {
super("noclip", "nc");
BiFunction<Player, ServerboundMovePlayerPacket, Object> first = (player, o) -> {
NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null);
if (noClipData == null) return o;
if (noClipData.lastTick == TPSUtils.currentTick.get()) return o;
setInternalGameMode(player, GameMode.SPECTATOR);
noClipData.lastTick = TPSUtils.currentTick.get();
return o;
};
TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, first);
TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, first);
BiFunction<Player, Object, Object> second = (player, o) -> {
NoClipData noClipData = SWPlayer.of(player).getComponent(NoClipData.class).orElse(null);
if (noClipData == null) return o;
setInternalGameMode(player, GameMode.CREATIVE);
noClipData.lastTick = TPSUtils.currentTick.get();
return o;
};
TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, second);
TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, second);
TinyProtocol.instance.addFilter(ServerboundContainerClickPacket.class, second);
BiFunction<Player, ServerboundSetCreativeModeSlotPacket, Object> third = (player, o) -> {
if (SWPlayer.of(player).hasComponent(NoClipData.class)) {
int index = o.slotNum();
if (index >= 36 && index <= 44) {
index -= 36;
} else if (index > 44) {
index -= 5;
} else if (index <= 8) {
index = index - 8 + 36;
}
player.getInventory().setItem(index, CraftItemStack.asBukkitCopy(o.itemStack()));
if (index < 9) player.getInventory().setHeldItemSlot(index);
player.updateInventory();
}
return o;
};
TinyProtocol.instance.addFilter(ServerboundSetCreativeModeSlotPacket.class, third);
}
private void setInternalGameMode(Player player, GameMode gameMode) {
((CraftPlayer) player).getHandle().gameMode.gameModeForPlayer = GameType.byId(gameMode.getValue());
}
@Register(help = true)
@Register
public void genericCommand(@Validator Player player) {
SWPlayer swPlayer = SWPlayer.of(player);
if (swPlayer.hasComponent(NoClipData.class)) {
swPlayer.removeComponent(NoClipData.class);
} else {
player.setGameMode(GameMode.SPECTATOR);
Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities();
abilities.mayBuild = true;
abilities.mayfly = true;
abilities.instabuild = true;
player.setGameMode(GameMode.CREATIVE);
player.setAllowFlight(true);
player.setFlying(true);
swPlayer.setComponent(new NoClipData());
BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player);
TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, 1F));
pseudoGameMode(player, GameMode.SPECTATOR);
showClientGameMode(player, GameMode.CREATIVE, GameMode.SPECTATOR);
}
}
@EventHandler
public void onBauMemberUpdate(BauMemberUpdateEvent event) {
event.getNewSpectator().forEach(player -> {
SWPlayer.of(player).removeComponent(NoClipData.class);
});
public void onPlayerFailMove(PlayerFailMoveEvent event) {
if (event.getFailReason() != PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK) return;
if (!SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) return;
event.setAllowed(true);
}
@EventHandler(ignoreCancelled = true)
@EventHandler
public void onBauMemberUpdate(BauMemberUpdateEvent event) {
event.getNewSpectator().forEach(player -> SWPlayer.of(player).removeComponent(NoClipData.class));
}
@EventHandler
public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) {
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
@EventHandler
public void onBlock(BlockCanBuildEvent event) {
if (event.getPlayer() == null) return;
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
@@ -146,7 +94,7 @@ public class NoClipCommand extends SWCommand implements Listener {
}
}
@EventHandler(ignoreCancelled = true)
@EventHandler
public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
event.setCancelled(true);
@@ -154,20 +102,13 @@ public class NoClipCommand extends SWCommand implements Listener {
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) {
return;
}
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
event.setCancelled(true);
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
event.getPlayer().setSpectatorTarget(null);
}, 1L);
}
/**
* @param player player
* @param localMode GameMode for breaking and placing
* @param listedMode GameMode for noPhysics
*/
private static void showClientGameMode(Player player, GameMode localMode, GameMode listedMode) {
TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, localMode.getValue()));
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), listedMode));
}
private static void pseudoGameMode(Player player, GameMode gameMode) {
TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), gameMode));
}
}
}
@@ -23,15 +23,17 @@ import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.entity.*;
import de.steamwar.linkage.Linked;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.piston.PistonStructureResolver;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.PistonMoveReaction;
import org.bukkit.block.TileState;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Piston;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
@@ -92,41 +94,48 @@ public class PistonCalculator implements Listener {
@EventHandler
public void onBlockPistonExtend(BlockPistonExtendEvent event) {
if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
movePistOrders(event.getDirection(), event.getBlocks());
pistOrders.values().forEach(PistOrder::calculate);
}, 3);
removeMovedPistOrders(event.getBlocks());
scheduleRecalculate();
}
@EventHandler
public void onBlockPistonRetract(BlockPistonRetractEvent event) {
if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
movePistOrders(event.getDirection(), event.getBlocks());
pistOrders.values().forEach(PistOrder::calculate);
}, 3);
removeMovedPistOrders(event.getBlocks());
scheduleRecalculate();
}
private void movePistOrders(BlockFace direction, List<Block> blocks) {
Set<PistOrder> orders = new HashSet<>();
private void removeMovedPistOrders(List<Block> blocks) {
blocks.forEach(block -> {
PistOrder pistOrder = pistOrders.get(block.getLocation());
if (pistOrder == null) return;
pistOrders.remove(block.getLocation());
pistOrder.piston = pistOrder.piston.getRelative(direction);
orders.add(pistOrder);
});
orders.forEach(pistOrder -> {
pistOrders.put(pistOrder.piston.getLocation(), pistOrder);
PistOrder pistOrder = pistOrders.remove(block.getLocation());
if (pistOrder != null) pistOrder.server.close();
});
}
private static final long RECALCULATE_INTERVAL_MS = 200;
private boolean recalculationScheduled = false;
private long lastRecalculation = 0;
private void scheduleRecalculate() {
if (recalculationScheduled) return;
recalculationScheduled = true;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1);
}
private void recalculateIfDue() {
if (System.currentTimeMillis() - lastRecalculation < RECALCULATE_INTERVAL_MS) {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), this::recalculateIfDue, 1);
return;
}
recalculationScheduled = false;
lastRecalculation = System.currentTimeMillis();
pistOrders.values().forEach(PistOrder::calculate);
}
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
pistOrders.values().forEach(PistOrder::calculate);
}, 1);
scheduleRecalculate();
}
@EventHandler
@@ -136,9 +145,7 @@ public class PistonCalculator implements Listener {
PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation());
pistOrder.server.close();
}
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
pistOrders.values().forEach(PistOrder::calculate);
}, 1);
scheduleRecalculate();
}
@EventHandler
@@ -164,13 +171,13 @@ public class PistonCalculator implements Listener {
private final Map<Location, PistOrder> pistOrders = new HashMap<>();
private final class PistOrder {
private Block piston;
private final Block piston;
private REntityServer server = new REntityServer();
private boolean pulling = false;
private List<Location> movedBlocks = new ArrayList<>();
private List<Location> brokenBlocks = new ArrayList<>();
private Set<Location> immovableBlocks = new HashSet<>();
private boolean blocked = false;
public PistOrder(Block piston) {
this.piston = piston;
@@ -179,7 +186,7 @@ public class PistonCalculator implements Listener {
public void calculate() {
movedBlocks.clear();
brokenBlocks.clear();
immovableBlocks.clear();
blocked = false;
if (piston.isEmpty()) {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
@@ -199,7 +206,7 @@ public class PistonCalculator implements Listener {
pulling = piston.getType() == Material.STICKY_PISTON && (piston.getRelative(pistonData.getFacing()).getType() == Material.AIR || pistonData.isExtended());
calculate(piston, pistonData.getFacing(), (pulling ? pistonData.getFacing().getOppositeFace() : pistonData.getFacing()));
resolve(pistonData.getFacing());
Collections.reverse(movedBlocks);
Collections.reverse(brokenBlocks);
@@ -207,86 +214,23 @@ public class PistonCalculator implements Listener {
visualize();
}
private void calculate(Block origin, BlockFace facing, BlockFace direction) {
Block calcOrigin = origin;
if (facing != direction) calcOrigin = origin.getRelative(facing, 3);
List<Block> toCalc = new LinkedList<>();
calcDirection(origin, null, calcOrigin, facing != direction ? origin.getRelative(facing) : null, facing, direction, toCalc);
while (!toCalc.isEmpty()) {
Block current = toCalc.remove(0);
if (!movedBlocks.contains(current.getLocation())) {
movedBlocks.add(current.getLocation());
}
Material type = current.getType();
if (type != Material.SLIME_BLOCK && type != Material.HONEY_BLOCK) continue;
Material oppositeType = type == Material.SLIME_BLOCK ? Material.HONEY_BLOCK : Material.SLIME_BLOCK;
for (BlockFace face : FACES) {
Block block = current.getRelative(face);
if (block.getType().isAir()) continue;
if (isImmovable(block) || block.getPistonMoveReaction() == PistonMoveReaction.BREAK) continue;
if (face != direction || face == direction.getOppositeFace()) {
switch (block.getType()) {
case BLACK_GLAZED_TERRACOTTA:
case GRAY_GLAZED_TERRACOTTA:
case BLUE_GLAZED_TERRACOTTA:
case BROWN_GLAZED_TERRACOTTA:
case CYAN_GLAZED_TERRACOTTA:
case GREEN_GLAZED_TERRACOTTA:
case LIGHT_BLUE_GLAZED_TERRACOTTA:
case LIGHT_GRAY_GLAZED_TERRACOTTA:
case LIME_GLAZED_TERRACOTTA:
case MAGENTA_GLAZED_TERRACOTTA:
case ORANGE_GLAZED_TERRACOTTA:
case PINK_GLAZED_TERRACOTTA:
case PURPLE_GLAZED_TERRACOTTA:
case RED_GLAZED_TERRACOTTA:
case WHITE_GLAZED_TERRACOTTA:
case YELLOW_GLAZED_TERRACOTTA:
continue;
default:
break;
}
}
if (block.getType() != oppositeType) {
if (!movedBlocks.contains(block.getLocation())) toCalc.add(block);
calcDirection(null, origin, block, null, facing, direction, toCalc);
}
}
private void resolve(BlockFace facing) {
PistonStructureResolver resolver = new PistonStructureResolver(
((CraftWorld) piston.getWorld()).getHandle(),
new BlockPos(piston.getX(), piston.getY(), piston.getZ()),
Direction.valueOf(facing.name()),
!pulling
);
if (!resolver.resolve()) {
blocked = true;
return;
}
movedBlocks.remove(origin.getLocation());
if (facing != direction) movedBlocks.remove(origin.getRelative(facing, 1).getLocation());
if (pulling) immovableBlocks.remove(origin.getRelative(facing).getLocation());
resolver.getToPush().forEach(pos -> movedBlocks.add(toLocation(pos)));
resolver.getToDestroy().forEach(pos -> brokenBlocks.add(toLocation(pos)));
}
private void calcDirection(Block origin, Block blockOrigin, Block calcOrigin, Block ignore, BlockFace facing, BlockFace direction, List<Block> toCalc) {
for (int i = 1; i < 24; i++) {
Block block = calcOrigin.getRelative(direction, i);
if (block.equals(ignore)) return;
if (block.getPistonMoveReaction() == PistonMoveReaction.BREAK) {
if (!brokenBlocks.contains(block.getLocation())) {
brokenBlocks.add(block.getLocation());
}
return;
}
if (isImmovable(block)) {
immovableBlocks.add(block.getLocation());
return;
}
if (block.getType().isAir()) return;
if (facing == direction && block.equals(blockOrigin)) {
immovableBlocks.add(block.getLocation());
return;
}
if (facing != direction && (block.equals(origin) || block.getRelative(facing.getOppositeFace()).equals(origin))) {
return;
}
if (!movedBlocks.contains(block.getLocation())) toCalc.add(block);
}
private Location toLocation(BlockPos pos) {
return new Location(piston.getWorld(), pos.getX(), pos.getY(), pos.getZ());
}
private void visualize() {
@@ -321,21 +265,11 @@ public class PistonCalculator implements Listener {
textDisplay.setBrightness(new Display.Brightness(15, 15));
}
for (Location location : immovableBlocks) {
CWireframe wireframe = new CWireframe(server);
wireframe.setPos1(location);
wireframe.setPos2(location);
wireframe.setWidth(1 / 32f);
wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
}
CWireframe wireframe = new CWireframe(server);
wireframe.setPos1(piston.getLocation());
wireframe.setPos2(piston.getLocation());
if (!immovableBlocks.isEmpty()) {
if (blocked) {
wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
} else if (movedBlocks.size() > 12) {
wireframe.setBlock(Material.YELLOW_CONCRETE.createBlockData());
} else {
wireframe.setBlock(Material.LIME_CONCRETE.createBlockData());
}
@@ -348,7 +282,11 @@ public class PistonCalculator implements Listener {
} else {
text.append("§ePush\n");
}
text.append("§f").append(movedBlocks.size()).append(" §eBlocks");
if (blocked) {
text.append("§cBlocked");
} else {
text.append("§f").append(movedBlocks.size()).append(" §eBlocks");
}
textDisplay.setText(text.toString());
textDisplay.setBillboard(Display.Billboard.CENTER);
textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER);
@@ -358,26 +296,4 @@ public class PistonCalculator implements Listener {
textDisplay.setBrightness(new Display.Brightness(15, 15));
}
}
private boolean isImmovable(Block block) {
if (block.isEmpty() || block.isLiquid()) {
return false;
}
BlockData blockData = block.getBlockData();
if (blockData instanceof Piston) {
return ((Piston) blockData).isExtended();
}
if (block.getState() instanceof TileState) {
return true;
}
PistonMoveReaction reaction = block.getPistonMoveReaction();
if (reaction == PistonMoveReaction.IGNORE) return true;
if (reaction == PistonMoveReaction.BLOCK) return true;
switch (block.getType()) {
case OBSIDIAN:
return true;
default:
return false;
}
}
}
@@ -32,26 +32,22 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
@Linked
public class TNTClickListener extends SWCommand implements Listener {
public static final String TNT_CLICK_DETAILS = "tnt_click_details";
public TNTClickListener() {
super("tntdetails");
}
@Register(description = "TNT_DETAILS_COMMAND")
public void toggle(Player player) {
YAPIONObject yapionObject = Config.getInstance().get(player);
if (yapionObject.getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
yapionObject.put(TNT_CLICK_DETAILS, false);
Config config = Config.STORE.get(player);
if (config.isTntClickDetails()) {
config.setTntClickDetails(false);
BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player);
} else {
yapionObject.put(TNT_CLICK_DETAILS, true);
config.setTntClickDetails(true);
BauSystem.MESSAGE.send("TNT_DETAILS_ON", player);
}
}
@@ -60,7 +56,7 @@ public class TNTClickListener extends SWCommand implements Listener {
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (event.getHand() != EquipmentSlot.HAND) return;
if (!Config.getInstance().get(event.getPlayer()).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
if (!Config.STORE.get(event.getPlayer()).isTntClickDetails()) {
return;
}
@@ -25,7 +25,6 @@ import lombok.Getter;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*;
@@ -56,27 +55,22 @@ public class Warp {
warpMap.put(name, this);
}
public Warp(String name, YAPIONObject object) {
public Warp(String name, WorldData.WarpEntry warp) {
this.name = name;
double x = object.getPlainValue("x");
double y = object.getPlainValue("y");
double z = object.getPlainValue("z");
float yaw = object.getPlainValue("yaw");
float pitch = object.getPlainValue("pitch");
location = new Location(Bukkit.getWorlds().get(0), x, y, z, yaw, pitch);
mat = Material.getMaterial(object.getPlainValue("material"));
location = new Location(Bukkit.getWorlds().get(0), warp.getX(), warp.getY(), warp.getZ(), warp.getYaw(), warp.getPitch());
mat = Material.getMaterial(warp.getMaterial());
warpMap.put(name, this);
}
public YAPIONObject output() {
YAPIONObject yapionObject = new YAPIONObject();
yapionObject.add("x", location.getX());
yapionObject.add("y", location.getY());
yapionObject.add("z", location.getZ());
yapionObject.add("yaw", location.getYaw());
yapionObject.add("pitch", location.getPitch());
yapionObject.add("material", mat.toString());
return yapionObject;
public WorldData.WarpEntry output() {
WorldData.WarpEntry warp = new WorldData.WarpEntry();
warp.setX(location.getX());
warp.setY(location.getY());
warp.setZ(location.getZ());
warp.setYaw(location.getYaw());
warp.setPitch(location.getPitch());
warp.setMaterial(mat.toString());
return warp;
}
public static Set<String> getWarpNames() {
@@ -101,7 +95,7 @@ public class Warp {
public void delete() {
warpMap.remove(name);
WorldData.getWarpData().remove(name);
WorldData.STORE.get().getWarps().remove(name);
}
public void teleport(Player player) {
@@ -34,9 +34,9 @@ import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.Map;
@Linked
public class WarpCommand extends SWCommand implements Disable, Enable {
@@ -135,17 +135,15 @@ public class WarpCommand extends SWCommand implements Disable, Enable {
@Override
public void enable() {
WorldData.getWarpData().forEach((name, yapionAnyType) -> {
new Warp(name, (YAPIONObject) yapionAnyType);
});
WorldData.STORE.get().getWarps().forEach(Warp::new);
Warp.enable();
}
@Override
public void disable() {
YAPIONObject yapionObject = WorldData.getWarpData();
Map<String, WorldData.WarpEntry> warps = WorldData.STORE.get().getWarps();
for (Warp warp : Warp.getWarps()) {
yapionObject.add(warp.getName(), warp.output());
warps.put(warp.getName(), warp.output());
}
}
}
@@ -28,6 +28,7 @@ import de.steamwar.bausystem.utils.bossbar.BossBarService;
import de.steamwar.linkage.Linked;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.BaumemberUpdatePacket;
import de.steamwar.sql.BauweltMember;
import io.papermc.paper.event.player.PlayerPickBlockEvent;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
@@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener {
}
public static void baumemberUpdate() {
// Packet handlers run in no guaranteed order, so don't rely on CoreNetworkHandler having cleared the cache yet.
BauweltMember.clear();
Set<Player> newSpectator = new HashSet<>();
Set<Player> newBuilder = new HashSet<>();
Bukkit.getOnlinePlayers().forEach(player -> {
@@ -20,78 +20,39 @@
package de.steamwar.bausystem.region;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.sql.SchematicNode;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public abstract class RegionData {
private final List<Property<?, ?>> properties = new ArrayList<>();
protected final YAPIONObject data;
protected final YAPIONObject flagData;
protected final WorldData.RegionEntry data;
protected final Map<String, String> flagData;
protected final Runnable onChange;
protected final Map<Flag<?>, Flag.Value<?>> flagMap = new HashMap<>();
private final class Property<T, K> {
private final String field;
private final Function<K, T> loader;
private final Function<T, K> writer;
private SchematicNode testblockSchematic;
private T value;
public Property(String field, Function<K, T> loader, Function<T, K> writer) {
this.field = field;
this.loader = loader;
this.writer = writer;
properties.add(this);
}
public void load() {
if (flagData.containsKey(field)) {
value = loader.apply(flagData.getPlainValue(field));
} else {
value = null;
}
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
if (value == null) {
flagData.remove(field);
} else {
flagData.put(field, writer.apply(value));
}
}
}
private Property<SchematicNode, Integer> testblockSchematic = new Property<>("testblockSchematic", SchematicNode::byId, SchematicNode::getId);
protected RegionData(YAPIONObject data, Runnable onChange) {
protected RegionData(WorldData.RegionEntry data, Runnable onChange) {
this.data = data;
this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject());
this.flagData = data.getFlags();
this.onChange = onChange;
initialize();
for (final Flag flag : Flag.getFlags()) {
if (!has(flag).isWritable()) continue;
try {
String s = flagData.getPlainValue(flag.name());
String s = flagData.get(flag.name());
flagMap.put(flag, flag.valueOfValue(s));
} catch (Exception e) {
flagMap.put(flag, (Flag.Value<?>) flag.getDefaultValue());
}
}
properties.forEach(Property::load);
if (data.getTestblockSchematic() != null) {
testblockSchematic = SchematicNode.byId(data.getTestblockSchematic());
}
}
protected void initialize() {
@@ -126,7 +87,8 @@ public abstract class RegionData {
flagData.remove(flag.name());
}
}
properties.forEach(property -> property.set(null));
testblockSchematic = null;
data.setTestblockSchematic(null);
onChange.run();
}
@@ -135,11 +97,12 @@ public abstract class RegionData {
}
public SchematicNode getTestblockSchematic() {
return testblockSchematic.get();
return testblockSchematic;
}
public void setTestblockSchematic(SchematicNode schematic) {
testblockSchematic.set(schematic);
testblockSchematic = schematic;
data.setTestblockSchematic(schematic == null ? null : schematic.getId());
onChange.run();
}
@@ -1,101 +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.bausystem.worlddata;
import de.steamwar.sql.SteamwarUser;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.InputStreamCharsets;
import yapion.parser.YAPIONParser;
import yapion.parser.options.FileOptions;
import java.io.File;
import java.util.List;
@UtilityClass
public class SimulatorData {
private final File simulatorsDir = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "simulators");
static {
simulatorsDir.mkdirs();
}
@SneakyThrows
public YAPIONObject getSimulator(Player player) {
File file = getFile(player);
if (!file.exists()) {
return new YAPIONObject();
}
return YAPIONParser.parse(file, new FileOptions().charset(InputStreamCharsets.UTF_8));
}
@SneakyThrows
private void internalSaveSimulator(Player player, YAPIONObject yapionObject) {
File file = getFile(player);
yapionObject.toYAPION(new FileOutput(file)).close();
}
@SneakyThrows
void saveSimulator(SteamwarUser steamwarUser, YAPIONObject yapionObject) {
File file = new File(simulatorsDir, steamwarUser.getId() + ".yapion");
yapionObject.toYAPION(new FileOutput(file)).close();
}
public List<String> listSimulator(Player player) {
List<String> strings = getSimulator(player).getKeys();
strings.remove("");
return strings;
}
public void saveSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put(name, yapionObject.getArray("").copy());
internalSaveSimulator(player, yapionObject);
}
public void saveSimulator(Player player, YAPIONArray simulator) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put("", simulator);
internalSaveSimulator(player, yapionObject);
}
public void loadSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put("", yapionObject.getArray(name).copy());
internalSaveSimulator(player, yapionObject);
}
public void removeSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.remove(name);
internalSaveSimulator(player, yapionObject);
}
private File getFile(Player player) {
SteamwarUser steamwarUser = SteamwarUser.get(player.getUniqueId());
return new File(simulatorsDir, steamwarUser.getId() + ".yapion");
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* 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
@@ -19,58 +19,83 @@
package de.steamwar.bausystem.worlddata;
import lombok.experimental.UtilityClass;
import com.google.gson.GsonBuilder;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.worlddata.converter.WorldDataV1ToV2Converter;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Bukkit;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.InputStreamCharsets;
import yapion.parser.YAPIONParser;
import yapion.parser.options.StreamOptions;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@UtilityClass
public class WorldData {
/**
* The current world data. It is stored as JSON, fields missing in the
* stored JSON keep the default value of their initializer, so adding a new
* field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to WorldDataV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class WorldData implements VersionedConfig {
private final File optionsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion");
private YAPIONObject worldData;
public static final int VERSION = 2;
public YAPIONObject getWorldData() {
if (worldData == null) {
read();
}
return worldData;
public static final ConfigStore<WorldData> STORE = new ConfigStore<>(WorldData.class, VERSION, WorldData::new)
.gson(new GsonBuilder().setPrettyPrinting().create())
.converter(new WorldDataV1ToV2Converter())
.file(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.json"))
.legacyFile(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"), WorldDataV1::parse);
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Region name to the data of the region
*/
protected Map<String, RegionEntry> regions = new HashMap<>();
protected RegionEntry globalRegion = new RegionEntry();
/**
* Warp name to warp
*/
protected Map<String, WarpEntry> warps = new HashMap<>();
@Getter
@Setter
public static class RegionEntry {
/**
* Overrides the prototype of the region config, null if not overridden
*/
protected String prototype = null;
/**
* Flag name to flag value name
*/
protected Map<String, String> flags = new HashMap<>();
/**
* Id of the testblock SchematicNode, null if the default testblock is used
*/
protected Integer testblockSchematic = null;
}
public YAPIONObject getRegionsData() {
return getWorldData().getYAPIONObjectOrSetDefault("regions", new YAPIONObject());
}
@Getter
@Setter
public static class WarpEntry {
public YAPIONObject getWarpData() {
return getWorldData().getYAPIONObjectOrSetDefault("warps", new YAPIONObject());
}
private void read() {
worldData = new YAPIONObject();
if (optionsFile.length() != 0) {
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(optionsFile))) {
worldData = YAPIONParser.parse(bufferedInputStream, new StreamOptions().charset(InputStreamCharsets.UTF_8));
} catch (IOException e) {
// Ignored
}
} else {
write();
}
}
public void write() {
try {
worldData.toYAPION(new FileOutput(optionsFile)).close();
} catch (IOException e) {
e.printStackTrace();
}
protected double x;
protected double y;
protected double z;
protected float yaw;
protected float pitch;
protected String material;
}
}
@@ -0,0 +1,111 @@
/*
* 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.worlddata;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
/**
* The legacy world data that was stored in the YAPION format in options.yapion.
*/
@Getter
@Setter
@Deprecated
public class WorldDataV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Region name to the data of the region, the global region is stored with the name "global"
*/
protected Map<String, RegionEntry> regions = new HashMap<>();
/**
* Warp name to warp
*/
protected Map<String, WarpEntry> warps = new HashMap<>();
@Getter
@Setter
public static class RegionEntry {
protected String prototype = null;
/**
* Flag name to flag value name and "testblockSchematic" to the SchematicNode id
*/
protected Map<String, Object> flagStorage = new HashMap<>();
}
@Getter
@Setter
public static class WarpEntry {
protected double x;
protected double y;
protected double z;
protected float yaw;
protected float pitch;
protected String material;
}
public static WorldDataV1 parse(String yapion) {
Map<String, Object> object = YapionReader.parse(yapion);
WorldDataV1 worldData = new WorldDataV1();
if (object.get("regions") instanceof Map<?, ?> regions) {
regions.forEach((name, value) -> {
if (!(value instanceof Map<?, ?> region)) return;
RegionEntry entry = new RegionEntry();
if (region.get("prototype") instanceof String prototype) {
entry.prototype = prototype;
}
if (region.get("flagStorage") instanceof Map<?, ?> flagStorage) {
flagStorage.forEach((key, flag) -> entry.flagStorage.put((String) key, flag));
}
worldData.regions.put((String) name, entry);
});
}
if (object.get("warps") instanceof Map<?, ?> warps) {
warps.forEach((name, value) -> {
if (!(value instanceof Map<?, ?> warp)) return;
WarpEntry entry = new WarpEntry();
entry.x = ((Number) warp.get("x")).doubleValue();
entry.y = ((Number) warp.get("y")).doubleValue();
entry.z = ((Number) warp.get("z")).doubleValue();
entry.yaw = ((Number) warp.get("yaw")).floatValue();
entry.pitch = ((Number) warp.get("pitch")).floatValue();
entry.material = (String) warp.get("material");
worldData.warps.put((String) name, entry);
});
}
return worldData;
}
}
@@ -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.bausystem.worlddata.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.bausystem.worlddata.WorldDataV1;
@Deprecated
public class WorldDataV1ToV2Converter implements ConfigConverter<WorldDataV1, WorldData> {
private static final String GLOBAL_REGION = "global";
private static final String TESTBLOCK_SCHEMATIC = "testblockSchematic";
@Override
public int version() {
return WorldDataV1.VERSION;
}
@Override
public WorldData convert(WorldDataV1 old) {
WorldData worldData = new WorldData();
old.getRegions().forEach((name, region) -> {
if (name.equals(GLOBAL_REGION)) {
worldData.setGlobalRegion(convert(region));
} else {
worldData.getRegions().put(name, convert(region));
}
});
old.getWarps().forEach((name, warp) -> worldData.getWarps().put(name, convert(warp)));
return worldData;
}
private WorldData.RegionEntry convert(WorldDataV1.RegionEntry old) {
WorldData.RegionEntry region = new WorldData.RegionEntry();
region.setPrototype(old.getPrototype());
old.getFlagStorage().forEach((key, value) -> {
if (value == null) return;
if (key.equals(TESTBLOCK_SCHEMATIC)) {
region.setTestblockSchematic(((Number) value).intValue());
} else {
region.getFlags().put(key, value.toString());
}
});
return region;
}
private WorldData.WarpEntry convert(WorldDataV1.WarpEntry old) {
WorldData.WarpEntry warp = new WorldData.WarpEntry();
warp.setX(old.getX());
warp.setY(old.getY());
warp.setZ(old.getZ());
warp.setYaw(old.getYaw());
warp.setPitch(old.getPitch());
warp.setMaterial(old.getMaterial());
return warp;
}
}
@@ -25,12 +25,12 @@ import de.steamwar.bausystem.region.flags.ColorMode;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.ProtectMode;
import de.steamwar.bausystem.region.flags.TNTMode;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedGlobalRegionData extends RegionData {
public FixedGlobalRegionData(YAPIONObject data, Runnable onChange) {
public FixedGlobalRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, onChange);
}
@@ -26,6 +26,7 @@ import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.TestblockMode;
import de.steamwar.bausystem.utils.FlatteningWrapper;
import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.core.Core;
import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.SchematicType;
@@ -139,7 +140,7 @@ public class FixedRegion implements Region {
}
}
public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, YAPIONObject regionData) {
public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, WorldData.RegionEntry regionData) {
this.name = name;
uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
this.flagStorage = flagStorage;
@@ -22,12 +22,12 @@ package de.steamwar.bausystem.region.fixed;
import de.steamwar.bausystem.region.RegionData;
import de.steamwar.bausystem.region.RegionFlagPolicy;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedRegionData extends RegionData {
public FixedRegionData(YAPIONObject data, Runnable onChange) {
public FixedRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, onChange);
}
@@ -210,14 +210,14 @@ public class Prototype {
}
}
public static void generateRegion(String name, YAPIONObject regionConfig, YAPIONObject regionData) {
public static void generateRegion(String name, YAPIONObject regionConfig, WorldData.RegionEntry regionData) {
Prototype prototype;
if (regionData.containsKey("prototype", String.class)) {
prototype = PROTOTYPE_MAP.get(regionData.getPlainValue("prototype"));
if (regionData.getPrototype() != null) {
prototype = PROTOTYPE_MAP.get(regionData.getPrototype());
} else {
prototype = PROTOTYPE_MAP.get(regionConfig.getPlainValue("prototype"));
}
FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData::write);
FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData.STORE::save);
FixedRegionSystem.addRegion(new FixedRegion(name, flagStorage, prototype, regionConfig, regionData));
}
}
@@ -28,7 +28,6 @@ import org.bukkit.Bukkit;
import yapion.hierarchy.diff.DiffChange;
import yapion.hierarchy.diff.YAPIONDiff;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONType;
import yapion.parser.YAPIONParser;
import java.io.BufferedInputStream;
@@ -55,7 +54,7 @@ public class RegionLoader {
}
loaded = yapionObject;
YAPIONObject optionsYapionObject = WorldData.getRegionsData();
WorldData worldData = WorldData.STORE.get();
yapionObject.forEach((key, yapionAnyType) -> {
if (key.equals("global")) {
return;
@@ -65,21 +64,10 @@ public class RegionLoader {
}
YAPIONObject regionConfig = (YAPIONObject) yapionAnyType;
YAPIONObject regionData = new YAPIONObject();
if (optionsYapionObject.containsKey(key, YAPIONType.OBJECT)) {
regionData = optionsYapionObject.getObject(key);
} else {
optionsYapionObject.add(key, regionData);
}
WorldData.RegionEntry regionData = worldData.getRegions().computeIfAbsent(key, k -> new WorldData.RegionEntry());
Prototype.generateRegion(key, regionConfig, regionData);
});
YAPIONObject globalOptions = optionsYapionObject.getObject("global");
if (globalOptions == null) {
globalOptions = new YAPIONObject();
optionsYapionObject.add("global", globalOptions);
}
FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(globalOptions, WorldData::write));
FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(worldData.getGlobalRegion(), WorldData.STORE::save));
}
}
@@ -0,0 +1,40 @@
/*
* 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.network.packets.server;
import de.steamwar.network.packets.NetworkPacket;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* A chat message written on an arena. The arena only records it for the replay and never displays it.
*/
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Getter
public class ChatRecordPacket extends NetworkPacket {
private static final long serialVersionUID = -4718360932583127458L;
private int playerId;
private String message;
}
@@ -30,6 +30,7 @@ import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore
import java.util.*
import java.util.concurrent.ConcurrentHashMap
object BauweltMemberTable : CompositeIdTable("BauweltMember") {
val bauweltId = reference("BauweltID", SteamwarUserTable).index()
@@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") {
class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<BauweltMember>(BauweltMemberTable) {
private val cache = mutableMapOf<Int, BauweltMember>()
// Keyed by (bauweltId, memberId). Optional.empty() caches "is not a member", which is the
// common case for spectators and would otherwise hit the DB on every permission check.
private val cache = ConcurrentHashMap<Pair<Int, Int>, Optional<BauweltMember>>()
private fun cache(member: BauweltMember) =
cache.put(member.memberID, member)
cache.put(member.bauweltID to member.memberID, Optional.of(member))
@JvmStatic
fun clear() =
@@ -75,20 +78,20 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
it[worldEdit] = false
it[world] = false
}
}
}.also { cache.remove(ownerId.value to newMemberId.value) }
@JvmStatic
@Deprecated("Use getBauMember(bauwelt: Int, member: Int)")
fun getBauMember(bauwelt: UUID, member: UUID) =
useDb {
find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) }
}
getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value)
@JvmStatic
fun getBauMember(bauwelt: Int, member: Int) =
useDb {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) }
}
fun getBauMember(bauwelt: Int, member: Int): BauweltMember? =
cache.computeIfAbsent(bauwelt to member) {
Optional.ofNullable(useDb {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()
})
}.orElse(null)
@JvmStatic
@Deprecated("Use getMembers(bauwelt: Int)")
@@ -140,8 +143,11 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
fun isWorld() =
world
fun remove() =
fun remove() {
val key = bauweltID to memberID
useDb {
delete()
}
cache.remove(key)
}
}
@@ -19,6 +19,7 @@
package de.steamwar.sql;
import de.steamwar.sql.internal.SQLConfig;
import lombok.ToString;
import java.io.File;
@@ -250,6 +251,11 @@ public final class GameModeConfig<M, W> {
Times = new TimesConfig(loader.with("Times"));
// Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards
Schematic = new SchematicConfig<>(loader.with("Schematic"));
for (int i = 0; i < Schematic.Limited.size(); i++) {
if (Schematic.Limited.get(i).Materials.isEmpty()) {
SQLConfig.impl.getLogger().warning(file.getName() + ": Schematic.Limited[" + i + "] matches no blocks, check its 'Materials' entry");
}
}
GameName = loader.getString("GameName", defaultGameName.apply(file));
ActiveMonths = loader.getIntList("ActiveMonths");
TeamChatPrefix = loader.getString("TeamChatPrefix", "+");
@@ -611,6 +617,15 @@ public final class GameModeConfig<M, W> {
*/
public final List<SchematicType> SubTypes;
/**
* List of publics that should be used. Mainly useful for an Event Mode
* where a subset of the regular publics should be used without duplicating
* those in the Database.
*
* @implSpec {@code []} by default, thus the GameModes publics are used.
*/
public final List<Integer> Publics;
/**
* Shortcut of the schematic type
*
@@ -703,7 +718,10 @@ public final class GameModeConfig<M, W> {
public final double MaxDesignBlastResistance;
/**
* List of limited block materials
* List of limited block materials<br/>
* Every inventory block is counted towards exactly one rule: a filled block towards a rule whose
* {@link BlockRule#Content} allows all of its items, an empty block towards a rule without
* {@link BlockRule#Content}. Empty inventory blocks without such a rule are unlimited.
*
* @implSpec {@link SQLWrapper#getInventoryMaterials)} are all disallowed by default to have any content but are unlimited in number.
* @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0.
@@ -716,6 +734,7 @@ public final class GameModeConfig<M, W> {
Inset = new InsetConfig(loader.with("Inset"));
Type = null;
SubTypesStrings = loader.getStringList("SubTypes");
Publics = loader.getIntList("Publics");
SubTypes = new ArrayList<>();
Shortcut = loader.getString("Shortcut", "");
Material = loader.getMaterial("Material", "STONE_BUTTON");
@@ -759,18 +778,13 @@ public final class GameModeConfig<M, W> {
}
public boolean isInventory(M material) {
return Limited.stream()
.filter(rule -> rule.Materials.contains(material))
.anyMatch(rule -> rule.Content.loaded);
return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material));
}
public Integer getItemAmount(M material, M item) {
public List<BlockRule<M>> getRules(M material) {
return Limited.stream()
.filter(rule -> rule.Materials.contains(material))
.filter(rule -> rule.Content.Items.contains(item))
.mapToInt(rule -> rule.Content.Amount)
.max()
.orElse(0);
.collect(Collectors.toList());
}
@ToString
@@ -46,7 +46,7 @@ public interface SQLWrapper<M> {
}
/**
* Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s
* Every inventory material - the {@code inventory} group usable in {@code GameModeConfig}'s
* {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}.
*/
default Set<M> getInventoryMaterials() {
@@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready
TEAM_NOT_READY = §c§mTeam ready
SKIP_READY = §aSkipping to next event
SKIP_NOT_READY = §c§mSkipping to next event
TEAM_CHAT = {0}{1}§8» {0}{2}
CHOOSE_KIT = §eChoose kit
RESPAWN = §eRespawn
MANAGE_PLAYERS = §cManage players
@@ -20,19 +20,14 @@
package de.steamwar.fightsystem.ai;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.fight.FightSchematic;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.sql.SchematicNode;
import de.steamwar.sql.SteamwarUser;
import java.util.List;
import java.util.Random;
public class DummyAI extends AI {
private static final Random random = new Random();
public DummyAI(FightTeam team) {
super(team, SteamwarUser.get("public"));
@@ -41,8 +36,7 @@ public class DummyAI extends AI {
@Override
public SchematicNode chooseSchematic() {
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB());
return publics.get(random.nextInt(publics.size()));
return FightSchematic.getPublicSchematic().orElseThrow();
}
@Override
@@ -255,7 +255,15 @@ public class GUI {
private static void schemDialog(Player p, SchematicType type, boolean publicSchems, boolean unchecked) {
SchematicSelector selector = new SchematicSelector(p, Config.test() ? SchematicSelector.selectSchematic() : SchematicSelector.selectSchematicType(unchecked ? type.checkType() : type), node -> {
schemSelect(p, node);
});
}) {
@Override
protected List<SchematicNode> getSchematicList(SchematicNode parent) {
if (!publicSchems || parent != null) return super.getSchematicList(parent);
List<Integer> publics = Config.GameModeConfig.Schematic.Publics;
if (publics.isEmpty()) return super.getSchematicList(null);
return publics.stream().map(SchematicNode::byId).collect(Collectors.toList());
}
};
selector.setPublicMode(publicSchems ? SchematicSelector.PublicMode.PUBLIC_ONLY : SchematicSelector.PublicMode.PRIVATE_ONLY);
selector.open();
}
@@ -46,6 +46,7 @@ import org.bukkit.util.Vector;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.logging.Level;
@@ -109,31 +110,47 @@ public class FightSchematic extends StateDependent {
clipboard = null;
}
private static final Random random = new Random();
public static Optional<SchematicNode> getPublicSchematic() {
List<Integer> publicsOverridden = Config.GameModeConfig.Schematic.Publics;
if (!publicsOverridden.isEmpty()) {
int publicId = publicsOverridden.get(random.nextInt(publicsOverridden.size()));
SchematicNode schematicNode = SchematicNode.getSchematicNode(publicId);
if (schematicNode != null) {
return Optional.of(schematicNode);
}
}
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB());
if (publics.isEmpty()) {
for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) {
publics = SchematicNode.getAllSchematicsOfType(0, type.toDB());
if (!publics.isEmpty()) {
break;
}
}
if (publics.isEmpty()) {
return Optional.empty();
}
}
return Optional.of(publics.get(random.nextInt(publics.size())));
}
@Override
public void enable() {
if (FightState.getFightState() == FightState.SPECTATE) return;
if (clipboard == null) {
List<SchematicNode> publics = SchematicNode.getAllSchematicsOfType(0, Config.GameModeConfig.Schematic.Type.toDB());
if (publics.isEmpty()) {
for (SchematicType type : Config.GameModeConfig.Schematic.SubTypes) {
publics = SchematicNode.getAllSchematicsOfType(0, type.toDB());
if (!publics.isEmpty()) {
break;
}
}
if (publics.isEmpty()) {
return;
}
}
setSchematic(publics.get(new Random().nextInt(publics.size())));
Optional<SchematicNode> schematicNode = getPublicSchematic();
if (schematicNode.isEmpty()) return;
setSchematic(schematicNode.get());
}
if (ArenaMode.AntiReplay.contains(Config.mode)) {
boolean changeRotation = false;
if (Config.GameModeConfig.WinConditions.contains(Winconditions.RANDOM_ROTATE)) {
changeRotation = new Random().nextBoolean();
changeRotation = random.nextBoolean();
usedRotate = rotate ^ changeRotation;
}
if (team.isBlue()) {
@@ -256,10 +256,6 @@ public class FightTeam {
broadcast(player -> FightSystem.getMessage().send(message, player, params));
}
public void broadcastChat(Player sender, String message) {
broadcast(player -> FightSystem.getMessage().sendPrefixless("TEAM_CHAT", player, ChatMessageType.CHAT, prefix, sender.getName(), message));
}
private void broadcast(Consumer<Player> f) {
players.forEach((uuid, fightPlayer) -> {
Player player = Bukkit.getPlayer(uuid);
@@ -19,50 +19,41 @@
package de.steamwar.fightsystem.listener;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.record.GlobalRecorder;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.linkage.Linked;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.ChatRecordPacket;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.logging.Level;
/**
* Chat is handled by Velocity. Messages written on this server are only sent here to be recorded for the replay.
*/
@Linked
public class Chat implements Listener {
public class Chat extends PacketHandler {
public Chat() {
new StateDependentListener(ArenaMode.All, FightState.All, this);
}
@Handler
public void handleChatRecord(ChatRecordPacket packet) {
SteamwarUser user = SteamwarUser.byId(packet.getPlayerId());
String message = packet.getMessage();
FightSystem.getPlugin().getLogger().log(Level.INFO, user.getUserName() + "» " + message);
@EventHandler
public void handlePlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
String message = event.getMessage();
FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message);
FightTeam fightTeam = Fight.getPlayerTeam(player);
Player player = Bukkit.getPlayer(user.getUUID());
FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null;
if (fightTeam != null) {
String teamName = fightTeam.getColoredName();
if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) {
fightTeam.broadcastChat(player, message.substring(1));
} else {
broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message);
}
} else if (Config.isReferee(player)) {
broadcastChat("FIGHTLEADER_CHAT", player.getName(), message);
GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message);
} else if (player != null && Config.isReferee(player)) {
GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message);
} else {
broadcastChat("SPECTATOR_CHAT", player.getName(), message);
GlobalRecorder.getInstance().chat("SPECTATOR_CHAT", user.getUserName(), message);
}
event.setCancelled(true);
}
public static void broadcastChat(String message, Object... params) {
@@ -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!
@@ -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));
}
}
}
@@ -23,20 +23,25 @@ import de.steamwar.linkage.Linked;
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
@@ -55,17 +60,14 @@ public class ParticleListener implements Listener {
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;
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)));
}
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);
}
@@ -77,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
@@ -85,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());
@@ -111,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);
}
}
@@ -0,0 +1,59 @@
/*
* 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.special.easter.EggHunt;
import de.steamwar.sql.Event;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.TeamTeilnahme;
import de.steamwar.sql.UserConfig;
import lombok.Getter;
import java.util.Set;
import java.util.stream.Collectors;
// Everything requirements check against, loaded only when a requirement needs it
public class UnlockContext {
@Getter
private final SteamwarUser user;
private Set<Integer> events;
private String eggHuntConfig;
private boolean eggHuntConfigLoaded;
public UnlockContext(SteamwarUser user) {
this.user = user;
}
public Set<Integer> getEvents() {
if (events == null) {
events = user.getTeam() == 0 ? Set.of() : TeamTeilnahme.getEvents(user.getTeam()).stream().map(Event::getEventID).collect(Collectors.toSet());
}
return events;
}
public String getEggHuntConfig() {
if (!eggHuntConfigLoaded) {
eggHuntConfig = UserConfig.getConfig(user.getId(), EggHunt.EGG_HUNT_CONFIG_KEY);
eggHuntConfigLoaded = true;
}
return eggHuntConfig;
}
}
@@ -0,0 +1,40 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
public class BooleanParameter extends Parameter<Boolean> {
public BooleanParameter(String key, String nameKey, boolean defaultValue) {
super(key, nameKey, defaultValue);
}
@Override
public JsonElement toJson(Boolean value) {
return new JsonPrimitive(value);
}
@Override
protected Boolean read(JsonElement json) {
return json.getAsBoolean();
}
}
@@ -0,0 +1,102 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import de.steamwar.lobby.particle.ParticleRequirement;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@Getter
public class ChoiceParameter<E> extends Parameter<E> {
// Internal choices are usable by presets but never offered in the builder or accepted from storage. Locked choices
// are offered with their requirement; the requirement is looked up lazily as it may live in a reward design.
public record Choice<E>(String id, E value, boolean internal, Supplier<ParticleRequirement> requirement) {
public Choice(String id, E value, boolean internal) {
this(id, value, internal, () -> ParticleRequirement.NO_REQUIRMENT);
}
}
private final List<Choice<E>> choices;
public ChoiceParameter(String key, String nameKey, E defaultValue, List<Choice<E>> choices) {
super(key, nameKey, defaultValue);
this.choices = choices;
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue, Predicate<E> internal, Function<E, ParticleRequirement> requirement) {
List<Choice<E>> choices = new ArrayList<>();
for (E value : defaultValue.getDeclaringClass().getEnumConstants()) {
choices.add(new Choice<>(value.name(), value, internal.test(value), () -> requirement.apply(value)));
}
return new ChoiceParameter<>(key, nameKey, defaultValue, choices);
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue, Predicate<E> internal) {
return ofEnum(key, nameKey, defaultValue, internal, value -> ParticleRequirement.NO_REQUIRMENT);
}
public static <E extends Enum<E>> ChoiceParameter<E> ofEnum(String key, String nameKey, E defaultValue) {
return ofEnum(key, nameKey, defaultValue, value -> false);
}
public List<E> getBuildableValues() {
return choices.stream().filter(choice -> !choice.internal()).map(Choice::value).toList();
}
public ParticleRequirement requirement(E value) {
return choiceOf(value).requirement().get();
}
@Override
public JsonElement toJson(E value) {
return new JsonPrimitive(choiceOf(value).id());
}
@Override
protected E read(JsonElement json) {
String id = json.getAsString();
return choices.stream()
.filter(choice -> !choice.internal() && choice.id().equals(id))
.map(Choice::value)
.findFirst()
.orElse(null);
}
@Override
public E validate(E value) {
return choices.stream().anyMatch(choice -> choice.value() == value) ? value : getDefaultValue();
}
private Choice<E> choiceOf(E value) {
return choices.stream()
.filter(choice -> choice.value() == value)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown choice for " + getKey()));
}
}
@@ -0,0 +1,29 @@
/*
* 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.design;
public enum DesignVisibility {
// Offered in the builder to everyone
BASIC,
// Offered in the builder, usable once its requirement is met
REWARD,
// Only used by presets, never offered in the builder or accepted from storage
INTERNAL,
}
@@ -0,0 +1,201 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.DrawnWings;
import de.steamwar.lobby.particle.WingDesign;
import de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.Cloud;
import de.steamwar.lobby.particle.elements.Wing;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@UtilityClass
public class Designs {
// Built on first use, so the design classes never depend on each other's initialisation order
private Map<String, ParticleDesign> registry;
public final ParticleDesign SIMPLE = new ParticleDesign("simple", Material.BLAZE_POWDER, "PARTICLE_DESIGN_SIMPLE") {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return particle;
}
};
public final DoubleParameter CIRCLE_RADIUS = new DoubleParameter("radius", "PARTICLE_PARAM_RADIUS", 1, 0.2, 3, 0.1);
public final DoubleParameter CIRCLE_SPEED = new DoubleParameter("circleSpeed", "PARTICLE_PARAM_CIRCLE_SPEED", 1, -20, 20, 0.5);
public final DoubleParameter CIRCLE_PHASE = new DoubleParameter("phase", "PARTICLE_PARAM_PHASE", 0, 0, 345, 15);
public final DoubleParameter CIRCLE_RISE = new DoubleParameter("rise", "PARTICLE_PARAM_RISE", 0, 0, 3, 0.1);
public final DoubleParameter CIRCLE_RISE_SPEED = new DoubleParameter("riseSpeed", "PARTICLE_PARAM_RISE_SPEED", 20, 0, 100, 5);
public final BooleanParameter CIRCLE_BOUNCE = new BooleanParameter("bounce", "PARTICLE_PARAM_BOUNCE", true);
public final BooleanParameter CIRCLE_INVERTED = new BooleanParameter("inverted", "PARTICLE_PARAM_INVERTED", false);
public final ParticleDesign CIRCLE = new ParticleDesign("circle", Material.MAGMA_CREAM, "PARTICLE_DESIGN_CIRCLE",
CIRCLE_RADIUS, CIRCLE_SPEED, CIRCLE_PHASE, CIRCLE_RISE, CIRCLE_RISE_SPEED, CIRCLE_BOUNCE, CIRCLE_INVERTED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
double rise = values.get(CIRCLE_RISE);
if (rise > 0) {
particle = new YOffset(particle, values.get(CIRCLE_RISE_SPEED), 0, rise, values.get(CIRCLE_INVERTED), values.get(CIRCLE_BOUNCE));
}
return new Circle(particle, values.get(CIRCLE_RADIUS), values.get(CIRCLE_SPEED), values.get(CIRCLE_PHASE));
}
};
// Images keep their old ids, so saved wings still load; preset and tournament images stay out of the builder
private final List<ChoiceParameter.Choice<WingShape>> WING_SHAPES = List.of(
new ChoiceParameter.Choice<>("simple", WingShape.image(WingDesign.SIMPLE, Material.FEATHER), false),
new ChoiceParameter.Choice<>("complex", WingShape.image(WingDesign.COMPLEX, Material.PHANTOM_MEMBRANE), false),
new ChoiceParameter.Choice<>("sword", WingShape.image(WingDesign.SWORD, Material.STONE_SWORD), false),
new ChoiceParameter.Choice<>("bat", WingShape.drawn(DrawnWings.BAT, Material.BAT_SPAWN_EGG), false),
new ChoiceParameter.Choice<>("butterfly", WingShape.drawn(DrawnWings.BUTTERFLY, Material.PINK_PETALS), false),
new ChoiceParameter.Choice<>("fairy", WingShape.drawn(DrawnWings.FAIRY, Material.ALLAY_SPAWN_EGG), false),
// Tournament wing images, unlocked by the placement that earns their reward
new ChoiceParameter.Choice<>("mwgl", WingShape.image(WingDesign.MWGL, Material.IRON_SWORD), false, Unlocks.MINI_WARGEAR_LIGA_PLACEMENT),
new ChoiceParameter.Choice<>("sw", WingShape.image(WingDesign.SW, Material.CYAN_CARPET), false, Unlocks.UNDERWATER_MWG_PLACEMENT),
new ChoiceParameter.Choice<>("wgs", WingShape.image(WingDesign.WGS, Material.DIAMOND_HELMET), false, Unlocks.WARGEAR_SEASON_2022_PLACEMENT),
new ChoiceParameter.Choice<>("sword_crossed", WingShape.image(WingDesign.SWORD_CROSSED, Material.GOLDEN_SWORD), false, Unlocks.WARGEAR_CLASH_PLACEMENT),
new ChoiceParameter.Choice<>("eclipse", WingShape.image(WingDesign.ECLIPSE), true),
new ChoiceParameter.Choice<>("pl", WingShape.image(WingDesign.PL), true),
new ChoiceParameter.Choice<>("plompa_easter", WingShape.image(WingDesign.PlompaEasterWings), true),
new ChoiceParameter.Choice<>("ely_e", WingShape.image(WingDesign.ELY_E), true),
new ChoiceParameter.Choice<>("ely_l", WingShape.image(WingDesign.ELY_L), true),
new ChoiceParameter.Choice<>("ely_y", WingShape.image(WingDesign.ELY_Y), true),
new ChoiceParameter.Choice<>("ev", WingShape.image(WingDesign.EV), true)
);
public final ChoiceParameter<WingShape> WING_SHAPE = new ChoiceParameter<>("image", "PARTICLE_PARAM_WING_IMAGE", WING_SHAPES.get(0).value(), WING_SHAPES);
// Points per wing of drawn shapes; it replaces the amount, as every point spawns one particle
public final IntParameter WING_DETAIL = new IntParameter("detail", "PARTICLE_PARAM_WING_DETAIL", 20, 6, 60);
public final DoubleParameter WING_SIZE = new DoubleParameter("size", "PARTICLE_PARAM_WING_SIZE", 0.15, 0.05, 0.3, 0.01);
public enum WingColoring {
// The whole wing changes colour through the entry's colours over time
OVER_TIME,
// The colours run from the spine to the wing tips
ALONG_WING,
}
public final ChoiceParameter<WingColoring> WING_COLORING = ChoiceParameter.ofEnum("coloring", "PARTICLE_PARAM_WING_COLORING", WingColoring.OVER_TIME);
// Percent of the points shown each tick; fewer makes the wing twinkle and costs less
public final IntParameter WING_SPARKLE = new IntParameter("sparkle", "PARTICLE_PARAM_WING_SPARKLE", 100, 10, 100);
public final ChoiceParameter<Wing.Motion> WING_MOTION = ChoiceParameter.ofEnum("motion", "PARTICLE_PARAM_WING_MOTION", Wing.Motion.NONE, motion -> false,
motion -> motion == Wing.Motion.BEAT_ON_JUMP ? Unlocks.WARGEAR_SEASON_2022.get() : ParticleRequirement.NO_REQUIRMENT);
// Moving wings need to be redrawn often to look smooth; still ones are fine once a second
private final int MOVING_WING_INTERVAL = 2;
private final int STILL_WING_INTERVAL = 20;
public final ParticleDesign WING = new ParticleDesign("wing", Material.ELYTRA, "PARTICLE_DESIGN_WING", WING_SHAPE, WING_DETAIL, WING_SIZE, WING_COLORING, WING_SPARKLE, WING_MOTION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
WingShape shape = values.get(WING_SHAPE);
return new Wing(particle, values.get(WING_SIZE), shape.design(values.get(WING_DETAIL)),
values.get(WING_COLORING) == WingColoring.ALONG_WING, values.get(WING_SPARKLE), values.get(WING_MOTION));
}
@Override
public boolean redrawsAfterJump(ParameterValues values) {
return values.get(WING_MOTION) == Wing.Motion.BEAT_ON_JUMP;
}
@Override
public int points(ParameterValues values) {
int points = values.get(WING_SHAPE).points(values.get(WING_DETAIL));
return (int) Math.ceil(points * values.get(WING_SPARKLE) / 100.0);
}
@Override
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
return parameter != WING_DETAIL || values.get(WING_SHAPE).isDrawn();
}
@Override
public boolean replacesAmount(ParameterValues values) {
return values.get(WING_SHAPE).isDrawn();
}
@Override
public void adjust(Parameter<?> changed, ParameterValues values) {
if (changed != WING_MOTION) return;
// A beat on jumping is redrawn every tick only while it beats
boolean moving = values.get(WING_MOTION) == Wing.Motion.FLAP_SLOW || values.get(WING_MOTION) == Wing.Motion.FLAP_FAST;
int interval = values.get(EntryParameters.INTERVAL);
if (moving && interval > MOVING_WING_INTERVAL) {
values.set(EntryParameters.INTERVAL, MOVING_WING_INTERVAL);
} else if (!moving && interval == MOVING_WING_INTERVAL) {
values.set(EntryParameters.INTERVAL, STILL_WING_INTERVAL);
}
}
// Image wings spawn a particle on every pixel, so they start sparse, and sit on the back even while gliding
@Override
public void applyDefaults(ParameterValues values) {
values.set(EntryParameters.COUNT, 1)
.set(EntryParameters.SPEED, 0.0)
.set(EntryParameters.TICK_MODE, EntryParameters.TickMode.ALWAYS)
.set(EntryParameters.INTERVAL, STILL_WING_INTERVAL)
.set(EntryParameters.PLACEMENT, EntryParameters.Placement.BODY);
}
};
// The wing shape that shows the given image, for presets
public WingShape wingShape(WingDesign image) {
return WING_SHAPE.getChoices().stream().map(ChoiceParameter.Choice::value).filter(shape -> shape.shows(image)).findFirst().orElseThrow();
}
public final ParticleDesign CLOUD = new ParticleDesign("cloud", Material.WHITE_WOOL, "PARTICLE_DESIGN_CLOUD") {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Cloud(particle);
}
};
private Map<String, ParticleDesign> registry() {
if (registry == null) {
Map<String, ParticleDesign> designs = new LinkedHashMap<>();
List.of(SIMPLE, CIRCLE, WING, CLOUD).forEach(design -> register(designs, design));
RewardDesigns.all().forEach(design -> register(designs, design));
InternalDesigns.all().forEach(design -> register(designs, design));
registry = designs;
}
return registry;
}
private void register(Map<String, ParticleDesign> designs, ParticleDesign design) {
if (designs.put(design.getId(), design) != null) {
throw new IllegalStateException("Duplicate particle design " + design.getId());
}
}
public ParticleDesign byId(String id) {
return registry().get(id);
}
public List<ParticleDesign> all() {
return List.copyOf(registry().values());
}
}
@@ -0,0 +1,57 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import lombok.Getter;
@Getter
public class DoubleParameter extends Parameter<Double> {
private final double min;
private final double max;
private final double step;
public DoubleParameter(String key, String nameKey, double defaultValue, double min, double max, double step) {
super(key, nameKey, defaultValue);
this.min = min;
this.max = max;
this.step = step;
}
@Override
public JsonElement toJson(Double value) {
return new JsonPrimitive(value);
}
@Override
protected Double read(JsonElement json) {
return json.getAsDouble();
}
@Override
public Double validate(Double value) {
if (value.isNaN()) {
return getDefaultValue();
}
return Math.max(min, Math.min(max, value));
}
}
@@ -0,0 +1,227 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.NonMoving;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.util.List;
import java.util.function.UnaryOperator;
// Parameters every entry has, independent of its design
@UtilityClass
public class EntryParameters {
public enum TickMode {
MOVE,
ALWAYS,
}
// Body places offsets, heights, rings and wings around the player's body, even while gliding or swimming
public enum Placement {
WORLD,
BODY,
}
// Enchanting glyphs can fly between each point and the player instead of floating down
public enum GlyphFlight {
NORMAL,
INTO_PLAYER,
OUT_OF_PLAYER,
}
public enum Movement {
ANY(element -> element),
MOVING(element -> data -> {
if (data.isMoving()) element.tick(data);
}),
STANDING_STILL(NonMoving::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Movement(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Gliding {
ANY(element -> element),
GLIDING(Flying::new),
NOT_GLIDING(NonFlying::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Gliding(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Ground {
ANY(element -> element),
ON_GROUND(Floor::new),
IN_AIR(NonFloor::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Ground(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Sneak {
ANY(element -> element),
SNEAKING(Sneaking::new),
NOT_SNEAKING(element -> data -> {
if (!data.getPlayer().isSneaking()) element.tick(data);
}),
;
private final UnaryOperator<ParticleElement> wrapper;
Sneak(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public enum Visibility {
EVERYONE(element -> element),
ONLY_SELF(OnlySelf::new),
ONLY_OTHERS(OnlyOthers::new),
;
private final UnaryOperator<ParticleElement> wrapper;
Visibility(UnaryOperator<ParticleElement> wrapper) {
this.wrapper = wrapper;
}
public ParticleElement wrap(ParticleElement element) {
return wrapper.apply(element);
}
}
public final ChoiceParameter<ParticleType> PARTICLE = ChoiceParameter.ofEnum("particle", "PARTICLE_PARAM_PARTICLE", ParticleType.FLAME, type -> !type.isBuildable(), ParticleType::getRequirement);
public final IntParameter COUNT = new IntParameter("count", "PARTICLE_PARAM_COUNT", 5, 1, 20);
// Minecraft scatters particles around their point by about this many blocks; beyond half a block a design dissolves
public final DoubleParameter SPREAD_X = new DoubleParameter("spreadX", "PARTICLE_PARAM_SPREAD_X", 0, 0, 0.5, 0.01);
public final DoubleParameter SPREAD_Y = new DoubleParameter("spreadY", "PARTICLE_PARAM_SPREAD_Y", 0, 0, 0.5, 0.01);
public final DoubleParameter SPREAD_Z = new DoubleParameter("spreadZ", "PARTICLE_PARAM_SPREAD_Z", 0, 0, 0.5, 0.01);
// For most particles the speed of their random spray; above 0.2 they shoot blocks away
public final DoubleParameter SPEED = new DoubleParameter("speed", "PARTICLE_PARAM_SPEED", 0.01, 0, 0.05, 0.005);
public final GradientParameter COLORS = new GradientParameter("colors", "PARTICLE_PARAM_COLORS", 8);
// Blocks that are also items, so they work for block and item particles alike
public final ChoiceParameter<Material> BLOCK = new ChoiceParameter<>("block", "PARTICLE_PARAM_BLOCK", Material.STONE, List.of(
new ChoiceParameter.Choice<>("stone", Material.STONE, false),
new ChoiceParameter.Choice<>("cobblestone", Material.COBBLESTONE, false),
new ChoiceParameter.Choice<>("dirt", Material.DIRT, false),
new ChoiceParameter.Choice<>("grass_block", Material.GRASS_BLOCK, false),
new ChoiceParameter.Choice<>("sand", Material.SAND, false),
new ChoiceParameter.Choice<>("red_sand", Material.RED_SAND, false),
new ChoiceParameter.Choice<>("gravel", Material.GRAVEL, false),
new ChoiceParameter.Choice<>("oak_planks", Material.OAK_PLANKS, false),
new ChoiceParameter.Choice<>("oak_leaves", Material.OAK_LEAVES, false),
new ChoiceParameter.Choice<>("cherry_leaves", Material.CHERRY_LEAVES, false),
new ChoiceParameter.Choice<>("snow_block", Material.SNOW_BLOCK, false),
new ChoiceParameter.Choice<>("ice", Material.ICE, false),
new ChoiceParameter.Choice<>("glass", Material.GLASS, false),
new ChoiceParameter.Choice<>("tnt", Material.TNT, false),
new ChoiceParameter.Choice<>("pumpkin", Material.PUMPKIN, false),
new ChoiceParameter.Choice<>("melon", Material.MELON, false),
new ChoiceParameter.Choice<>("honey_block", Material.HONEY_BLOCK, false),
new ChoiceParameter.Choice<>("slime_block", Material.SLIME_BLOCK, false),
new ChoiceParameter.Choice<>("netherrack", Material.NETHERRACK, false),
new ChoiceParameter.Choice<>("soul_sand", Material.SOUL_SAND, false),
new ChoiceParameter.Choice<>("magma_block", Material.MAGMA_BLOCK, false),
new ChoiceParameter.Choice<>("end_stone", Material.END_STONE, false),
new ChoiceParameter.Choice<>("obsidian", Material.OBSIDIAN, false),
new ChoiceParameter.Choice<>("prismarine", Material.PRISMARINE, false),
new ChoiceParameter.Choice<>("sculk", Material.SCULK, false),
new ChoiceParameter.Choice<>("amethyst_block", Material.AMETHYST_BLOCK, false),
new ChoiceParameter.Choice<>("coal_block", Material.COAL_BLOCK, false),
new ChoiceParameter.Choice<>("iron_block", Material.IRON_BLOCK, false),
new ChoiceParameter.Choice<>("gold_block", Material.GOLD_BLOCK, false),
new ChoiceParameter.Choice<>("redstone_block", Material.REDSTONE_BLOCK, false),
new ChoiceParameter.Choice<>("lapis_block", Material.LAPIS_BLOCK, false),
new ChoiceParameter.Choice<>("emerald_block", Material.EMERALD_BLOCK, false),
new ChoiceParameter.Choice<>("diamond_block", Material.DIAMOND_BLOCK, false),
new ChoiceParameter.Choice<>("white_wool", Material.WHITE_WOOL, false),
new ChoiceParameter.Choice<>("red_wool", Material.RED_WOOL, false),
new ChoiceParameter.Choice<>("orange_wool", Material.ORANGE_WOOL, false),
new ChoiceParameter.Choice<>("yellow_wool", Material.YELLOW_WOOL, false),
new ChoiceParameter.Choice<>("lime_wool", Material.LIME_WOOL, false),
new ChoiceParameter.Choice<>("light_blue_wool", Material.LIGHT_BLUE_WOOL, false),
new ChoiceParameter.Choice<>("blue_wool", Material.BLUE_WOOL, false),
new ChoiceParameter.Choice<>("purple_wool", Material.PURPLE_WOOL, false),
new ChoiceParameter.Choice<>("pink_wool", Material.PINK_WOOL, false),
new ChoiceParameter.Choice<>("black_wool", Material.BLACK_WOOL, false)
));
public final DoubleParameter OFFSET_X = new DoubleParameter("offsetX", "PARTICLE_PARAM_OFFSET_X", 0, -3, 3, 0.1);
public final DoubleParameter OFFSET_Y = new DoubleParameter("offsetY", "PARTICLE_PARAM_OFFSET_Y", 0, -3, 3, 0.1);
public final DoubleParameter OFFSET_Z = new DoubleParameter("offsetZ", "PARTICLE_PARAM_OFFSET_Z", 0, -3, 3, 0.1);
public final IntParameter INTERVAL = new IntParameter("interval", "PARTICLE_PARAM_INTERVAL", 1, 1, 100);
public final IntParameter TRAIL_DURATION = new IntParameter("trailDuration", "PARTICLE_PARAM_TRAIL_DURATION", 20, 1, 200);
// Where a trail particle flies to, relative to where it spawns
public final DoubleParameter TRAIL_TARGET_X = new DoubleParameter("trailTargetX", "PARTICLE_PARAM_TRAIL_TARGET_X", 0, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Y = new DoubleParameter("trailTargetY", "PARTICLE_PARAM_TRAIL_TARGET_Y", 1, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Z = new DoubleParameter("trailTargetZ", "PARTICLE_PARAM_TRAIL_TARGET_Z", 0, -5, 5, 0.1);
public final ChoiceParameter<GlyphFlight> GLYPH_FLIGHT = ChoiceParameter.ofEnum("glyphFlight", "PARTICLE_PARAM_GLYPH_FLIGHT", GlyphFlight.NORMAL, flight -> false,
flight -> switch (flight) {
case NORMAL -> ParticleRequirement.NO_REQUIRMENT;
case INTO_PLAYER -> Unlocks.WARGEAR_SEASON_PLACEMENT.get();
case OUT_OF_PLAYER -> Unlocks.WARGEAR_SEASON.get();
});
public final ChoiceParameter<Placement> PLACEMENT = ChoiceParameter.ofEnum("placement", "PARTICLE_PARAM_PLACEMENT", Placement.WORLD);
public final ChoiceParameter<TickMode> TICK_MODE = ChoiceParameter.ofEnum("tickMode", "PARTICLE_PARAM_TICK_MODE", TickMode.MOVE);
public final ChoiceParameter<Movement> MOVEMENT = ChoiceParameter.ofEnum("movement", "PARTICLE_PARAM_MOVEMENT", Movement.ANY);
public final ChoiceParameter<Gliding> GLIDING = ChoiceParameter.ofEnum("gliding", "PARTICLE_PARAM_GLIDING", Gliding.ANY);
public final ChoiceParameter<Ground> GROUND = ChoiceParameter.ofEnum("ground", "PARTICLE_PARAM_GROUND", Ground.ANY);
public final ChoiceParameter<Sneak> SNEAK = ChoiceParameter.ofEnum("sneak", "PARTICLE_PARAM_SNEAK", Sneak.ANY);
public final ChoiceParameter<Visibility> VISIBILITY = ChoiceParameter.ofEnum("visibility", "PARTICLE_PARAM_VISIBILITY", Visibility.EVERYONE);
public final List<Parameter<?>> ALL = List.of(
PARTICLE, GLYPH_FLIGHT, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS, BLOCK,
TRAIL_DURATION, TRAIL_TARGET_X, TRAIL_TARGET_Y, TRAIL_TARGET_Z,
OFFSET_X, OFFSET_Y, OFFSET_Z, PLACEMENT, INTERVAL, TICK_MODE,
MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY
);
}
@@ -0,0 +1,62 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import org.bukkit.Material;
import java.util.function.Consumer;
import java.util.function.Supplier;
// A design whose look, including its particles, is fixed; only the entry's conditions, timing and offset apply
public class FixedDesign extends ParticleDesign {
private final int points;
private final Consumer<ParameterValues> defaults;
private final Supplier<ParticleElement> shape;
public FixedDesign(String id, Material icon, String nameKey, ParticleRequirement requirement, int points, Consumer<ParameterValues> defaults, Supplier<ParticleElement> shape) {
super(id, icon, nameKey, DesignVisibility.REWARD, requirement);
this.points = points;
this.defaults = defaults;
this.shape = shape;
}
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return shape.get();
}
@Override
public int points(ParameterValues values) {
return points;
}
@Override
public boolean usesEntryParticle() {
return false;
}
@Override
public void applyDefaults(ParameterValues values) {
defaults.accept(values);
}
}
@@ -0,0 +1,61 @@
/*
* 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.design;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import lombok.Getter;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
// An empty gradient means a random colour per spawn
@Getter
public class GradientParameter extends Parameter<List<Color>> {
private final int maxColors;
public GradientParameter(String key, String nameKey, int maxColors) {
super(key, nameKey, List.of());
this.maxColors = maxColors;
}
@Override
public JsonElement toJson(List<Color> value) {
JsonArray array = new JsonArray();
value.forEach(color -> array.add(color.getRGB() & 0xFFFFFF));
return array;
}
@Override
protected List<Color> read(JsonElement json) {
List<Color> colors = new ArrayList<>();
for (JsonElement element : json.getAsJsonArray()) {
colors.add(new Color(element.getAsInt() & 0xFFFFFF));
}
return colors;
}
@Override
public List<Color> validate(List<Color> value) {
return List.copyOf(value.size() > maxColors ? value.subList(0, maxColors) : value);
}
}
@@ -0,0 +1,52 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import lombok.Getter;
@Getter
public class IntParameter extends Parameter<Integer> {
private final int min;
private final int max;
public IntParameter(String key, String nameKey, int defaultValue, int min, int max) {
super(key, nameKey, defaultValue);
this.min = min;
this.max = max;
}
@Override
public JsonElement toJson(Integer value) {
return new JsonPrimitive(value);
}
@Override
protected Integer read(JsonElement json) {
return json.getAsInt();
}
@Override
public Integer validate(Integer value) {
return Math.max(min, Math.min(max, value));
}
}
@@ -0,0 +1,85 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.Circle;
import de.steamwar.lobby.particle.elements.custom.HearthBeat;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import de.steamwar.lobby.particle.elements.custom.YOffset;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import java.util.List;
// Shapes only presets use; they are never offered in the builder
@UtilityClass
public class InternalDesigns {
public final IntParameter PULSE_SPEED = new IntParameter("pulseSpeed", "PARTICLE_PARAM_PULSE_SPEED", 80, 1, 200);
public final IntParameter PULSE_ROTATION = new IntParameter("pulseRotation", "PARTICLE_PARAM_PULSE_ROTATION", 2, 0, 20);
public final ParticleDesign PULSE_SHIMMER = new ParticleDesign("pulse_shimmer", Material.GRAY_CANDLE, "PARTICLE_DESIGN_PULSE_SHIMMER", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
PULSE_SPEED, PULSE_ROTATION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new PulseShimmer(particle, values.get(PULSE_SPEED), values.get(PULSE_ROTATION));
}
@Override
public int points(ParameterValues values) {
return 3;
}
};
public final DoubleParameter HEART_BEAT_SPEED = new DoubleParameter("heartBeatSpeed", "PARTICLE_PARAM_HEART_BEAT_SPEED", 80, 1, 200, 5);
public final ParticleDesign HEART_BEAT = new ParticleDesign("heart_beat", Material.APPLE, "PARTICLE_DESIGN_HEART_BEAT", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
HEART_BEAT_SPEED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new HearthBeat(particle, values.get(HEART_BEAT_SPEED));
}
};
public final DoubleParameter ORBIT_RADIUS = new DoubleParameter("orbitRadius", "PARTICLE_PARAM_ORBIT_RADIUS", 1.5, 0.2, 3, 0.1);
public final DoubleParameter ORBIT_SPEED = new DoubleParameter("orbitSpeed", "PARTICLE_PARAM_ORBIT_SPEED", 1, -20, 20, 0.5);
public final DoubleParameter ORBIT_PHASE = new DoubleParameter("orbitPhase", "PARTICLE_PARAM_ORBIT_PHASE", 0, 0, 345, 15);
// A circle whose centre itself moves on a circle around the player
public final ParticleDesign ORBIT = new ParticleDesign("orbit", Material.ENDER_PEARL, "PARTICLE_DESIGN_ORBIT", DesignVisibility.INTERNAL, ParticleRequirement.NO_REQUIRMENT,
ORBIT_RADIUS, ORBIT_SPEED, ORBIT_PHASE,
Designs.CIRCLE_RADIUS, Designs.CIRCLE_SPEED, Designs.CIRCLE_PHASE, Designs.CIRCLE_RISE, Designs.CIRCLE_RISE_SPEED, Designs.CIRCLE_BOUNCE, Designs.CIRCLE_INVERTED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
double rise = values.get(Designs.CIRCLE_RISE);
if (rise > 0) {
particle = new YOffset(particle, values.get(Designs.CIRCLE_RISE_SPEED), 0, rise, values.get(Designs.CIRCLE_INVERTED), values.get(Designs.CIRCLE_BOUNCE));
}
ParticleElement inner = new Circle(particle, values.get(Designs.CIRCLE_RADIUS), values.get(Designs.CIRCLE_SPEED), values.get(Designs.CIRCLE_PHASE));
return new Circle(inner, values.get(ORBIT_RADIUS), values.get(ORBIT_SPEED), values.get(ORBIT_PHASE));
}
};
public List<ParticleDesign> all() {
return List.of(PULSE_SHIMMER, HEART_BEAT, ORBIT);
}
}
@@ -0,0 +1,58 @@
/*
* 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.design;
import com.google.gson.JsonElement;
import lombok.Getter;
@Getter
public abstract class Parameter<T> {
private final String key;
private final String nameKey;
private final T defaultValue;
protected Parameter(String key, String nameKey, T defaultValue) {
this.key = key;
this.nameKey = nameKey;
this.defaultValue = defaultValue;
}
public abstract JsonElement toJson(T value);
// Returns null when the stored value cannot be used
protected abstract T read(JsonElement json);
public T fromJson(JsonElement json) {
if (json == null || json.isJsonNull()) {
return defaultValue;
}
try {
T value = read(json);
return value == null ? defaultValue : validate(value);
} catch (RuntimeException e) {
return defaultValue;
}
}
public T validate(T value) {
return value;
}
}
@@ -0,0 +1,76 @@
/*
* 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.design;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ParameterValues {
private final Map<String, Object> values = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> T get(Parameter<T> parameter) {
Object value = values.get(parameter.getKey());
return value == null ? parameter.getDefaultValue() : (T) value;
}
public <T> ParameterValues set(Parameter<T> parameter, T value) {
values.put(parameter.getKey(), parameter.validate(value));
return this;
}
// Skips the builder's limits; only for presets, which are never stored
public <T> ParameterValues setUnchecked(Parameter<T> parameter, T value) {
values.put(parameter.getKey(), value);
return this;
}
// Puts back values saved with copy()
public void restore(ParameterValues saved) {
values.clear();
values.putAll(saved.values);
}
public ParameterValues copy() {
ParameterValues copy = new ParameterValues();
copy.values.putAll(values);
return copy;
}
public JsonObject toJson(List<Parameter<?>> parameters) {
JsonObject json = new JsonObject();
parameters.forEach(parameter -> write(json, parameter));
return json;
}
public static ParameterValues fromJson(JsonObject json, List<Parameter<?>> parameters) {
ParameterValues values = new ParameterValues();
parameters.forEach(parameter -> values.values.put(parameter.getKey(), parameter.fromJson(json.get(parameter.getKey()))));
return values;
}
private <T> void write(JsonObject json, Parameter<T> parameter) {
json.add(parameter.getKey(), parameter.toJson(get(parameter)));
}
}
@@ -0,0 +1,88 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import lombok.Getter;
import org.bukkit.Material;
import java.util.List;
@Getter
public abstract class ParticleDesign {
private final String id;
private final Material icon;
private final String nameKey;
private final DesignVisibility visibility;
private final ParticleRequirement requirement;
private final List<Parameter<?>> parameters;
protected ParticleDesign(String id, Material icon, String nameKey, DesignVisibility visibility, ParticleRequirement requirement, Parameter<?>... parameters) {
this.id = id;
this.icon = icon;
this.nameKey = nameKey;
this.visibility = visibility;
this.requirement = requirement;
this.parameters = List.of(parameters);
}
protected ParticleDesign(String id, Material icon, String nameKey, Parameter<?>... parameters) {
this(id, icon, nameKey, DesignVisibility.BASIC, ParticleRequirement.NO_REQUIRMENT, parameters);
}
// Wraps the entry's particle into this design's shape
public abstract ParticleElement build(ParameterValues values, ParticleElement particle);
// Number of particle positions this design spawns per tick
public int points(ParameterValues values) {
return 1;
}
// False for designs with a fixed look that ignore the entry's particle type, count, spread, speed and colours
public boolean usesEntryParticle() {
return true;
}
// Whether the entry editor shows this design setting; some only matter for some values of others
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
return true;
}
// True when the design decides how many particles each point spawns, so the amount setting is hidden and counts as 1
public boolean replacesAmount(ParameterValues values) {
return false;
}
// Whether the design must be drawn every tick for a moment after a jump, whatever its interval; these extra
// redraws do not count towards the budget
public boolean redrawsAfterJump(ParameterValues values) {
return false;
}
// Called after a setting changed, to adjust others that depend on it
public void adjust(Parameter<?> changed, ParameterValues values) {
}
// Sets the conditions and timing a new entry of this design starts with
public void applyDefaults(ParameterValues values) {
}
}
@@ -0,0 +1,200 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleRequirement;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.Particle;
// Left out: ELDER_GUARDIAN, FLASH and the explosion/gust emitters cover other players' screens,
// BUBBLE, BUBBLE_COLUMN_UP and CURRENT_DOWN only show under water, VIBRATION needs a block or entity to fly to.
// Not buildable: particles a block or more in size or rising for seconds, which wall in players once a design
// spawns many of them (a wing spawns over a hundred); presets may still use them.
@AllArgsConstructor
@Getter
public enum ParticleType {
DUST(Material.REDSTONE, Particle.DUST, Data.DUST, true),
DUST_COLOR_TRANSITION(Material.REDSTONE_TORCH, Particle.DUST_COLOR_TRANSITION, Data.COLOR_TRANSITION, true),
ENTITY_EFFECT(Material.BREWING_STAND, Particle.ENTITY_EFFECT, Data.COLOR, true),
TINTED_LEAVES(Material.OAK_LEAVES, Particle.TINTED_LEAVES, Data.COLOR, true),
BLOCK(Material.STONE, Particle.BLOCK, Data.BLOCK, true),
BLOCK_CRUMBLE(Material.CRACKED_STONE_BRICKS, Particle.BLOCK_CRUMBLE, Data.BLOCK, true),
BLOCK_MARKER(Material.GLASS, Particle.BLOCK_MARKER, Data.BLOCK, false),
DUST_PILLAR(Material.MUD, Particle.DUST_PILLAR, Data.BLOCK, false),
FALLING_DUST(Material.SAND, Particle.FALLING_DUST, Data.BLOCK, true),
ITEM(Material.ITEM_FRAME, Particle.ITEM, Data.ITEM, true),
TRAIL(Material.LEAD, Particle.TRAIL, Data.TRAIL, true),
FLAME(Material.FLINT_AND_STEEL, Particle.FLAME, Data.NONE, true),
SMALL_FLAME(Material.TORCH, Particle.SMALL_FLAME, Data.NONE, true),
SOUL_FIRE_FLAME(Material.SOUL_TORCH, Particle.SOUL_FIRE_FLAME, Data.NONE, true),
SOUL(Material.ZOMBIE_HEAD, Particle.SOUL, Data.NONE, true),
LAVA(Material.MAGMA_CREAM, Particle.LAVA, Data.NONE, true),
SMOKE(Material.COBWEB, Particle.SMOKE, Data.NONE, true),
LARGE_SMOKE(Material.GRAY_WOOL, Particle.LARGE_SMOKE, Data.NONE, false),
WHITE_SMOKE(Material.LIGHT_GRAY_DYE, Particle.WHITE_SMOKE, Data.NONE, true),
CAMPFIRE_COSY_SMOKE(Material.CAMPFIRE, Particle.CAMPFIRE_COSY_SMOKE, Data.NONE, true),
CAMPFIRE_SIGNAL_SMOKE(Material.HAY_BLOCK, Particle.CAMPFIRE_SIGNAL_SMOKE, Data.NONE, false),
ASH(Material.BASALT, Particle.ASH, Data.NONE, true),
WHITE_ASH(Material.BONE_MEAL, Particle.WHITE_ASH, Data.NONE, true),
POOF(Material.WHITE_DYE, Particle.POOF, Data.NONE, true),
CLOUD(Material.WHITE_WOOL, Particle.CLOUD, Data.NONE, true),
SNEEZE(Material.SLIME_BLOCK, Particle.SNEEZE, Data.NONE, true),
SPIT(Material.LLAMA_SPAWN_EGG, Particle.SPIT, Data.NONE, true),
EXPLOSION(Material.TNT, Particle.EXPLOSION, Data.NONE, false),
SONIC_BOOM(Material.ECHO_SHARD, Particle.SONIC_BOOM, Data.NONE, false),
SWEEP_ATTACK(Material.IRON_SWORD, Particle.SWEEP_ATTACK, Data.NONE, false),
GUST(Material.WIND_CHARGE, Particle.GUST, Data.NONE, false),
SMALL_GUST(Material.BREEZE_ROD, Particle.SMALL_GUST, Data.NONE, false),
DRIPPING_WATER(Material.WATER_BUCKET, Particle.DRIPPING_WATER, Data.NONE, true),
FALLING_WATER(Material.LIGHT_BLUE_DYE, Particle.FALLING_WATER, Data.NONE, true),
DRIPPING_LAVA(Material.LAVA_BUCKET, Particle.DRIPPING_LAVA, Data.NONE, true),
FALLING_LAVA(Material.ORANGE_DYE, Particle.FALLING_LAVA, Data.NONE, true),
LANDING_LAVA(Material.FIRE_CORAL, Particle.LANDING_LAVA, Data.NONE, true),
DRIPPING_HONEY(Material.HONEY_BOTTLE, Particle.DRIPPING_HONEY, Data.NONE, true),
FALLING_HONEY(Material.HONEY_BLOCK, Particle.FALLING_HONEY, Data.NONE, true),
LANDING_HONEY(Material.HONEYCOMB_BLOCK, Particle.LANDING_HONEY, Data.NONE, true),
FALLING_NECTAR(Material.HONEYCOMB, Particle.FALLING_NECTAR, Data.NONE, true),
DRIPPING_OBSIDIAN_TEAR(Material.OBSIDIAN, Particle.DRIPPING_OBSIDIAN_TEAR, Data.NONE, true),
FALLING_OBSIDIAN_TEAR(Material.CRYING_OBSIDIAN, Particle.FALLING_OBSIDIAN_TEAR, Data.NONE, true),
LANDING_OBSIDIAN_TEAR(Material.RESPAWN_ANCHOR, Particle.LANDING_OBSIDIAN_TEAR, Data.NONE, true),
DRIPPING_DRIPSTONE_WATER(Material.DRIPSTONE_BLOCK, Particle.DRIPPING_DRIPSTONE_WATER, Data.NONE, true),
FALLING_DRIPSTONE_WATER(Material.BLUE_ICE, Particle.FALLING_DRIPSTONE_WATER, Data.NONE, true),
DRIPPING_DRIPSTONE_LAVA(Material.POINTED_DRIPSTONE, Particle.DRIPPING_DRIPSTONE_LAVA, Data.NONE, true),
FALLING_DRIPSTONE_LAVA(Material.MAGMA_BLOCK, Particle.FALLING_DRIPSTONE_LAVA, Data.NONE, true),
RAIN(Material.CYAN_DYE, Particle.RAIN, Data.NONE, true),
SPLASH(Material.KELP, Particle.SPLASH, Data.NONE, true),
FISHING(Material.FISHING_ROD, Particle.FISHING, Data.NONE, true),
UNDERWATER(Material.PRISMARINE_SHARD, Particle.UNDERWATER, Data.NONE, true),
BUBBLE_POP(Material.TUBE_CORAL, Particle.BUBBLE_POP, Data.NONE, true),
DOLPHIN(Material.BLUE_DYE, Particle.DOLPHIN, Data.NONE, true),
NAUTILUS(Material.NAUTILUS_SHELL, Particle.NAUTILUS, Data.NONE, true),
SQUID_INK(Material.INK_SAC, Particle.SQUID_INK, Data.NONE, true),
GLOW_SQUID_INK(Material.GLOW_INK_SAC, Particle.GLOW_SQUID_INK, Data.NONE, true),
GLOW(Material.GLOW_BERRIES, Particle.GLOW, Data.NONE, true),
FALLING_SPORE_BLOSSOM(Material.SPORE_BLOSSOM, Particle.FALLING_SPORE_BLOSSOM, Data.NONE, true),
SPORE_BLOSSOM_AIR(Material.PINK_PETALS, Particle.SPORE_BLOSSOM_AIR, Data.NONE, true),
CHERRY_LEAVES(Material.CHERRY_LEAVES, Particle.CHERRY_LEAVES, Data.NONE, true),
PALE_OAK_LEAVES(Material.PALE_OAK_LEAVES, Particle.PALE_OAK_LEAVES, Data.NONE, true),
CRIMSON_SPORE(Material.CRIMSON_FUNGUS, Particle.CRIMSON_SPORE, Data.NONE, true),
WARPED_SPORE(Material.WARPED_FUNGUS, Particle.WARPED_SPORE, Data.NONE, true),
MYCELIUM(Material.MYCELIUM, Particle.MYCELIUM, Data.NONE, true),
COMPOSTER(Material.COMPOSTER, Particle.COMPOSTER, Data.NONE, true),
FIREFLY(Material.FIREFLY_BUSH, Particle.FIREFLY, Data.NONE, true),
SNOWFLAKE(Material.SNOWBALL, Particle.SNOWFLAKE, Data.NONE, true),
ITEM_SNOWBALL(Material.SNOW_BLOCK, Particle.ITEM_SNOWBALL, Data.NONE, true),
ITEM_SLIME(Material.SLIME_BALL, Particle.ITEM_SLIME, Data.NONE, true),
ITEM_COBWEB(Material.STRING, Particle.ITEM_COBWEB, Data.NONE, true),
EGG_CRACK(Material.TURTLE_EGG, Particle.EGG_CRACK, Data.NONE, true),
DUST_PLUME(Material.SUSPICIOUS_SAND, Particle.DUST_PLUME, Data.NONE, true),
INFESTED(Material.INFESTED_STONE, Particle.INFESTED, Data.NONE, true),
HEART(Material.RED_DYE, Particle.HEART, Data.NONE, true),
NOTE(Material.NOTE_BLOCK, Particle.NOTE, Data.NONE, true),
HAPPY_VILLAGER(Material.EMERALD, Particle.HAPPY_VILLAGER, Data.NONE, true),
ANGRY_VILLAGER(Material.REDSTONE_BLOCK, Particle.ANGRY_VILLAGER, Data.NONE, true),
DAMAGE_INDICATOR(Material.SPIDER_EYE, Particle.DAMAGE_INDICATOR, Data.NONE, true),
CRIT(Material.CAULDRON, Particle.CRIT, Data.NONE, true),
ENCHANTED_HIT(Material.GLOWSTONE_DUST, Particle.ENCHANTED_HIT, Data.NONE, true),
ENCHANT(Material.ENCHANTING_TABLE, Particle.ENCHANT, Data.NONE, true),
EFFECT(Material.POTION, Particle.EFFECT, Data.NONE, true),
INSTANT_EFFECT(Material.SPLASH_POTION, Particle.INSTANT_EFFECT, Data.NONE, true),
WITCH(Material.EXPERIENCE_BOTTLE, Particle.WITCH, Data.NONE, true),
END_ROD(Material.END_ROD, Particle.END_ROD, Data.NONE, true),
PORTAL(Material.ENDER_PEARL, Particle.PORTAL, Data.NONE, true),
REVERSE_PORTAL(Material.ENDER_EYE, Particle.REVERSE_PORTAL, Data.NONE, true),
DRAGON_BREATH(Material.DRAGON_BREATH, Particle.DRAGON_BREATH, Data.NONE, true),
FIREWORK(Material.FIRE_CHARGE, Particle.FIREWORK, Data.NONE, true),
TOTEM_OF_UNDYING(Material.TOTEM_OF_UNDYING, Particle.TOTEM_OF_UNDYING, Data.NONE, true),
ELECTRIC_SPARK(Material.LIGHTNING_ROD, Particle.ELECTRIC_SPARK, Data.NONE, true),
SCRAPE(Material.COPPER_BLOCK, Particle.SCRAPE, Data.NONE, true),
WAX_ON(Material.WAXED_COPPER_BLOCK, Particle.WAX_ON, Data.NONE, true),
WAX_OFF(Material.WAXED_EXPOSED_COPPER, Particle.WAX_OFF, Data.NONE, true),
SCULK_SOUL(Material.SCULK_CATALYST, Particle.SCULK_SOUL, Data.NONE, true),
SCULK_CHARGE(Material.SCULK_SENSOR, Particle.SCULK_CHARGE, Data.ZERO_FLOAT, true),
SCULK_CHARGE_POP(Material.SCULK_VEIN, Particle.SCULK_CHARGE_POP, Data.NONE, true),
SHRIEK(Material.SCULK_SHRIEKER, Particle.SHRIEK, Data.ZERO_INT, false),
RAID_OMEN(Material.FIREWORK_STAR, Particle.RAID_OMEN, Data.NONE, true),
TRIAL_OMEN(Material.TRIAL_KEY, Particle.TRIAL_OMEN, Data.NONE, true),
OMINOUS_SPAWNING(Material.OMINOUS_TRIAL_KEY, Particle.OMINOUS_SPAWNING, Data.NONE, true),
TRIAL_SPAWNER_DETECTION(Material.TRIAL_SPAWNER, Particle.TRIAL_SPAWNER_DETECTION, Data.NONE, true),
TRIAL_SPAWNER_DETECTION_OMINOUS(Material.VAULT, Particle.TRIAL_SPAWNER_DETECTION_OMINOUS, Data.NONE, true),
VAULT_CONNECTION(Material.HEAVY_CORE, Particle.VAULT_CONNECTION, Data.NONE, true),
;
// The extra data a particle needs when it is spawned
public enum Data {
NONE,
// Colour and size, handled by the dust element
DUST,
// A colour from the entry's colours
COLOR,
// Fades between the entry's first two colours
COLOR_TRANSITION,
// Block data of the entry's block
BLOCK,
// An item of the entry's block
ITEM,
// Flies to the entry's trail target in its colour, taking the trail duration
TRAIL,
ZERO_INT,
ZERO_FLOAT,
}
private final Material icon;
private final Particle particle;
private final Data data;
private final boolean buildable;
public String getNameKey() {
return "PARTICLE_TYPE_" + name();
}
public boolean isDust() {
return data == Data.DUST;
}
public boolean usesColors() {
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION || data == Data.TRAIL;
}
// Leaves drift down slowly and pile up around a player, so only the server team may build with them. Cosy smoke is
// earned by taking part in Hells Bells.
public ParticleRequirement getRequirement() {
return switch (this) {
case CHERRY_LEAVES, PALE_OAK_LEAVES, TINTED_LEAVES -> ParticleRequirement.SERVER_TEAM;
case CAMPFIRE_COSY_SMOKE -> Unlocks.HELLS_BELLS.get();
default -> ParticleRequirement.NO_REQUIRMENT;
};
}
public boolean isTrail() {
return data == Data.TRAIL;
}
public boolean usesBlock() {
return data == Data.BLOCK || data == Data.ITEM;
}
}
@@ -0,0 +1,222 @@
/*
* 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.design;
import de.steamwar.lobby.particle.ParticleElement;
import de.steamwar.lobby.particle.ParticleRequirement;
import de.steamwar.lobby.particle.elements.*;
import de.steamwar.lobby.particle.elements.custom.HearthBeat;
import de.steamwar.lobby.particle.elements.custom.PulseShimmer;
import de.steamwar.lobby.particle.elements.touch.EventBurst;
import de.steamwar.lobby.particle.elements.touch.Footprints;
import de.steamwar.lobby.particle.elements.touch.Ring;
import de.steamwar.lobby.particle.elements.touch.TeamLink;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import java.util.List;
import java.util.function.Consumer;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
// Designs unlocked by event and egg hunt rewards. Most are styled like the basic designs, with a particle of the
// player's choice; the others have a fixed look. Rewards whose look the basic designs can already build unlock
// ingredients instead: wing images, a wing motion and particles.
@UtilityClass
public class RewardDesigns {
private final Consumer<ParameterValues> ALWAYS = values -> values.set(TICK_MODE, TickMode.ALWAYS);
public enum BurstShape {
PUFF,
RING,
}
private final int RING_POINTS = 16;
public final ChoiceParameter<EventBurst.Trigger> BURST_TRIGGER = ChoiceParameter.ofEnum("trigger", "PARTICLE_PARAM_BURST_TRIGGER", EventBurst.Trigger.JUMP, trigger -> false,
trigger -> switch (trigger) {
case JUMP -> Unlocks.AIRSHIP_PLACEMENT.get();
case LAND -> ParticleRequirement.anyOf(Unlocks.UNDERWATER_PLACEMENT.get(), Unlocks.ABSTURZ_PLACEMENT.get(), Unlocks.ANY_EVENT.get(), Unlocks.EGG_HUNT_EXTREME.get());
case SNEAK -> Unlocks.WARGEAR_SEASON.get();
});
public final ChoiceParameter<BurstShape> BURST_SHAPE = ChoiceParameter.ofEnum("burstShape", "PARTICLE_PARAM_BURST_SHAPE", BurstShape.PUFF);
public final DoubleParameter BURST_RADIUS = new DoubleParameter("burstRadius", "PARTICLE_PARAM_BURST_RADIUS", 1, 0.3, 2, 0.1);
public final DoubleParameter BURST_FALL = new DoubleParameter("minimumFall", "PARTICLE_PARAM_BURST_FALL", 1, 0, 10, 0.5);
// Once at the feet when the player jumps, lands or starts sneaking
public final ParticleDesign BURST = new ParticleDesign("burst", Material.FIREWORK_ROCKET, "PARTICLE_DESIGN_BURST", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.AIRSHIP_PLACEMENT.get(), Unlocks.UNDERWATER_PLACEMENT.get(), Unlocks.ABSTURZ_PLACEMENT.get(),
Unlocks.ANY_EVENT.get(), Unlocks.EGG_HUNT_EXTREME.get(), Unlocks.WARGEAR_SEASON.get()),
BURST_TRIGGER, BURST_SHAPE, BURST_RADIUS, BURST_FALL) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
ParticleElement burst = values.get(BURST_SHAPE) == BurstShape.RING ? new Ring(particle, values.get(BURST_RADIUS), RING_POINTS) : particle;
double minimumFall = values.get(BURST_TRIGGER) == EventBurst.Trigger.LAND ? values.get(BURST_FALL) : 0;
return new EventBurst(values.get(BURST_TRIGGER), minimumFall, burst);
}
// Shows only now and then, so it counts as a single point
@Override
public int points(ParameterValues values) {
return 1;
}
@Override
public boolean showsSetting(Parameter<?> parameter, ParameterValues values) {
if (parameter == BURST_RADIUS) return values.get(BURST_SHAPE) == BurstShape.RING;
if (parameter == BURST_FALL) return values.get(BURST_TRIGGER) == EventBurst.Trigger.LAND;
return true;
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 8).set(SPREAD_X, 0.3).set(SPREAD_Y, 0.05).set(SPREAD_Z, 0.3).set(SPEED, 0.02);
}
};
public final IntParameter FOOTPRINT_LIFETIME = new IntParameter("printLifetime", "PARTICLE_PARAM_FOOTPRINT_LIFETIME", 40, 10, 100);
public final DoubleParameter FOOTPRINT_STEP = new DoubleParameter("printStep", "PARTICLE_PARAM_FOOTPRINT_STEP", 0.6, 0.3, 2, 0.1);
// Blocks per tick while walking, to estimate how many prints are shown at once
private final double WALKING_SPEED = 0.22;
public final ParticleDesign FOOTPRINTS = new ParticleDesign("footprints", Material.LEATHER_BOOTS, "PARTICLE_DESIGN_FOOTPRINTS", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.ADVENT_WAR_SHIP_PLACEMENT.get(), Unlocks.ADVENT_WAR_SHIP.get(), Unlocks.AIRSHIP.get(), Unlocks.EGG_HUNT_EASY.get()),
FOOTPRINT_LIFETIME, FOOTPRINT_STEP) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new Footprints(particle, values.get(FOOTPRINT_LIFETIME), values.get(FOOTPRINT_STEP));
}
@Override
public int points(ParameterValues values) {
return (int) Math.ceil(values.get(FOOTPRINT_LIFETIME) * WALKING_SPEED / values.get(FOOTPRINT_STEP));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 1).set(SPREAD_X, 0.05).set(SPREAD_Z, 0.05).set(SPEED, 0.0);
}
};
public final DoubleParameter LINK_SPACING = new DoubleParameter("linkSpacing", "PARTICLE_PARAM_LINK_SPACING", 0.5, 0.3, 2, 0.1);
// A line to every nearby player who shows this design too
public final ParticleDesign TEAM_LINK = new ParticleDesign("team_link", Material.LEAD, "PARTICLE_DESIGN_TEAM_LINK", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.ANY_EVENT.get(), Unlocks.UNDERWATER.get()),
LINK_SPACING) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new TeamLink("team_link", particle, values.get(LINK_SPACING));
}
// A line to one partner half the range away
@Override
public int points(ParameterValues values) {
return (int) Math.ceil(TeamLink.RANGE / 2 / values.get(LINK_SPACING));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign SHIMMER = new ParticleDesign("shimmer", Material.AMETHYST_CLUSTER, "PARTICLE_DESIGN_PULSE_SHIMMER", DesignVisibility.REWARD,
ParticleRequirement.anyOf(Unlocks.MINI_WARGEAR_LIGA.get(), Unlocks.WARGEAR_SEASON_PLACEMENT.get()),
InternalDesigns.PULSE_SPEED, InternalDesigns.PULSE_ROTATION) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new PulseShimmer(particle, values.get(InternalDesigns.PULSE_SPEED), values.get(InternalDesigns.PULSE_ROTATION));
}
@Override
public int points(ParameterValues values) {
return 3;
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(MOVEMENT, Movement.STANDING_STILL).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign HEARTBEAT = new ParticleDesign("heartbeat", Material.RED_DYE, "PARTICLE_DESIGN_HEART_BEAT", DesignVisibility.REWARD,
Unlocks.EGG_HUNT_HALF.get(),
InternalDesigns.HEART_BEAT_SPEED) {
@Override
public ParticleElement build(ParameterValues values, ParticleElement particle) {
return new HearthBeat(particle, values.get(InternalDesigns.HEART_BEAT_SPEED));
}
@Override
public void applyDefaults(ParameterValues values) {
values.set(TICK_MODE, TickMode.ALWAYS).set(MOVEMENT, Movement.STANDING_STILL).set(COUNT, 1).set(SPEED, 0.0);
}
};
public final ParticleDesign EVENT_PARTICIPATION_UNDERWATER_MWG = new FixedDesign("event_participation_underwater_mwg", Material.BLUE_CARPET, "PARTICLE_EVENT_RAIN_CLOUD",
ParticleRequirement.eventParticipation(35), 15, ALWAYS,
() -> new RandomParticle(
rainCloud(Particle.RAIN, Particle.DRIPPING_WATER, 5),
rainCloud(null, Particle.DRIPPING_LAVA, 5),
rainCloud(null, Particle.FALLING_NECTAR, 5),
rainCloud(null, Particle.FALLING_HONEY, 5),
rainCloud(null, Particle.FALLING_SPORE_BLOSSOM, 5),
rainCloud(null, Particle.FALLING_OBSIDIAN_TEAR, 5),
rainCloud(null, Particle.NAUTILUS, 5),
rainCloud(null, Particle.ENCHANT, 5),
rainCloud(null, Particle.ITEM_SLIME, 5),
rainCloud(null, Particle.ITEM_SNOWBALL, 5),
rainCloud(null, Particle.SNOWFLAKE, 5),
rainCloud(null, Particle.TOTEM_OF_UNDYING, 2)
));
public final ParticleDesign EGG_HUNT_MEDIUM = new FixedDesign("egg_hunt_medium", Material.YELLOW_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_MEDIUM",
ParticleRequirement.EGG_HUNT_MEDIUM, 5, ALWAYS.andThen(values -> values.set(GROUND, Ground.ON_GROUND).set(SNEAK, Sneak.SNEAKING)),
() -> new SimpleParticle(Particle.GLOW_SQUID_INK, 0.4F, 0.9F, 0.4F, 0.01));
public final ParticleDesign EGG_HUNT_HARD = new FixedDesign("egg_hunt_hard", Material.RED_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_HARD",
ParticleRequirement.EGG_HUNT_HARD, 2, ALWAYS,
() -> new LocationMutator(new Circle(new DoubleCircle(new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null), new DustParticle(Particle.DUST, 0, 0, 0, 0.01, 1, null)), 0.8, 0.5), 0, 0.5, 0));
public final ParticleDesign EGG_HUNT_ADVANCED = new FixedDesign("egg_hunt_advanced", Material.BLACK_CONCRETE_POWDER, "PARTICLE_EGG_HUNT_ADVANCED",
ParticleRequirement.EGG_HUNT_ADVANCED, 2, ALWAYS.andThen(values -> values.set(SNEAK, Sneak.SNEAKING)),
() -> new LocationMutator(new Group(
new SimpleParticle(Particle.SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
new SimpleParticle(Particle.LARGE_SMOKE, 0.02F, 0, 0.02F, 0.05, 1),
particleTickData -> {
Player player = particleTickData.getPlayer();
player.setVelocity(player.getVelocity().add(new Vector(0, 0.1, 0)));
}
), 0, -0.2, 0));
// A cloud above the head that drips the given particle; rain is an optional second particle inside the cloud
private ParticleElement rainCloud(Particle rain, Particle drip, int dripCount) {
ParticleElement cloud = new SimpleParticle(Particle.CLOUD, 0.3F, 0.1F, 0.3F, 0.01);
ParticleElement drops = new LocationMutator(new SimpleParticle(drip, 0.3F, 0.0F, 0.3F, 0.01, dripCount), 0, -0.3, 0);
ParticleElement group = rain == null ? new Group(cloud, drops) : new Group(cloud, new SimpleParticle(rain, 0.3F, 0.1F, 0.3F, 0.01), drops);
return new LocationMutator(group, 0, 2.4, 0);
}
public List<ParticleDesign> all() {
return List.of(BURST, FOOTPRINTS, TEAM_LINK, SHIMMER, HEARTBEAT, EVENT_PARTICIPATION_UNDERWATER_MWG, EGG_HUNT_MEDIUM, EGG_HUNT_HARD, EGG_HUNT_ADVANCED);
}
}
@@ -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.lobby.particle.design;
import de.steamwar.lobby.particle.ParticleRequirement;
import lombok.experimental.UtilityClass;
import java.util.function.Supplier;
// Requirements of the reward ingredients: wing images, particles and touches. Each is created on first use, as event
// requirements look up the event's name.
@UtilityClass
public class Unlocks {
public final Supplier<ParticleRequirement> WARGEAR_SEASON_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(22, 12, 285, 54));
public final Supplier<ParticleRequirement> AIRSHIP_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(26, 205, 9, 54, 120, 292));
public final Supplier<ParticleRequirement> UNDERWATER_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(31, 9, 210, 520));
public final Supplier<ParticleRequirement> ADVENT_WAR_SHIP_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(32, 9, 205, 210));
public final Supplier<ParticleRequirement> MINI_WARGEAR_LIGA_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(33, 9, 34, 205));
public final Supplier<ParticleRequirement> ABSTURZ_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(34, 210, 205, 527, 286));
public final Supplier<ParticleRequirement> UNDERWATER_MWG_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(35, 9, 210));
public final Supplier<ParticleRequirement> WARGEAR_SEASON_2022_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(37, 285, 210, 122));
public final Supplier<ParticleRequirement> WARGEAR_CLASH_PLACEMENT = lazy(() -> ParticleRequirement.eventPlacement(38, 210, 158, 167, 286));
public final Supplier<ParticleRequirement> ANY_EVENT = () -> ParticleRequirement.EVENT_PARTICIPATION;
public final Supplier<ParticleRequirement> WARGEAR_SEASON = lazy(() -> ParticleRequirement.eventParticipation(22));
public final Supplier<ParticleRequirement> AIRSHIP = lazy(() -> ParticleRequirement.eventParticipation(26));
public final Supplier<ParticleRequirement> HELLS_BELLS = lazy(() -> ParticleRequirement.eventParticipation(28));
public final Supplier<ParticleRequirement> UNDERWATER = lazy(() -> ParticleRequirement.eventParticipation(31));
public final Supplier<ParticleRequirement> ADVENT_WAR_SHIP = lazy(() -> ParticleRequirement.eventParticipation(32));
public final Supplier<ParticleRequirement> MINI_WARGEAR_LIGA = lazy(() -> ParticleRequirement.eventParticipation(33));
public final Supplier<ParticleRequirement> WARGEAR_SEASON_2022 = lazy(() -> ParticleRequirement.eventParticipation(37));
public final Supplier<ParticleRequirement> EGG_HUNT_EASY = () -> ParticleRequirement.EGG_HUNT_EASY;
public final Supplier<ParticleRequirement> EGG_HUNT_EXTREME = () -> ParticleRequirement.EGG_HUNT_EXTREME;
public final Supplier<ParticleRequirement> EGG_HUNT_HALF = () -> ParticleRequirement.EGG_HUNT_FOUND_HALF;
private Supplier<ParticleRequirement> lazy(Supplier<ParticleRequirement> create) {
return new Supplier<>() {
private ParticleRequirement requirement;
@Override
public ParticleRequirement get() {
if (requirement == null) requirement = create.get();
return requirement;
}
};
}
}

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