Compare commits

...
40 Commits
Author SHA1 Message Date
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
119 changed files with 4576 additions and 1882 deletions
+6
View File
@@ -7,8 +7,14 @@
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
steamwar.properties steamwar.properties
# Kotlin
.kotlin
# IntelliJ IDEA # IntelliJ IDEA
*.iml *.iml
.idea
!.idea/copyright
!.idea/runConfigurations
# VSCode # VSCode
bin/ bin/
+1 -1
View File
@@ -47,7 +47,7 @@ dependencies {
compileOnly(libs.netty) compileOnly(libs.netty)
implementation(libs.luaj) implementation(libs.luaj)
implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar")) // implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar"))
} }
widener { widener {
@@ -20,8 +20,7 @@
package de.steamwar.bausystem; package de.steamwar.bausystem;
import de.steamwar.bausystem.config.BauServer; import de.steamwar.bausystem.config.BauServer;
import de.steamwar.bausystem.configplayer.Config; import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.configplayer.ConfigConverter;
import de.steamwar.bausystem.features.gui.BauGUI; import de.steamwar.bausystem.features.gui.BauGUI;
import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin; import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin;
import de.steamwar.bausystem.features.script.lua.libs.LuaLib; 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.region.RegionSystem;
import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.bausystem.utils.TickManager; import de.steamwar.bausystem.utils.TickManager;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.command.AbstractValidator; import de.steamwar.command.AbstractValidator;
import de.steamwar.command.SWCommandUtils; import de.steamwar.command.SWCommandUtils;
import de.steamwar.core.CRIUSleepEvent; import de.steamwar.core.CRIUSleepEvent;
@@ -112,9 +110,6 @@ public class BauSystem extends JavaPlugin implements Listener {
if (any instanceof PanzernAlgorithm) { if (any instanceof PanzernAlgorithm) {
Panzern.add((PanzernAlgorithm) any); Panzern.add((PanzernAlgorithm) any);
} }
if (any instanceof ConfigConverter) {
Config.addConfigConverter((ConfigConverter) any);
}
if (any instanceof BoundingBoxLoader) { if (any instanceof BoundingBoxLoader) {
((BoundingBoxLoader) any).load(); ((BoundingBoxLoader) any).load();
} }
@@ -160,9 +155,8 @@ public class BauSystem extends JavaPlugin implements Listener {
@Override @Override
public void onDisable() { public void onDisable() {
linker.unlink(); linker.unlink();
WorldData.write();
RegionSystem.INSTANCE.save(); RegionSystem.INSTANCE.save();
Config.getInstance().saveAll(); ConfigStore.saveAll();
} }
private <T extends CommandSender> AbstractValidator<T, ?> validator(Permission permission) { 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. * 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 * 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 * 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/>. * 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 de.steamwar.linkage.Linked;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; 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 @Linked
public class ChattingEvent implements Listener { public class ConfigStoreListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @EventHandler(priority = EventPriority.MONITOR)
private void onChat(AsyncPlayerChatEvent event) { public void onPlayerQuit(PlayerQuitEvent event) {
event.setCancelled(true); ConfigStore.unloadAll(event.getPlayer());
Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage()); }
@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. * 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 * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -19,129 +19,77 @@
package de.steamwar.bausystem.configplayer; package de.steamwar.bausystem.configplayer;
import de.steamwar.bausystem.configplayer.serializer.ConfigurationSerializableSerializer; import com.google.gson.GsonBuilder;
import de.steamwar.linkage.Linked; import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.sql.UserConfig; 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 lombok.Getter;
import org.bukkit.Bukkit; import lombok.Setter;
import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack;
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 java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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 { public static final int VERSION = 2;
SerializeManager.add(new ConfigurationSerializableSerializer());
}
@Getter public static final ConfigStore<Config> STORE = new ConfigStore<>(Config.class, VERSION, Config::new)
private static Config instance; .gson(new GsonBuilder().registerTypeHierarchyAdapter(ItemStack.class, new ItemStackTypeAdapter()).create())
.legacy(s -> s.startsWith("{@version(1)"), ConfigV1::parse)
.converter(new ConfigV1ToV2Converter())
.userConfig("bausystem");
{ @Setter(AccessLevel.NONE)
instance = this; protected int version = VERSION;
}
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());
}
/** /**
* Get a PlayerConfig, optionally loads it from the DataBase and migrates it if necessary. * BauGui item id to inventory slot
*
* @param player the player from whom to get the config.
* @return the config object
*/ */
public YAPIONObject get(Player player) { protected Map<Integer, Integer> bauGui = defaultBauGui();
UUID uuid = player.getUniqueId(); protected int bauGuiSize = 45;
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();
}
/** /**
* Save a PlayerConfig, this does not remove the key value mapping from the map. * Slots 0-8 are the hotbar, slots 9-12 the armor contents
*
* @param player the player to save the config.
*/ */
public void save(Player player) { protected ItemStack[] hotbar = DefaultHotbar.defaultHotbar();
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);
}
}
private YAPIONObject update(YAPIONObject yapionObject) { protected boolean nightVision = false;
int version = yapionObject.getPlainValue("@version"); protected boolean tntClickDetails = true;
while (version < ConfigCreator.currentVersion) { protected boolean detonatorAutostart = false;
ConfigConverter configConverter = CONFIG_CONVERTER_MAP.getOrDefault(version, null); protected boolean inventoryFill = false;
if (configConverter == null) { protected boolean rayVisualizer = false;
Bukkit.getLogger().log(Level.SEVERE, "No updater found for version " + version); protected boolean smartPlace = false;
return ConfigCreator.createDefaultConfig(); protected boolean blockCounter = false;
} protected boolean depthMessage = true;
try {
configConverter.update(yapionObject); public static Map<Integer, Integer> defaultBauGui() {
} catch (Exception e) { // 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ?
Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e); // 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16
return ConfigCreator.createDefaultConfig(); // 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
int newVersion = yapionObject.getPlainValue("@version"); // 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ?
if (version == newVersion) { Map<Integer, Integer> bauGui = new HashMap<>();
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was the same after conversion"); bauGui.put(10, 2); bauGui.put(3, 3); bauGui.put(7, 4); bauGui.put(17, 5); bauGui.put(15, 6);
return ConfigCreator.createDefaultConfig(); bauGui.put(5, 9); bauGui.put(4, 18); bauGui.put(6, 27);
} bauGui.put(16, 17); bauGui.put(11, 26); bauGui.put(18, 35);
if (newVersion < version) { bauGui.put(19, 20); bauGui.put(21, 21); bauGui.put(9, 22); bauGui.put(0, 23); bauGui.put(1, 24);
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was earlier after conversion"); 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 ConfigCreator.createDefaultConfig(); return bauGui;
}
version = newVersion;
}
return yapionObject;
} }
} }
@@ -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())); dimensions.setZ(Math.abs(dimensions.getZ()));
RegionUtils.message(region, player -> { 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)); 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 @Register
public void toggle(Player player) { public void toggle(Player player) {
if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) { if (Config.STORE.get(player).isDepthMessage()) {
Config.getInstance().get(player).put("depth_message", false); Config.STORE.get(player).setDepthMessage(false);
BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player); BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player);
} else { } else {
Config.getInstance().get(player).put("depth_message", true); Config.STORE.get(player).setDepthMessage(true);
BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player); BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player);
} }
} }
@@ -95,7 +95,7 @@ public class Detonator {
public static void activateDetonator(DetonatorStorage detonator) { public static void activateDetonator(DetonatorStorage detonator) {
Player p = detonator.getPlayer(); Player p = detonator.getPlayer();
if (Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false)) { if (Config.STORE.get(p).isDetonatorAutostart()) {
AutostartListener.instance.activate(p); AutostartListener.instance.activate(p);
} }
@@ -76,8 +76,8 @@ public class DetonatorCommand extends SWCommand {
@Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART") @Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART")
public void toggleAutostartTimer(Player p) { public void toggleAutostartTimer(Player p) {
boolean current = Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false); boolean current = Config.STORE.get(p).isDetonatorAutostart();
Config.getInstance().get(p).put("detonator-autostart", !current); Config.STORE.get(p).setDetonatorAutostart(!current);
if (!current) { if (!current) {
BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p); BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p);
} else { } else {
@@ -28,7 +28,6 @@ import lombok.Setter;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask; import org.bukkit.scheduler.BukkitTask;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*; import java.util.*;
@@ -52,15 +51,15 @@ public class BauGuiMapping {
} }
@Getter @Getter
private final YAPIONObject object; private final Config config;
@Getter @Getter
private final Player owner; private final Player owner;
@Getter @Getter
@Setter @Setter
private boolean saved; private boolean saved;
protected BauGuiMapping(YAPIONObject object, Player p) { protected BauGuiMapping(Config config, Player p) {
this.object = object; this.config = config;
this.owner = p; this.owner = p;
fromUUID.put(p.getUniqueId(), this); fromUUID.put(p.getUniqueId(), this);
mappings.add(this); mappings.add(this);
@@ -70,19 +69,18 @@ public class BauGuiMapping {
BauGuiMapping mapping = fromUUID.get(p.getUniqueId()); BauGuiMapping mapping = fromUUID.get(p.getUniqueId());
if (mapping == null) { if (mapping == null) {
YAPIONObject yapionObject = Config.getInstance().get(p); mapping = new BauGuiMapping(Config.STORE.get(p), p);
mapping = new BauGuiMapping(yapionObject.getObject("baugui"), p);
} }
return mapping; return mapping;
} }
public boolean isShown(int id) { 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) { public int getSlot(int id) {
return object.getPlainValue(Integer.toString(id)); return config.getBauGui().get(id);
} }
public Map<Integer, Integer> getMapping() { public Map<Integer, Integer> getMapping() {
@@ -94,7 +92,7 @@ public class BauGuiMapping {
} }
public int getSize() { public int getSize() {
return object.getPlainValueOrDefault("size", 45); return config.getBauGuiSize();
} }
public void setSize(int size) { public void setSize(int size) {
@@ -102,7 +100,7 @@ public class BauGuiMapping {
return; return;
} }
object.add("size", size); config.setBauGuiSize(size);
} }
private void tick() { private void tick() {
@@ -111,16 +109,17 @@ public class BauGuiMapping {
public void save() { public void save() {
if (saved) return; if (saved) return;
YAPIONObject config = Config.getInstance().get(owner); Config current = Config.STORE.get(owner);
config.add("baugui", object); current.setBauGui(config.getBauGui());
Config.getInstance().save(owner); current.setBauGuiSize(config.getBauGuiSize());
Config.STORE.save(owner);
saved = true; saved = true;
} }
private Map<Integer, Integer> internalReadMap() { private Map<Integer, Integer> internalReadMap() {
Map<Integer, Integer> map = new HashMap<>(); Map<Integer, Integer> map = new HashMap<>();
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) { 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; if (value == -1) continue;
map.put(e.getKey(), value); map.put(e.getKey(), value);
} }
@@ -129,10 +128,10 @@ public class BauGuiMapping {
private void internalWriteMap(Map<Integer, Integer> map) { private void internalWriteMap(Map<Integer, Integer> map) {
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) { 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); Integer value = map.getOrDefault(e.getKey(), -1);
if (value == -1) continue; 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; package de.steamwar.bausystem.features.hotbar;
import de.steamwar.bausystem.configplayer.Config; import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.core.Core;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import yapion.hierarchy.types.YAPIONArray; import org.bukkit.inventory.meta.ItemMeta;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
import yapion.parser.YAPIONParser;
import yapion.serializing.YAPIONDeserializer;
import yapion.serializing.YAPIONSerializer;
import java.util.HashSet; import java.util.List;
import java.util.Set;
@UtilityClass @UtilityClass
public class DefaultHotbar { 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) { public void updateHotbar(Player p) {
ItemStack[] hotbar = new ItemStack[13]; ItemStack[] hotbar = new ItemStack[13];
System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9); System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9);
System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4); System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4);
YAPIONArray yapionArray = new YAPIONArray(); Config.STORE.get(p).setHotbar(copy(hotbar));
for (ItemStack itemStack : hotbar) { Config.STORE.save(p);
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);
} }
public void setHotbar(Player p) { public void setHotbar(Player p) {
@@ -66,28 +50,34 @@ public class DefaultHotbar {
} }
public ItemStack[] getItems(Player p) { public ItemStack[] getItems(Player p) {
Config.getInstance().get(p).remap("hotbar", "hotbar-19"); return copy(Config.STORE.get(p).getHotbar());
YAPIONArray yapionArray = Config.getInstance().get(p).getYAPIONArrayOrSetDefault("hotbar-" + Core.getVersion(), defaultHotbar()); }
public ItemStack[] defaultHotbar() {
ItemStack[] hotbar = new ItemStack[13]; ItemStack[] hotbar = new ItemStack[13];
Set<Integer> invalid = new HashSet<>(); hotbar[0] = item(Material.WOODEN_AXE, "WorldEdit Wand", "Left click: select pos #1", "Right click: select pos #2");
yapionArray.forEach((integer, yapionAnyType) -> { hotbar[1] = item(Material.COMPASS, "Navigation Wand", "Left click: jump to location", "Right click: pass through walls");
if (yapionAnyType instanceof YAPIONValue) { hotbar[8] = item(Material.NETHER_STAR, "§eBau GUI");
hotbar[integer] = null; hotbar[11] = new ItemStack(Material.ELYTRA);
} 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);
return hotbar; return hotbar;
} }
public YAPIONArray defaultHotbar() { private ItemStack item(Material material, String name, String... lore) {
return DEFAULT_HOTBAR; 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 @Override
public ItemStack getItem(Player player) { 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 -> { return new SWItem(Material.HOPPER, BauSystem.MESSAGE.parse("OTHER_ITEMS_INVENTORY_FILL_NAME", player), Collections.singletonList(BauSystem.MESSAGE.parse(loreKey, player)), false, clickType -> {
}).getItemStack(); }).getItemStack();
} }
@@ -40,7 +40,7 @@ public class InventoryFiller implements Listener {
@EventHandler @EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) { public void onPlayerDropItem(PlayerDropItemEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; 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; if (!event.getPlayer().isSneaking()) return;
Block block = event.getPlayer().getTargetBlockExact(5); Block block = event.getPlayer().getTargetBlockExact(5);
if (block == null) return; if (block == null) return;
@@ -62,7 +62,7 @@ public class InventoryFiller implements Listener {
@EventHandler @EventHandler
public void onPlayerItemHeld(PlayerItemHeldEvent event) { public void onPlayerItemHeld(PlayerItemHeldEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; 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; if (!event.getPlayer().isSneaking()) return;
ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand(); ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR) return; if (itemStack.getType() == Material.AIR) return;
@@ -68,8 +68,8 @@ public class InventoryFillerCommand extends SWCommand {
@Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"}) @Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"})
public void toggle(Player player) { public void toggle(Player player) {
boolean inventoryFill = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false); boolean inventoryFill = Config.STORE.get(player).isInventoryFill();
Config.getInstance().get(player).put("inventoryfill", !inventoryFill); Config.STORE.get(player).setInventoryFill(!inventoryFill);
if (!inventoryFill) { if (!inventoryFill) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player)); SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player));
BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player); BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player);
@@ -167,7 +167,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { public void onPlayerJoin(PlayerJoinEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; 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()); if (rayvisualizer) server.addPlayer(event.getPlayer());
} }
@@ -181,7 +181,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
for (Player player : Bukkit.getOnlinePlayers()) { for (Player player : Bukkit.getOnlinePlayers()) {
if (Permission.BUILD.hasPermission(player)) { 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); if (rayvisualizer) server.addPlayer(player);
} else { } else {
server.removePlayer(player); server.removePlayer(player);
@@ -192,8 +192,8 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@Register @Register
public void toggle(@Validator Player player) { public void toggle(@Validator Player player) {
boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false); boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer();
Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer); Config.STORE.get(player).setRayVisualizer(!rayvisualizer);
if (!rayvisualizer) { if (!rayvisualizer) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player)); SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player));
server.addPlayer(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.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; 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.InvCallback;
import de.steamwar.inventory.SWItem; import de.steamwar.inventory.SWItem;
import lombok.Getter; import lombok.Getter;
@@ -28,7 +29,6 @@ import lombok.Setter;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
@@ -121,9 +121,9 @@ public abstract class SimulatorElement<T extends SimulatorPhase> {
public abstract String getType(); 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; package de.steamwar.bausystem.features.simulator.data;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer; 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 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.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -31,7 +32,6 @@ import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.block.data.type.Observer; import org.bukkit.block.data.type.Observer;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@@ -70,12 +70,12 @@ public final class ObserverPhase extends SimulatorPhase {
} }
@Override @Override
public void saveExtra(YAPIONObject phaseObject) { public void saveExtra(SimulatorConfig.Phase phase) {
phaseObject.add("orientation", orientation.name()); phase.setOrientation(orientation);
} }
@Override @Override
public void loadExtra(YAPIONObject phaseObject) { public void loadExtra(SimulatorConfig.Phase phase) {
orientation = BlockFace.valueOf(phaseObject.getPlainValue("orientation")); 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.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui; import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui; 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.InvCallback;
import de.steamwar.inventory.SWItem; import de.steamwar.inventory.SWItem;
import lombok.Getter; import lombok.Getter;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
public final class TNTElement extends SimulatorElement<TNTPhase> { public final class TNTElement extends SimulatorElement<TNTPhase> {
@@ -100,14 +100,14 @@ public final class TNTElement extends SimulatorElement<TNTPhase> {
} }
@Override @Override
public void saveExtra(YAPIONObject elementObject) { public void saveExtra(SimulatorConfig.Element element) {
elementObject.add("alignmentX", alignment.getX()); element.setAlignmentX(alignment.getX());
elementObject.add("alignmentZ", alignment.getZ()); element.setAlignmentZ(alignment.getZ());
} }
@Override @Override
public void loadExtra(YAPIONObject elementObject) { public void loadExtra(SimulatorConfig.Element element) {
alignment.setX(elementObject.getDoubleOrSetDefault("alignmentX", 0)); alignment.setX(element.getAlignmentX() != null ? element.getAlignmentX() : 0);
alignment.setZ(elementObject.getDoubleOrSetDefault("alignmentZ", 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.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction; 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.Region;
import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.FreezeMode; import de.steamwar.bausystem.region.flags.FreezeMode;
@@ -31,7 +32,6 @@ import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.entity.TNTPrimed; import org.bukkit.entity.TNTPrimed;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@@ -83,18 +83,18 @@ public final class TNTPhase extends SimulatorPhase {
} }
@Override @Override
public void saveExtra(YAPIONObject phaseObject) { public void saveExtra(SimulatorConfig.Phase phase) {
phaseObject.add("count", count); phase.setCount(count);
phaseObject.add("xJump", xJump); phase.setXJump(xJump);
phaseObject.add("yJump", yJump); phase.setYJump(yJump);
phaseObject.add("zJump", zJump); phase.setZJump(zJump);
} }
@Override @Override
public void loadExtra(YAPIONObject phaseObject) { public void loadExtra(SimulatorConfig.Phase phase) {
count = phaseObject.getPlainValue("count"); if (phase.getCount() != null) count = phase.getCount();
xJump = phaseObject.getPlainValue("xJump"); if (phase.getXJump() != null) xJump = phase.getXJump();
yJump = phaseObject.getPlainValue("yJump"); if (phase.getYJump() != null) yJump = phase.getYJump();
zJump = phaseObject.getPlainValue("zJump"); if (phase.getZJump() != null) zJump = phase.getZJump();
} }
} }
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage; 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.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorElement; import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; 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.redstone.RedstonePhase;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement; import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material;
import org.bukkit.util.Vector; 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.File;
import java.io.IOException; import java.io.IOException;
@@ -51,44 +47,41 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty(); return Optional.empty();
} }
YAPIONObject yapionObject; SimulatorConfig config;
try { try {
yapionObject = YAPIONParser.parse(file); config = SimulatorConfig.STORE.load(file);
} catch (YAPIONException | IOException e) { } catch (JsonParseException | IllegalStateException | IOException e) {
return Optional.empty(); return Optional.empty();
} }
String name = file.getName().substring(0, file.getName().length() - ".sim".length()); String name = file.getName().substring(0, file.getName().length() - ".sim".length());
Simulator simulator = new Simulator(name); Simulator simulator = new Simulator(name);
loadSimulator(yapionObject, simulator); loadSimulator(config, simulator);
return Optional.of(Collections.singletonList(simulator)); return Optional.of(Collections.singletonList(simulator));
} }
private void loadSimulator(YAPIONObject simulatorObject, Simulator simulator) { private void loadSimulator(SimulatorConfig config, Simulator simulator) {
simulator.setMaterial(Material.valueOf(simulatorObject.getPlainValue("material"))); if (config.getMaterial() != null) simulator.setMaterial(config.getMaterial());
simulator.setAutoTrace(simulatorObject.getPlainValue("autoTrace")); simulator.setAutoTrace(config.isAutoTrace());
simulator.setAutoTestblock(simulatorObject.getPlainValueOrDefault("autoTestblock", false)); simulator.setAutoTestblock(config.isAutoTestblock());
YAPIONArray groups = simulatorObject.getArray("groups"); config.getGroups().forEach(groupConfig -> {
groups.streamObject().forEach(groupObject -> {
SimulatorGroup simulatorGroup = new SimulatorGroup(); SimulatorGroup simulatorGroup = new SimulatorGroup();
loadGroup(groupObject, simulatorGroup); loadGroup(groupConfig, simulatorGroup);
simulator.add(simulatorGroup); simulator.add(simulatorGroup);
}); });
} }
private void loadGroup(YAPIONObject groupObject, SimulatorGroup group) { private void loadGroup(SimulatorConfig.Group groupConfig, SimulatorGroup group) {
group.setMaterial(Material.valueOf(groupObject.getPlainValue("material"))); if (groupConfig.getMaterial() != null) group.setMaterial(groupConfig.getMaterial());
group.setDisabled(groupObject.getPlainValue("disabled")); group.setDisabled(groupConfig.isDisabled());
YAPIONArray elements = groupObject.getArray("elements"); groupConfig.getElements().forEach(elementConfig -> {
elements.streamObject().forEach(elementObject -> {
SimulatorElement<?> element; SimulatorElement<?> element;
Supplier<SimulatorPhase> phaseConstructor; Supplier<SimulatorPhase> phaseConstructor;
Vector position = new Vector(elementObject.getDouble("x"), elementObject.getDouble("y"), elementObject.getDouble("z")); Vector position = new Vector(elementConfig.getX(), elementConfig.getY(), elementConfig.getZ());
String type = elementObject.getPlainValue("type"); switch (String.valueOf(elementConfig.getType())) {
switch (type) {
case "TNT": case "TNT":
element = new TNTElement(position); element = new TNTElement(position);
phaseConstructor = TNTPhase::new; phaseConstructor = TNTPhase::new;
@@ -109,28 +102,27 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
if (element == null) { if (element == null) {
return; return;
} }
loadElement(elementObject, element, phaseConstructor); loadElement(elementConfig, element, phaseConstructor);
group.add(element); group.add(element);
}); });
} }
private void loadElement(YAPIONObject elementObject, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) { private void loadElement(SimulatorConfig.Element elementConfig, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) {
element.setMaterial(Material.valueOf(elementObject.getPlainValue("material"))); if (elementConfig.getMaterial() != null) element.setMaterial(elementConfig.getMaterial());
element.setDisabled(elementObject.getPlainValue("disabled")); element.setDisabled(elementConfig.isDisabled());
element.loadExtra(elementObject); element.loadExtra(elementConfig);
YAPIONArray phases = elementObject.getArray("phases"); elementConfig.getPhases().forEach(phaseConfig -> {
phases.streamObject().forEach(phaseObject -> {
SimulatorPhase phase = phaseConstructor.get(); SimulatorPhase phase = phaseConstructor.get();
loadPhase(phaseObject, phase); loadPhase(phaseConfig, phase);
((SimulatorElement) element).add(phase); ((SimulatorElement) element).add(phase);
}); });
} }
private void loadPhase(YAPIONObject phaseObject, SimulatorPhase phase) { private void loadPhase(SimulatorConfig.Phase phaseConfig, SimulatorPhase phase) {
phase.setTickOffset(phaseObject.getPlainValue("tickOffset")); phase.setTickOffset(phaseConfig.getTickOffset());
phase.setLifetime(phaseObject.getPlainValue("lifetime")); phase.setLifetime(phaseConfig.getLifetime());
phase.setOrder(phaseObject.getPlainValue("order")); phase.setOrder(phaseConfig.getOrder());
phase.loadExtra(phaseObject); 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; 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.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; 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.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase; import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.util.Vector; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; 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 { public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
@Override @Override
@@ -45,20 +51,19 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty(); return Optional.empty();
} }
YAPIONObject yapionObject; Map<String, Object> yapionObject;
try { try {
yapionObject = YAPIONParser.parse(file); yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (YAPIONException | IOException e) { } catch (IllegalArgumentException | IOException e) {
return Optional.empty(); return Optional.empty();
} }
String name = file.getName().substring(0, file.getName().length() - ".simulator".length()); String name = file.getName().substring(0, file.getName().length() - ".simulator".length());
Simulator simulator = new Simulator(name); 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 (Map<?, ?> element : getObjects(yapionObject, "tntElements")) {
for (YAPIONObject element : tntElements.streamObject().collect(Collectors.toList())) {
if (element.containsKey("elements")) { if (element.containsKey("elements")) {
simulator.add(loadGroup(element)); simulator.add(loadGroup(element));
} else { } else {
@@ -70,27 +75,26 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.of(Collections.singletonList(simulator)); return Optional.of(Collections.singletonList(simulator));
} }
private SimulatorGroup loadGroup(YAPIONObject element) { private SimulatorGroup loadGroup(Map<?, ?> element) {
double x = element.getDoubleOrDefault("x", 0); double x = getDouble(element, "x", 0);
double y = element.getDoubleOrDefault("y", 0); double y = getDouble(element, "y", 0);
double z = element.getDoubleOrDefault("z", 0); double z = getDouble(element, "z", 0);
int tickOffset = element.getIntOrDefault("tickOffset", 0); int tickOffset = getInt(element, "tickOffset", 0);
SimulatorGroup simulatorGroup = new SimulatorGroup(); SimulatorGroup simulatorGroup = new SimulatorGroup();
simulatorGroup.setDisabled(element.getBooleanOrDefault("disabled", false)); simulatorGroup.setDisabled(getBoolean(element, "disabled", false));
YAPIONArray elements = element.getArrayOrDefault("elements", new YAPIONArray()); for (Map<?, ?> e : getObjects(element, "elements")) {
for (YAPIONObject e : elements.streamObject().collect(Collectors.toList())) {
simulatorGroup.add(loadElement(e, x, y, z, tickOffset)); simulatorGroup.add(loadElement(e, x, y, z, tickOffset));
} }
return simulatorGroup; return simulatorGroup;
} }
private TNTElement loadElement(YAPIONObject element, double dx, double dy, double dz, int dTickOffset) { private TNTElement loadElement(Map<?, ?> 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 tntElement = new TNTElement(new Vector(requireDouble(element, "x") + dx, requireDouble(element, "y") + dy, requireDouble(element, "z") + dz));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
TNTPhase tntPhase = loadPhase(element); TNTPhase tntPhase = loadPhase(element);
tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset); tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset);
@@ -99,12 +103,12 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return tntElement; return tntElement;
} }
private SimulatorGroup loadElement(YAPIONObject element) { private SimulatorGroup loadElement(Map<?, ?> element) {
SimulatorGroup simulatorGroup = new SimulatorGroup(); SimulatorGroup simulatorGroup = new SimulatorGroup();
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x"), element.getDouble("y"), element.getDouble("z"))); TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x"), requireDouble(element, "y"), requireDouble(element, "z")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement); simulatorGroup.add(tntElement);
tntElement.add(loadPhase(element)); tntElement.add(loadPhase(element));
@@ -112,15 +116,15 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return simulatorGroup; return simulatorGroup;
} }
private TNTPhase loadPhase(YAPIONObject element) { private TNTPhase loadPhase(Map<?, ?> element) {
TNTPhase tntPhase = new TNTPhase(); TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1)); tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); tntPhase.setZJump(getBoolean(element, "zVelocity", false));
switch (element.getStringOrDefault("order", "REPEATER")) { switch (getString(element, "order", "REPEATER")) {
case "REPEATER": case "REPEATER":
tntPhase.setOrder(0); tntPhase.setOrder(0);
break; break;
@@ -133,4 +137,8 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
} }
return tntPhase; 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 de.steamwar.bausystem.features.simulator.data.Simulator;
import lombok.experimental.UtilityClass; 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.File;
import java.io.IOException; import java.io.IOException;
@@ -36,48 +33,42 @@ public class SimulatorSaver {
directory.mkdirs(); directory.mkdirs();
} }
YAPIONObject simulatorObject = new YAPIONObject(); SimulatorConfig config = new SimulatorConfig();
simulatorObject.add("material", simulator.getMaterial().name()); config.setMaterial(simulator.getMaterial());
simulatorObject.add("autoTrace", simulator.isAutoTrace()); config.setAutoTrace(simulator.isAutoTrace());
simulatorObject.add("autoTestblock", simulator.isAutoTestblock()); config.setAutoTestblock(simulator.isAutoTestblock());
YAPIONArray groups = new YAPIONArray();
simulator.getGroups().forEach(group -> { simulator.getGroups().forEach(group -> {
YAPIONObject groupObject = new YAPIONObject(); SimulatorConfig.Group groupConfig = new SimulatorConfig.Group();
groupObject.add("material", group.getMaterial().name()); groupConfig.setMaterial(group.getMaterial());
groupObject.add("disabled", group.isDisabled()); groupConfig.setDisabled(group.isDisabled());
YAPIONArray elements = new YAPIONArray();
group.getElements().forEach(element -> { group.getElements().forEach(element -> {
YAPIONObject elementObject = new YAPIONObject(); SimulatorConfig.Element elementConfig = new SimulatorConfig.Element();
elementObject.add("type", element.getType()); elementConfig.setType(element.getType());
elementObject.add("material", element.getMaterial().name()); elementConfig.setMaterial(element.getMaterial());
elementObject.add("disabled", element.isDisabled()); elementConfig.setDisabled(element.isDisabled());
elementObject.add("x", element.getPosition().getX()); elementConfig.setX(element.getPosition().getX());
elementObject.add("y", element.getPosition().getY()); elementConfig.setY(element.getPosition().getY());
elementObject.add("z", element.getPosition().getZ()); elementConfig.setZ(element.getPosition().getZ());
element.saveExtra(elementObject); element.saveExtra(elementConfig);
YAPIONArray phases = new YAPIONArray();
element.getPhases().forEach(phase -> { element.getPhases().forEach(phase -> {
YAPIONObject phaseObject = new YAPIONObject(); SimulatorConfig.Phase phaseConfig = new SimulatorConfig.Phase();
phaseObject.add("tickOffset", phase.getTickOffset()); phaseConfig.setTickOffset(phase.getTickOffset());
phaseObject.add("lifetime", phase.getLifetime()); phaseConfig.setLifetime(phase.getLifetime());
phaseObject.add("order", phase.getOrder()); phaseConfig.setOrder(phase.getOrder());
phase.saveExtra(phaseObject); phase.saveExtra(phaseConfig);
phases.add(phaseObject); elementConfig.getPhases().add(phaseConfig);
}); });
elementObject.add("phases", phases); groupConfig.getElements().add(elementConfig);
elements.add(elementObject);
}); });
groupObject.add("elements", elements); config.getGroups().add(groupConfig);
groups.add(groupObject);
}); });
simulatorObject.add("groups", groups);
File file = new File(directory, simulator.getName() + ".sim"); File file = new File(directory, simulator.getName() + ".sim");
try { try {
simulatorObject.toJSONLossy(new FileOutput(file)).close(); SimulatorConfig.STORE.save(file, config);
} catch (IOException e) { } catch (IOException e) {
// Ignore // Ignore
} }
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage; 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.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup; 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.TNTElement;
@@ -26,18 +27,23 @@ import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.util.Vector; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; 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 { public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
@Override @Override
@@ -46,10 +52,10 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty(); return Optional.empty();
} }
YAPIONObject yapionObject; Map<String, Object> yapionObject;
try { try {
yapionObject = YAPIONParser.parse(file); yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (YAPIONException | IOException e) { } catch (IllegalArgumentException | IOException e) {
return Optional.empty(); return Optional.empty();
} }
@@ -57,28 +63,28 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name)); SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name));
List<Simulator> simulators = new ArrayList<>(); List<Simulator> simulators = new ArrayList<>();
for (String s : yapionObject.getKeys()) { for (String s : yapionObject.keySet()) {
String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s); String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s);
YAPIONArray content = yapionObject.getArray(s); List<Map<?, ?>> content = getObjects(yapionObject, s);
if (content.isEmpty()) continue; if (content.isEmpty()) continue;
Simulator simulator = new Simulator(newName); Simulator simulator = new Simulator(newName);
for (YAPIONObject element : content.streamObject().collect(Collectors.toList())) { for (Map<?, ?> element : content) {
SimulatorGroup simulatorGroup = new SimulatorGroup(); SimulatorGroup simulatorGroup = new SimulatorGroup();
simulator.add(simulatorGroup); simulator.add(simulatorGroup);
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("positionX"), element.getDouble("positionY"), element.getDouble("positionZ"))); TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "positionX"), requireDouble(element, "positionY"), requireDouble(element, "positionZ")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); // IDK if this existed back then? tntElement.setDisabled(getBoolean(element, "disabled", false)); // IDK if this existed back then?
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT"))); tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement); simulatorGroup.add(tntElement);
TNTPhase tntPhase = new TNTPhase(); TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80)); tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1)); tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0)); tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false)); tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false)); tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false)); tntPhase.setZJump(getBoolean(element, "zVelocity", false));
tntPhase.setOrder(element.getBooleanOrDefault("comparator", false) ? 2 : 0); tntPhase.setOrder(getBoolean(element, "comparator", false) ? 2 : 0);
tntElement.add(tntPhase); tntElement.add(tntPhase);
} }
simulators.add(simulator); simulators.add(simulator);
@@ -87,4 +93,8 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
file.delete(); file.delete();
return Optional.of(simulators); 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; package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.BauSystem;
import de.steamwar.core.SWPlayer;
import de.steamwar.inventory.SWItem; import de.steamwar.inventory.SWItem;
import de.steamwar.sql.UserConfig;
import lombok.Cleanup;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.ItemMeta; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@UtilityClass @UtilityClass
public class LaufbauUtils { 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) { 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)); return new Cuboid(pixelToBlock(pixelX), pixelToBlock(pixelY), pixelToBlock(pixelZ), pixelToBlock(pixelDX), pixelToBlock(pixelDY), pixelToBlock(pixelDZ));
} }
@@ -123,7 +82,7 @@ public class LaufbauUtils {
return false; return false;
} }
String identifier = identifier(bb); 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 @SneakyThrows
@@ -132,11 +91,9 @@ public class LaufbauUtils {
return; return;
} }
String identifier = identifier(bb); String identifier = identifier(bb);
YAPIONObject yapionObject = SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject; Set<String> deactivated = LaufbauConfig.STORE.get(p).getDeactivated();
if (yapionObject.containsKey(identifier)) { if (!deactivated.remove(identifier)) {
yapionObject.remove(identifier); deactivated.add(identifier);
} else {
yapionObject.put(identifier, new YAPIONObject());
} }
} }
} }
@@ -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"}) @Register(description = {"SMART_PLACE_HELP", "SMART_PLACE_INFO"})
public void genericToggle(Player p) { public void genericToggle(Player p) {
boolean smartPlace = Config.getInstance().get(p).getPlainValueOrDefault("smartPlace", false); boolean smartPlace = Config.STORE.get(p).isSmartPlace();
Config.getInstance().get(p).put("smartPlace", !smartPlace); Config.STORE.get(p).setSmartPlace(!smartPlace);
if (!smartPlace) { if (!smartPlace) {
SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p)); SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p));
BauSystem.MESSAGE.send("SMART_PLACE_INFO", 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.bausystem.configplayer.Config;
import de.steamwar.linkage.Linked; import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket;
import net.minecraft.world.InteractionHand;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; import org.bukkit.block.BlockFace;
@@ -87,7 +88,9 @@ public class SmartPlaceListener implements Listener {
public SmartPlaceListener() { public SmartPlaceListener() {
TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> {
if (!Permission.BUILD.hasPermission(player)) return 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); RayTraceResult rayTraceResult = player.rayTraceBlocks(6);
Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null; Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null;
BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null; BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null;
@@ -127,7 +130,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler @EventHandler
public void onPlayerInteract(PlayerInteractEvent event) { public void onPlayerInteract(PlayerInteractEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; 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()); WAS_EXECUTED.add(event.getPlayer());
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return; if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return;
@@ -146,7 +149,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler @EventHandler
public void onBlockPlace(BlockPlaceEvent event) { public void onBlockPlace(BlockPlaceEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; 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 (!SMART_PLACING.contains(event.getPlayer())) {
if (CONTAINERS.contains(event.getBlockAgainst().getType())) { if (CONTAINERS.contains(event.getBlockAgainst().getType())) {
SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup(); SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup();
@@ -35,18 +35,15 @@ public class BlockCounter {
public final Map<Region, BlockCount> blockCountMap = new HashMap<>(); public final Map<Region, BlockCount> blockCountMap = new HashMap<>();
public boolean isActive(final Player p) { public boolean isActive(final Player p) {
if (!Config.getInstance().get(p).containsKey("blockCounter")) { return Config.STORE.get(p).isBlockCounter();
return false;
}
return Config.getInstance().get(p).getPlainValue("blockCounter");
} }
public void enable(final Player p) { 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) { 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) { 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.bukkit.util.Transformation;
import org.joml.Quaternionf; import org.joml.Quaternionf;
import org.joml.Vector3f; import org.joml.Vector3f;
import yapion.hierarchy.types.YAPIONValue;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static de.steamwar.bausystem.features.util.TNTClickListener.TNT_CLICK_DETAILS;
/** /**
* Wrapper for the rendering of a record bundle * 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 * @param player the player the message should be printed for
*/ */
public void printIntoChat(Player player) { 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; return;
} }
TNTPoint representative = records.get(0); TNTPoint representative = records.get(0);
@@ -30,7 +30,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import yapion.hierarchy.types.YAPIONObject;
@Linked @Linked
public class NightVisionCommand extends SWCommand implements Listener { public class NightVisionCommand extends SWCommand implements Listener {
@@ -41,15 +40,15 @@ public class NightVisionCommand extends SWCommand implements Listener {
@Register(description = "NIGHT_VISION_HELP") @Register(description = "NIGHT_VISION_HELP")
public void genericCommand(Player p) { public void genericCommand(Player p) {
YAPIONObject yapionObject = Config.getInstance().get(p); Config config = Config.STORE.get(p);
boolean value = !yapionObject.getBooleanOrDefault("nightvision", false); boolean value = !config.isNightVision();
yapionObject.put("nightvision", value); config.setNightVision(value);
setNightVision(p, value); setNightVision(p, value);
} }
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { 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) { private void setNightVision(Player p, boolean value) {
@@ -22,123 +22,71 @@ package de.steamwar.bausystem.features.util;
import com.comphenix.tinyprotocol.TinyProtocol; import com.comphenix.tinyprotocol.TinyProtocol;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.tpslimit.TPSUtils;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent; import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.command.SWCommand; import de.steamwar.command.SWCommand;
import de.steamwar.core.ProtocolWrapper; import de.steamwar.core.ProtocolWrapper;
import de.steamwar.core.SWPlayer; import de.steamwar.core.SWPlayer;
import de.steamwar.linkage.Linked; import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.*; import io.papermc.paper.event.player.PlayerFailMoveEvent;
import net.minecraft.world.entity.player.Abilities; import net.minecraft.network.protocol.game.ClientboundGameEventPacket;
import net.minecraft.world.level.GameType;
import org.bukkit.Bukkit;
import org.bukkit.GameMode; import org.bukkit.GameMode;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockCanBuildEvent; import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.player.PlayerGameModeChangeEvent; import org.bukkit.event.player.PlayerGameModeChangeEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent; import org.bukkit.event.player.PlayerToggleFlightEvent;
import java.util.function.BiFunction;
@Linked @Linked
public class NoClipCommand extends SWCommand implements Listener { public class NoClipCommand extends SWCommand implements Listener {
public static class NoClipData implements SWPlayer.Component { public static class NoClipData implements SWPlayer.Component {
private long lastTick = -1;
@Override @Override
public void onUnmount(SWPlayer player) { public void onUnmount(SWPlayer swPlayer) {
player.getPlayer().setGameMode(GameMode.CREATIVE); Player player = swPlayer.getPlayer();
showClientGameMode(player, GameMode.CREATIVE, GameMode.CREATIVE);
} }
} }
public NoClipCommand() { public NoClipCommand() {
super("noclip", "nc"); 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) { @Register
((CraftPlayer) player).getHandle().gameMode.gameModeForPlayer = GameType.byId(gameMode.getValue());
}
@Register(help = true)
public void genericCommand(@Validator Player player) { public void genericCommand(@Validator Player player) {
SWPlayer swPlayer = SWPlayer.of(player); SWPlayer swPlayer = SWPlayer.of(player);
if (swPlayer.hasComponent(NoClipData.class)) { if (swPlayer.hasComponent(NoClipData.class)) {
swPlayer.removeComponent(NoClipData.class); swPlayer.removeComponent(NoClipData.class);
} else { } else {
player.setGameMode(GameMode.SPECTATOR); player.setGameMode(GameMode.CREATIVE);
Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities(); player.setAllowFlight(true);
abilities.mayBuild = true; player.setFlying(true);
abilities.mayfly = true;
abilities.instabuild = true;
swPlayer.setComponent(new NoClipData()); swPlayer.setComponent(new NoClipData());
BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player); BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player);
TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, 1F)); showClientGameMode(player, GameMode.CREATIVE, GameMode.SPECTATOR);
pseudoGameMode(player, GameMode.SPECTATOR);
} }
} }
@EventHandler @EventHandler
public void onBauMemberUpdate(BauMemberUpdateEvent event) { public void onPlayerFailMove(PlayerFailMoveEvent event) {
event.getNewSpectator().forEach(player -> { if (event.getFailReason() != PlayerFailMoveEvent.FailReason.CLIPPED_INTO_BLOCK) return;
SWPlayer.of(player).removeComponent(NoClipData.class); 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) { public void onPlayerGameModeChange(PlayerGameModeChangeEvent event) {
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
event.setCancelled(true); event.setCancelled(true);
} }
} }
@EventHandler(ignoreCancelled = true) @EventHandler
public void onBlock(BlockCanBuildEvent event) { public void onBlock(BlockCanBuildEvent event) {
if (event.getPlayer() == null) return; if (event.getPlayer() == null) return;
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { 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) { public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) {
event.setCancelled(true); event.setCancelled(true);
@@ -154,20 +102,13 @@ public class NoClipCommand extends SWCommand implements Listener {
} }
} }
@EventHandler(ignoreCancelled = true) /**
public void onPlayerTeleport(PlayerTeleportEvent event) { * @param player player
if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) { * @param localMode GameMode for breaking and placing
return; * @param listedMode GameMode for noPhysics
} */
if (SWPlayer.of(event.getPlayer()).hasComponent(NoClipData.class)) { private static void showClientGameMode(Player player, GameMode localMode, GameMode listedMode) {
event.setCancelled(true); TinyProtocol.instance.sendPacket(player, new ClientboundGameEventPacket(ClientboundGameEventPacket.CHANGE_GAME_MODE, localMode.getValue()));
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.GAMEMODE, new GameProfile(player.getUniqueId(), player.getName()), listedMode));
event.getPlayer().setSpectatorTarget(null);
}, 1L);
}
} }
}
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.bausystem.Permission;
import de.steamwar.entity.*; import de.steamwar.entity.*;
import de.steamwar.linkage.Linked; 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.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockFace; 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.BlockData;
import org.bukkit.block.data.type.Piston; import org.bukkit.block.data.type.Piston;
import org.bukkit.craftbukkit.CraftWorld;
import org.bukkit.entity.Display; import org.bukkit.entity.Display;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay; import org.bukkit.entity.TextDisplay;
@@ -92,41 +94,48 @@ public class PistonCalculator implements Listener {
@EventHandler @EventHandler
public void onBlockPistonExtend(BlockPistonExtendEvent event) { public void onBlockPistonExtend(BlockPistonExtendEvent event) {
if (pistOrders.isEmpty()) return; if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { removeMovedPistOrders(event.getBlocks());
movePistOrders(event.getDirection(), event.getBlocks()); scheduleRecalculate();
pistOrders.values().forEach(PistOrder::calculate);
}, 3);
} }
@EventHandler @EventHandler
public void onBlockPistonRetract(BlockPistonRetractEvent event) { public void onBlockPistonRetract(BlockPistonRetractEvent event) {
if (pistOrders.isEmpty()) return; if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { removeMovedPistOrders(event.getBlocks());
movePistOrders(event.getDirection(), event.getBlocks()); scheduleRecalculate();
pistOrders.values().forEach(PistOrder::calculate);
}, 3);
} }
private void movePistOrders(BlockFace direction, List<Block> blocks) { private void removeMovedPistOrders(List<Block> blocks) {
Set<PistOrder> orders = new HashSet<>();
blocks.forEach(block -> { blocks.forEach(block -> {
PistOrder pistOrder = pistOrders.get(block.getLocation()); PistOrder pistOrder = pistOrders.remove(block.getLocation());
if (pistOrder == null) return; if (pistOrder != null) pistOrder.server.close();
pistOrders.remove(block.getLocation());
pistOrder.piston = pistOrder.piston.getRelative(direction);
orders.add(pistOrder);
});
orders.forEach(pistOrder -> {
pistOrders.put(pistOrder.piston.getLocation(), pistOrder);
}); });
} }
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 @EventHandler
public void onBlockPlace(BlockPlaceEvent event) { public void onBlockPlace(BlockPlaceEvent event) {
if (pistOrders.isEmpty()) return; if (pistOrders.isEmpty()) return;
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { scheduleRecalculate();
pistOrders.values().forEach(PistOrder::calculate);
}, 1);
} }
@EventHandler @EventHandler
@@ -136,9 +145,7 @@ public class PistonCalculator implements Listener {
PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation()); PistOrder pistOrder = pistOrders.remove(event.getBlock().getLocation());
pistOrder.server.close(); pistOrder.server.close();
} }
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { scheduleRecalculate();
pistOrders.values().forEach(PistOrder::calculate);
}, 1);
} }
@EventHandler @EventHandler
@@ -164,13 +171,13 @@ public class PistonCalculator implements Listener {
private final Map<Location, PistOrder> pistOrders = new HashMap<>(); private final Map<Location, PistOrder> pistOrders = new HashMap<>();
private final class PistOrder { private final class PistOrder {
private Block piston; private final Block piston;
private REntityServer server = new REntityServer(); private REntityServer server = new REntityServer();
private boolean pulling = false; private boolean pulling = false;
private List<Location> movedBlocks = new ArrayList<>(); private List<Location> movedBlocks = new ArrayList<>();
private List<Location> brokenBlocks = new ArrayList<>(); private List<Location> brokenBlocks = new ArrayList<>();
private Set<Location> immovableBlocks = new HashSet<>(); private boolean blocked = false;
public PistOrder(Block piston) { public PistOrder(Block piston) {
this.piston = piston; this.piston = piston;
@@ -179,7 +186,7 @@ public class PistonCalculator implements Listener {
public void calculate() { public void calculate() {
movedBlocks.clear(); movedBlocks.clear();
brokenBlocks.clear(); brokenBlocks.clear();
immovableBlocks.clear(); blocked = false;
if (piston.isEmpty()) { if (piston.isEmpty()) {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { 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()); 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(movedBlocks);
Collections.reverse(brokenBlocks); Collections.reverse(brokenBlocks);
@@ -207,86 +214,23 @@ public class PistonCalculator implements Listener {
visualize(); visualize();
} }
private void calculate(Block origin, BlockFace facing, BlockFace direction) { private void resolve(BlockFace facing) {
Block calcOrigin = origin; PistonStructureResolver resolver = new PistonStructureResolver(
if (facing != direction) calcOrigin = origin.getRelative(facing, 3); ((CraftWorld) piston.getWorld()).getHandle(),
new BlockPos(piston.getX(), piston.getY(), piston.getZ()),
List<Block> toCalc = new LinkedList<>(); Direction.valueOf(facing.name()),
calcDirection(origin, null, calcOrigin, facing != direction ? origin.getRelative(facing) : null, facing, direction, toCalc); !pulling
);
while (!toCalc.isEmpty()) { if (!resolver.resolve()) {
Block current = toCalc.remove(0); blocked = true;
if (!movedBlocks.contains(current.getLocation())) { return;
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);
}
}
} }
resolver.getToPush().forEach(pos -> movedBlocks.add(toLocation(pos)));
movedBlocks.remove(origin.getLocation()); resolver.getToDestroy().forEach(pos -> brokenBlocks.add(toLocation(pos)));
if (facing != direction) movedBlocks.remove(origin.getRelative(facing, 1).getLocation());
if (pulling) immovableBlocks.remove(origin.getRelative(facing).getLocation());
} }
private void calcDirection(Block origin, Block blockOrigin, Block calcOrigin, Block ignore, BlockFace facing, BlockFace direction, List<Block> toCalc) { private Location toLocation(BlockPos pos) {
for (int i = 1; i < 24; i++) { return new Location(piston.getWorld(), pos.getX(), pos.getY(), pos.getZ());
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 void visualize() { private void visualize() {
@@ -321,21 +265,11 @@ public class PistonCalculator implements Listener {
textDisplay.setBrightness(new Display.Brightness(15, 15)); 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); CWireframe wireframe = new CWireframe(server);
wireframe.setPos1(piston.getLocation()); wireframe.setPos1(piston.getLocation());
wireframe.setPos2(piston.getLocation()); wireframe.setPos2(piston.getLocation());
if (!immovableBlocks.isEmpty()) { if (blocked) {
wireframe.setBlock(Material.RED_CONCRETE.createBlockData()); wireframe.setBlock(Material.RED_CONCRETE.createBlockData());
} else if (movedBlocks.size() > 12) {
wireframe.setBlock(Material.YELLOW_CONCRETE.createBlockData());
} else { } else {
wireframe.setBlock(Material.LIME_CONCRETE.createBlockData()); wireframe.setBlock(Material.LIME_CONCRETE.createBlockData());
} }
@@ -348,7 +282,11 @@ public class PistonCalculator implements Listener {
} else { } else {
text.append("§ePush\n"); 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.setText(text.toString());
textDisplay.setBillboard(Display.Billboard.CENTER); textDisplay.setBillboard(Display.Billboard.CENTER);
textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER); textDisplay.setAlignment(TextDisplay.TextAlignment.CENTER);
@@ -358,26 +296,4 @@ public class PistonCalculator implements Listener {
textDisplay.setBrightness(new Display.Brightness(15, 15)); 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.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.EquipmentSlot;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
@Linked @Linked
public class TNTClickListener extends SWCommand implements Listener { public class TNTClickListener extends SWCommand implements Listener {
public static final String TNT_CLICK_DETAILS = "tnt_click_details";
public TNTClickListener() { public TNTClickListener() {
super("tntdetails"); super("tntdetails");
} }
@Register(description = "TNT_DETAILS_COMMAND") @Register(description = "TNT_DETAILS_COMMAND")
public void toggle(Player player) { public void toggle(Player player) {
YAPIONObject yapionObject = Config.getInstance().get(player); Config config = Config.STORE.get(player);
if (yapionObject.getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) { if (config.isTntClickDetails()) {
yapionObject.put(TNT_CLICK_DETAILS, false); config.setTntClickDetails(false);
BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player); BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player);
} else { } else {
yapionObject.put(TNT_CLICK_DETAILS, true); config.setTntClickDetails(true);
BauSystem.MESSAGE.send("TNT_DETAILS_ON", player); BauSystem.MESSAGE.send("TNT_DETAILS_ON", player);
} }
} }
@@ -60,7 +56,7 @@ public class TNTClickListener extends SWCommand implements Listener {
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return; if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (event.getHand() != EquipmentSlot.HAND) 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; return;
} }
@@ -25,7 +25,6 @@ import lombok.Getter;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerTeleportEvent;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*; import java.util.*;
@@ -56,27 +55,22 @@ public class Warp {
warpMap.put(name, this); warpMap.put(name, this);
} }
public Warp(String name, YAPIONObject object) { public Warp(String name, WorldData.WarpEntry warp) {
this.name = name; this.name = name;
double x = object.getPlainValue("x"); location = new Location(Bukkit.getWorlds().get(0), warp.getX(), warp.getY(), warp.getZ(), warp.getYaw(), warp.getPitch());
double y = object.getPlainValue("y"); mat = Material.getMaterial(warp.getMaterial());
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"));
warpMap.put(name, this); warpMap.put(name, this);
} }
public YAPIONObject output() { public WorldData.WarpEntry output() {
YAPIONObject yapionObject = new YAPIONObject(); WorldData.WarpEntry warp = new WorldData.WarpEntry();
yapionObject.add("x", location.getX()); warp.setX(location.getX());
yapionObject.add("y", location.getY()); warp.setY(location.getY());
yapionObject.add("z", location.getZ()); warp.setZ(location.getZ());
yapionObject.add("yaw", location.getYaw()); warp.setYaw(location.getYaw());
yapionObject.add("pitch", location.getPitch()); warp.setPitch(location.getPitch());
yapionObject.add("material", mat.toString()); warp.setMaterial(mat.toString());
return yapionObject; return warp;
} }
public static Set<String> getWarpNames() { public static Set<String> getWarpNames() {
@@ -101,7 +95,7 @@ public class Warp {
public void delete() { public void delete() {
warpMap.remove(name); warpMap.remove(name);
WorldData.getWarpData().remove(name); WorldData.STORE.get().getWarps().remove(name);
} }
public void teleport(Player player) { 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.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map;
@Linked @Linked
public class WarpCommand extends SWCommand implements Disable, Enable { public class WarpCommand extends SWCommand implements Disable, Enable {
@@ -135,17 +135,15 @@ public class WarpCommand extends SWCommand implements Disable, Enable {
@Override @Override
public void enable() { public void enable() {
WorldData.getWarpData().forEach((name, yapionAnyType) -> { WorldData.STORE.get().getWarps().forEach(Warp::new);
new Warp(name, (YAPIONObject) yapionAnyType);
});
Warp.enable(); Warp.enable();
} }
@Override @Override
public void disable() { public void disable() {
YAPIONObject yapionObject = WorldData.getWarpData(); Map<String, WorldData.WarpEntry> warps = WorldData.STORE.get().getWarps();
for (Warp warp : Warp.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.linkage.Linked;
import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.BaumemberUpdatePacket; import de.steamwar.network.packets.server.BaumemberUpdatePacket;
import de.steamwar.sql.BauweltMember;
import io.papermc.paper.event.player.PlayerPickBlockEvent; import io.papermc.paper.event.player.PlayerPickBlockEvent;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor; import org.bukkit.boss.BarColor;
@@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener {
} }
public static void baumemberUpdate() { 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> newSpectator = new HashSet<>();
Set<Player> newBuilder = new HashSet<>(); Set<Player> newBuilder = new HashSet<>();
Bukkit.getOnlinePlayers().forEach(player -> { Bukkit.getOnlinePlayers().forEach(player -> {
@@ -20,78 +20,39 @@
package de.steamwar.bausystem.region; package de.steamwar.bausystem.region;
import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SchematicNode;
import lombok.NonNull; import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
public abstract class RegionData { public abstract class RegionData {
private final List<Property<?, ?>> properties = new ArrayList<>(); protected final WorldData.RegionEntry data;
protected final Map<String, String> flagData;
protected final YAPIONObject data;
protected final YAPIONObject flagData;
protected final Runnable onChange; protected final Runnable onChange;
protected final Map<Flag<?>, Flag.Value<?>> flagMap = new HashMap<>(); protected final Map<Flag<?>, Flag.Value<?>> flagMap = new HashMap<>();
private final class Property<T, K> { private SchematicNode testblockSchematic;
private final String field;
private final Function<K, T> loader;
private final Function<T, K> writer;
private T value; protected RegionData(WorldData.RegionEntry data, Runnable onChange) {
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) {
this.data = data; this.data = data;
this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject()); this.flagData = data.getFlags();
this.onChange = onChange; this.onChange = onChange;
initialize(); initialize();
for (final Flag flag : Flag.getFlags()) { for (final Flag flag : Flag.getFlags()) {
if (!has(flag).isWritable()) continue; if (!has(flag).isWritable()) continue;
try { try {
String s = flagData.getPlainValue(flag.name()); String s = flagData.get(flag.name());
flagMap.put(flag, flag.valueOfValue(s)); flagMap.put(flag, flag.valueOfValue(s));
} catch (Exception e) { } catch (Exception e) {
flagMap.put(flag, (Flag.Value<?>) flag.getDefaultValue()); flagMap.put(flag, (Flag.Value<?>) flag.getDefaultValue());
} }
} }
properties.forEach(Property::load); if (data.getTestblockSchematic() != null) {
testblockSchematic = SchematicNode.byId(data.getTestblockSchematic());
}
} }
protected void initialize() { protected void initialize() {
@@ -126,7 +87,8 @@ public abstract class RegionData {
flagData.remove(flag.name()); flagData.remove(flag.name());
} }
} }
properties.forEach(property -> property.set(null)); testblockSchematic = null;
data.setTestblockSchematic(null);
onChange.run(); onChange.run();
} }
@@ -135,11 +97,12 @@ public abstract class RegionData {
} }
public SchematicNode getTestblockSchematic() { public SchematicNode getTestblockSchematic() {
return testblockSchematic.get(); return testblockSchematic;
} }
public void setTestblockSchematic(SchematicNode schematic) { public void setTestblockSchematic(SchematicNode schematic) {
testblockSchematic.set(schematic); testblockSchematic = schematic;
data.setTestblockSchematic(schematic == null ? null : schematic.getId());
onChange.run(); 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. * 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 * 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 * it under the terms of the GNU Affero General Public License as published by
@@ -19,58 +19,83 @@
package de.steamwar.bausystem.worlddata; 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 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.File;
import java.io.FileInputStream; import java.util.HashMap;
import java.io.IOException; 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"); public static final int VERSION = 2;
private YAPIONObject worldData;
public YAPIONObject getWorldData() { public static final ConfigStore<WorldData> STORE = new ConfigStore<>(WorldData.class, VERSION, WorldData::new)
if (worldData == null) { .gson(new GsonBuilder().setPrettyPrinting().create())
read(); .converter(new WorldDataV1ToV2Converter())
} .file(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.json"))
return worldData; .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() { @Getter
return getWorldData().getYAPIONObjectOrSetDefault("regions", new YAPIONObject()); @Setter
} public static class WarpEntry {
public YAPIONObject getWarpData() { protected double x;
return getWorldData().getYAPIONObjectOrSetDefault("warps", new YAPIONObject()); protected double y;
} protected double z;
protected float yaw;
private void read() { protected float pitch;
worldData = new YAPIONObject(); protected String material;
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();
}
} }
} }
@@ -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.Flag;
import de.steamwar.bausystem.region.flags.ProtectMode; import de.steamwar.bausystem.region.flags.ProtectMode;
import de.steamwar.bausystem.region.flags.TNTMode; import de.steamwar.bausystem.region.flags.TNTMode;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull; import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedGlobalRegionData extends RegionData { public class FixedGlobalRegionData extends RegionData {
public FixedGlobalRegionData(YAPIONObject data, Runnable onChange) { public FixedGlobalRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, 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.region.flags.TestblockMode;
import de.steamwar.bausystem.utils.FlatteningWrapper; import de.steamwar.bausystem.utils.FlatteningWrapper;
import de.steamwar.bausystem.utils.PasteBuilder; import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.core.Core; import de.steamwar.core.Core;
import de.steamwar.sql.GameModeConfig; import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.SchematicType; 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; this.name = name;
uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
this.flagStorage = flagStorage; this.flagStorage = flagStorage;
@@ -22,12 +22,12 @@ package de.steamwar.bausystem.region.fixed;
import de.steamwar.bausystem.region.RegionData; import de.steamwar.bausystem.region.RegionData;
import de.steamwar.bausystem.region.RegionFlagPolicy; import de.steamwar.bausystem.region.RegionFlagPolicy;
import de.steamwar.bausystem.region.flags.Flag; import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull; import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedRegionData extends RegionData { public class FixedRegionData extends RegionData {
public FixedRegionData(YAPIONObject data, Runnable onChange) { public FixedRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, 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; Prototype prototype;
if (regionData.containsKey("prototype", String.class)) { if (regionData.getPrototype() != null) {
prototype = PROTOTYPE_MAP.get(regionData.getPlainValue("prototype")); prototype = PROTOTYPE_MAP.get(regionData.getPrototype());
} else { } else {
prototype = PROTOTYPE_MAP.get(regionConfig.getPlainValue("prototype")); 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)); 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.DiffChange;
import yapion.hierarchy.diff.YAPIONDiff; import yapion.hierarchy.diff.YAPIONDiff;
import yapion.hierarchy.types.YAPIONObject; import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONType;
import yapion.parser.YAPIONParser; import yapion.parser.YAPIONParser;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
@@ -55,7 +54,7 @@ public class RegionLoader {
} }
loaded = yapionObject; loaded = yapionObject;
YAPIONObject optionsYapionObject = WorldData.getRegionsData(); WorldData worldData = WorldData.STORE.get();
yapionObject.forEach((key, yapionAnyType) -> { yapionObject.forEach((key, yapionAnyType) -> {
if (key.equals("global")) { if (key.equals("global")) {
return; return;
@@ -65,21 +64,10 @@ public class RegionLoader {
} }
YAPIONObject regionConfig = (YAPIONObject) yapionAnyType; YAPIONObject regionConfig = (YAPIONObject) yapionAnyType;
YAPIONObject regionData = new YAPIONObject(); WorldData.RegionEntry regionData = worldData.getRegions().computeIfAbsent(key, k -> new WorldData.RegionEntry());
if (optionsYapionObject.containsKey(key, YAPIONType.OBJECT)) {
regionData = optionsYapionObject.getObject(key);
} else {
optionsYapionObject.add(key, regionData);
}
Prototype.generateRegion(key, regionConfig, regionData); Prototype.generateRegion(key, regionConfig, regionData);
}); });
YAPIONObject globalOptions = optionsYapionObject.getObject("global"); FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(worldData.getGlobalRegion(), WorldData.STORE::save));
if (globalOptions == null) {
globalOptions = new YAPIONObject();
optionsYapionObject.add("global", globalOptions);
}
FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(globalOptions, WorldData::write));
} }
} }
@@ -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.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore import org.jetbrains.exposed.v1.jdbc.insertIgnore
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
object BauweltMemberTable : CompositeIdTable("BauweltMember") { object BauweltMemberTable : CompositeIdTable("BauweltMember") {
val bauweltId = reference("BauweltID", SteamwarUserTable).index() val bauweltId = reference("BauweltID", SteamwarUserTable).index()
@@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") {
class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) { class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<BauweltMember>(BauweltMemberTable) { 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) = private fun cache(member: BauweltMember) =
cache.put(member.memberID, member) cache.put(member.bauweltID to member.memberID, Optional.of(member))
@JvmStatic @JvmStatic
fun clear() = fun clear() =
@@ -75,20 +78,20 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
it[worldEdit] = false it[worldEdit] = false
it[world] = false it[world] = false
} }
} }.also { cache.remove(ownerId.value to newMemberId.value) }
@JvmStatic @JvmStatic
@Deprecated("Use getBauMember(bauwelt: Int, member: Int)") @Deprecated("Use getBauMember(bauwelt: Int, member: Int)")
fun getBauMember(bauwelt: UUID, member: UUID) = fun getBauMember(bauwelt: UUID, member: UUID) =
useDb { getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value)
find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) }
}
@JvmStatic @JvmStatic
fun getBauMember(bauwelt: Int, member: Int) = fun getBauMember(bauwelt: Int, member: Int): BauweltMember? =
useDb { cache.computeIfAbsent(bauwelt to member) {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) } Optional.ofNullable(useDb {
} find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()
})
}.orElse(null)
@JvmStatic @JvmStatic
@Deprecated("Use getMembers(bauwelt: Int)") @Deprecated("Use getMembers(bauwelt: Int)")
@@ -140,8 +143,11 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
fun isWorld() = fun isWorld() =
world world
fun remove() = fun remove() {
val key = bauweltID to memberID
useDb { useDb {
delete() delete()
} }
cache.remove(key)
}
} }
@@ -19,6 +19,7 @@
package de.steamwar.sql; package de.steamwar.sql;
import de.steamwar.sql.internal.SQLConfig;
import lombok.ToString; import lombok.ToString;
import java.io.File; import java.io.File;
@@ -250,6 +251,11 @@ public final class GameModeConfig<M, W> {
Times = new TimesConfig(loader.with("Times")); Times = new TimesConfig(loader.with("Times"));
// Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards // Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards
Schematic = new SchematicConfig<>(loader.with("Schematic")); 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)); GameName = loader.getString("GameName", defaultGameName.apply(file));
ActiveMonths = loader.getIntList("ActiveMonths"); ActiveMonths = loader.getIntList("ActiveMonths");
TeamChatPrefix = loader.getString("TeamChatPrefix", "+"); TeamChatPrefix = loader.getString("TeamChatPrefix", "+");
@@ -703,7 +709,10 @@ public final class GameModeConfig<M, W> {
public final double MaxDesignBlastResistance; 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 {@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. * @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0.
@@ -759,18 +768,13 @@ public final class GameModeConfig<M, W> {
} }
public boolean isInventory(M material) { public boolean isInventory(M material) {
return Limited.stream() return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material));
.filter(rule -> rule.Materials.contains(material))
.anyMatch(rule -> rule.Content.loaded);
} }
public Integer getItemAmount(M material, M item) { public List<BlockRule<M>> getRules(M material) {
return Limited.stream() return Limited.stream()
.filter(rule -> rule.Materials.contains(material)) .filter(rule -> rule.Materials.contains(material))
.filter(rule -> rule.Content.Items.contains(item)) .collect(Collectors.toList());
.mapToInt(rule -> rule.Content.Amount)
.max()
.orElse(0);
} }
@ToString @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()}. * {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}.
*/ */
default Set<M> getInventoryMaterials() { default Set<M> getInventoryMaterials() {
@@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready
TEAM_NOT_READY = §c§mTeam ready TEAM_NOT_READY = §c§mTeam ready
SKIP_READY = §aSkipping to next event SKIP_READY = §aSkipping to next event
SKIP_NOT_READY = §c§mSkipping to next event SKIP_NOT_READY = §c§mSkipping to next event
TEAM_CHAT = {0}{1}§8» {0}{2}
CHOOSE_KIT = §eChoose kit CHOOSE_KIT = §eChoose kit
RESPAWN = §eRespawn RESPAWN = §eRespawn
MANAGE_PLAYERS = §cManage players MANAGE_PLAYERS = §cManage players
@@ -256,10 +256,6 @@ public class FightTeam {
broadcast(player -> FightSystem.getMessage().send(message, player, params)); 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) { private void broadcast(Consumer<Player> f) {
players.forEach((uuid, fightPlayer) -> { players.forEach((uuid, fightPlayer) -> {
Player player = Bukkit.getPlayer(uuid); Player player = Bukkit.getPlayer(uuid);
@@ -19,50 +19,41 @@
package de.steamwar.fightsystem.listener; package de.steamwar.fightsystem.listener;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config; import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem; import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.Fight; import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.record.GlobalRecorder; 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.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.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.logging.Level; 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 @Linked
public class Chat implements Listener { public class Chat extends PacketHandler {
public Chat() { @Handler
new StateDependentListener(ArenaMode.All, FightState.All, this); 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 Player player = Bukkit.getPlayer(user.getUUID());
public void handlePlayerChat(AsyncPlayerChatEvent event) { FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null;
Player player = event.getPlayer();
String message = event.getMessage();
FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message);
FightTeam fightTeam = Fight.getPlayerTeam(player);
if (fightTeam != null) { if (fightTeam != null) {
String teamName = fightTeam.getColoredName(); GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message);
if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) { } else if (player != null && Config.isReferee(player)) {
fightTeam.broadcastChat(player, message.substring(1)); GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message);
} else {
broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message);
}
} else if (Config.isReferee(player)) {
broadcastChat("FIGHTLEADER_CHAT", player.getName(), message);
} else { } 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) { public static void broadcastChat(String message, Object... params) {
@@ -1,58 +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.misslewars.listener;
import de.steamwar.linkage.Linked;
import de.steamwar.misslewars.FightState;
import de.steamwar.misslewars.MWTeam;
import de.steamwar.misslewars.MissileWars;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.EnumSet;
@Linked
public class ChatListener extends BasicListener {
public ChatListener() {
super(EnumSet.allOf(FightState.class));
}
@EventHandler
public void onChat(AsyncPlayerChatEvent e) {
e.setCancelled(true);
Player p = e.getPlayer();
MWTeam team = MissileWars.getTeam(p);
String message = e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage();
if (team == null) {
Bukkit.broadcastMessage(message);
} else {
if (e.getMessage().startsWith("+")) {
String teamMessage = "§8[" + team.getColorCode() + "TEAM§8] " + e.getPlayer().getDisplayName() + "§8» §7" + e.getMessage().substring(1);
team.getPlayers().forEach(player -> player.sendMessage(teamMessage));
} else {
Bukkit.broadcastMessage(message);
}
}
}
}
@@ -262,9 +262,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blocks: §c{0}§7, Max: §e{1}
AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unknown block: §c{0} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unknown block: §c{0}
AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2}
AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Forbidden block: §c{0} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Forbidden block: §c{0}
AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048
AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Dispenser: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7
AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Forbidden Item: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Forbidden Item: [{0}, {1}, {2}] -> §c{3}
AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Items of different rules in one inventory: [{0}, {1}, {2}] -> §c{3}
AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Defunct NBT: §7[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Defunct NBT: §7[{0}, {1}, {2}]
AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}]
AUTO_CHECKER_RESULT_ENTITY = §7Entity: §7[{0}, {1}, {2}] AUTO_CHECKER_RESULT_ENTITY = §7Entity: §7[{0}, {1}, {2}]
@@ -242,9 +242,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blöcke: §c{0}§7, Max: §e{1}
AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unbekannter Block: §c{0} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unbekannter Block: §c{0}
AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2}
AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Verbotener Block: §c{0} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Verbotener Block: §c{0}
AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048
AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Werfer: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7
AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Verbotener gegenstand: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Verbotener gegenstand: [{0}, {1}, {2}] -> §c{3}
AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Gegenstände verschiedener Regeln in einem Inventar: [{0}, {1}, {2}] -> §c{3}
AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Keine NBT-Daten: §c[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Keine NBT-Daten: §c[{0}, {1}, {2}]
AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}]
AUTO_CHECKER_RESULT_RECORD = §7Schallplatte: §c[{0}, {1}, {2}] AUTO_CHECKER_RESULT_RECORD = §7Schallplatte: §c[{0}, {1}, {2}]
@@ -25,6 +25,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BaseBlock;
import de.steamwar.sql.GameModeConfig; import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.GameModeConfig.SchematicConfig.BlockRule;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; import lombok.ToString;
import org.bukkit.Material; import org.bukkit.Material;
@@ -62,6 +63,7 @@ public class AutoChecker {
public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig<Material, String> type) { public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig<Material, String> type) {
AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult(); AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult();
Map<BlockPos, List<BlockRule<Material>>> ruleCandidates = new HashMap<>();
BlockVector3 min = clipboard.getMinimumPoint(); BlockVector3 min = clipboard.getMinimumPoint();
BlockVector3 max = clipboard.getMaximumPoint(); BlockVector3 max = clipboard.getMaximumPoint();
for (int x = min.x(); x <= max.x(); x++) { for (int x = min.x(); x <= max.x(); x++) {
@@ -75,13 +77,13 @@ public class AutoChecker {
BlockPos pos = new BlockPos(x, y, z); BlockPos pos = new BlockPos(x, y, z);
result.getBlockCounts().merge(material, 1, Integer::sum);
if (type.Schematic.isInventory(material)) { if (type.Schematic.isInventory(material)) {
checkInventory(result, block, material, pos, type); result.getInventoryBlockType().put(pos, material);
if (!result.getInventoryItemCounts().getOrDefault(pos, Collections.emptyMap()).isEmpty()) { List<BlockRule<Material>> candidates = checkInventory(result, block, material, pos, type);
result.getBlockCounts().merge(material, 1, Integer::sum); if (!candidates.isEmpty()) {
ruleCandidates.put(pos, candidates);
} }
} else {
result.getBlockCounts().merge(material, 1, Integer::sum);
} }
if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) { if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) {
@@ -90,28 +92,28 @@ public class AutoChecker {
} }
} }
} }
assignRules(result, ruleCandidates);
return result; return result;
} }
private void checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig<Material, String> type) { /**
* Scans the inventory and returns the rules this block may be counted towards.
* An empty list means the block is either faulty (reported in the result) or unlimited.
*/
private List<BlockRule<Material>> checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig<Material, String> type) {
CompoundTag nbt = block.getNbtData(); CompoundTag nbt = block.getNbtData();
if (nbt == null) { if (nbt == null) {
result.getDefunctNbt().add(pos); result.getDefunctNbt().add(pos);
return; return Collections.emptyList();
} }
if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) { if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) {
result.getRecords().add(pos); result.getRecords().add(pos);
return; return Collections.emptyList();
} }
List<CompoundTag> items = nbt.getList("Items", CompoundTag.class);
if (items.isEmpty()) return; // Leeres Inventar
Map<Material, Integer> itemCounts = new EnumMap<>(Material.class); Map<Material, Integer> itemCounts = new EnumMap<>(Material.class);
int windChargeCount = 0; for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) {
for (CompoundTag item : items) {
if (!item.containsKey("id")) { if (!item.containsKey("id")) {
result.getDefunctNbt().add(pos); result.getDefunctNbt().add(pos);
continue; continue;
@@ -121,22 +123,69 @@ public class AutoChecker {
if (itemType == null) // Leere Slots if (itemType == null) // Leere Slots
continue; continue;
if (type.Schematic.Type.getName().equals("wargearseason26") && material == Material.DISPENSER && itemType == Material.WIND_CHARGE) { itemCounts.merge(itemType, item.getInt("count"), Integer::sum);
windChargeCount += item.getInt("count");
} else if (type.Schematic.getItemAmount(material, itemType) == 0) {
result.getForbiddenItems().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType);
} else {
itemCounts.merge(itemType, item.getInt("count"), Integer::sum);
}
if (item.containsKey("tag")) { if (item.containsKey("tag")) {
result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType);
} }
} }
if (!itemCounts.isEmpty()) {
result.getInventoryItemCounts().put(pos, itemCounts); List<BlockRule<Material>> rules = type.Schematic.getRules(material);
result.getInventoryBlockType().put(pos, material); if (itemCounts.isEmpty()) { // Leeres Inventar
return rules.stream().filter(rule -> !rule.Content.loaded).collect(Collectors.toList());
} }
result.getWindChargeCount().put(pos, windChargeCount); result.getInventoryItemCounts().put(pos, itemCounts);
List<BlockRule<Material>> contentRules = rules.stream().filter(rule -> rule.Content.loaded).collect(Collectors.toList());
List<BlockRule<Material>> itemsFitting = contentRules.stream()
.filter(rule -> rule.Content.Items.containsAll(itemCounts.keySet()))
.collect(Collectors.toList());
List<BlockRule<Material>> fitting = itemsFitting.stream()
.filter(rule -> itemCounts.values().stream().allMatch(count -> count <= rule.Content.Amount))
.collect(Collectors.toList());
if (!fitting.isEmpty()) {
return fitting;
}
if (!itemsFitting.isEmpty()) {
BlockRule<Material> best = Collections.max(itemsFitting, Comparator.comparingInt(rule -> rule.Content.Amount));
itemCounts.forEach((itemType, count) -> {
if (count > best.Content.Amount) {
result.getTooManyItems().computeIfAbsent(pos, blockVector3 -> new EnumMap<>(Material.class)).put(itemType, best.Content.Amount);
}
});
return Collections.singletonList(best);
}
Set<Material> allowed = contentRules.stream().flatMap(rule -> rule.Content.Items.stream()).collect(Collectors.toSet());
Set<Material> forbidden = itemCounts.keySet().stream().filter(itemType -> !allowed.contains(itemType)).collect(Collectors.toSet());
if (!forbidden.isEmpty()) {
result.getForbiddenItems().put(pos, forbidden);
}
Set<Material> rest = itemCounts.keySet().stream().filter(allowed::contains).collect(Collectors.toSet());
if (!rest.isEmpty()) {
// Every remaining item is allowed on its own, but no single rule allows all of them together
BlockRule<Material> best = Collections.max(contentRules, Comparator.comparingLong(rule -> rest.stream().filter(rule.Content.Items::contains).count()));
Set<Material> mixed = rest.stream().filter(itemType -> !best.Content.Items.contains(itemType)).collect(Collectors.toSet());
if (!mixed.isEmpty()) {
result.getMixedItems().put(pos, mixed);
}
}
return Collections.emptyList();
}
/**
* Counts every block towards one of its candidate rules, preferring the rule with the most capacity left.
* Blocks with fewer candidates are assigned first.
*/
private void assignRules(AutoChecker.BlockScanResult result, Map<BlockPos, List<BlockRule<Material>>> ruleCandidates) {
ruleCandidates.entrySet().stream()
.sorted(Comparator.comparingInt(entry -> entry.getValue().size()))
.forEach(entry -> {
BlockRule<Material> rule = Collections.max(entry.getValue(), Comparator.comparingLong(candidate -> (long) candidate.Amount - result.getRuleCounts().getOrDefault(candidate, 0)));
result.getRuleCounts().merge(rule, 1, Integer::sum);
result.getRuleBlockTypes().computeIfAbsent(rule, r -> EnumSet.noneOf(Material.class)).add(result.getInventoryBlockType().get(entry.getKey()));
});
} }
@Getter @Getter
@@ -148,8 +197,11 @@ public class AutoChecker {
private final Map<Material, List<BlockPos>> designBlocks = new EnumMap<>(Material.class); private final Map<Material, List<BlockPos>> designBlocks = new EnumMap<>(Material.class);
private final Map<BlockPos, Map<Material, Integer>> inventoryItemCounts = new HashMap<>(); private final Map<BlockPos, Map<Material, Integer>> inventoryItemCounts = new HashMap<>();
private final Map<BlockPos, Material> inventoryBlockType = new HashMap<>(); private final Map<BlockPos, Material> inventoryBlockType = new HashMap<>();
private final Map<BlockPos, Integer> windChargeCount = new HashMap<>(); private final Map<BlockRule<Material>, Integer> ruleCounts = new HashMap<>();
private final Map<BlockRule<Material>, Set<Material>> ruleBlockTypes = new HashMap<>();
private final Map<BlockPos, Map<Material, Integer>> tooManyItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenItems = new HashMap<>(); private final Map<BlockPos, Set<Material>> forbiddenItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> mixedItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenNbt = new HashMap<>(); private final Map<BlockPos, Set<Material>> forbiddenNbt = new HashMap<>();
} }
} }
@@ -31,6 +31,7 @@ import org.bukkit.entity.Player;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Builder @Builder
@Getter @Getter
@@ -46,12 +47,12 @@ public class AutoCheckerResult {
public boolean isOk() { public boolean isOk() {
return blockScanResult.getRecords().isEmpty() && return blockScanResult.getRecords().isEmpty() &&
blockScanResult.getForbiddenItems().isEmpty() && blockScanResult.getForbiddenItems().isEmpty() &&
blockScanResult.getMixedItems().isEmpty() &&
blockScanResult.getForbiddenNbt().isEmpty() && blockScanResult.getForbiddenNbt().isEmpty() &&
isSizeOk() && isSizeOk() &&
isBlockCountOk() && isBlockCountOk() &&
isLimitedBlocksOK() && isLimitedBlocksOK() &&
isInventoryItemsOK() && isInventoryItemsOK() &&
isWindchargeCountOK() &&
!type.isAfterDeadline() && !type.isAfterDeadline() &&
entities.isEmpty() && entities.isEmpty() &&
isDesignBlastResistanceOK(); isDesignBlastResistanceOK();
@@ -62,23 +63,8 @@ public class AutoCheckerResult {
!type.isAfterDeadline(); !type.isAfterDeadline();
} }
public boolean isWindchargeCountOK() {
if (type.Schematic.Type.getName().equals("wargearseason26")) {
int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0);
return windChargesCount <= 2048;
} else {
return true;
}
}
public boolean isInventoryItemsOK() { public boolean isInventoryItemsOK() {
return blockScanResult.getInventoryItemCounts().entrySet().stream().allMatch(posEntry -> { return blockScanResult.getTooManyItems().isEmpty();
Material inventory = blockScanResult.getInventoryBlockType().get(posEntry.getKey());
return posEntry.getValue().entrySet().stream().allMatch(itemEntry -> {
int cap = type.Schematic.getItemAmount(inventory, itemEntry.getKey());
return itemEntry.getValue() <= cap;
});
});
} }
public boolean hasWarnings() { public boolean hasWarnings() {
@@ -107,7 +93,10 @@ public class AutoCheckerResult {
public boolean isLimitedBlocksOK() { public boolean isLimitedBlocksOK() {
return blockScanResult.getBlockCounts().entrySet().stream() return blockScanResult.getBlockCounts().entrySet().stream()
.allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())); .filter(entry -> !type.Schematic.isInventory(entry.getKey()))
.allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())) &&
blockScanResult.getRuleCounts().entrySet().stream()
.allMatch(entry -> entry.getValue() <= entry.getKey().Amount);
} }
public boolean isDesignBlastResistanceOK() { public boolean isDesignBlastResistanceOK() {
@@ -132,6 +121,7 @@ public class AutoCheckerResult {
} }
if (!isLimitedBlocksOK()) { if (!isLimitedBlocksOK()) {
blockScanResult.getBlockCounts().forEach((mat, count) -> { blockScanResult.getBlockCounts().forEach((mat, count) -> {
if (type.Schematic.isInventory(mat)) return;
int maxCount = type.Schematic.getMaxCount(mat); int maxCount = type.Schematic.getMaxCount(mat);
if (count > maxCount) { if (count > maxCount) {
if (maxCount == 0) { if (maxCount == 0) {
@@ -141,30 +131,25 @@ public class AutoCheckerResult {
} }
} }
}); });
} blockScanResult.getRuleCounts().forEach((rule, count) -> {
if (count <= rule.Amount) return;
if (!isWindchargeCountOK()) { String blocks = blockScanResult.getRuleBlockTypes().get(rule).stream().map(Material::name).collect(Collectors.joining(", "));
int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); if (rule.Amount == 0) {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES", p, windChargesCount, 2048); SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK", p, blocks);
blockScanResult.getWindChargeCount().entrySet().stream().filter(blockVector3IntegerEntry -> blockVector3IntegerEntry.getValue() > 0).forEach(blockVector3IntegerEntry -> { } else {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3IntegerEntry.getKey()), SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_BLOCK", p, blocks, count, rule.Amount);
blockVector3IntegerEntry.getKey().getBlockX(), }
blockVector3IntegerEntry.getKey().getBlockY(),
blockVector3IntegerEntry.getKey().getBlockZ(),
blockVector3IntegerEntry.getValue());
}); });
} }
blockScanResult.getInventoryItemCounts().forEach((pos, itemCounts) -> { blockScanResult.getTooManyItems().forEach((pos, caps) -> {
Material inventory = blockScanResult.getInventoryBlockType().get(pos); Material inventory = blockScanResult.getInventoryBlockType().get(pos);
itemCounts.forEach((itemType, count) -> { caps.forEach((itemType, cap) -> {
int cap = type.Schematic.getItemAmount(inventory, itemType);
if (count <= cap) return;
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_INVENTORY_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(pos), SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_INVENTORY_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(pos),
pos.getBlockX(), pos.getBlockX(),
pos.getBlockY(), pos.getBlockY(),
pos.getBlockZ(), pos.getBlockZ(),
count, blockScanResult.getInventoryItemCounts().get(pos).get(itemType),
cap, cap,
itemType.name(), itemType.name(),
inventory.name()); inventory.name());
@@ -177,6 +162,9 @@ public class AutoCheckerResult {
blockScanResult.getForbiddenItems().forEach((blockVector3, materials) -> { blockScanResult.getForbiddenItems().forEach((blockVector3, materials) -> {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials));
}); });
blockScanResult.getMixedItems().forEach((blockVector3, materials) -> {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_MIXED_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials));
});
blockScanResult.getForbiddenNbt().forEach((blockVector3, materials) -> { blockScanResult.getForbiddenNbt().forEach((blockVector3, materials) -> {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM_NBT", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM_NBT", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials));
}); });
@@ -48,6 +48,7 @@ import java.util.*;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandHelp.*; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandHelp.*;
import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandUtils.togglePublic; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandUtils.togglePublic;
@@ -171,8 +172,8 @@ public class SchematicCommand extends SWCommand {
Map<BlockPos, Set<Material>> toBeCheckedInvs = new HashMap<>(); Map<BlockPos, Set<Material>> toBeCheckedInvs = new HashMap<>();
toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenItems()); Stream.of(result.getBlockScanResult().getForbiddenItems(), result.getBlockScanResult().getMixedItems(), result.getBlockScanResult().getForbiddenNbt())
toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenNbt()); .forEach(invs -> invs.forEach((pos, materials) -> toBeCheckedInvs.computeIfAbsent(pos, p -> new HashSet<>()).addAll(materials)));
for (Map.Entry<BlockPos, Set<Material>> entry : toBeCheckedInvs.entrySet()) { for (Map.Entry<BlockPos, Set<Material>> entry : toBeCheckedInvs.entrySet()) {
BlockPos pos = entry.getKey(); BlockPos pos = entry.getKey();
@@ -197,16 +198,13 @@ public class SchematicCommand extends SWCommand {
clipboard.setBlock(vector, block.toBaseBlock(builder.build())); clipboard.setBlock(vector, block.toBaseBlock(builder.build()));
} }
for (Map.Entry<BlockPos, Map<Material, Integer>> posEntry : result.getBlockScanResult().getInventoryItemCounts().entrySet()) { for (Map.Entry<BlockPos, Map<Material, Integer>> posEntry : result.getBlockScanResult().getTooManyItems().entrySet()) {
BlockPos pos = posEntry.getKey(); BlockPos pos = posEntry.getKey();
Material inventory = result.getBlockScanResult().getInventoryBlockType().get(pos); Map<Material, Integer> itemCounts = result.getBlockScanResult().getInventoryItemCounts().get(pos);
for (Map.Entry<Material, Integer> itemEntry : posEntry.getValue().entrySet()) { for (Map.Entry<Material, Integer> itemEntry : posEntry.getValue().entrySet()) {
Material itemType = itemEntry.getKey(); Material itemType = itemEntry.getKey();
Integer cap = type.Schematic.getItemAmount(inventory, itemType); int cap = itemEntry.getValue();
if (itemEntry.getValue() <= cap) {
continue;
}
BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()); BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ());
BaseBlock block = clipboard.getFullBlock(vector); BaseBlock block = clipboard.getFullBlock(vector);
@@ -215,7 +213,7 @@ public class SchematicCommand extends SWCommand {
List<CompoundTag> items = new ArrayList<>(tag.getList("Items", CompoundTag.class)); List<CompoundTag> items = new ArrayList<>(tag.getList("Items", CompoundTag.class));
Collections.reverse(items); // To let the first item be in the inventory Collections.reverse(items); // To let the first item be in the inventory
List<CompoundTag> list = new ArrayList<>(); List<CompoundTag> list = new ArrayList<>();
int diff = itemEntry.getValue() - cap; int diff = itemCounts.get(itemType) - cap;
for (CompoundTag item : items) { for (CompoundTag item : items) {
if (item == null) { if (item == null) {
continue; continue;
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# #
LOCAL_CHAT = §eLocal §r{0}§8»§7 {1}
COMMAND_SYSTEM_ERROR = §cError executing the command! COMMAND_SYSTEM_ERROR = §cError executing the command!
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
# #
LOCAL_CHAT = §eLokal §r{0}§8»§7 {1}
COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls! COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls!
@@ -19,21 +19,23 @@
package de.steamwar.entity; package de.steamwar.entity;
import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.block.data.BlockData; import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Display; import org.bukkit.entity.Display;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.util.Consumer;
import org.bukkit.util.Transformation; import org.bukkit.util.Transformation;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import org.joml.Quaternionf; import org.joml.Quaternionf;
import org.joml.Vector3f; import org.joml.Vector3f;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
/** /**
* Can be used for Axis Aligned Lines of near infinite length. * Can be used for Axis Aligned Lines of near infinite length.
* The line is a single display, which is shown to players based on their view distance.
*/ */
@Getter @Getter
public class CLine extends CEntity { public class CLine extends CEntity {
@@ -42,6 +44,8 @@ public class CLine extends CEntity {
private static final float offset = 1 / 1024f; private static final float offset = 1 / 1024f;
private static final Vector offsetVec = new Vector(offset, offset, offset); private static final Vector offsetVec = new Vector(offset, offset, offset);
@Getter(AccessLevel.NONE)
private LineDisplay line;
private Location from; private Location from;
private Location to; private Location to;
private float width = DEFAULT_WIDTH; private float width = DEFAULT_WIDTH;
@@ -99,9 +103,7 @@ public class CLine extends CEntity {
if (hide == this.hide) return; if (hide == this.hide) return;
this.hide = hide; this.hide = hide;
if (hide) { if (hide) {
if (startLine != null) startLine.hide(true); if (line != null) line.hide(true);
if (middleLine != null) middleLine.hide(true);
if (endLine != null) endLine.hide(true);
} else { } else {
tick(); tick();
} }
@@ -109,148 +111,26 @@ public class CLine extends CEntity {
@Override @Override
void tick() { void tick() {
if (from == null || to == null) { if (from == null || to == null || hide) {
if (startLine != null) startLine.hide(true); if (line != null) line.hide(true);
if (middleLine != null) middleLine.hide(true);
if (endLine != null) endLine.hide(true);
return;
} else {
if (startLine != null) startLine.hide(false);
if (middleLine != null) middleLine.hide(false);
if (endLine != null) endLine.hide(false);
}
if (hide) return;
updateStart();
updateMiddle();
updateEnd();
}
private RBlockDisplay startLine;
private void updateStart() {
Vector vec = to.clone().subtract(from).toVector();
if (vec.length() > 35) vec.normalize().multiply(35);
if (startLine == null) {
startLine = new RBlockDisplay(server, new Location(null, 0, 0, 0));
startLine.setBrightness(new Display.Brightness(15, 15));
startLine.setViewRange(560);
startLine.setBlock(blockData);
entities.add(startLine);
} else {
startLine.hide(false);
}
startLine.move(from.clone().subtract(offsetVec));
startLine.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
}
private RBlockDisplay middleLine;
private void updateMiddle() {
Vector vec = to.clone().subtract(from).toVector();
if (vec.length() <= 70) {
if (middleLine != null) middleLine.hide(true);
return; return;
} }
if (vec.length() > 280) vec.normalize().multiply(280);
else vec = vec.clone().normalize().multiply(vec.length() - 60);
if (middleLine == null) { if (line == null) {
middleLine = new RBlockDisplay(server, new Location(null, 0, 0, 0)); line = new LineDisplay(server);
middleLine.setBrightness(new Display.Brightness(15, 15)); line.setBrightness(new Display.Brightness(15, 15));
middleLine.setViewRange(560); line.setViewRange(560);
middleLine.setBlock(blockData); line.setBlock(blockData);
entities.add(middleLine); entities.add(line);
} else { } else {
middleLine.hide(false); line.hide(false);
} }
Player player = server.getPlayers().stream().findFirst().orElse(null);
if (player == null) return;
Vector tempVector = vec.clone().normalize().multiply(30);
Location from = this.from.clone().add(tempVector);
Location to = this.to.clone().subtract(tempVector);
Vector lineVec = to.clone().subtract(from).toVector();
Vector playerVec = player.getLocation().toVector().subtract(from.toVector());
double lineVecDotItself = lineVec.dot(lineVec);
Vector projectionVec = lineVec.clone().multiply(lineVec.dot(playerVec)).divide(new Vector(lineVecDotItself, lineVecDotItself, lineVecDotItself));
Vector moveVec = from.toVector().add(projectionVec);
if (moveVec.getX() < from.getX()) {
moveVec.setX(from.getX());
}
if (moveVec.getX() > to.getX()) {
moveVec.setX(to.getX());
}
if (moveVec.getY() < from.getY()) {
moveVec.setY(from.getY());
}
if (moveVec.getY() > to.getY()) {
moveVec.setY(to.getY());
}
if (moveVec.getZ() < from.getZ()) {
moveVec.setZ(from.getZ());
}
if (moveVec.getZ() > to.getZ()) {
moveVec.setZ(to.getZ());
}
Vector translation = vec.clone().divide(new Vector(2, 2, 2));
translation.setX(-translation.getX());
translation.setY(-translation.getY());
translation.setZ(-translation.getZ());
Vector first = moveVec.clone().add(translation).subtract(from.toVector());
if (first.getX() < 0) {
translation.setX(translation.getX() - first.getX());
}
if (first.getY() < 0) {
translation.setY(translation.getY() - first.getY());
}
if (first.getZ() < 0) {
translation.setZ(translation.getZ() - first.getZ());
}
Vector second = to.toVector().subtract(moveVec.clone().subtract(translation));
if (second.getX() < 0) {
translation.setX(translation.getX() + second.getX());
}
if (second.getY() < 0) {
translation.setY(translation.getY() + second.getY());
}
if (second.getZ() < 0) {
translation.setZ(translation.getZ() + second.getZ());
}
middleLine.move(moveVec.toLocation(player.getWorld()).subtract(offsetVec));
middleLine.setTransform(new Transformation(translation.toVector3f(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
}
private RBlockDisplay endLine;
private void updateEnd() {
Vector vec = to.clone().subtract(from).toVector(); Vector vec = to.clone().subtract(from).toVector();
if (vec.length() <= 35) { line.setSpan(from, to);
if (endLine != null) endLine.hide(true); line.move(from.clone().subtract(offsetVec));
return; line.setTransform(new Transformation(new Vector3f(0, 0, 0), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
} server.updateViewers(line);
if (vec.length() > 35) vec.normalize().multiply(35);
if (endLine == null) {
endLine = new RBlockDisplay(server, new Location(null, 0, 0, 0));
endLine.setBrightness(new Display.Brightness(15, 15));
endLine.setViewRange(560);
endLine.setBlock(blockData);
entities.add(endLine);
} else {
endLine.hide(false);
}
endLine.move(to.clone().subtract(offsetVec));
endLine.setTransform(new Transformation(vec.toVector3f().negate(), new Quaternionf(0, 0, 0, 1), addWidth(vec).toVector3f(), new Quaternionf(0, 0, 0, 1)));
} }
private Vector addWidth(Vector vector) { private Vector addWidth(Vector vector) {
@@ -267,4 +147,31 @@ public class CLine extends CEntity {
vector.add(offsetVec).add(offsetVec); vector.add(offsetVec).add(offsetVec);
return vector; return vector;
} }
/**
* The line is one display that is visible for a player as soon as any chunk it passes through is in the view distance,
* independent of the chunk its origin is in.
*/
private static class LineDisplay extends RBlockDisplay {
private int minChunkX, maxChunkX, minChunkZ, maxChunkZ;
private LineDisplay(REntityServer server) {
super(server, new Location(null, 0, 0, 0));
}
private void setSpan(Location from, Location to) {
minChunkX = Math.floorDiv((int) Math.floor(Math.min(from.getX(), to.getX())), 16);
maxChunkX = Math.floorDiv((int) Math.floor(Math.max(from.getX(), to.getX())), 16);
minChunkZ = Math.floorDiv((int) Math.floor(Math.min(from.getZ(), to.getZ())), 16);
maxChunkZ = Math.floorDiv((int) Math.floor(Math.max(from.getZ(), to.getZ())), 16);
}
@Override
protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) {
int distanceX = Math.max(0, Math.max(minChunkX - playerChunkX, playerChunkX - maxChunkX));
int distanceZ = Math.max(0, Math.max(minChunkZ - playerChunkZ, playerChunkZ - maxChunkZ));
return inViewDistance(player, Math.max(distanceX, distanceZ));
}
}
} }
@@ -26,6 +26,7 @@ import net.minecraft.network.syncher.EntityDataSerializers;
import org.bukkit.Color; import org.bukkit.Color;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Display; import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.util.Transformation; import org.bukkit.util.Transformation;
import org.joml.Quaternionf; import org.joml.Quaternionf;
@@ -135,6 +136,15 @@ public abstract class RDisplay extends REntity {
public void setViewRange(float viewRange) { public void setViewRange(float viewRange) {
this.viewRange = viewRange; this.viewRange = viewRange;
sendPacket(updatePacketSink, this::getViewRange); sendPacket(updatePacketSink, this::getViewRange);
server.updateViewers(this);
}
/**
* A view range of 1 equals 64 blocks. This is only a comparison, so huge values like {@link Float#MAX_VALUE} are cheap.
*/
@Override
protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) {
return chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ) <= Math.ceil(viewRange * 64.0 / 16.0);
} }
private static final EntityDataAccessor<Float> viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT); private static final EntityDataAccessor<Float> viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT);
@@ -39,6 +39,7 @@ import org.bukkit.Location;
import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.craftbukkit.util.CraftNamespacedKey;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import java.util.*; import java.util.*;
@@ -56,6 +57,10 @@ public class REntity {
private static final Random random = new Random(); private static final Random random = new Random();
protected final REntityServer server; protected final REntityServer server;
/**
* Players this entity is currently spawned for, maintained by the {@link REntityServer}.
*/
final Set<Player> viewers = new HashSet<>();
private final EntityType entityType; private final EntityType entityType;
@Getter @Getter
protected final int entityId; protected final int entityId;
@@ -89,7 +94,7 @@ public class REntity {
private final int objectData; private final int objectData;
@Getter @Getter
private String displayName; private String displayName;
protected final Map<Object, ItemStack> itemSlots; protected final Map<org.bukkit.inventory.EquipmentSlot, ItemStack> itemSlots;
public REntity(REntityServer server, EntityType entityType, Location location) { public REntity(REntityServer server, EntityType entityType, Location location) {
this(server, entityType, location, 0); this(server, entityType, location, 0);
@@ -120,6 +125,28 @@ public class REntity {
this.objectData = objectData; this.objectData = objectData;
} }
/**
* Whether the given player should see this entity.
* Defaults to the view distance of the player.
*
* @param playerChunkX the chunk the player is in
* @param entityChunkX the chunk the entity is in, the entity itself might not be moved there yet
*/
protected boolean canView(Player player, int playerChunkX, int playerChunkZ, int entityChunkX, int entityChunkZ) {
return inViewDistance(player, chunkDistance(playerChunkX, playerChunkZ, entityChunkX, entityChunkZ));
}
/**
* @return the larger of the chunk distances on the x and z axis
*/
protected static int chunkDistance(int chunkX1, int chunkZ1, int chunkX2, int chunkZ2) {
return Math.max(Math.abs(chunkX1 - chunkX2), Math.abs(chunkZ1 - chunkZ2));
}
protected boolean inViewDistance(Player player, int chunkDistance) {
return chunkDistance <= server.viewRadius(player);
}
public void hide(boolean hide) { public void hide(boolean hide) {
if (hidden == hide) return; if (hidden == hide) return;
@@ -180,7 +207,7 @@ public class REntity {
} }
public void setVelocity(double dX, double dY, double dZ) { public void setVelocity(double dX, double dY, double dZ) {
server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(calcVelocity(dX), calcVelocity(dY), calcVelocity(dZ)))); server.updateEntity(this, new ClientboundSetEntityMotionPacket(entityId, new Vec3(dX, dY, dZ)));
} }
public void showDamage() { public void showDamage() {
@@ -220,7 +247,7 @@ public class REntity {
.add(nameVisibleWatcher, displayName != null).build()); .add(nameVisibleWatcher, displayName != null).build());
} }
public void setItem(Object slot, ItemStack stack) { public void setItem(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) {
itemSlots.put(slot, stack); itemSlots.put(slot, stack);
server.updateEntity(this, getEquipmentPacket(slot, stack)); server.updateEntity(this, getEquipmentPacket(slot, stack));
} }
@@ -340,24 +367,22 @@ public class REntity {
} }
private Object getTeleportPacket() { private Object getTeleportPacket() {
PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, pitch, yaw); PositionMoveRotation rot = new PositionMoveRotation(new Vec3(x, y, z), Vec3.ZERO, yaw * 360F / 256F, pitch * 360F / 256F);
return new ClientboundTeleportEntityPacket(entityId, rot, Collections.emptySet(), false); return new ClientboundEntityPositionSyncPacket(entityId, rot, false);
} }
private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) { private Object getMoveLookPacket(double diffX, double diffY, double diffZ, boolean rotEq) {
short x = (short) (diffX * 4096); short x = (short) (diffX * 4096);
short y = (short) (diffY * 4096); short y = (short) (diffY * 4096);
short z = (short) (diffZ * 4096); short z = (short) (diffZ * 4096);
byte yaw = (byte) (this.yaw * 256 / 360);
byte pitch = (byte) (this.pitch * 256 / 360);
if (diffX == 0 && diffY == 0 && diffZ == 0) { if (diffX == 0 && diffY == 0 && diffZ == 0) {
if (rotEq) return null; if (rotEq) return null;
return new ClientboundMoveEntityPacket.Rot(entityId, pitch, yaw, false); return new ClientboundMoveEntityPacket.Rot(entityId, yaw, pitch, false);
} else if (rotEq) { } else if (rotEq) {
return new ClientboundMoveEntityPacket.Pos(entityId, x, y, z, false); return new ClientboundMoveEntityPacket.Pos(entityId, x, y, z, false);
} else { } else {
return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, pitch, yaw, false); return new ClientboundMoveEntityPacket.PosRot(entityId, x, y, z, yaw, pitch, false);
} }
} }
@@ -368,18 +393,25 @@ public class REntity {
return packet; return packet;
} }
protected Object getEquipmentPacket(Object slot, ItemStack stack) { protected Object getEquipmentPacket(org.bukkit.inventory.EquipmentSlot slot, ItemStack stack) {
return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of((EquipmentSlot) slot, CraftItemStack.asNMSCopy(stack)))); return new ClientboundSetEquipmentPacket(entityId, Collections.singletonList(Pair.of(toNmsSlot(slot), CraftItemStack.asNMSCopy(stack))));
}
private static EquipmentSlot toNmsSlot(org.bukkit.inventory.EquipmentSlot slot) {
return switch (slot) {
case HEAD -> EquipmentSlot.HEAD;
case CHEST -> EquipmentSlot.CHEST;
case LEGS -> EquipmentSlot.LEGS;
case FEET -> EquipmentSlot.FEET;
case OFF_HAND -> EquipmentSlot.OFFHAND;
default -> EquipmentSlot.MAINHAND;
};
} }
private byte rotToByte(float rot) { private byte rotToByte(float rot) {
return (byte) ((int) (rot * 256.0F / 360.0F)); return (byte) ((int) (rot * 256.0F / 360.0F));
} }
private int calcVelocity(double value) {
return (int) (Math.max(-3.9, Math.min(value, 3.9)) * 8000);
}
private static Optional<Component> formatDisplayName(String displayName) { private static Optional<Component> formatDisplayName(String displayName) {
return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty(); return displayName != null ? Optional.of(MutableComponent.create(PlainTextContents.create(displayName))) : Optional.empty();
} }
@@ -42,14 +42,8 @@ import java.util.stream.Collectors;
public class REntityServer implements Listener { public class REntityServer implements Listener {
private static final HashSet<REntity> emptyEntities = new HashSet<>(0);
private static final HashSet<Player> emptyPlayers = new HashSet<>(0);
private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>();
private final HashMap<Long, HashSet<REntity>> entities = new HashMap<>();
private final HashMap<Long, Set<Player>> players = new HashMap<>();
private final HashMap<Player, Location> lastLocation = new HashMap<>(); private final HashMap<Player, Location> lastLocation = new HashMap<>();
private final HashMap<Player, Integer> viewDistance = new HashMap<>();
private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>()); private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>());
@@ -78,20 +72,22 @@ public class REntityServer implements Listener {
public void addPlayer(Player player) { public void addPlayer(Player player) {
Location location = player.getLocation(); Location location = player.getLocation();
lastLocation.put(player, location); lastLocation.put(player, location);
viewDistance.put(player, viewRadius(player)); updateViewer(player, posToChunk(location.getX()), posToChunk(location.getZ()));
forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z));
} }
public void removePlayer(Player player) { public void removePlayer(Player player) {
if (!viewDistance.containsKey(player)) { if (lastLocation.remove(player) == null) {
return; return;
} }
forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z)); for (REntity entity : entityMap.values()) {
viewDistance.remove(player); if (entity.viewers.remove(player)) {
hideFrom(entity, player);
}
}
} }
public List<Player> getPlayers() { public List<Player> getPlayers() {
return new ArrayList<>(viewDistance.keySet()); return new ArrayList<>(lastLocation.keySet());
} }
public void close() { public void close() {
@@ -104,50 +100,77 @@ public class REntityServer implements Listener {
void addEntity(REntity entity) { void addEntity(REntity entity) {
entityMap.put(entity.entityId, entity); entityMap.put(entity.entityId, entity);
addEntityToChunk(entity); updateViewers(entity);
entity.list(packet -> updateEntity(entity, packet)); entity.list(packet -> updateEntity(entity, packet));
entity.spawn(packet -> updateEntity(entity, packet)); entity.spawn(packet -> updateEntity(entity, packet));
} }
void preEntityMove(REntity entity, double toX, double toZ) { void preEntityMove(REntity entity, double toX, double toZ) {
long fromId = entityToId(entity); if (posToChunk(entity.x()) == posToChunk(toX) && posToChunk(entity.z()) == posToChunk(toZ)) return;
long toId = posToId(toX, toZ);
if (fromId == toId) return;
if (!entity.isHidden()) { int chunkX = posToChunk(toX);
onMissing(players.get(fromId), players.get(toId), entity::despawn); int chunkZ = posToChunk(toZ);
} entity.viewers.removeIf(player -> {
onMissing(players.get(fromId), players.get(toId), entity::delist); if (inView(entity, player, chunkX, chunkZ)) return false;
removeEntityFromChunk(entity); hideFrom(entity, player);
return true;
});
} }
void postEntityMove(REntity entity, double fromX, double fromZ) { void postEntityMove(REntity entity, double fromX, double fromZ) {
long fromId = posToId(fromX, fromZ); if (posToChunk(fromX) == posToChunk(entity.x()) && posToChunk(fromZ) == posToChunk(entity.z())) return;
long toId = entityToId(entity);
if (fromId == toId) return;
addEntityToChunk(entity); addMissingViewers(entity);
onMissing(players.get(toId), players.get(fromId), entity::list);
if (!entity.isHidden()) {
onMissing(players.get(toId), players.get(fromId), entity::spawn);
}
} }
void updateEntity(REntity entity, Object packet) { void updateEntity(REntity entity, Object packet) {
if (entity.isHidden()) return; if (entity.isHidden()) return;
for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { for (Player player : entity.viewers) {
TinyProtocol.instance.sendPacket(player, packet); TinyProtocol.instance.sendPacket(player, packet);
} }
} }
/**
* Re-evaluates which players see the entity, e.g. after its view range changed.
*/
void updateViewers(REntity entity) {
if (!entityMap.containsKey(entity.entityId)) return;
int chunkX = posToChunk(entity.x());
int chunkZ = posToChunk(entity.z());
entity.viewers.removeIf(player -> {
if (inView(entity, player, chunkX, chunkZ)) return false;
hideFrom(entity, player);
return true;
});
addMissingViewers(entity);
}
/**
* Re-sends list and spawn packets of an already added entity to all current viewers,
* e.g. when information that is part of the tab list entry changed after the entity was added.
*/
void refreshEntity(REntity entity) {
if (!entityMap.containsKey(entity.entityId)) return;
for (Player player : entity.viewers) {
Consumer<Object> sink = packet -> TinyProtocol.instance.sendPacket(player, packet);
if (!entity.isHidden()) entity.despawn(sink);
entity.delist(sink);
entity.list(sink);
if (!entity.isHidden()) entity.spawn(sink);
}
}
void removeEntity(REntity entity) { void removeEntity(REntity entity) {
entity.despawn(packet -> updateEntity(entity, packet)); entity.despawn(packet -> updateEntity(entity, packet));
removeEntityFromChunk(entity);
entity.delist(packet -> updateEntity(entity, packet)); entity.delist(packet -> updateEntity(entity, packet));
entity.viewers.clear();
entityMap.remove(entity.entityId); entityMap.remove(entity.entityId);
} }
public List<REntity> getEntities() { public List<REntity> getEntities() {
return new ArrayList<>(entityMap.values()); return new ArrayList<>(entityMap.values());
} }
@@ -156,18 +179,6 @@ public class REntityServer implements Listener {
return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList()); return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList());
} }
private void addEntityToChunk(REntity entity) {
entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity);
}
private void removeEntityFromChunk(REntity entity) {
long id = entityToId(entity);
HashSet<REntity> entitiesInChunk = entities.get(id);
if (entitiesInChunk == null) return;
entitiesInChunk.remove(entity);
if (entitiesInChunk.isEmpty()) entities.remove(id);
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onMove(PlayerMoveEvent e) { public void onMove(PlayerMoveEvent e) {
Player player = e.getPlayer(); Player player = e.getPlayer();
@@ -175,90 +186,62 @@ public class REntityServer implements Listener {
Location to = e.getTo(); Location to = e.getTo();
if (from == null || to == null) return; if (from == null || to == null) return;
int fromX = posToChunk(from.getX());
int fromZ = posToChunk(from.getZ());
int toX = posToChunk(to.getX()); int toX = posToChunk(to.getX());
int toZ = posToChunk(to.getZ()); int toZ = posToChunk(to.getZ());
if (fromX == toX && fromZ == toZ) return; if (posToChunk(from.getX()) == toX && posToChunk(from.getZ()) == toZ) return;
lastLocation.put(player, to); lastLocation.put(player, to);
updateViewer(player, toX, toZ);
int toViewDistance = viewRadius(player);
forChunkInView(player, from, (x, z) -> {
if (Math.abs(x - toX) > toViewDistance || Math.abs(z - toZ) > toViewDistance) {
removePlayerFromChunk(player, x, z);
}
});
int fromViewDistance = this.viewDistance.put(player, toViewDistance);
forChunkInView(player, to, (x, z) -> {
if (Math.abs(x - fromX) > fromViewDistance || Math.abs(z - fromZ) > fromViewDistance) {
addPlayerToChunk(player, x, z);
}
});
} }
@EventHandler @EventHandler
public void onQuit(PlayerQuitEvent e) { public void onQuit(PlayerQuitEvent e) {
Player player = e.getPlayer(); Player player = e.getPlayer();
Location location = lastLocation.remove(player); if (lastLocation.remove(player) == null) return;
if (location == null) return;
forChunkInView(player, location, (x, z) -> { for (REntity entity : entityMap.values()) {
long id = chunkToId(x, z); entity.viewers.remove(player);
Set<Player> playersInChunk = players.get(id); }
playersInChunk.remove(player);
if (playersInChunk.isEmpty()) players.remove(id);
});
viewDistance.remove(player);
} }
private void onMissing(Set<Player> of, Set<Player> in, Consumer<Consumer<Object>> packetProvider) { private void updateViewer(Player player, int chunkX, int chunkZ) {
if (of == null) return; for (REntity entity : entityMap.values()) {
boolean inView = inView(entity, player, chunkX, chunkZ);
for (Player player : of) { if (inView) {
if (in == null || !in.contains(player)) { if (entity.viewers.add(player)) showTo(entity, player);
packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet)); } else if (entity.viewers.remove(player)) {
hideFrom(entity, player);
} }
} }
} }
private void forChunkInView(Player player, Location location, BiConsumer<Integer, Integer> func) { private void addMissingViewers(REntity entity) {
int chunkX = posToChunk(location.getX()); int chunkX = posToChunk(entity.x());
int chunkZ = posToChunk(location.getZ()); int chunkZ = posToChunk(entity.z());
int viewDistance = this.viewDistance.get(player); for (Player player : lastLocation.keySet()) {
if (inView(entity, player, chunkX, chunkZ) && entity.viewers.add(player)) {
for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) { showTo(entity, player);
for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) {
func.accept(x, z);
} }
} }
} }
private void addPlayerToChunk(Player player, int x, int z) { private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) {
long id = chunkToId(x, z); Location location = lastLocation.get(player);
players.computeIfAbsent(id, i -> new HashSet<>()).add(player); return entity.canView(player, posToChunk(location.getX()), posToChunk(location.getZ()), entityChunkX, entityChunkZ);
for (REntity entity : entities.getOrDefault(id, emptyEntities)) { }
entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet));
if (!entity.isHidden()) { private void showTo(REntity entity, Player player) {
entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet));
} if (!entity.isHidden()) {
entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
} }
} }
private void removePlayerFromChunk(Player player, int x, int z) { private void hideFrom(REntity entity, Player player) {
long id = chunkToId(x, z); if (!entity.isHidden()) {
entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
Set<Player> playersInChunk = players.get(id);
playersInChunk.remove(player);
if (playersInChunk.isEmpty()) players.remove(id);
for (REntity entity : entities.getOrDefault(id, emptyEntities)) {
if (!entity.isHidden()) {
entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
}
entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet));
} }
entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet));
} }
public void tick() { public void tick() {
@@ -271,19 +254,10 @@ public class REntityServer implements Listener {
return (int) (coord / 16) - (coord < 0 ? 1 : 0); return (int) (coord / 16) - (coord < 0 ? 1 : 0);
} }
private int viewRadius(Player player) { /**
* Default view radius in chunks for entities without a view range of their own.
*/
int viewRadius(Player player) {
return player.getClientViewDistance() / 2; return player.getClientViewDistance() / 2;
} }
private long entityToId(REntity entity) {
return posToId(entity.x(), entity.z());
}
private long posToId(double x, double z) {
return chunkToId(posToChunk(x), posToChunk(z));
}
private long chunkToId(int x, int z) {
return ((long) x << 32) + z;
}
} }
@@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity<RPlayer> {
} }
private GameProfile saved; private GameProfile saved;
private boolean awaitingSkin = false;
@Override @Override
void list(Consumer<Object> packetSink) { void list(Consumer<Object> packetSink) {
saved = getGameProfile(); saved = getGameProfile();
if (saved.getProperties().isEmpty() && !awaitingSkin) {
// The skin is requested asynchronously, so it is usually not available yet when the entity is created.
// Without a refresh, players that already see this entity would keep the default skin.
awaitingSkin = true;
CoreNetworkHandler.onSkinReceived(actualUUID, () -> {
awaitingSkin = false;
server.refreshEntity(this);
});
}
packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE)); packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE));
} }
@@ -88,7 +98,7 @@ public class RPlayer extends REntity implements RInteractableEntity<RPlayer> {
packetSink.accept(getNamedSpawnPacket()); packetSink.accept(getNamedSpawnPacket());
entityDataPacket().add(skinPartsDataWatcher, (byte) 0x7F).send(packetSink); entityDataPacket().add(skinPartsDataWatcher, (byte) 0x7F).send(packetSink);
for (Map.Entry<Object, ItemStack> entry : itemSlots.entrySet()) { for (Map.Entry<org.bukkit.inventory.EquipmentSlot, ItemStack> entry : itemSlots.entrySet()) {
packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue())); packetSink.accept(getEquipmentPacket(entry.getKey(), entry.getValue()));
} }
@@ -21,6 +21,7 @@ package de.steamwar.network;
import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.Property;
import de.steamwar.core.BountifulWrapper; import de.steamwar.core.BountifulWrapper;
import de.steamwar.core.Core;
import de.steamwar.network.handlers.InventoryHandler; import de.steamwar.network.handlers.InventoryHandler;
import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.common.PlayerSkinResponsePacket; import de.steamwar.network.packets.common.PlayerSkinResponsePacket;
@@ -31,8 +32,11 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class CoreNetworkHandler extends PacketHandler { public class CoreNetworkHandler extends PacketHandler {
@@ -85,9 +89,23 @@ public class CoreNetworkHandler extends PacketHandler {
} }
}; };
private static final Map<UUID, List<Runnable>> SKIN_LISTENERS = new ConcurrentHashMap<>();
/**
* Runs the given callback on the main thread once the skin of the given player has been received.
*/
public static void onSkinReceived(UUID uuid, Runnable callback) {
SKIN_LISTENERS.computeIfAbsent(uuid, __ -> new CopyOnWriteArrayList<>()).add(callback);
}
@Handler @Handler
public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) { public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) {
if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return; if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return;
SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature())); SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature()));
Bukkit.getScheduler().runTask(Core.getInstance(), () -> {
List<Runnable> listeners = SKIN_LISTENERS.remove(packet.getUuid());
if (listeners != null) listeners.forEach(Runnable::run);
});
} }
} }
@@ -53,6 +53,3 @@ DEALER_REDSTONE = §eRedstone
DEALER_BLOCKS = §eBlocks DEALER_BLOCKS = §eBlocks
DEALER_TOOLS = §eTools DEALER_TOOLS = §eTools
DEALER_ANGLES = §eAngle Blocks DEALER_ANGLES = §eAngle Blocks
PARTICIPANT_CHAT = {0} {1}§8» §7{2}
SPECTATOR_CHAT = §7{0}§8» §7{1}
@@ -22,7 +22,6 @@ package de.steamwar.tntleague.events
import de.steamwar.tntleague.config.TNTLeagueWorldConfig import de.steamwar.tntleague.config.TNTLeagueWorldConfig
import de.steamwar.tntleague.game.TNTLeagueGame import de.steamwar.tntleague.game.TNTLeagueGame
import de.steamwar.tntleague.game.TNTLeagueTeam import de.steamwar.tntleague.game.TNTLeagueTeam
import de.steamwar.tntleague.message
import de.steamwar.tntleague.plugin import de.steamwar.tntleague.plugin
import org.bukkit.GameMode import org.bukkit.GameMode
import org.bukkit.Material import org.bukkit.Material
@@ -90,20 +89,4 @@ object GlobalListener : Listener {
null -> e.respawnLocation = TNTLeagueWorldConfig.lobby null -> e.respawnLocation = TNTLeagueWorldConfig.lobby
} }
} }
@EventHandler
fun handlePlayerChat(event: AsyncPlayerChatEvent) {
val player = event.player
val msg = event.message
val fightTeam = TNTLeagueGame.getTeam(player)
if (fightTeam != null) {
message.broadcastPrefixless("PARTICIPANT_CHAT", fightTeam.name, player.name, msg)
} else {
message.broadcastPrefixless("SPECTATOR_CHAT", player.name, msg)
}
event.isCancelled = true
}
} }
-3
View File
@@ -34,6 +34,3 @@ GAME_TIME = §a{0}:{1}
CATCH_UP_WARNING = §4!! §cYou should catch up §4!! CATCH_UP_WARNING = §4!! §cYou should catch up §4!!
COMMAND_START = §aThe game will start soon§8! COMMAND_START = §aThe game will start soon§8!
PARTICIPANT_CHAT = {0} {1}§8» §7{2}
SPECTATOR_CHAT = §7{0}§8» §7{1}
@@ -20,19 +20,15 @@
package de.steamwar.towerrun.listener; package de.steamwar.towerrun.listener;
import de.steamwar.linkage.Linked; import de.steamwar.linkage.Linked;
import de.steamwar.towerrun.TowerRun;
import de.steamwar.towerrun.config.WorldConfig; import de.steamwar.towerrun.config.WorldConfig;
import de.steamwar.towerrun.game.TowerRunGame; import de.steamwar.towerrun.game.TowerRunGame;
import de.steamwar.towerrun.game.TowerRunPlayer; import de.steamwar.towerrun.game.TowerRunPlayer;
import de.steamwar.towerrun.state.GameState;
import de.steamwar.towerrun.state.GameStateBukkitListener; import de.steamwar.towerrun.state.GameStateBukkitListener;
import de.steamwar.towerrun.state.GameStates; import de.steamwar.towerrun.state.GameStates;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
@@ -71,25 +67,4 @@ public class GlobalListener extends GameStateBukkitListener implements Listener
public void onPlayerRespawn(PlayerRespawnEvent event) { public void onPlayerRespawn(PlayerRespawnEvent event) {
event.setRespawnLocation(WorldConfig.SPAWN); event.setRespawnLocation(WorldConfig.SPAWN);
} }
@EventHandler
public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
TowerRunPlayer player = TowerRunPlayer.get(event.getPlayer());
if (GameState.getCurrentState() == GameStates.RUNNING && (TowerRunGame.isAlive(player) || TowerRunGame.isEscaped(player))) {
String prefix;
if (TowerRunGame.isAlive(player)) {
if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) {
prefix = "§c☠";
} else {
prefix = "§6❤";
}
} else {
prefix = "§a✔";
}
TowerRun.getMessage().broadcastPrefixless("PARTICIPANT_CHAT", prefix, event.getPlayer().getName(), event.getMessage());
} else {
TowerRun.getMessage().broadcastPrefixless("SPECTATOR_CHAT", event.getPlayer().getName(), event.getMessage());
}
event.setCancelled(true);
}
} }
@@ -22,6 +22,7 @@ package de.steamwar.persistent;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.scheduler.ScheduledTask;
import lombok.Getter; import lombok.Getter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@@ -32,7 +33,10 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.logging.Level; import java.util.logging.Level;
@@ -98,7 +102,13 @@ public class Subserver {
@Getter @Getter
private boolean started; private boolean started;
private final List<Player> cachedPlayers = new LinkedList<>(); private static final int MAX_PROGRESS = 10;
private static final Pattern PLUGIN_COUNT = Pattern.compile("Bukkit plugins \\((\\d+)\\)");
private final List<Player> cachedPlayers = new CopyOnWriteArrayList<>();
private volatile int progress = 0;
private int pluginCount = 0;
private int enabledPlugins = 0;
@Getter @Getter
private final Map<Player, String> tablistNames = new HashMap<>(); private final Map<Player, String> tablistNames = new HashMap<>();
@@ -127,7 +137,7 @@ public class Subserver {
public void sendPlayer(Player p) { public void sendPlayer(Player p) {
if (!started) { if (!started) {
p.sendActionBar(generateBar(0)); p.sendActionBar(generateBar(progress));
cachedPlayers.add(p); cachedPlayers.add(p);
} else { } else {
p.createConnectionRequest(registeredServer).connect(); p.createConnectionRequest(registeredServer).connect();
@@ -246,31 +256,32 @@ public class Subserver {
private void run() { private void run() {
Exception ex = null; Exception ex = null;
try { try {
if (checkpoint) { ScheduledTask refresh = Persistent.getInstance().getProxy().getScheduler()
start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); .buildTask(Persistent.getInstance(), () -> sendProgress(progress))
start(process.getInputStream(), line -> line.contains("Checkpoint restored")); .repeat(2, TimeUnit.SECONDS).schedule();
Thread.sleep(100);
} else {
sendProgress(0);
start(process.getInputStream(), line -> {
if (line.contains("Loading libraries, please wait")) {
sendProgress(2);
} else if (line.contains("Starting Minecraft server on")) {
sendProgress(5);
} else if (line.contains("Preparing start region")) {
sendProgress(8);
}
return line.contains("Done (");
});
}
if (!started) return; try {
if (checkpoint) {
start(process.getErrorStream(), line -> line.contains("Restore finished successfully."));
start(process.getInputStream(), line -> line.contains("Checkpoint restored"));
Thread.sleep(100);
} else {
start(process.getInputStream(), line -> {
updateProgress(line);
return line.contains("Done (");
});
}
sendProgress(10); if (!started) return;
for (Player cachedPlayer : cachedPlayers) {
sendPlayer(cachedPlayer); setProgress(MAX_PROGRESS);
for (Player cachedPlayer : cachedPlayers) {
sendPlayer(cachedPlayer);
}
cachedPlayers.clear();
} finally {
refresh.cancel();
} }
cachedPlayers.clear();
process.waitFor(); process.waitFor();
} catch (IOException e) { } catch (IOException e) {
@@ -285,6 +296,39 @@ public class Subserver {
} }
} }
private void updateProgress(String line) {
if (line.startsWith("Loading Paper")) {
setProgress(1);
} else if (line.startsWith("Bukkit plugins (")) {
Matcher matcher = PLUGIN_COUNT.matcher(line);
if (matcher.find()) pluginCount = Integer.parseInt(matcher.group(1));
} else if (line.startsWith("Starting minecraft server version")) {
setProgress(2);
} else if (line.contains("Starting Minecraft server on")) {
setProgress(3);
} else if (line.contains("libraries... please wait")) {
setProgress(4);
} else if (line.startsWith("Loading server plugin")) {
setProgress(5);
} else if (line.startsWith("Preparing level")) {
setProgress(6);
} else if (line.startsWith("Preparing start region")) {
setProgress(7);
} else if (line.startsWith("Enabling ")) {
enabledPlugins++;
// 7 -> 9 over all plugins, 9 is reached with the last plugin
setProgress(7 + (pluginCount > 0 ? Math.min(2, 2 * enabledPlugins / pluginCount) : 0));
} else if (line.startsWith("Done preparing level")) {
setProgress(9);
}
}
private void setProgress(int newProgress) {
if (newProgress <= progress) return;
progress = newProgress;
sendProgress(newProgress);
}
private Component generateBar(int progress) { private Component generateBar(int progress) {
return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW) return Component.text("⬛".repeat(Math.max(0, progress))).color(NamedTextColor.YELLOW)
.append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY)); .append(Component.text("⬛".repeat(Math.max(0, 10 - progress))).color(NamedTextColor.DARK_GRAY));
@@ -573,12 +573,60 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8]
CHAT_NO_RECEIVER = §cNobody receives your message CHAT_NO_RECEIVER = §cNobody receives your message
CHAT_EMPTY = §cDon\'t write meaningless empty messages. CHAT_EMPTY = §cDon\'t write meaningless empty messages.
CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} CHAT_SERVERTEAM_SENDER = §e{0}
CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} CHAT_SERVERTEAM = §8» §f{2}
CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} CHAT_GLOBAL_SENDER = {4}{5}{6}{0}
CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} CHAT_GLOBAL = §8» {7}{2}
CHAT_TEAM = §8TC §e{0}§8» §f{2} CHAT_TEAM_SENDER = §e{0}
CHAT_MSG = §e{0}§8»§e{1} §7{2} CHAT_TEAM = §8» §f{2}
CHAT_MSG_SENDER = §e{0}
CHAT_MSG_SEPARATOR = §8»
CHAT_MSG_RECEIVER = §e{0}
CHAT_MSG = §7{0}
CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2}
CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1}
CHAT_FIGHT_PARTICIPANT = §8» §7{0}
CHAT_FIGHT_SPECTATOR_SENDER = §7{1}
CHAT_FIGHT_SPECTATOR = §8» §7{0}
CHAT_FIGHT_REFEREE_SENDER = §e{1}
CHAT_FIGHT_REFEREE = §8» §e{0}
CHAT_FIGHT_TEAM = §8» §f{0}
CHAT_ICON = {0}{1}
CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Prefix: §r{1}{2}
CHAT_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Prefix: §r{1}{2}\n§8Click to insert §7/team info
CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1}
CHAT_ICON_GLOBAL = 🌍
CHAT_ICON_LOCAL = ⌂
CHAT_ICON_FIGHT = ⚔
CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0}
CHAT_ICON_FIGHT_TEAM = {0}⚑
CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1}
CHAT_ICON_TEAM = 👥
CHAT_ICON_SERVERTEAM = ⚙
CHAT_ICON_MSG = ✉
CHAT_CHANNEL_GLOBAL = §eGlobal
CHAT_CHANNEL_ARENA = §eFight
CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1}
CHAT_CHANNEL_TEAM = §eTeamChat
CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat
CHAT_CHANNEL_MSG = §eMSG §7{0}
CHAT_CHANNEL_ALERT = §cAlert
CHAT_CHANNEL_LOCAL = §eLocal
CHAT_CHANNEL_DEFAULT = §ais your default channel
CHAT_CHANNEL_NOT_SWITCHABLE = §cYou cannot write into this channel by default.
CHAT_NO_WRITE = §cYou cannot write into this channel.
CHAT_FIGHT_NO_TEAM = §cYou are not in a fight team.
GLOBAL_USAGE = §8/§7global §8[§emessage§8]
CHAT_USAGE = §8/§7chat §8[§echannel§8] [§ejoin§8|§eleave§8]
CHAT_MSG_USAGE = §8/§7chat §8[§echannel§8] §7msg §8[§emessage§8]
CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8]
CHAT_ARENAS_JOINED = §7You now read the chats of all fights.
CHAT_ARENAS_LEFT = §7You no longer read the chats of all fights.
CHAT_GLOBAL_AFTER_WRITING = §7You read §e{0} §7on this server until you leave it.
CHAT_CHANNEL_UNKNOWN = §cUnknown chat channel.
CHAT_CHANNEL_JOINED = §7You are reading §e{0} §7again.
CHAT_CHANNEL_LEFT = §7You no longer read §e{0}§7.
CHAT_CHANNEL_LEAVE_DEFAULT = §cYou cannot leave the channel you are writing into. Switch to another channel first.
#CheckListner #CheckListner
CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)! CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)!
@@ -538,12 +538,60 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8]
CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht
CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten. CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten.
CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2} CHAT_SERVERTEAM_SENDER = §e{0}
CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2} CHAT_SERVERTEAM = §8» §f{2}
CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} CHAT_GLOBAL_SENDER = {4}{5}{6}{0}
CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2} CHAT_GLOBAL = §8» {7}{2}
CHAT_TEAM = §8TC §e{0}§8» §f{2} CHAT_TEAM_SENDER = §e{0}
CHAT_MSG = §e{0}§8»§e{1} §7{2} CHAT_TEAM = §8» §f{2}
CHAT_MSG_SENDER = §e{0}
CHAT_MSG_SEPARATOR = §8»
CHAT_MSG_RECEIVER = §e{0}
CHAT_MSG = §7{0}
CHAT_SPECIAL_ALERT = §e{0}§8»§e{1} §7{2}
CHAT_FIGHT_PARTICIPANT_SENDER = {0}{1}
CHAT_FIGHT_PARTICIPANT = §8» §7{0}
CHAT_FIGHT_SPECTATOR_SENDER = §7{1}
CHAT_FIGHT_SPECTATOR = §8» §7{0}
CHAT_FIGHT_REFEREE_SENDER = §e{1}
CHAT_FIGHT_REFEREE = §8» §e{0}
CHAT_FIGHT_TEAM = §8» §f{0}
CHAT_ICON = {0}{1}
CHAT_HOVER_TEAM = §7Team: §e{0}\n§7Kürzel: §r{1}{2}
CHAT_HOVER_TEAM_CLICK = §7Team: §e{0}\n§7Kürzel: §r{1}{2}\n§8Klicke, um §7/team info §8einzufügen
CHAT_HOVER_LOCAL = {0}\n§7Server: §e{1}
CHAT_ICON_GLOBAL = 🌍
CHAT_ICON_LOCAL = ⌂
CHAT_ICON_FIGHT = ⚔
CHAT_ICON_FIGHT_FOREIGN = ⚔ §8{0}
CHAT_ICON_FIGHT_TEAM = {0}⚑
CHAT_ICON_FIGHT_TEAM_FOREIGN = {0}⚑ §8{1}
CHAT_ICON_TEAM = 👥
CHAT_ICON_SERVERTEAM = ⚙
CHAT_ICON_MSG = ✉
CHAT_CHANNEL_GLOBAL = §eGlobal
CHAT_CHANNEL_ARENA = §eFight
CHAT_CHANNEL_FIGHT_TEAM = §e{0} {1}
CHAT_CHANNEL_TEAM = §eTeamChat
CHAT_CHANNEL_SERVERTEAM = §eServerTeamChat
CHAT_CHANNEL_MSG = §eMSG §7{0}
CHAT_CHANNEL_ALERT = §cAlert
CHAT_CHANNEL_LOCAL = §eLokal
CHAT_CHANNEL_DEFAULT = §aist dein Standardkanal
CHAT_CHANNEL_NOT_SWITCHABLE = §cIn diesen Kanal kannst du nicht standardmäßig schreiben.
CHAT_NO_WRITE = §cDu kannst nicht in diesen Kanal schreiben.
CHAT_FIGHT_NO_TEAM = §cDu bist in keinem Kampfteam.
GLOBAL_USAGE = §8/§7global §8[§eNachricht§8]
CHAT_USAGE = §8/§7chat §8[§eKanal§8] [§ejoin§8|§eleave§8]
CHAT_MSG_USAGE = §8/§7chat §8[§eKanal§8] §7msg §8[§eNachricht§8]
CHAT_ARENAS_USAGE = §8/§7chat §7arenas §8[§ejoin§8|§eleave§8]
CHAT_ARENAS_JOINED = §7Du liest jetzt die Chats aller Fights mit.
CHAT_ARENAS_LEFT = §7Du liest die Chats aller Fights nicht mehr mit.
CHAT_GLOBAL_AFTER_WRITING = §7Du liest §e{0} §7auf diesem Server mit, bis du ihn verlässt.
CHAT_CHANNEL_UNKNOWN = §cUnbekannter Chatkanal.
CHAT_CHANNEL_JOINED = §7Du liest §e{0} §7wieder mit.
CHAT_CHANNEL_LEFT = §7Du liest §e{0} §7nicht mehr mit.
CHAT_CHANNEL_LEAVE_DEFAULT = §cDu kannst den Kanal, in den du schreibst, nicht verlassen. Wechsle zuerst in einen anderen Kanal.
#CheckListner #CheckListner
CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)! CHECK_UNCHECKED = §7Du hast noch §e{0} §7ungeprüfte Schematic§8(§7s§8)!
@@ -23,8 +23,6 @@ import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import de.steamwar.persistent.Subserver;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm; import de.steamwar.sql.UserPerm;
import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.VelocityCore;
@@ -61,13 +59,6 @@ public interface Chatter {
return new ChatterGroup(allStream()); return new ChatterGroup(allStream());
} }
static ChatterGroup globalChat() {
return new ChatterGroup(Stream.concat(Stream.of(Chatter.console()), allPlayers().filter(player -> {
Subserver subserver = Subserver.getSubserver(player);
return !(Subserver.isArena(subserver) && subserver.getServer() == player.getCurrentServer().map(ServerConnection::getServerInfo).orElse(null));
}).map(Chatter::of)));
}
static ChatterGroup serverteam() { static ChatterGroup serverteam() {
return new ChatterGroup(allStream() return new ChatterGroup(allStream()
.filter(player -> player.user().hasPerm(UserPerm.TEAM)) .filter(player -> player.user().hasPerm(UserPerm.TEAM))
@@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static de.steamwar.persistent.Storage.eventServer; import static de.steamwar.persistent.Storage.eventServer;
@@ -43,8 +45,15 @@ public class EventStarter {
@Getter @Getter
private static final Map<Integer, String> spectatePorts = new HashMap<>(); private static final Map<Integer, String> spectatePorts = new HashMap<>();
public static void addSpectateServer(int port, String command) { private static final Set<String> spectateServers = new HashSet<>();
public static void addSpectateServer(int port, String command, String serverName) {
spectatePorts.put(port, command); spectatePorts.put(port, command);
spectateServers.add(serverName);
}
public static boolean isSpectateServer(String serverName) {
return spectateServers.contains(serverName);
} }
public EventStarter() { public EventStarter() {
@@ -303,7 +303,7 @@ public class ServerStarter {
worldSetup.run(); worldSetup.run();
arguments.put("logPath", worldName); arguments.put("logPath", worldName);
File checkpointDir = new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + directory.getName() + "/" + worldName); File checkpointDir = checkpointDir(directory, worldName);
if (checkpoint) { if (checkpoint) {
arguments.put("checkpoint", checkpointDir.getPath()); arguments.put("checkpoint", checkpointDir.getPath());
} }
@@ -326,6 +326,10 @@ public class ServerStarter {
}).schedule(); }).schedule();
} }
public static File checkpointDir(File serverDirectory, String worldName) {
return new File("/tmp/" + System.getProperty("user.name") + ".checkpoints/" + serverDirectory.getName() + "/" + worldName);
}
private void regularStart(String serverName, int port) { private void regularStart(String serverName, int port) {
postStart(constructor.construct(serverName, port, node.startServer( postStart(constructor.construct(serverName, port, node.startServer(
version, directory, worldDir, worldName, port, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new) version, directory, worldDir, worldName, port, arguments.entrySet().stream().map(entry -> entry.getKey() + "=" + entry.getValue()).toArray(String[]::new)
@@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin {
String cmd = cmds.remove(0); String cmd = cmds.remove(0);
if (server.getSpectatePort() != 0) { if (server.getSpectatePort() != 0) {
EventStarter.addSpectateServer(server.getSpectatePort(), cmd); EventStarter.addSpectateServer(server.getSpectatePort(), cmd, entry.getKey());
} }
new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0])); new ServerSwitchCommand(cmd, entry.getKey(), server.getSpectatePort() != 0, cmds.toArray(new String[0]));
@@ -0,0 +1,71 @@
/*
* 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.velocitycore.chat;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.sql.UserPerm;
import net.kyori.adventure.text.Component;
import java.util.Arrays;
import java.util.stream.Stream;
public class AlertChannel extends ChatChannel {
@Override
public boolean switchable() {
return false;
}
@Override
public Message tag(Chatter receiver) {
return new Message("CHAT_CHANNEL_ALERT");
}
@Override
public Stream<Chatter> readers() {
return Arrays.stream(Chatter.broadcast().getChatters());
}
@Override
public boolean canWrite(Chatter sender) {
if (!sender.user().hasPerm(UserPerm.MODERATION)) {
sender.system("CHAT_NO_WRITE");
return false;
}
return true;
}
@Override
protected Component format(Chatter receiver, Chatter sender, String message) {
return receiver.parse(true, new Message("ALERT", message.replace('&', '§')));
}
/**
* Alerts are system messages: they are shown even with hidden chat and bypass the chat filter.
*/
@Override
public void send(Chatter sender, String message, boolean mirror) {
if (!canWrite(sender)) return;
readers().forEach(reader -> reader.sendMessage(format(reader, sender, message)));
}
}
@@ -0,0 +1,146 @@
/*
* 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.velocitycore.chat;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.sql.AuditLog;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;
public abstract class ChatChannel {
/**
* @return the id under which this channel is stored as default channel of a player, null if it cannot be stored
*/
public String persistId() {
return null;
}
public boolean switchable() {
return true;
}
/**
* @return the channel name shown in channel related system messages
*/
public abstract Message tag(Chatter receiver);
/**
* @return the text shown when hovering the channel icon
*/
public Message hover(Chatter receiver) {
return tag(receiver);
}
/**
* @return the icon always shown in front of messages of this channel, null if the message format already shows the channel.
* The icon is highlighted if this is the default channel of the receiver.
*/
protected Message icon(Chatter receiver) {
return null;
}
/**
* @return the text put into the chat input when clicking the channel icon to write a single message, null if there is none
*/
protected String suggestion(Chatter receiver) {
return null;
}
/**
* @return the channel icon with its name on hover and its write command on click
*/
Component iconComponent(Chatter receiver, String color) {
Message icon = icon(receiver);
Component component = icon == null ? receiver.parse(tag(receiver)) : receiver.parse(new Message("CHAT_ICON", color, icon));
component = component.hoverEvent(HoverEvent.showText(receiver.parse(hover(receiver))));
String suggestion = suggestion(receiver);
if (suggestion != null) {
component = component.clickEvent(ClickEvent.suggestCommand(suggestion));
}
return component;
}
public abstract Stream<Chatter> readers();
/**
* Checks if the sender is allowed to write into this channel and informs the sender otherwise.
*/
public abstract boolean canWrite(Chatter sender);
protected abstract Component format(Chatter receiver, Chatter sender, String message);
/**
* Called after a message was delivered to all readers.
*
* @param mirror false if the message originates from the mirror of this channel and should not be sent back
* @return true if the message was delivered to a receiver outside the game
*/
protected boolean delivered(Chatter sender, String message, boolean mirror) {
return false;
}
public void audit(SteamwarUser sender, String message) {
AuditLog.createChat(AuditLog.SERVER_NAME_VELOCITY, null, sender, message);
}
public void send(Chatter sender, String message) {
send(sender, message, true);
}
public void send(Chatter sender, String message, boolean mirror) {
if (!canWrite(sender)) return;
SteamwarUser user = sender.user();
String coloredMessage = user.hasPerm(UserPerm.COLOR_CHAT) ? message.replace('&', '§') : message;
if (ChatFilter.filtered(sender, coloredMessage)) return;
Map<Integer, Chatter> receivers = new LinkedHashMap<>();
readers().forEach(reader -> receivers.putIfAbsent(reader.user().getId(), reader));
receivers.putIfAbsent(user.getId(), sender);
boolean noReceiver = true;
for (Chatter receiver : receivers.values()) {
if (!receiver.chatShown()) continue;
Component msg = ChatFormat.withMentionHovers(receiver, format(receiver, sender, coloredMessage));
if (icon(receiver) != null) {
Component icon = iconComponent(receiver, ChatManager.getDefault(receiver) == this ? "§e" : "§7");
msg = Component.text().append(icon).append(Component.text(" ")).append(msg).build();
}
receiver.sendMessage(msg);
if (receiver.user().getId() != user.getId()) noReceiver = false;
}
if (!delivered(sender, coloredMessage, mirror) && noReceiver) {
sender.system("CHAT_NO_RECEIVER");
}
}
}
@@ -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.velocitycore.chat;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.sql.Punishment;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.commands.PunishmentCommand;
import java.util.concurrent.TimeUnit;
public class ChatFilter {
private ChatFilter() {}
public static boolean filtered(Chatter sender, String message) {
if (!sender.chatShown()) {
sender.system("CHAT_RECEIVE");
return true;
}
if (message.replace("§[a-f0-9klmno]", "").trim().isEmpty()) {
sender.system("CHAT_EMPTY");
return true;
}
SteamwarUser user = sender.user();
if (!user.hasPerm(UserPerm.TEAM) && (message.contains("http:") || message.contains("https:") || message.contains("www."))) {
sender.system("CHAT_NO_LINKS");
return true;
}
if (PunishmentCommand.isPunishedWithMessage(sender, Punishment.PunishmentType.Mute)) return true;
if (message.contains("LIXFEL")) {
specialAlert(sender, "Lixfel", "CHAT_LIXFEL_", 3, 6, 11, 12, 15);
}
if (message.contains("YOYONOW")) {
specialAlert(sender, "YoyoNow", "CHAT_YOYONOW_", 3, 6, 11, 12);
}
if (message.contains("CHAOSCAOT")) {
specialAlert(sender, "Chaoscaot", "CHAT_CHAOSCAOT_", 3, 6, 11, 12, 15, 17);
}
return false;
}
private static void specialAlert(Chatter sender, String name, String baseMessage, int... delay) {
sender.system("CHAT_LIXFEL_ACTION_BAR");
for (int i = 0; i < delay.length; i++) {
int finalI = i;
VelocityCore.schedule(() -> sender.prefixless("CHAT_SPECIAL_ALERT", name, sender.user(), new Message(baseMessage + (finalI + 1)))).delay(delay[i], TimeUnit.SECONDS).schedule();
}
}
}
@@ -0,0 +1,179 @@
/*
* 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.velocitycore.chat;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.network.packets.server.PingPacket;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.Team;
import de.steamwar.sql.UserPerm;
import de.steamwar.velocitycore.network.NetworkSender;
import de.steamwar.velocitycore.util.SteamwarPrefix;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import java.util.Arrays;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ChatFormat {
private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})");
// Formats that show the team prefix in front of the sender name
private static final Set<String> TEAM_PREFIX_FORMATS = Set.of("CHAT_GLOBAL");
private ChatFormat() {}
/**
* Formats a message with the rank and team prefixes of the sender. The sender part ({@code <format>_SENDER})
* shows the team of the sender on hover.
*/
public static Component player(String format, Chatter receiver, Chatter sender, Chatter msgReceiver, String message) {
SteamwarUser user = sender.user();
UserPerm.Prefix prefix = user.prefix();
// The team prefix is its own component, so that clicking it suggests /team info
Component teamPrefix = Component.empty();
if (TEAM_PREFIX_FORMATS.contains(format)) {
if (useSwPrefix(user)) {
teamPrefix = Chatter.SERIALIZER.deserialize("§eS§8W")
.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", "SteamWar", "§e", "S§8W"))))
.clickEvent(ClickEvent.suggestCommand("/team info SW"))
.append(Component.text(" "));
} else if (user.getTeam() != 0) {
Team team = Team.byId(user.getTeam());
teamPrefix = clickableTeam(receiver, team, Chatter.SERIALIZER.deserialize("§" + team.getTeamColor() + team.getTeamKuerzel())).append(Component.text(" "));
}
}
String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7";
Object[] params = {
user,
msgReceiver == null ? receiver : msgReceiver,
highlightMentions(message, chatColorCode, receiver),
"",
"",
prefix == UserPerm.emptyPrefix ? "§f" : prefix.getColorCode(),
prefix.getChatPrefix().length() == 0 ? "§f" : prefix.getChatPrefix() + " ",
chatColorCode};
Component senderPart = withTeamHover(receiver, user, receiver.parse(new Message(format + "_SENDER", params)));
return Component.text().append(teamPrefix).append(senderPart).append(receiver.parse(new Message(format, params))).build();
}
/**
* Formats a fight chat message. The fight team name is only hoverable in event fights, where it is the actual team.
*
* @param coloredTeamName the fight team of the sender, null if the sender is not in a fight team
*/
public static Component fight(Chatter receiver, RegisteredServer server, Chatter sender, String coloredTeamName, String senderFormat, String format, String message) {
TextComponent.Builder line = Component.text();
String nameColor = "";
if (coloredTeamName != null) {
Component team = Chatter.SERIALIZER.deserialize(coloredTeamName);
Team eventTeam = ChatManager.eventTeam(server, coloredTeamName);
if (eventTeam != null) {
team = clickableTeam(receiver, eventTeam, team);
}
line.append(team).append(Component.text(" "));
nameColor = coloredTeamName.length() >= 2 && coloredTeamName.charAt(0) == '§' ? coloredTeamName.substring(0, 2) : "";
}
line.append(withTeamHover(receiver, sender.user(), receiver.parse(new Message(senderFormat, nameColor, sender.user()))));
line.append(receiver.parse(new Message(format, message)));
return line.build();
}
/**
* Shows the team of the user when hovering the component, the SteamWar team for server team members using the SW prefix.
*/
public static Component withTeamHover(Chatter receiver, SteamwarUser user, Component component) {
// Players of the public team (id 0) get no hover
if (user.getTeam() == 0) return component;
if (useSwPrefix(user)) {
return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W"))));
}
return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam()))));
}
/**
* Shows the team on hover and suggests {@code /team info} for it on click.
*/
private static Component clickableTeam(Chatter receiver, Team team, Component component) {
return component
.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM_CLICK", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel()))))
.clickEvent(ClickEvent.suggestCommand("/team info " + team.getTeamKuerzel()));
}
private static Component teamHover(Chatter receiver, Team team) {
return receiver.parse(new Message("CHAT_HOVER_TEAM", team.getTeamName(), "§" + team.getTeamColor(), team.getTeamKuerzel()));
}
private static boolean useSwPrefix(SteamwarUser user) {
if (!user.hasPerm(UserPerm.TEAM)) {
return false;
}
UserPerm.Prefix prefix = user.prefix();
boolean hasTeamPrefix = user.getTeam() != 0;
boolean hasSwPrefix = prefix != UserPerm.emptyPrefix;
if (!hasSwPrefix) return false;
if (!hasTeamPrefix) return true;
return SteamwarPrefix.usesSWPrefix(user);
}
/**
* Shows the team of every mentioned player ({@code @name}) on hover, like at the sender name.
*/
public static Component withMentionHovers(Chatter receiver, Component component) {
return component.replaceText(TextReplacementConfig.builder()
.match(MENTION)
.replacement((match, builder) -> {
SteamwarUser user = SteamwarUser.get(match.group(1));
return user == null ? builder : withTeamHover(receiver, user, builder.build());
})
.build());
}
public static String highlightMentions(String message, String returnColor, Chatter player) {
if (!message.contains("@")) return message;
String mark = "@" + player.user().getUserName();
return Arrays.stream(message.split(" ")).map(cur -> {
if (cur.equalsIgnoreCase(mark) && player.getPlayer() != null) {
NetworkSender.send(player.getPlayer(), new PingPacket(player.user().getId()));
return "§e" + cur + returnColor;
}
return cur;
}).collect(Collectors.joining(" "));
}
}
@@ -0,0 +1,514 @@
/*
* 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.velocitycore.chat;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.steamwar.linkage.Linked;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.messages.PlayerChatter;
import de.steamwar.network.packets.common.FightInfoPacket;
import de.steamwar.persistent.Subserver;
import de.steamwar.sql.Event;
import de.steamwar.sql.Referee;
import de.steamwar.sql.Team;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserConfig;
import de.steamwar.sql.UserPerm;
import de.steamwar.velocitycore.EventStarter;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.listeners.BasicListener;
import de.steamwar.velocitycore.tablist.TablistManager;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/**
* Keeps track of all chat channels and the default channel every player writes into.
*/
@Linked
public class ChatManager extends BasicListener {
private static final String CONFIG_KEY = "chat.channel";
private static final String HIDDEN_KEY = "chat.hidden";
private static final String ARENAS_KEY = "chat.arenas";
private static final long REFEREE_CACHE_MILLIS = 60_000;
@Getter
private static final GlobalChannel global = new GlobalChannel();
@Getter
private static final ServerTeamChannel serverTeam = new ServerTeamChannel();
@Getter
private static final AlertChannel alert = new AlertChannel();
private static final Map<RegisteredServer, LocalChannel> locals = new ConcurrentHashMap<>();
private static final Map<RegisteredServer, FightTeamChannel[]> fightTeams = new ConcurrentHashMap<>();
private static final Map<Integer, TeamChannel> teams = new ConcurrentHashMap<>();
private static final Set<DirectChannel> directs = ConcurrentHashMap.newKeySet();
private static final Map<UUID, ChatChannel> defaults = new ConcurrentHashMap<>();
private static final Map<UUID, String> persisted = new ConcurrentHashMap<>();
private static final Set<UUID> globalOnArena = ConcurrentHashMap.newKeySet();
// Players reading the global chat on their current fight server until they leave it (after /global <text> or /chat global join)
private static final Set<UUID> globalOnServer = ConcurrentHashMap.newKeySet();
private static final Set<UUID> announced = ConcurrentHashMap.newKeySet();
private static final Map<UUID, Set<RegisteredServer>> spies = new ConcurrentHashMap<>();
private static final Map<UUID, Boolean> allArenas = new ConcurrentHashMap<>();
private static final Map<UUID, Set<RegisteredServer>> leftArenas = new ConcurrentHashMap<>();
private static final Map<UUID, Set<String>> hidden = new ConcurrentHashMap<>();
private static int refereeEvent = -1;
private static long refereesLoaded = 0;
private static Set<Integer> referees = Collections.emptySet();
public static LocalChannel local(RegisteredServer server) {
locals.keySet().removeIf(ChatManager::unregistered);
return locals.computeIfAbsent(server, LocalChannel::new);
}
public static FightTeamChannel fightTeam(RegisteredServer server, boolean blue) {
fightTeams.keySet().removeIf(ChatManager::unregistered);
return fightTeams.computeIfAbsent(server, s -> new FightTeamChannel[]{new FightTeamChannel(s, true), new FightTeamChannel(s, false)})[blue ? 0 : 1];
}
public static TeamChannel team(int team) {
return teams.computeIfAbsent(team, TeamChannel::new);
}
public static DirectChannel direct(UUID first, UUID second) {
for (DirectChannel channel : directs) {
if (channel.contains(first) && channel.partner(first).equals(second)) {
return channel;
}
}
DirectChannel channel = new DirectChannel(first, second);
directs.add(channel);
return channel;
}
private static boolean unregistered(RegisteredServer server) {
return VelocityCore.getProxy().getServer(server.getServerInfo().getName()).orElse(null) != server;
}
/**
* @return true for arenas and event spectate servers, which use a fight chat instead of the global chat
*/
public static boolean isArena(RegisteredServer server) {
return Subserver.isArena(Subserver.getSubserver(server.getServerInfo())) || EventStarter.isSpectateServer(server.getServerInfo().getName());
}
/**
* @return all servers using a fight chat (arenas and event spectate servers)
*/
public static Stream<RegisteredServer> fightServers() {
return VelocityCore.getProxy().getAllServers().stream().filter(ChatManager::isArena);
}
/**
* @return the local channel of the server the player is on, null while the player is on no server
*/
public static LocalChannel currentLocal(Player player) {
return currentServer(player).map(ChatManager::local).orElse(null);
}
static boolean onServer(Chatter chatter, RegisteredServer server) {
Player player = chatter.getPlayer();
return player != null && server.getPlayersConnected().contains(player);
}
/**
* Sends the message into the channel or makes it the default channel of the player if there is no message.
*/
public static void sendOrSwitch(Chatter sender, ChatChannel channel, String[] message, String usage) {
if (message.length > 0) {
channel.send(sender, String.join(" ", message));
} else if (sender instanceof PlayerChatter player) {
switchTo(player, channel);
} else {
sender.system(usage);
}
}
private static Optional<RegisteredServer> currentServer(Player player) {
return player.getCurrentServer().map(ServerConnection::getServer);
}
static boolean readsGlobal(Player player) {
UUID uuid = player.getUniqueId();
return !currentServer(player).map(ChatManager::isArena).orElse(false) || globalOnArena.contains(uuid) || globalOnServer.contains(uuid);
}
/**
* Shows the global chat to a player who wrote into it on a fight server until they leave the server.
*/
public static void readGlobalAfterWriting(PlayerChatter player) {
if (!currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) return;
if (!reads(player, global.persistId())) return;
if (globalOnServer.add(player.user().getUUID())) {
player.system("CHAT_GLOBAL_AFTER_WRITING", global.tag(player));
}
}
/**
* @return the moderators reading the fight chats of the server without being on it
*/
static Stream<Chatter> spies(RegisteredServer server) {
return Chatter.allPlayers()
.map(Chatter::of)
.filter(chatter -> !onServer(chatter, server))
.filter(chatter -> chatter.user().hasPerm(UserPerm.MODERATION))
.filter(chatter -> readsArena(chatter.user().getUUID(), server))
.map(Chatter.class::cast);
}
private static boolean readsArena(UUID uuid, RegisteredServer server) {
Set<RegisteredServer> joined = spies.get(uuid);
if (joined != null && joined.contains(server)) return true;
Set<RegisteredServer> left = leftArenas.get(uuid);
return readsAllArenas(uuid) && (left == null || !left.contains(server));
}
private static boolean readsAllArenas(UUID uuid) {
return allArenas.computeIfAbsent(uuid, u -> "true".equals(UserConfig.getConfig(u, ARENAS_KEY)));
}
/**
* Lets a moderator read the fight chats of every fight server automatically, saved across relogs.
*/
public static void setReadAllArenas(PlayerChatter player, boolean read) {
UUID uuid = player.user().getUUID();
allArenas.put(uuid, read);
leftArenas.remove(uuid);
UserConfig.updatePlayerConfig(uuid, ARENAS_KEY, read ? "true" : null);
player.system(read ? "CHAT_ARENAS_JOINED" : "CHAT_ARENAS_LEFT");
}
/**
* @return true if the user is a referee of the event fight running on the server
*/
static boolean isReferee(RegisteredServer server, int userId) {
Subserver subserver = Subserver.getSubserver(server.getServerInfo());
if (subserver == null || !EventStarter.getEventServer().containsValue(subserver)) return false;
Event event = Event.get();
return event != null && referees(event.getEventID()).contains(userId);
}
private static synchronized Set<Integer> referees(int eventId) {
if (eventId != refereeEvent || System.currentTimeMillis() - refereesLoaded > REFEREE_CACHE_MILLIS) {
referees = Referee.get(eventId);
refereeEvent = eventId;
refereesLoaded = System.currentTimeMillis();
}
return referees;
}
private static Set<String> hidden(UUID uuid) {
return hidden.computeIfAbsent(uuid, u -> {
Set<String> channels = ConcurrentHashMap.newKeySet();
String config = UserConfig.getConfig(u, HIDDEN_KEY);
if (config != null && !config.isEmpty()) {
channels.addAll(Arrays.asList(config.split(",")));
}
return channels;
});
}
/**
* @return false if the player left the channel with the given persist id as reader
*/
static boolean reads(Chatter chatter, String persistId) {
Player player = chatter.getPlayer();
return player == null || !hidden(player.getUniqueId()).contains(persistId);
}
private static void saveHidden(UUID uuid) {
Set<String> channels = hidden(uuid);
UserConfig.updatePlayerConfig(uuid, HIDDEN_KEY, channels.isEmpty() ? null : String.join(",", channels));
}
/**
* @return true if the channel is the fight chat of an arena the player is not on, which moderators can read without joining the arena
*/
private static boolean isForeignArena(PlayerChatter player, ChatChannel channel) {
return channel instanceof LocalChannel local && local.isArena() && !onServer(player, local.getServer());
}
public static void join(PlayerChatter player, ChatChannel channel) {
UUID uuid = player.user().getUUID();
if (isForeignArena(player, channel)) {
RegisteredServer server = ((LocalChannel) channel).getServer();
spies.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server);
Set<RegisteredServer> left = leftArenas.get(uuid);
if (left != null) left.remove(server);
player.system("CHAT_CHANNEL_JOINED", channel.tag(player));
return;
}
if (hidden(uuid).remove(channel.persistId())) {
saveHidden(uuid);
}
if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) {
globalOnServer.add(uuid);
}
player.system("CHAT_CHANNEL_JOINED", channel.tag(player));
}
public static void leave(PlayerChatter player, ChatChannel channel) {
UUID uuid = player.user().getUUID();
if (isForeignArena(player, channel)) {
RegisteredServer server = ((LocalChannel) channel).getServer();
Set<RegisteredServer> servers = spies.get(uuid);
if (servers != null) servers.remove(server);
// Excludes the arena for this session while reading all arenas
leftArenas.computeIfAbsent(uuid, u -> ConcurrentHashMap.newKeySet()).add(server);
player.system("CHAT_CHANNEL_LEFT", channel.tag(player));
return;
}
String id = channel.persistId();
if (id.equals(getDefault(player).persistId())) {
player.system("CHAT_CHANNEL_LEAVE_DEFAULT");
return;
}
// On fight servers leaving only stops reading the global chat on this server, it does not leave the global chat everywhere
if (channel == global && globalOnServer.remove(uuid)) {
player.system("CHAT_CHANNEL_LEFT", channel.tag(player));
return;
}
if (hidden(uuid).add(id)) {
saveHidden(uuid);
}
player.system("CHAT_CHANNEL_LEFT", channel.tag(player));
}
public static FightInfoPacket getFightInfo(RegisteredServer server) {
return TablistManager.getFightInfo(server);
}
/**
* @return the team of an event fight on the server whose colored name is shown, null if the server runs no event fight
*/
static Team eventTeam(RegisteredServer server, String coloredTeamName) {
Subserver subserver = Subserver.getSubserver(server.getServerInfo());
if (subserver == null) return null;
String kuerzel = coloredTeamName.replaceAll("§.", "");
for (Map.Entry<Integer, Subserver> entry : new ArrayList<>(EventStarter.getEventServer().entrySet())) {
if (entry.getValue() != subserver) continue;
Team team = Team.byId(entry.getKey());
if (team != null && team.getTeamKuerzel().equals(kuerzel)) return team;
}
return null;
}
/**
* Updates the fight teams of an arena. Players who just joined a fight team write into the fight chat by default.
*/
public static void updateFightInfo(RegisteredServer server, FightInfoPacket previous, FightInfoPacket packet) {
Set<Integer> joined = new HashSet<>(packet.getBluePlayers());
joined.addAll(packet.getRedPlayers());
if (previous != null) {
previous.getBluePlayers().forEach(joined::remove);
previous.getRedPlayers().forEach(joined::remove);
}
for (Player player : server.getPlayersConnected()) {
if (joined.contains(SteamwarUser.get(player.getUniqueId()).getId())) {
setDefault(Chatter.of(player), local(server));
}
}
}
/**
* @return the channel the receiver writes into by default; non-player chatters (console, Discord) use the global chat
*/
public static ChatChannel getDefault(Chatter chatter) {
Player player = chatter.getPlayer();
if (player == null) return global;
return defaults.computeIfAbsent(player.getUniqueId(), uuid -> initialDefault(player, currentServer(player).orElse(null)));
}
private static ChatChannel initialDefault(Player player, RegisteredServer server) {
if (server != null && isArena(server)) return local(server);
return resolvePersisted(player, server);
}
private static ChatChannel resolvePersisted(Player player, RegisteredServer server) {
String id = persisted.computeIfAbsent(player.getUniqueId(), uuid -> Optional.ofNullable(UserConfig.getConfig(uuid, CONFIG_KEY)).orElse(global.persistId()));
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
return switch (id) {
case "local" -> server != null && !isArena(server) ? local(server) : global;
case "team" -> user.getTeam() != 0 ? team(user.getTeam()) : global;
case "serverteam" -> user.hasPerm(UserPerm.TEAM) ? serverTeam : global;
default -> global;
};
}
/**
* Makes the channel the default channel of the player because the player chose it.
*/
public static void switchTo(PlayerChatter player, ChatChannel channel) {
if (!channel.switchable() || isForeignArena(player, channel)) {
player.system("CHAT_CHANNEL_NOT_SWITCHABLE");
return;
}
if (!channel.canWrite(player)) return;
UUID uuid = player.user().getUUID();
if (channel == global && currentServer(player.getPlayer()).map(ChatManager::isArena).orElse(false)) {
globalOnArena.add(uuid);
} else if (channel != global) {
globalOnArena.remove(uuid);
}
String id = channel.persistId();
if (id != null && !id.equals(persisted.get(uuid))) {
persisted.put(uuid, id);
UserConfig.updatePlayerConfig(uuid, CONFIG_KEY, id);
}
if (id != null && hidden(uuid).remove(id)) {
saveHidden(uuid);
}
defaults.put(uuid, channel);
notifyDefault(player, channel);
}
private static void setDefault(PlayerChatter player, ChatChannel channel) {
UUID uuid = player.user().getUUID();
if (channel != global) globalOnArena.remove(uuid);
ChatChannel previous = defaults.put(uuid, channel);
if (announced.add(uuid)) {
notifyDefault(player, channel);
} else if (previous != channel && !player.parseToPlain(previous.tag(player)).equals(player.parseToPlain(channel.tag(player)))) {
notifyDefault(player, channel);
}
}
/**
* Tells the player which channel they write into, showing the channel icon with its name on hover.
*/
public static void notifyDefault(PlayerChatter player, ChatChannel channel) {
player.sendMessage(Component.text()
.append(channel.iconComponent(player, "§a"))
.append(Component.text(" "))
.append(player.parse(new Message("CHAT_CHANNEL_DEFAULT")))
.build());
}
/**
* Handles a chat message the player typed without a command.
*/
public static void chat(PlayerChatter sender, String message) {
ChatChannel channel = getDefault(sender);
if (channel instanceof TeamChannel team && team.getTeam() != sender.user().getTeam()) {
channel = resolvePersisted(sender.getPlayer(), currentServer(sender.getPlayer()).orElse(null));
setDefault(sender, channel);
}
if (message.startsWith("+")) {
channel = quickChannel(sender, channel);
message = message.substring(1);
}
channel.send(sender, message);
channel.audit(sender.user(), message);
}
/**
* The channel a message prefixed with '+' is sent to: the local chat when writing into the global chat,
* the fight team chat when writing into the local/fight chat.
*/
private static ChatChannel quickChannel(PlayerChatter sender, ChatChannel current) {
RegisteredServer server = currentServer(sender.getPlayer()).orElse(null);
if (server == null) return current;
if (current instanceof LocalChannel local) {
FightTeamChannel blue = fightTeam(server, true);
if (blue.isMember(sender)) return blue;
FightTeamChannel red = fightTeam(server, false);
if (red.isMember(sender)) return red;
// Spectators on fight servers stay in the fight chat
return local.isArena() ? local : global;
}
return local(server);
}
@Subscribe
public void onServerPostConnect(ServerPostConnectEvent event) {
Player player = event.getPlayer();
RegisteredServer server = currentServer(player).orElse(null);
if (server == null) return;
UUID uuid = player.getUniqueId();
globalOnArena.remove(uuid);
globalOnServer.remove(uuid);
ChatChannel current = defaults.get(uuid);
ChatChannel next;
if (isArena(server)) {
next = local(server);
} else if (!announced.contains(uuid) || current instanceof LocalChannel || (event.getPreviousServer() != null && isArena(event.getPreviousServer()))) {
next = resolvePersisted(player, server);
} else {
next = current;
}
setDefault(Chatter.of(player), next);
}
@Subscribe
public void onDisconnect(DisconnectEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
defaults.remove(uuid);
persisted.remove(uuid);
globalOnArena.remove(uuid);
globalOnServer.remove(uuid);
announced.remove(uuid);
spies.remove(uuid);
allArenas.remove(uuid);
leftArenas.remove(uuid);
hidden.remove(uuid);
directs.removeIf(channel -> channel.contains(uuid));
}
}
@@ -0,0 +1,128 @@
/*
* 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.velocitycore.chat;
import com.velocitypowered.api.proxy.Player;
import de.steamwar.messages.Chatter;
import de.steamwar.messages.Message;
import de.steamwar.sql.AuditLog;
import de.steamwar.sql.IgnoreSystem;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.velocitycore.VelocityCore;
import net.kyori.adventure.text.Component;
import java.util.UUID;
import java.util.stream.Stream;
import static de.steamwar.persistent.Storage.lastChats;
/**
* A temporary channel between two players, closed as soon as one of them goes offline.
*/
public class DirectChannel extends ChatChannel {
private final UUID first;
private final UUID second;
DirectChannel(UUID first, UUID second) {
this.first = first;
this.second = second;
}
public boolean contains(UUID uuid) {
return first.equals(uuid) || second.equals(uuid);
}
public UUID partner(UUID uuid) {
return first.equals(uuid) ? second : first;
}
@Override
public Message tag(Chatter receiver) {
return new Message("CHAT_CHANNEL_MSG", SteamwarUser.get(partner(receiver.user().getUUID())));
}
@Override
public boolean switchable() {
return false;
}
@Override
protected Message icon(Chatter receiver) {
return new Message("CHAT_ICON_MSG");
}
@Override
protected String suggestion(Chatter receiver) {
return "/msg " + SteamwarUser.get(partner(receiver.user().getUUID())).getUserName() + " ";
}
@Override
public Stream<Chatter> readers() {
return Stream.of(first, second)
.map(uuid -> VelocityCore.getProxy().getPlayer(uuid).orElse(null))
.filter(player -> player != null)
.map(Chatter::of);
}
@Override
public boolean canWrite(Chatter sender) {
UUID partner = partner(sender.user().getUUID());
if (VelocityCore.getProxy().getPlayer(partner).isEmpty()) {
sender.system("MSG_OFFLINE");
return false;
}
if (IgnoreSystem.isIgnored(partner, sender.user().getUUID())) {
sender.system("MSG_IGNORED");
return false;
}
return true;
}
@Override
protected Component format(Chatter receiver, Chatter sender, String message) {
SteamwarUser partner = SteamwarUser.get(partner(sender.user().getUUID()));
return Component.text()
.append(ChatFormat.withTeamHover(receiver, sender.user(), receiver.parse(new Message("CHAT_MSG_SENDER", sender.user()))))
.append(receiver.parse(new Message("CHAT_MSG_SEPARATOR")))
.append(ChatFormat.withTeamHover(receiver, partner, receiver.parse(new Message("CHAT_MSG_RECEIVER", partner))))
.append(Component.text(" "))
.append(receiver.parse(new Message("CHAT_MSG", ChatFormat.highlightMentions(message, "§7", receiver))))
.build();
}
@Override
protected boolean delivered(Chatter sender, String message, boolean mirror) {
Player senderPlayer = VelocityCore.getProxy().getPlayer(sender.user().getUUID()).orElse(null);
Player partnerPlayer = VelocityCore.getProxy().getPlayer(partner(sender.user().getUUID())).orElse(null);
if (senderPlayer != null && partnerPlayer != null) {
lastChats.put(senderPlayer, partnerPlayer);
lastChats.put(partnerPlayer, senderPlayer);
}
return false;
}
@Override
public void audit(SteamwarUser sender, String message) {
AuditLog.createSensitiveCommand(AuditLog.SERVER_NAME_VELOCITY, null, sender, "/msg " + SteamwarUser.get(partner(sender.getUUID())).getUserName() + " " + message);
}
}

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