Compare commits

...
47 Commits
Author SHA1 Message Date
Jakob Schulz 1389f81c7c make world identifier valid by hex encoding them 2026-11-01 00:46:13 +01:00
Jakob Schulz e22317f1fa optimize imports 2026-10-31 23:02:23 +01:00
Jakob Schulz ded46c28a6 change arena map identifier to not use gamemode 2026-10-31 22:50:54 +01:00
Jakob Schulz 641962ff01 change arena map identifier to arena/<... 2026-10-31 22:47:18 +01:00
Jakob Schulz d196f49ba2 Add arena map world name identifier for TowerRun and TNTLeague 2026-10-31 22:45:32 +01:00
Jakob Schulz 460d016295 Change naming to fight/<version>/<arenaMap> 2026-10-31 22:35:42 +01:00
Jakob Schulz 99f94c5278 Add arena map name to world identifier 2026-10-16 23:00:38 +02:00
Jaki ad1b2c3185 merge upstream 2026-10-16 22:53:40 +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
12312 3e3a209c80 Removed Extra Line 2026-09-24 21:49:20 +02:00
12312 1d43021853 Remove TEAM_HELP_SERVER Deutsch 2026-09-24 21:47:22 +02:00
12312 5a224c1bcc Remove TEAM_HELP_SERVER English 2026-09-24 21:46:18 +02:00
12312 6330dbc2b1 VelocityCore/src/de/steamwar/velocitycore/commands/TeamCommand.java aktualisiert 2026-09-24 21:43:20 +02:00
YoyoNow 4306fe7bc9 Merge pull request 'Fix SteamwarUser.setLocale' (#489) from Hut512/SteamWar:fix/setlocale-typo into main
Reviewed-on: SteamWar/SteamWar#489
Reviewed-by: YoyoNow <4+yoyonow@noreply.localhost>
2026-09-14 12:18:40 +02:00
YoyoNow 58df6087f0 Merge pull request 'Fix NoClip' (#491) from Hut512/SteamWar:fix/swplayer-invalid-reference into main
Reviewed-on: SteamWar/SteamWar#491
Reviewed-by: YoyoNow <4+yoyonow@noreply.localhost>
2026-09-14 12:18:16 +02:00
YoyoNow b9b7869124 Merge pull request 'Fix region water level' (#494) from Hut512/SteamWar:fix/testblock-water-level into main
Reviewed-on: SteamWar/SteamWar#494
2026-09-14 12:17:30 +02:00
YoyoNow 1e85b14667 Improve SteamEditWand 2026-09-14 10:02:39 +02:00
Hut512 35c27923aa Fix region water level 2026-09-12 20:07:07 +02:00
Hut512 c3b3e3e60f Fix NoClip instant block break 2026-09-07 22:21:53 +02:00
Hut512 3aa1bf7ffd Fix scoreboard cleanup 2026-09-07 21:15:01 +02:00
Hut512 5a02a58437 Fix SteamwarUser.setLocale 2026-09-07 15:24:32 +02:00
YoyoNow 6209b86149 Fix TeamCommand.info 2026-09-07 14:03:06 +02:00
YoyoNow 6f1403df9c Cleanup SWPlayer 2026-09-05 14:52:59 +02:00
YoyoNow 0113788da2 Fix SteamEditWand 2026-09-05 09:50:47 +02:00
YoyoNow 8a7f65e000 Merge pull request 'Fix CRIU subserver stop' (#485) from Hut512/SteamWar:main into main
Reviewed-on: SteamWar/SteamWar#485
Reviewed-by: YoyoNow <4+yoyonow@noreply.localhost>
2026-09-03 21:39:15 +02:00
Hut512 8401f67b24 Fix CRIU subserver stop 2026-09-03 02:24:49 +02:00
119 changed files with 4204 additions and 1426 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ dependencies {
compileOnly(libs.netty)
implementation(libs.luaj)
implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar"))
// implementation(files("$projectDir/../libs/YAPION-SNAPSHOT.jar"))
}
widener {
@@ -20,8 +20,7 @@
package de.steamwar.bausystem;
import de.steamwar.bausystem.config.BauServer;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.bausystem.configplayer.ConfigConverter;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.features.gui.BauGUI;
import de.steamwar.bausystem.features.script.lua.SteamWarLuaPlugin;
import de.steamwar.bausystem.features.script.lua.libs.LuaLib;
@@ -36,7 +35,6 @@ import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.RegionSystem;
import de.steamwar.bausystem.utils.ScoreboardElement;
import de.steamwar.bausystem.utils.TickManager;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.command.AbstractValidator;
import de.steamwar.command.SWCommandUtils;
import de.steamwar.core.CRIUSleepEvent;
@@ -112,9 +110,6 @@ public class BauSystem extends JavaPlugin implements Listener {
if (any instanceof PanzernAlgorithm) {
Panzern.add((PanzernAlgorithm) any);
}
if (any instanceof ConfigConverter) {
Config.addConfigConverter((ConfigConverter) any);
}
if (any instanceof BoundingBoxLoader) {
((BoundingBoxLoader) any).load();
}
@@ -160,9 +155,8 @@ public class BauSystem extends JavaPlugin implements Listener {
@Override
public void onDisable() {
linker.unlink();
WorldData.write();
RegionSystem.INSTANCE.save();
Config.getInstance().saveAll();
ConfigStore.saveAll();
}
private <T extends CommandSender> AbstractValidator<T, ?> validator(Permission permission) {
@@ -0,0 +1,47 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config;
/**
* Converts a config of one version into the next version, e.g. ConfigV1 to
* ConfigV2. The converter for the second newest version converts into the
* current version. Converters are registered in the {@link ConfigStore}
* of the config.
*
* @param <F> the config version to convert from
* @param <T> the config version to convert to
*/
public interface ConfigConverter<F extends VersionedConfig, T extends VersionedConfig> {
/**
* @return the version this converter converts from
*/
int version();
/**
* Converts the config to the next version. If anything goes wrong do not
* silently exit this method, throw an Exception. The {@link ConfigStore}
* will deal with it.
*
* @param config the config to convert
* @return the converted config
*/
T convert(F config);
}
@@ -0,0 +1,364 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.steamwar.sql.UserConfig;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* Loads, converts and saves a versioned config stored as JSON. Older versions
* are converted with the registered {@link ConfigConverter}s, legacy formats
* that are not JSON can be read with {@link #legacy(Predicate, Function)}.
* <p>
* A store can be bound to where the config is stored:
* <ul>
* <li>{@link #userConfig(String)}: one config per player in the {@link UserConfig}, use {@link #get(Player)}</li>
* <li>{@link #file(File)}: one config in a file, use {@link #get()}</li>
* <li>unbound: use {@link #load(File)} and {@link #save(File, VersionedConfig)}</li>
* </ul>
*
* @param <T> the current config version
*/
public class ConfigStore<T extends VersionedConfig> {
private static final List<ConfigStore<?>> STORES = new ArrayList<>();
private final Class<T> type;
private final int version;
private final Supplier<T> defaultConfig;
private final Map<Integer, Class<? extends VersionedConfig>> jsonVersions = new HashMap<>();
private final Map<Integer, ConfigConverter<?, ?>> converters = new HashMap<>();
private final List<LegacyFormat> legacyFormats = new ArrayList<>();
private Gson gson = new Gson();
private Integer unversionedVersion = null;
private String userConfigKey = null;
private Predicate<T> removeIf = config -> false;
private final Map<UUID, T> playerConfigs = new ConcurrentHashMap<>();
private File file = null;
private File legacyFile = null;
private Function<String, ? extends VersionedConfig> legacyFileReader = null;
private T fileConfig = null;
private record LegacyFormat(Predicate<String> matches, Function<String, ? extends VersionedConfig> reader) {
}
public ConfigStore(Class<T> type, int version, Supplier<T> defaultConfig) {
this.type = type;
this.version = version;
this.defaultConfig = defaultConfig;
jsonVersions.put(version, type);
STORES.add(this);
}
public ConfigStore<T> gson(Gson gson) {
this.gson = gson;
return this;
}
/**
* Registers an older config version that is stored as JSON.
*/
public ConfigStore<T> jsonVersion(int version, Class<? extends VersionedConfig> type) {
jsonVersions.put(version, type);
return this;
}
public ConfigStore<T> converter(ConfigConverter<?, ?> converter) {
if (converters.putIfAbsent(converter.version(), converter) != null) {
throw new IllegalStateException("Duplicate converter for version " + converter.version() + " of " + type.getSimpleName());
}
return this;
}
/**
* Registers a legacy format that is not JSON, stored strings matching the predicate are read by the reader.
*/
public ConfigStore<T> legacy(Predicate<String> matches, Function<String, ? extends VersionedConfig> reader) {
legacyFormats.add(new LegacyFormat(matches, reader));
return this;
}
/**
* The version of stored JSON without a version field.
*/
public ConfigStore<T> unversioned(int version) {
this.unversionedVersion = version;
return this;
}
/**
* Binds this store to the {@link UserConfig} with the given key, one config per player.
*/
public ConfigStore<T> userConfig(String key) {
this.userConfigKey = key;
return this;
}
/**
* Removes the config from the {@link UserConfig} instead of saving it, if the predicate matches.
*/
public ConfigStore<T> removeIf(Predicate<T> removeIf) {
this.removeIf = removeIf;
return this;
}
/**
* Binds this store to a single file.
*/
public ConfigStore<T> file(File file) {
this.file = file;
return this;
}
/**
* Legacy file to read, if the file of this store does not exist yet.
*/
public ConfigStore<T> legacyFile(File legacyFile, Function<String, ? extends VersionedConfig> reader) {
this.legacyFile = legacyFile;
this.legacyFileReader = reader;
return this;
}
/**
* Reads a stored config and converts it to the current version.
*
* @throws RuntimeException if the config could not be read or converted
*/
public T read(String stored) {
for (LegacyFormat legacyFormat : legacyFormats) {
if (legacyFormat.matches().test(stored)) {
return update(legacyFormat.reader().apply(stored));
}
}
JsonObject json = JsonParser.parseString(stored).getAsJsonObject();
int storedVersion;
if (json.has("version")) {
storedVersion = json.get("version").getAsInt();
} else if (unversionedVersion != null) {
storedVersion = unversionedVersion;
json.addProperty("version", storedVersion);
} else {
throw new IllegalStateException("Missing version of " + type.getSimpleName());
}
Class<? extends VersionedConfig> storedType = jsonVersions.get(storedVersion);
if (storedType == null) {
throw new IllegalStateException("Unknown version " + storedVersion + " of " + type.getSimpleName());
}
return update(gson.fromJson(json, storedType));
}
public String write(T config) {
return gson.toJson(config);
}
/**
* Converts a config to the current version.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public T update(VersionedConfig config) {
while (config.getVersion() < version) {
ConfigConverter converter = converters.get(config.getVersion());
if (converter == null) {
throw new IllegalStateException("No converter found for version " + config.getVersion() + " of " + type.getSimpleName());
}
VersionedConfig converted = converter.convert(config);
if (converted.getVersion() <= config.getVersion()) {
throw new IllegalStateException("Version did not increase after conversion of version " + config.getVersion() + " of " + type.getSimpleName());
}
config = converted;
}
if (!type.isInstance(config)) {
throw new IllegalStateException("Version " + config.getVersion() + " is not the current version of " + type.getSimpleName());
}
return type.cast(config);
}
/**
* @return the config stored in the file, or the default config if the file is missing or empty
* @throws RuntimeException if the config could not be read or converted
*/
public T load(File file) throws IOException {
if (file.length() == 0) {
return defaultConfig.get();
}
return read(Files.readString(file.toPath(), StandardCharsets.UTF_8));
}
public void save(File file, T config) throws IOException {
Files.writeString(file.toPath(), write(config), StandardCharsets.UTF_8);
}
/**
* Get the config of a player, loads it from the {@link UserConfig} and converts it if necessary.
*/
public T get(Player player) {
requireUserConfig();
return playerConfigs.computeIfAbsent(player.getUniqueId(), this::loadPlayer);
}
/**
* Save the config of a player, this does not unload the config.
*/
public void save(Player player) {
requireUserConfig();
T config = playerConfigs.get(player.getUniqueId());
if (config != null) {
savePlayer(player.getUniqueId(), config);
}
}
/**
* Save and unload the config of a player.
*/
public void unload(Player player) {
if (userConfigKey == null) return;
T config = playerConfigs.remove(player.getUniqueId());
if (config != null) {
savePlayer(player.getUniqueId(), config);
}
}
private T loadPlayer(UUID uuid) {
String stored = UserConfig.getConfig(uuid, userConfigKey);
if (stored == null) {
return defaultConfig.get();
}
try {
return read(stored);
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not load " + type.getSimpleName() + " of " + uuid, e);
return defaultConfig.get();
}
}
private void savePlayer(UUID uuid, T config) {
if (removeIf.test(config)) {
UserConfig.removePlayerConfig(uuid, userConfigKey);
} else {
UserConfig.updatePlayerConfig(uuid, userConfigKey, write(config));
}
}
private void requireUserConfig() {
if (userConfigKey == null) {
throw new IllegalStateException(type.getSimpleName() + " is not stored in the UserConfig");
}
}
/**
* Get the config of the file of this store, loads it and converts it if necessary.
*/
public T get() {
requireFile();
if (fileConfig == null) {
fileConfig = loadFile();
save();
}
return fileConfig;
}
/**
* Save the config to the file of this store, if it was loaded.
*/
public void save() {
requireFile();
if (fileConfig == null) return;
try {
save(file, fileConfig);
} catch (IOException e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not write " + file, e);
}
}
private T loadFile() {
try {
if (file.length() != 0) {
return load(file);
}
if (legacyFile != null && legacyFile.length() != 0) {
return update(legacyFileReader.apply(Files.readString(legacyFile.toPath(), StandardCharsets.UTF_8)));
}
return defaultConfig.get();
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not load " + file + ", using default " + type.getSimpleName(), e);
backupBrokenFile();
return defaultConfig.get();
}
}
private void backupBrokenFile() {
if (file.length() == 0) return;
try {
Files.copy(file.toPath(), new File(file.getParentFile(), file.getName() + ".broken").toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Bukkit.getLogger().log(Level.SEVERE, "Could not backup broken " + file, e);
}
}
private void requireFile() {
if (file == null) {
throw new IllegalStateException(type.getSimpleName() + " is not bound to a file");
}
}
static void unloadAll(Player player) {
STORES.forEach(store -> store.unload(player));
}
/**
* Saves every loaded config and unloads the configs of the players.
*/
public static void saveAll() {
for (ConfigStore<?> store : STORES) {
if (store.userConfigKey != null) {
store.playerConfigs.forEach((uuid, config) -> saveUnchecked(store, uuid, config));
store.playerConfigs.clear();
}
if (store.file != null) {
store.save();
}
}
}
@SuppressWarnings("unchecked")
private static <T extends VersionedConfig> void saveUnchecked(ConfigStore<T> store, UUID uuid, Object config) {
store.savePlayer(uuid, (T) config);
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -17,21 +17,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.core.events;
package de.steamwar.bausystem.config;
import de.steamwar.core.Core;
import de.steamwar.core.CRIUSleepEvent;
import de.steamwar.linkage.Linked;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerQuitEvent;
/**
* Saves and unloads the configs of the {@link ConfigStore}s bound to the UserConfig
* when a player quits, configs are loaded on first use.
*/
@Linked
public class ChattingEvent implements Listener {
public class ConfigStoreListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onChat(AsyncPlayerChatEvent event) {
event.setCancelled(true);
Core.MESSAGE.broadcastPrefixless("LOCAL_CHAT", event.getPlayer().getDisplayName(), event.getMessage());
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
ConfigStore.unloadAll(event.getPlayer());
}
@EventHandler
public void onCRIUSleep(CRIUSleepEvent event) {
ConfigStore.saveAll();
}
}
@@ -0,0 +1,29 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config;
/**
* Common interface of every version of a config stored by a {@link ConfigStore},
* the current version (e.g. Config) and every older version (ConfigV1, ConfigV2, ...).
*/
public interface VersionedConfig {
int getVersion();
}
@@ -0,0 +1,95 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config.legacy;
import lombok.experimental.UtilityClass;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Deserializes {@link ItemStack}s that were serialized by YAPION, read with {@link YapionReader}.
*/
@UtilityClass
@Deprecated
public class YapionItemDeserializer {
private static final String TYPE_IDENTIFIER = "@type";
public ItemStack deserializeItem(Object yapion) {
if (yapion == null) return null;
Object result = deserialize(yapion);
if (result instanceof ItemStack itemStack) return itemStack;
throw new IllegalArgumentException("Not an ItemStack: " + result);
}
private Object deserialize(Object yapion) {
if (yapion instanceof List<?> list) {
List<Object> result = new ArrayList<>();
list.forEach(o -> result.add(deserialize(o)));
return result;
}
if (yapion instanceof YapionReader.YapionMap map) {
Map<Object, Object> result = new LinkedHashMap<>();
map.entries().forEach((key, value) -> result.put(deserialize(key), deserialize(value)));
return result;
}
if (yapion instanceof Map<?, ?> object) {
return deserializeObject(object);
}
return yapion;
}
private Object deserializeObject(Map<?, ?> object) {
Map<String, Object> data = new LinkedHashMap<>();
object.forEach((key, value) -> {
if (!TYPE_IDENTIFIER.equals(key)) data.put((String) key, deserialize(value));
});
if (!(object.get(TYPE_IDENTIFIER) instanceof String type)) {
return data;
}
Class<?> javaType = javaUtilClass(type);
if (javaType != null && Collection.class.isAssignableFrom(javaType)) {
return data.getOrDefault("values", new ArrayList<>());
}
if (javaType != null && Map.class.isAssignableFrom(javaType)) {
return data.containsKey("values") ? data.get("values") : data;
}
data.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, type.equals("org.bukkit.inventory.meta.ItemMeta") ? "ItemMeta" : type);
return ConfigurationSerialization.deserializeObject(data);
}
private Class<?> javaUtilClass(String type) {
if (!type.startsWith("java.util.")) return null;
try {
return Class.forName(type);
} catch (ClassNotFoundException e) {
return null;
}
}
}
@@ -0,0 +1,323 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config.legacy;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Minimal reader for the legacy YAPION format, only used to migrate old data.
* <p>
* The result is built from plain java objects:
* <ul>
* <li>YAPION objects {@code {key(value)}} become a {@link LinkedHashMap} of String to value</li>
* <li>YAPION arrays {@code [a,b]} become a {@link List}</li>
* <li>YAPION maps {@code <(a):(b)>} become a {@link YapionMap}</li>
* <li>YAPION values {@code (value)} become null, Boolean, Number or String</li>
* </ul>
*/
@Deprecated
public class YapionReader {
private static final Pattern INTEGER = Pattern.compile("[-+]?\\d+");
private static final Pattern DECIMAL = Pattern.compile("[-+]?(\\d+\\.\\d*|\\.\\d+|\\d+)([eE][-+]?\\d+)?");
public record YapionMap(Map<Object, Object> entries) {
}
public static Map<String, Object> parse(String yapion) {
YapionReader reader = new YapionReader(yapion);
reader.skipWhitespace();
Map<String, Object> result = reader.readObject();
reader.skipWhitespace();
if (reader.index != yapion.length()) {
throw reader.error("Unexpected trailing content");
}
return result;
}
public static double getDouble(Map<?, ?> object, String key, double defaultValue) {
return object.get(key) instanceof Number value ? value.doubleValue() : defaultValue;
}
public static int getInt(Map<?, ?> object, String key, int defaultValue) {
return object.get(key) instanceof Number value ? value.intValue() : defaultValue;
}
public static boolean getBoolean(Map<?, ?> object, String key, boolean defaultValue) {
return object.get(key) instanceof Boolean value ? value : defaultValue;
}
public static String getString(Map<?, ?> object, String key, String defaultValue) {
return object.get(key) instanceof String value ? value : defaultValue;
}
/**
* @return the entries of the array that are YAPION objects
*/
public static List<Map<?, ?>> getObjects(Map<?, ?> object, String key) {
List<Map<?, ?>> objects = new ArrayList<>();
if (object.get(key) instanceof List<?> array) {
array.forEach(o -> {
if (o instanceof Map<?, ?> map) objects.add(map);
});
}
return objects;
}
private final String input;
private int index = 0;
private YapionReader(String input) {
this.input = input;
}
private Object readAny() {
return switch (peek()) {
case '{' -> readObject();
case '[' -> readArray();
case '<' -> readMap();
case '(' -> readValue();
default -> throw error("Expected '{', '[', '<' or '('");
};
}
private Map<String, Object> readObject() {
expect('{');
Map<String, Object> object = new LinkedHashMap<>();
while (true) {
skipWhitespace();
if (peek() == '}') {
index++;
return object;
}
StringBuilder key = new StringBuilder();
while (true) {
char c = peek();
if (c == '(' || c == '{' || c == '[' || c == '<') break;
if (c == '}') throw error("Key without value");
key.append(readChar());
}
object.put(key.toString(), readAny());
}
}
private List<Object> readArray() {
expect('[');
List<Object> array = new ArrayList<>();
skipWhitespace();
if (peek() == ']') {
index++;
return array;
}
while (true) {
int start = index;
skipWhitespace();
char c = peek();
if (c == '{' || c == '[' || c == '<' || c == '(') {
array.add(readAny());
skipWhitespace();
} else {
// Whitespace is part of plain values
index = start;
StringBuilder value = new StringBuilder();
while (peek() != ',' && peek() != ']') {
value.append(readChar());
}
array.add(typed(value.toString()));
}
char separator = input.charAt(index++);
if (separator == ']') return array;
if (separator != ',') throw error("Expected ',' or ']'");
}
}
private YapionMap readMap() {
expect('<');
Map<Object, Object> map = new LinkedHashMap<>();
while (true) {
skipWhitespace();
if (peek() == '>') {
index++;
return new YapionMap(map);
}
Object key = readAny();
skipWhitespace();
expect(':');
skipWhitespace();
map.put(key, readAny());
}
}
/**
* YAPION does not escape parentheses in values, balanced parentheses are part of the value.
* A backslash only escapes a backslash, a unicode sequence or a control character, otherwise it is kept.
*/
private Object readValue() {
expect('(');
StringBuilder value = new StringBuilder();
int depth = 0;
while (true) {
char c = peek();
if (c == '\\') {
value.append(readValueEscape());
continue;
}
index++;
if (c == '(') {
depth++;
} else if (c == ')') {
if (depth == 0) break;
depth--;
}
value.append(c);
}
return typed(value.toString());
}
private String readValueEscape() {
index++;
if (index >= input.length()) return "\\";
char escaped = input.charAt(index);
switch (escaped) {
case '\\', '(', ')':
index++;
return String.valueOf(escaped);
case 'n':
index++;
return "\n";
case 't':
index++;
return "\t";
case 'r':
index++;
return "\r";
case 'u':
if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) {
char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16);
index += 5;
return String.valueOf(unicode);
}
return "\\";
default:
return "\\";
}
}
/**
* Reads one character of a key or plain array value, resolving backslash escapes.
* A backslash that does not escape anything is kept, as the old player config
* collapsed escaped backslashes into a single one.
*/
private char readChar() {
char c = input.charAt(index++);
if (c != '\\') return c;
if (index >= input.length()) throw error("Unterminated escape");
char escaped = input.charAt(index);
switch (escaped) {
case 'n':
index++;
return '\n';
case 't':
index++;
return '\t';
case 'r':
index++;
return '\r';
case 'u':
if (index + 5 <= input.length() && input.substring(index + 1, index + 5).matches("[0-9a-fA-F]{4}")) {
char unicode = (char) Integer.parseInt(input.substring(index + 1, index + 5), 16);
index += 5;
return unicode;
}
return '\\';
case '\\', ',', ' ', '(', ')', '{', '}', '[', ']', '<', '>', '"', ':', '@':
index++;
return escaped;
default:
return '\\';
}
}
private static Object typed(String value) {
if (value.equals("null")) return null;
if (value.equals("true")) return true;
if (value.equals("false")) return false;
if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) {
return value.substring(1, value.length() - 1);
}
try {
if (INTEGER.matcher(value).matches()) {
long l = Long.parseLong(value);
if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) return (int) l;
return l;
}
if (value.length() > 1) {
String number = value.substring(0, value.length() - 1);
switch (value.charAt(value.length() - 1)) {
case 'L':
if (INTEGER.matcher(number).matches()) return Long.parseLong(number);
break;
case 'S':
if (INTEGER.matcher(number).matches()) return Short.parseShort(number);
break;
case 'B':
if (INTEGER.matcher(number).matches()) return Byte.parseByte(number);
break;
case 'F':
if (DECIMAL.matcher(number).matches()) return Float.parseFloat(number);
break;
case 'D':
if (DECIMAL.matcher(number).matches()) return Double.parseDouble(number);
break;
default:
break;
}
}
if (DECIMAL.matcher(value).matches()) return Double.parseDouble(value);
} catch (NumberFormatException e) {
// Not a number, fall through to String
}
return value;
}
private void skipWhitespace() {
while (index < input.length() && Character.isWhitespace(input.charAt(index))) {
index++;
}
}
private char peek() {
if (index >= input.length()) throw error("Unexpected end of input");
return input.charAt(index);
}
private void expect(char c) {
if (peek() != c) throw error("Expected '" + c + "'");
index++;
}
private IllegalArgumentException error(String message) {
return new IllegalArgumentException(message + " at index " + index);
}
}
@@ -0,0 +1,63 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.config.serializer;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import java.io.IOException;
import java.util.Base64;
import java.util.logging.Level;
/**
* Stores an {@link ItemStack} as Base64 of {@link ItemStack#serializeAsBytes()}, which
* contains the data version and gets upgraded on newer Minecraft versions.
* Empty items are stored as null, items that can't be read are deserialized as null.
*/
public class ItemStackTypeAdapter extends TypeAdapter<ItemStack> {
@Override
public void write(JsonWriter out, ItemStack itemStack) throws IOException {
if (itemStack == null || itemStack.isEmpty()) {
out.nullValue();
return;
}
out.value(Base64.getEncoder().encodeToString(itemStack.serializeAsBytes()));
}
@Override
public ItemStack read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String value = in.nextString();
try {
return ItemStack.deserializeBytes(Base64.getDecoder().decode(value));
} catch (Exception e) {
Bukkit.getLogger().log(Level.WARNING, "Could not deserialize ItemStack", e);
return null;
}
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -19,129 +19,77 @@
package de.steamwar.bausystem.configplayer;
import de.steamwar.bausystem.configplayer.serializer.ConfigurationSerializableSerializer;
import de.steamwar.linkage.Linked;
import de.steamwar.sql.UserConfig;
import com.google.gson.GsonBuilder;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.serializer.ItemStackTypeAdapter;
import de.steamwar.bausystem.configplayer.converter.ConfigV1ToV2Converter;
import de.steamwar.bausystem.features.hotbar.DefaultHotbar;
import lombok.AccessLevel;
import lombok.Getter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import yapion.hierarchy.output.StringOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import yapion.serializing.SerializeManager;
import lombok.Setter;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
@Linked
public class Config implements Listener {
/**
* The current player config. It is stored as JSON, fields missing in the
* stored JSON keep the default value of their initializer, so adding a new
* field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to ConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION},
* write a {@link ConfigConverter} from the copy to this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class Config implements VersionedConfig {
static {
SerializeManager.add(new ConfigurationSerializableSerializer());
}
public static final int VERSION = 2;
@Getter
private static Config instance;
public static final ConfigStore<Config> STORE = new ConfigStore<>(Config.class, VERSION, Config::new)
.gson(new GsonBuilder().registerTypeHierarchyAdapter(ItemStack.class, new ItemStackTypeAdapter()).create())
.legacy(s -> s.startsWith("{@version(1)"), ConfigV1::parse)
.converter(new ConfigV1ToV2Converter())
.userConfig("bausystem");
{
instance = this;
}
private final Map<UUID, YAPIONObject> playerConfigurations = new HashMap<>();
private static final Map<Integer, ConfigConverter> CONFIG_CONVERTER_MAP = new HashMap<>();
public static void addConfigConverter(ConfigConverter configConverter) {
CONFIG_CONVERTER_MAP.putIfAbsent(configConverter.version(), configConverter);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
get(event.getPlayer());
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
save(event.getPlayer());
playerConfigurations.remove(event.getPlayer().getUniqueId());
}
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Get a PlayerConfig, optionally loads it from the DataBase and migrates it if necessary.
*
* @param player the player from whom to get the config.
* @return the config object
* BauGui item id to inventory slot
*/
public YAPIONObject get(Player player) {
UUID uuid = player.getUniqueId();
if (!playerConfigurations.containsKey(uuid)) {
String s = UserConfig.getConfig(uuid, "bausystem");
YAPIONObject yapionObject;
if (s == null) {
yapionObject = ConfigCreator.createDefaultConfig();
} else {
yapionObject = YAPIONParser.parse(s);
}
yapionObject = update(yapionObject);
playerConfigurations.put(uuid, yapionObject);
return yapionObject;
}
return playerConfigurations.get(uuid);
}
public void saveAll() {
playerConfigurations.forEach((uuid, yapionObject) -> {
String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\+", "\\");
UserConfig.updatePlayerConfig(uuid, "bausystem", string);
});
playerConfigurations.clear();
}
protected Map<Integer, Integer> bauGui = defaultBauGui();
protected int bauGuiSize = 45;
/**
* Save a PlayerConfig, this does not remove the key value mapping from the map.
*
* @param player the player to save the config.
* Slots 0-8 are the hotbar, slots 9-12 the armor contents
*/
public void save(Player player) {
UUID uuid = player.getUniqueId();
if (playerConfigurations.containsKey(uuid)) {
YAPIONObject yapionObject = playerConfigurations.get(uuid);
String string = yapionObject.toYAPION(new StringOutput()).getResult().replaceAll("\\\\+", "\\\\");
UserConfig.updatePlayerConfig(uuid, "bausystem", string);
}
}
protected ItemStack[] hotbar = DefaultHotbar.defaultHotbar();
private YAPIONObject update(YAPIONObject yapionObject) {
int version = yapionObject.getPlainValue("@version");
while (version < ConfigCreator.currentVersion) {
ConfigConverter configConverter = CONFIG_CONVERTER_MAP.getOrDefault(version, null);
if (configConverter == null) {
Bukkit.getLogger().log(Level.SEVERE, "No updater found for version " + version);
return ConfigCreator.createDefaultConfig();
}
try {
configConverter.update(yapionObject);
} catch (Exception e) {
Bukkit.getLogger().log(Level.SEVERE, e.getMessage(), e);
return ConfigCreator.createDefaultConfig();
}
int newVersion = yapionObject.getPlainValue("@version");
if (version == newVersion) {
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was the same after conversion");
return ConfigCreator.createDefaultConfig();
}
if (newVersion < version) {
Bukkit.getLogger().log(Level.SEVERE, "Version Tag was earlier after conversion");
return ConfigCreator.createDefaultConfig();
}
version = newVersion;
}
return yapionObject;
protected boolean nightVision = false;
protected boolean tntClickDetails = true;
protected boolean detonatorAutostart = false;
protected boolean inventoryFill = false;
protected boolean rayVisualizer = false;
protected boolean smartPlace = false;
protected boolean blockCounter = false;
protected boolean depthMessage = true;
public static Map<Integer, Integer> defaultBauGui() {
// 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ?
// 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16
// 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11
// 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18
// 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ?
Map<Integer, Integer> bauGui = new HashMap<>();
bauGui.put(10, 2); bauGui.put(3, 3); bauGui.put(7, 4); bauGui.put(17, 5); bauGui.put(15, 6);
bauGui.put(5, 9); bauGui.put(4, 18); bauGui.put(6, 27);
bauGui.put(16, 17); bauGui.put(11, 26); bauGui.put(18, 35);
bauGui.put(19, 20); bauGui.put(21, 21); bauGui.put(9, 22); bauGui.put(0, 23); bauGui.put(1, 24);
bauGui.put(23, 37); bauGui.put(20, 38); bauGui.put(8, 39); bauGui.put(22, 40); bauGui.put(26, 41); bauGui.put(12, 42); bauGui.put(14, 43);
return bauGui;
}
}
@@ -1,56 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.configplayer;
import yapion.hierarchy.types.YAPIONObject;
/**
* A new {@link ConfigConverter} should be written when you remove anything
* from the Config or modify any mayor part. When you move anything from
* any key to any other key you should write a new {@link ConfigConverter}.
* For adding any new key you should be able to get the default without
* having it in the Config. Anything you need to change you should also
* change the {@link ConfigCreator} accordingly, to produce the new Config.
*/
public interface ConfigConverter {
/**
* This describes the version this Converter can convert from. The version
* it should convert to is the version 1 above this number. But this is not
* a necessity. In the config Object as parameter given in {@link #update(YAPIONObject)}
* you should update the <b>@version</b> variable in the root object to the
* new version this converter produced.
*
* @return the version number
*/
int version();
/**
* This method should update everything needed to go from a lower config
* version to a higher. It should update the <b>@version</b> variable
* accordingly. Anything else is up the implementation. If anything goes wrong
* do not silently exit this method, throw an Exception. The updater Code will
* deal with it. Never leave the inputted object in a corrupted state.
*
* @param config the config object to update
*/
void update(YAPIONObject config);
}
@@ -1,61 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.configplayer;
import de.steamwar.bausystem.features.hotbar.DefaultHotbar;
import lombok.experimental.UtilityClass;
import yapion.hierarchy.types.YAPIONObject;
@UtilityClass
public class ConfigCreator {
public static final int currentVersion = 1;
public YAPIONObject createDefaultConfig() {
YAPIONObject yapionObject = new YAPIONObject();
// This call should never be touched
yapionObject.add("@version", currentVersion);
// Any initialising goes into here
yapionObject.add("baugui", defaultBauGui());
// Default Hotbar Gui
yapionObject.add("hotbar", DefaultHotbar.defaultHotbar());
return yapionObject;
}
private YAPIONObject defaultBauGui() {
YAPIONObject yapionObject = new YAPIONObject();
// 0: ? | 1: ? | 2: 10 | 3: 3 | 4: 7 | 5: 17 | 6: 15 | 7: ? | 8: ?
// 9: 5 | 10: ? | 11: ? | 12: ? | 13: ? | 14: ? | 15: ? | 16: ? | 17: 16
// 18: 4 | 19: ? | 20: 19 | 21: 21 | 22: 9 | 23: 0 | 24: 1 | 25: ? | 26: 11
// 27: 6 | 28: ? | 29: ? | 30: ? | 31: ? | 32: ? | 33: ? | 34: ? | 35: 18
// 36: ? | 37: 23 | 38: 20 | 39: 8 | 40: 22 | 41: 26 | 42: 12 | 43: 14 | 44: ?
yapionObject.add("10", 2).add("3", 3).add("7", 4).add("17", 5).add("15", 6);
yapionObject.add("5", 9).add("4", 18).add("6", 27);
yapionObject.add("16", 17).add("11", 26).add("18", 35);
yapionObject.add("19", 20).add("21", 21).add("9", 22).add("0", 23).add("1", 24);
yapionObject.add("23", 37).add("20", 38).add("8", 39).add("22", 40).add("26", 41).add("12", 42).add("14", 43);
yapionObject.add("size", 45);
return yapionObject;
}
}
@@ -0,0 +1,104 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.configplayer;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The legacy config that was stored in the YAPION format.
*/
@Getter
@Setter
public class ConfigV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* BauGui item id to inventory slot
*/
protected Map<Integer, Integer> bauGui = new HashMap<>();
protected int bauGuiSize = 45;
/**
* Minecraft version to the hotbar serialized by YAPION, read with {@link YapionReader}.
* Use {@link de.steamwar.bausystem.config.legacy.YapionItemDeserializer} to get the items.
*/
protected Map<Integer, List<?>> hotbars = new HashMap<>();
protected boolean nightVision = false;
protected boolean tntClickDetails = true;
protected boolean detonatorAutostart = false;
protected boolean inventoryFill = false;
protected boolean rayVisualizer = false;
protected boolean smartPlace = false;
protected boolean blockCounter = false;
protected boolean depthMessage = true;
public static ConfigV1 parse(String yapion) {
Map<String, Object> object = YapionReader.parse(yapion);
ConfigV1 config = new ConfigV1();
config.version = (Integer) object.get("@version");
if (object.get("baugui") instanceof Map<?, ?> bauGui) {
bauGui.forEach((key, value) -> {
if (key.equals("size")) {
config.bauGuiSize = (Integer) value;
} else {
config.bauGui.put(Integer.parseInt((String) key), (Integer) value);
}
});
}
object.forEach((key, value) -> {
if (!(value instanceof List<?> hotbar)) return;
if (key.equals("hotbar")) {
// Hotbars were stored without version prior to 1.19
config.hotbars.putIfAbsent(19, hotbar);
} else if (key.startsWith("hotbar-")) {
config.hotbars.put(Integer.parseInt(key.substring("hotbar-".length())), hotbar);
}
});
config.nightVision = getBoolean(object, "nightvision", config.nightVision);
config.tntClickDetails = getBoolean(object, "tnt_click_details", config.tntClickDetails);
config.detonatorAutostart = getBoolean(object, "detonator-autostart", config.detonatorAutostart);
config.inventoryFill = getBoolean(object, "inventoryfill", config.inventoryFill);
config.rayVisualizer = getBoolean(object, "rayvisualizer", config.rayVisualizer);
config.smartPlace = getBoolean(object, "smartPlace", config.smartPlace);
config.blockCounter = getBoolean(object, "blockCounter", config.blockCounter);
config.depthMessage = getBoolean(object, "depth_message", config.depthMessage);
return config;
}
private static boolean getBoolean(Map<String, Object> object, String key, boolean defaultValue) {
return object.get(key) instanceof Boolean value ? value : defaultValue;
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.configplayer.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.config.legacy.YapionItemDeserializer;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.bausystem.configplayer.ConfigV1;
import de.steamwar.core.Core;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
@Deprecated
public class ConfigV1ToV2Converter implements ConfigConverter<ConfigV1, Config> {
@Override
public int version() {
return ConfigV1.VERSION;
}
@Override
public Config convert(ConfigV1 old) {
Config config = new Config();
config.setBauGui(new HashMap<>(old.getBauGui()));
config.setBauGuiSize(old.getBauGuiSize());
List<?> hotbar = old.getHotbars().get(Core.getVersion());
if (hotbar != null) {
config.setHotbar(convertHotbar(hotbar));
}
config.setNightVision(old.isNightVision());
config.setTntClickDetails(old.isTntClickDetails());
config.setDetonatorAutostart(old.isDetonatorAutostart());
config.setInventoryFill(old.isInventoryFill());
config.setRayVisualizer(old.isRayVisualizer());
config.setSmartPlace(old.isSmartPlace());
config.setBlockCounter(old.isBlockCounter());
config.setDepthMessage(old.isDepthMessage());
return config;
}
private ItemStack[] convertHotbar(List<?> hotbar) {
ItemStack[] items = new ItemStack[13];
for (int i = 0; i < Math.min(hotbar.size(), items.length); i++) {
try {
items[i] = YapionItemDeserializer.deserializeItem(hotbar.get(i));
} catch (Exception e) {
Bukkit.getLogger().log(Level.WARNING, "Could not convert hotbar item " + i, e);
}
}
return items;
}
}
@@ -1,90 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.configplayer.serializer;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import yapion.hierarchy.api.groups.YAPIONAnyType;
import yapion.hierarchy.types.YAPIONObject;
import yapion.serializing.api.SerializerObject;
import yapion.serializing.data.DeserializeData;
import yapion.serializing.data.SerializeData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import static yapion.utils.IdentifierUtils.TYPE_IDENTIFIER;
public class ConfigurationSerializableSerializer extends SerializerObject<ConfigurationSerializable> {
@Override
public Class<ConfigurationSerializable> type() {
return ConfigurationSerializable.class;
}
@Override
public boolean isInterface() {
return true;
}
@Override
public YAPIONObject serialize(SerializeData<ConfigurationSerializable> serializeData) {
YAPIONObject yapionObject = new YAPIONObject();
yapionObject.add(TYPE_IDENTIFIER, serializeData.object.getClass().getTypeName());
if (serializeData.object instanceof ItemStack) {
yapionObject.add(TYPE_IDENTIFIER, ItemStack.class.getTypeName());
}
if (serializeData.object instanceof ItemMeta) {
yapionObject.add(TYPE_IDENTIFIER, ItemMeta.class.getTypeName());
}
Map<String, Object> serializeDataMap = serializeData.object.serialize();
serializeDataMap.forEach((s, o) -> {
YAPIONAnyType yapionAnyType = serializeData.serialize(o);
if (yapionAnyType instanceof YAPIONObject) {
YAPIONObject object = (YAPIONObject) yapionAnyType;
if (object.containsKey(TYPE_IDENTIFIER) && object.getPlainValue(TYPE_IDENTIFIER).equals("com.google.common.collect.RegularImmutableList")) {
object.put(TYPE_IDENTIFIER, ArrayList.class.getTypeName());
}
}
yapionObject.add(s, yapionAnyType);
});
return yapionObject;
}
@Override
public ConfigurationSerializable deserialize(DeserializeData<YAPIONObject> deserializeData) {
Map<String, Object> deserializeDataMap = new HashMap<>();
deserializeData.object.forEach((s, yapionAnyType) -> {
if (s.equals(TYPE_IDENTIFIER)) {
if (yapionAnyType.toString().equals("(org.bukkit.inventory.meta.ItemMeta)")) {
deserializeDataMap.put("==", "ItemMeta");
} else {
deserializeDataMap.put("==", deserializeData.deserialize(yapionAnyType));
}
return;
}
deserializeDataMap.put(s, deserializeData.deserialize(yapionAnyType));
});
return ConfigurationSerialization.deserializeObject(deserializeDataMap);
}
}
@@ -64,7 +64,7 @@ public class Depth {
dimensions.setZ(Math.abs(dimensions.getZ()));
RegionUtils.message(region, player -> {
if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) {
if (Config.STORE.get(player).isDepthMessage()) {
player.spigot().sendMessage(getMessage(player, dimensions.getBlockX() + 1, dimensions.getBlockY() + 1, dimensions.getBlockZ() + 1, tntCount));
}
});
@@ -34,11 +34,11 @@ public class DepthCommand extends SWCommand {
@Register
public void toggle(Player player) {
if (Config.getInstance().get(player).getPlainValueOrDefault("depth_message", true)) {
Config.getInstance().get(player).put("depth_message", false);
if (Config.STORE.get(player).isDepthMessage()) {
Config.STORE.get(player).setDepthMessage(false);
BauSystem.MESSAGE.send("DEPTH_COUNTER_DISABLE", player);
} else {
Config.getInstance().get(player).put("depth_message", true);
Config.STORE.get(player).setDepthMessage(true);
BauSystem.MESSAGE.send("DEPTH_COUNTER_ENABLE", player);
}
}
@@ -95,7 +95,7 @@ public class Detonator {
public static void activateDetonator(DetonatorStorage detonator) {
Player p = detonator.getPlayer();
if (Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false)) {
if (Config.STORE.get(p).isDetonatorAutostart()) {
AutostartListener.instance.activate(p);
}
@@ -76,8 +76,8 @@ public class DetonatorCommand extends SWCommand {
@Register(value = "autostart", description = "DETONATOR_HELP_AUTOSTART")
public void toggleAutostartTimer(Player p) {
boolean current = Config.getInstance().get(p).getPlainValueOrDefault("detonator-autostart", false);
Config.getInstance().get(p).put("detonator-autostart", !current);
boolean current = Config.STORE.get(p).isDetonatorAutostart();
Config.STORE.get(p).setDetonatorAutostart(!current);
if (!current) {
BauSystem.MESSAGE.send("DETONATOR_AUTOSTART_ENABLE", p);
} else {
@@ -28,7 +28,6 @@ import lombok.Setter;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*;
@@ -52,15 +51,15 @@ public class BauGuiMapping {
}
@Getter
private final YAPIONObject object;
private final Config config;
@Getter
private final Player owner;
@Getter
@Setter
private boolean saved;
protected BauGuiMapping(YAPIONObject object, Player p) {
this.object = object;
protected BauGuiMapping(Config config, Player p) {
this.config = config;
this.owner = p;
fromUUID.put(p.getUniqueId(), this);
mappings.add(this);
@@ -70,19 +69,18 @@ public class BauGuiMapping {
BauGuiMapping mapping = fromUUID.get(p.getUniqueId());
if (mapping == null) {
YAPIONObject yapionObject = Config.getInstance().get(p);
mapping = new BauGuiMapping(yapionObject.getObject("baugui"), p);
mapping = new BauGuiMapping(Config.STORE.get(p), p);
}
return mapping;
}
public boolean isShown(int id) {
return object.getPlainValueOrDefault(Integer.toString(id), -1) >= 0;
return config.getBauGui().getOrDefault(id, -1) >= 0;
}
public int getSlot(int id) {
return object.getPlainValue(Integer.toString(id));
return config.getBauGui().get(id);
}
public Map<Integer, Integer> getMapping() {
@@ -94,7 +92,7 @@ public class BauGuiMapping {
}
public int getSize() {
return object.getPlainValueOrDefault("size", 45);
return config.getBauGuiSize();
}
public void setSize(int size) {
@@ -102,7 +100,7 @@ public class BauGuiMapping {
return;
}
object.add("size", size);
config.setBauGuiSize(size);
}
private void tick() {
@@ -111,16 +109,17 @@ public class BauGuiMapping {
public void save() {
if (saved) return;
YAPIONObject config = Config.getInstance().get(owner);
config.add("baugui", object);
Config.getInstance().save(owner);
Config current = Config.STORE.get(owner);
current.setBauGui(config.getBauGui());
current.setBauGuiSize(config.getBauGuiSize());
Config.STORE.save(owner);
saved = true;
}
private Map<Integer, Integer> internalReadMap() {
Map<Integer, Integer> map = new HashMap<>();
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) {
Integer value = object.getPlainValueOrDefault(e.getKey().toString(), -1);
Integer value = config.getBauGui().getOrDefault(e.getKey(), -1);
if (value == -1) continue;
map.put(e.getKey(), value);
}
@@ -129,10 +128,10 @@ public class BauGuiMapping {
private void internalWriteMap(Map<Integer, Integer> map) {
for (Map.Entry<Integer, BauGuiItem> e : BauGUI.getITEMS().entrySet()) {
object.remove(e.getKey().toString());
config.getBauGui().remove(e.getKey());
Integer value = map.getOrDefault(e.getKey(), -1);
if (value == -1) continue;
object.add(e.getKey().toString(), value);
config.getBauGui().put(e.getKey(), value);
}
}
}
@@ -20,39 +20,23 @@
package de.steamwar.bausystem.features.hotbar;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.core.Core;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
import yapion.parser.YAPIONParser;
import yapion.serializing.YAPIONDeserializer;
import yapion.serializing.YAPIONSerializer;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
@UtilityClass
public class DefaultHotbar {
private final YAPIONArray DEFAULT_HOTBAR = YAPIONParser.parse("{[{@type(org.bukkit.inventory.ItemStack)v(2230)type(WOODEN_AXE)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(WorldEdit Wand)lore{@type(java.util.ArrayList)values[Left click: select pos #1,Right click: select pos #2]}}},{@type(org.bukkit.inventory.ItemStack)v(2230)type(COMPASS)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(Navigation Wand)lore{@type(java.util.ArrayList)values[Left click: jump to location,Right click: pass through walls]}}},null,null,null,null,null,null,{@type(org.bukkit.inventory.ItemStack)v(2230)type(NETHER_STAR)meta{@type(org.bukkit.inventory.meta.ItemMeta)meta-type(UNSPECIFIC)display-name(§eBau GUI)}},null,null,{@type(org.bukkit.inventory.ItemStack)v(3117)type(ELYTRA)},null]}").getArray("");
public void updateHotbar(Player p) {
ItemStack[] hotbar = new ItemStack[13];
System.arraycopy(p.getInventory().getContents(), 0, hotbar, 0, 9);
System.arraycopy(p.getInventory().getArmorContents(), 0, hotbar, 9, 4);
YAPIONArray yapionArray = new YAPIONArray();
for (ItemStack itemStack : hotbar) {
if (itemStack != null) {
yapionArray.add(YAPIONSerializer.serialize(itemStack));
} else {
yapionArray.add(new YAPIONValue<>(null));
}
}
Config.getInstance().get(p).add("hotbar-" + Core.getVersion(), yapionArray);
Config.getInstance().save(p);
Config.STORE.get(p).setHotbar(copy(hotbar));
Config.STORE.save(p);
}
public void setHotbar(Player p) {
@@ -66,28 +50,34 @@ public class DefaultHotbar {
}
public ItemStack[] getItems(Player p) {
Config.getInstance().get(p).remap("hotbar", "hotbar-19");
YAPIONArray yapionArray = Config.getInstance().get(p).getYAPIONArrayOrSetDefault("hotbar-" + Core.getVersion(), defaultHotbar());
return copy(Config.STORE.get(p).getHotbar());
}
public ItemStack[] defaultHotbar() {
ItemStack[] hotbar = new ItemStack[13];
Set<Integer> invalid = new HashSet<>();
yapionArray.forEach((integer, yapionAnyType) -> {
if (yapionAnyType instanceof YAPIONValue) {
hotbar[integer] = null;
} else {
try {
hotbar[integer] = YAPIONDeserializer.deserialize((YAPIONObject) yapionAnyType);
} catch (Exception e) {
invalid.add(integer);
hotbar[integer] = null;
}
}
});
invalid.forEach(i -> yapionArray.set(i, new YAPIONValue<>(null)));
if (!invalid.isEmpty()) Config.getInstance().save(p);
hotbar[0] = item(Material.WOODEN_AXE, "WorldEdit Wand", "Left click: select pos #1", "Right click: select pos #2");
hotbar[1] = item(Material.COMPASS, "Navigation Wand", "Left click: jump to location", "Right click: pass through walls");
hotbar[8] = item(Material.NETHER_STAR, "§eBau GUI");
hotbar[11] = new ItemStack(Material.ELYTRA);
return hotbar;
}
public YAPIONArray defaultHotbar() {
return DEFAULT_HOTBAR;
private ItemStack item(Material material, String name, String... lore) {
ItemStack itemStack = new ItemStack(material);
ItemMeta meta = itemStack.getItemMeta();
meta.setDisplayName(name);
if (lore.length > 0) {
meta.setLore(List.of(lore));
}
itemStack.setItemMeta(meta);
return itemStack;
}
private ItemStack[] copy(ItemStack[] hotbar) {
ItemStack[] copy = new ItemStack[13];
for (int i = 0; i < Math.min(hotbar.length, copy.length); i++) {
copy[i] = hotbar[i] == null ? null : hotbar[i].clone();
}
return copy;
}
}
@@ -49,7 +49,7 @@ public class InventoryFillBauGuiItem extends BauGuiItem {
@Override
public ItemStack getItem(Player player) {
String loreKey = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false) ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE";
String loreKey = Config.STORE.get(player).isInventoryFill() ? "OTHER_ITEMS_INVENTORY_FILL_LORE_ACTIVE" : "OTHER_ITEMS_INVENTORY_FILL_LORE_INACTIVE";
return new SWItem(Material.HOPPER, BauSystem.MESSAGE.parse("OTHER_ITEMS_INVENTORY_FILL_NAME", player), Collections.singletonList(BauSystem.MESSAGE.parse(loreKey, player)), false, clickType -> {
}).getItemStack();
}
@@ -40,7 +40,7 @@ public class InventoryFiller implements Listener {
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return;
if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return;
if (!event.getPlayer().isSneaking()) return;
Block block = event.getPlayer().getTargetBlockExact(5);
if (block == null) return;
@@ -62,7 +62,7 @@ public class InventoryFiller implements Listener {
@EventHandler
public void onPlayerItemHeld(PlayerItemHeldEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("inventoryfill", false)) return;
if (!Config.STORE.get(event.getPlayer()).isInventoryFill()) return;
if (!event.getPlayer().isSneaking()) return;
ItemStack itemStack = event.getPlayer().getInventory().getItemInMainHand();
if (itemStack.getType() == Material.AIR) return;
@@ -68,8 +68,8 @@ public class InventoryFillerCommand extends SWCommand {
@Register(description = {"INVENTORY_FILL_HELP", "INVENTORY_FILL_INFO"})
public void toggle(Player player) {
boolean inventoryFill = Config.getInstance().get(player).getPlainValueOrDefault("inventoryfill", false);
Config.getInstance().get(player).put("inventoryfill", !inventoryFill);
boolean inventoryFill = Config.STORE.get(player).isInventoryFill();
Config.STORE.get(player).setInventoryFill(!inventoryFill);
if (!inventoryFill) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("INVENTORY_FILL_ENABLE", player));
BauSystem.MESSAGE.send("INVENTORY_FILL_INFO", player);
@@ -46,7 +46,7 @@ public class ObserverTracerListener implements Listener {
public ObserverTracerListener() {
Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> {
SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> {
if (pair.getPlayer().getGameMode() != GameMode.SPECTATOR) return;
if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return;
pair.getComponent().show();
});
}, 15L, 15L);
@@ -167,7 +167,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
boolean rayvisualizer = Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("rayvisualizer", false);
boolean rayvisualizer = Config.STORE.get(event.getPlayer()).isRayVisualizer();
if (rayvisualizer) server.addPlayer(event.getPlayer());
}
@@ -181,7 +181,7 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (Permission.BUILD.hasPermission(player)) {
boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer();
if (rayvisualizer) server.addPlayer(player);
} else {
server.removePlayer(player);
@@ -192,8 +192,8 @@ public class RayVisualizerCommand extends SWCommand implements Listener {
@Register
public void toggle(@Validator Player player) {
boolean rayvisualizer = Config.getInstance().get(player).getPlainValueOrDefault("rayvisualizer", false);
Config.getInstance().get(player).put("rayvisualizer", !rayvisualizer);
boolean rayvisualizer = Config.STORE.get(player).isRayVisualizer();
Config.STORE.get(player).setRayVisualizer(!rayvisualizer);
if (!rayvisualizer) {
SWUtils.sendToActionbar(player, BauSystem.MESSAGE.parse("RAY_VISUALIZER_ENABLE", player));
server.addPlayer(player);
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.inventory.InvCallback;
import de.steamwar.inventory.SWItem;
import lombok.Getter;
@@ -28,7 +29,6 @@ import lombok.Setter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.Comparator;
@@ -121,9 +121,9 @@ public abstract class SimulatorElement<T extends SimulatorPhase> {
public abstract String getType();
public void saveExtra(YAPIONObject elementObject) {
public void saveExtra(SimulatorConfig.Element element) {
}
public void loadExtra(YAPIONObject elementObject) {
public void loadExtra(SimulatorConfig.Element element) {
}
}
@@ -20,10 +20,10 @@
package de.steamwar.bausystem.features.simulator.data;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -38,9 +38,9 @@ public abstract class SimulatorPhase {
public abstract void toSimulatorActions(Vector position, BiConsumer<Integer, SimulatorAction> tickStart, BiConsumer<Integer, SimulatorAction> tickEnd);
public void saveExtra(YAPIONObject phaseObject) {
public void saveExtra(SimulatorConfig.Phase phase) {
}
public void loadExtra(YAPIONObject phaseObject) {
public void loadExtra(SimulatorConfig.Phase phase) {
}
}
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.observer;
import de.steamwar.bausystem.features.simulator.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -31,7 +32,6 @@ import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.type.Observer;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -70,12 +70,12 @@ public final class ObserverPhase extends SimulatorPhase {
}
@Override
public void saveExtra(YAPIONObject phaseObject) {
phaseObject.add("orientation", orientation.name());
public void saveExtra(SimulatorConfig.Phase phase) {
phase.setOrientation(orientation);
}
@Override
public void loadExtra(YAPIONObject phaseObject) {
orientation = BlockFace.valueOf(phaseObject.getPlainValue("orientation"));
public void loadExtra(SimulatorConfig.Phase phase) {
if (phase.getOrientation() != null) orientation = phase.getOrientation();
}
}
@@ -24,13 +24,13 @@ import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.gui.SimulatorTNTGui;
import de.steamwar.bausystem.features.simulator.gui.base.SimulatorBaseGui;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.inventory.InvCallback;
import de.steamwar.inventory.SWItem;
import lombok.Getter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
public final class TNTElement extends SimulatorElement<TNTPhase> {
@@ -100,14 +100,14 @@ public final class TNTElement extends SimulatorElement<TNTPhase> {
}
@Override
public void saveExtra(YAPIONObject elementObject) {
elementObject.add("alignmentX", alignment.getX());
elementObject.add("alignmentZ", alignment.getZ());
public void saveExtra(SimulatorConfig.Element element) {
element.setAlignmentX(alignment.getX());
element.setAlignmentZ(alignment.getZ());
}
@Override
public void loadExtra(YAPIONObject elementObject) {
alignment.setX(elementObject.getDoubleOrSetDefault("alignmentX", 0));
alignment.setZ(elementObject.getDoubleOrSetDefault("alignmentZ", 0));
public void loadExtra(SimulatorConfig.Element element) {
alignment.setX(element.getAlignmentX() != null ? element.getAlignmentX() : 0);
alignment.setZ(element.getAlignmentZ() != null ? element.getAlignmentZ() : 0);
}
}
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.simulator.data.tnt;
import de.steamwar.bausystem.features.simulator.data.SimulatorPhase;
import de.steamwar.bausystem.features.simulator.execute.SimulatorAction;
import de.steamwar.bausystem.features.simulator.storage.SimulatorConfig;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.FreezeMode;
@@ -31,7 +32,6 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.TNTPrimed;
import org.bukkit.util.Vector;
import yapion.hierarchy.types.YAPIONObject;
import java.util.function.BiConsumer;
@@ -83,18 +83,18 @@ public final class TNTPhase extends SimulatorPhase {
}
@Override
public void saveExtra(YAPIONObject phaseObject) {
phaseObject.add("count", count);
phaseObject.add("xJump", xJump);
phaseObject.add("yJump", yJump);
phaseObject.add("zJump", zJump);
public void saveExtra(SimulatorConfig.Phase phase) {
phase.setCount(count);
phase.setXJump(xJump);
phase.setYJump(yJump);
phase.setZJump(zJump);
}
@Override
public void loadExtra(YAPIONObject phaseObject) {
count = phaseObject.getPlainValue("count");
xJump = phaseObject.getPlainValue("xJump");
yJump = phaseObject.getPlainValue("yJump");
zJump = phaseObject.getPlainValue("zJump");
public void loadExtra(SimulatorConfig.Phase phase) {
if (phase.getCount() != null) count = phase.getCount();
if (phase.getXJump() != null) xJump = phase.getXJump();
if (phase.getYJump() != null) yJump = phase.getYJump();
if (phase.getZJump() != null) zJump = phase.getZJump();
}
}
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage;
import com.google.gson.JsonParseException;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorElement;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
@@ -29,12 +30,7 @@ import de.steamwar.bausystem.features.simulator.data.redstone.RedstoneElement;
import de.steamwar.bausystem.features.simulator.data.redstone.RedstonePhase;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
@@ -51,44 +47,41 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
SimulatorConfig config;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
config = SimulatorConfig.STORE.load(file);
} catch (JsonParseException | IllegalStateException | IOException e) {
return Optional.empty();
}
String name = file.getName().substring(0, file.getName().length() - ".sim".length());
Simulator simulator = new Simulator(name);
loadSimulator(yapionObject, simulator);
loadSimulator(config, simulator);
return Optional.of(Collections.singletonList(simulator));
}
private void loadSimulator(YAPIONObject simulatorObject, Simulator simulator) {
simulator.setMaterial(Material.valueOf(simulatorObject.getPlainValue("material")));
simulator.setAutoTrace(simulatorObject.getPlainValue("autoTrace"));
simulator.setAutoTestblock(simulatorObject.getPlainValueOrDefault("autoTestblock", false));
private void loadSimulator(SimulatorConfig config, Simulator simulator) {
if (config.getMaterial() != null) simulator.setMaterial(config.getMaterial());
simulator.setAutoTrace(config.isAutoTrace());
simulator.setAutoTestblock(config.isAutoTestblock());
YAPIONArray groups = simulatorObject.getArray("groups");
groups.streamObject().forEach(groupObject -> {
config.getGroups().forEach(groupConfig -> {
SimulatorGroup simulatorGroup = new SimulatorGroup();
loadGroup(groupObject, simulatorGroup);
loadGroup(groupConfig, simulatorGroup);
simulator.add(simulatorGroup);
});
}
private void loadGroup(YAPIONObject groupObject, SimulatorGroup group) {
group.setMaterial(Material.valueOf(groupObject.getPlainValue("material")));
group.setDisabled(groupObject.getPlainValue("disabled"));
private void loadGroup(SimulatorConfig.Group groupConfig, SimulatorGroup group) {
if (groupConfig.getMaterial() != null) group.setMaterial(groupConfig.getMaterial());
group.setDisabled(groupConfig.isDisabled());
YAPIONArray elements = groupObject.getArray("elements");
elements.streamObject().forEach(elementObject -> {
groupConfig.getElements().forEach(elementConfig -> {
SimulatorElement<?> element;
Supplier<SimulatorPhase> phaseConstructor;
Vector position = new Vector(elementObject.getDouble("x"), elementObject.getDouble("y"), elementObject.getDouble("z"));
String type = elementObject.getPlainValue("type");
switch (type) {
Vector position = new Vector(elementConfig.getX(), elementConfig.getY(), elementConfig.getZ());
switch (String.valueOf(elementConfig.getType())) {
case "TNT":
element = new TNTElement(position);
phaseConstructor = TNTPhase::new;
@@ -109,28 +102,27 @@ public class SimFormatSimulatorLoader implements SimulatorLoader {
if (element == null) {
return;
}
loadElement(elementObject, element, phaseConstructor);
loadElement(elementConfig, element, phaseConstructor);
group.add(element);
});
}
private void loadElement(YAPIONObject elementObject, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) {
element.setMaterial(Material.valueOf(elementObject.getPlainValue("material")));
element.setDisabled(elementObject.getPlainValue("disabled"));
element.loadExtra(elementObject);
private void loadElement(SimulatorConfig.Element elementConfig, SimulatorElement<?> element, Supplier<SimulatorPhase> phaseConstructor) {
if (elementConfig.getMaterial() != null) element.setMaterial(elementConfig.getMaterial());
element.setDisabled(elementConfig.isDisabled());
element.loadExtra(elementConfig);
YAPIONArray phases = elementObject.getArray("phases");
phases.streamObject().forEach(phaseObject -> {
elementConfig.getPhases().forEach(phaseConfig -> {
SimulatorPhase phase = phaseConstructor.get();
loadPhase(phaseObject, phase);
loadPhase(phaseConfig, phase);
((SimulatorElement) element).add(phase);
});
}
private void loadPhase(YAPIONObject phaseObject, SimulatorPhase phase) {
phase.setTickOffset(phaseObject.getPlainValue("tickOffset"));
phase.setLifetime(phaseObject.getPlainValue("lifetime"));
phase.setOrder(phaseObject.getPlainValue("order"));
phase.loadExtra(phaseObject);
private void loadPhase(SimulatorConfig.Phase phaseConfig, SimulatorPhase phase) {
phase.setTickOffset(phaseConfig.getTickOffset());
phase.setLifetime(phaseConfig.getLifetime());
phase.setOrder(phaseConfig.getOrder());
phase.loadExtra(phaseConfig);
}
}
@@ -0,0 +1,117 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import java.util.ArrayList;
import java.util.List;
/**
* The current stored form of a simulator, saved as JSON in a .sim file. Files
* saved before the version was stored have no version and are version 1.
* Fields missing in the stored JSON keep the default value of their initializer,
* so adding a new field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to SimulatorConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to
* this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class SimulatorConfig implements VersionedConfig {
public static final int VERSION = 1;
/**
* Files saved before the version was stored are version 1
*/
public static final ConfigStore<SimulatorConfig> STORE = new ConfigStore<>(SimulatorConfig.class, VERSION, SimulatorConfig::new)
.unversioned(1);
@Setter(AccessLevel.NONE)
protected int version = VERSION;
protected Material material = Material.BARREL;
protected boolean autoTrace = false;
protected boolean autoTestblock = false;
protected List<Group> groups = new ArrayList<>();
@Getter
@Setter
public static class Group {
protected Material material = Material.CHEST;
protected boolean disabled = false;
protected List<Element> elements = new ArrayList<>();
}
@Getter
@Setter
public static class Element {
/**
* One of "TNT", "Redstone" or "Observer"
*/
protected String type;
protected Material material;
protected boolean disabled = false;
protected double x;
protected double y;
protected double z;
/**
* Only used by TNT
*/
protected Double alignmentX;
protected Double alignmentZ;
protected List<Phase> phases = new ArrayList<>();
}
@Getter
@Setter
public static class Phase {
protected int tickOffset;
protected int lifetime;
protected int order;
/**
* Only used by TNT
*/
protected Integer count;
protected Boolean xJump;
protected Boolean yJump;
protected Boolean zJump;
/**
* Only used by Observer
*/
protected BlockFace orientation;
}
}
@@ -19,24 +19,30 @@
package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.config.legacy.YapionReader;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.config.legacy.YapionReader.*;
/**
* Loads the legacy .simulator files stored as YAPION, they are deleted after loading
* and saved in the current format by the {@link SimulatorSaver}.
*/
@Deprecated
public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
@Override
@@ -45,20 +51,19 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
Map<String, Object> yapionObject;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IllegalArgumentException | IOException e) {
return Optional.empty();
}
String name = file.getName().substring(0, file.getName().length() - ".simulator".length());
Simulator simulator = new Simulator(name);
simulator.setMaterial(Material.valueOf(yapionObject.getStringOrDefault("material", "BARREL")));
simulator.setMaterial(Material.valueOf(getString(yapionObject, "material", "BARREL")));
YAPIONArray tntElements = yapionObject.getArrayOrDefault("tntElements", new YAPIONArray());
for (YAPIONObject element : tntElements.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> element : getObjects(yapionObject, "tntElements")) {
if (element.containsKey("elements")) {
simulator.add(loadGroup(element));
} else {
@@ -70,27 +75,26 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return Optional.of(Collections.singletonList(simulator));
}
private SimulatorGroup loadGroup(YAPIONObject element) {
double x = element.getDoubleOrDefault("x", 0);
double y = element.getDoubleOrDefault("y", 0);
double z = element.getDoubleOrDefault("z", 0);
int tickOffset = element.getIntOrDefault("tickOffset", 0);
private SimulatorGroup loadGroup(Map<?, ?> element) {
double x = getDouble(element, "x", 0);
double y = getDouble(element, "y", 0);
double z = getDouble(element, "z", 0);
int tickOffset = getInt(element, "tickOffset", 0);
SimulatorGroup simulatorGroup = new SimulatorGroup();
simulatorGroup.setDisabled(element.getBooleanOrDefault("disabled", false));
simulatorGroup.setDisabled(getBoolean(element, "disabled", false));
YAPIONArray elements = element.getArrayOrDefault("elements", new YAPIONArray());
for (YAPIONObject e : elements.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> e : getObjects(element, "elements")) {
simulatorGroup.add(loadElement(e, x, y, z, tickOffset));
}
return simulatorGroup;
}
private TNTElement loadElement(YAPIONObject element, double dx, double dy, double dz, int dTickOffset) {
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x") + dx, element.getDouble("y") + dy, element.getDouble("z") + dz));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
private TNTElement loadElement(Map<?, ?> element, double dx, double dy, double dz, int dTickOffset) {
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x") + dx, requireDouble(element, "y") + dy, requireDouble(element, "z") + dz));
tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
TNTPhase tntPhase = loadPhase(element);
tntPhase.setTickOffset(tntPhase.getTickOffset() + dTickOffset);
@@ -99,12 +103,12 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return tntElement;
}
private SimulatorGroup loadElement(YAPIONObject element) {
private SimulatorGroup loadElement(Map<?, ?> element) {
SimulatorGroup simulatorGroup = new SimulatorGroup();
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("x"), element.getDouble("y"), element.getDouble("z")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false));
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "x"), requireDouble(element, "y"), requireDouble(element, "z")));
tntElement.setDisabled(getBoolean(element, "disabled", false));
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement);
tntElement.add(loadPhase(element));
@@ -112,15 +116,15 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
return simulatorGroup;
}
private TNTPhase loadPhase(YAPIONObject element) {
private TNTPhase loadPhase(Map<?, ?> element) {
TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false));
switch (element.getStringOrDefault("order", "REPEATER")) {
tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(getBoolean(element, "zVelocity", false));
switch (getString(element, "order", "REPEATER")) {
case "REPEATER":
tntPhase.setOrder(0);
break;
@@ -133,4 +137,8 @@ public class SimulatorFormatSimulatorLoader implements SimulatorLoader {
}
return tntPhase;
}
private double requireDouble(Map<?, ?> element, String key) {
return ((Number) element.get(key)).doubleValue();
}
}
@@ -21,9 +21,6 @@ package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import lombok.experimental.UtilityClass;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import java.io.File;
import java.io.IOException;
@@ -36,48 +33,42 @@ public class SimulatorSaver {
directory.mkdirs();
}
YAPIONObject simulatorObject = new YAPIONObject();
simulatorObject.add("material", simulator.getMaterial().name());
simulatorObject.add("autoTrace", simulator.isAutoTrace());
simulatorObject.add("autoTestblock", simulator.isAutoTestblock());
SimulatorConfig config = new SimulatorConfig();
config.setMaterial(simulator.getMaterial());
config.setAutoTrace(simulator.isAutoTrace());
config.setAutoTestblock(simulator.isAutoTestblock());
YAPIONArray groups = new YAPIONArray();
simulator.getGroups().forEach(group -> {
YAPIONObject groupObject = new YAPIONObject();
groupObject.add("material", group.getMaterial().name());
groupObject.add("disabled", group.isDisabled());
SimulatorConfig.Group groupConfig = new SimulatorConfig.Group();
groupConfig.setMaterial(group.getMaterial());
groupConfig.setDisabled(group.isDisabled());
YAPIONArray elements = new YAPIONArray();
group.getElements().forEach(element -> {
YAPIONObject elementObject = new YAPIONObject();
elementObject.add("type", element.getType());
elementObject.add("material", element.getMaterial().name());
elementObject.add("disabled", element.isDisabled());
elementObject.add("x", element.getPosition().getX());
elementObject.add("y", element.getPosition().getY());
elementObject.add("z", element.getPosition().getZ());
element.saveExtra(elementObject);
SimulatorConfig.Element elementConfig = new SimulatorConfig.Element();
elementConfig.setType(element.getType());
elementConfig.setMaterial(element.getMaterial());
elementConfig.setDisabled(element.isDisabled());
elementConfig.setX(element.getPosition().getX());
elementConfig.setY(element.getPosition().getY());
elementConfig.setZ(element.getPosition().getZ());
element.saveExtra(elementConfig);
YAPIONArray phases = new YAPIONArray();
element.getPhases().forEach(phase -> {
YAPIONObject phaseObject = new YAPIONObject();
phaseObject.add("tickOffset", phase.getTickOffset());
phaseObject.add("lifetime", phase.getLifetime());
phaseObject.add("order", phase.getOrder());
phase.saveExtra(phaseObject);
phases.add(phaseObject);
SimulatorConfig.Phase phaseConfig = new SimulatorConfig.Phase();
phaseConfig.setTickOffset(phase.getTickOffset());
phaseConfig.setLifetime(phase.getLifetime());
phaseConfig.setOrder(phase.getOrder());
phase.saveExtra(phaseConfig);
elementConfig.getPhases().add(phaseConfig);
});
elementObject.add("phases", phases);
elements.add(elementObject);
groupConfig.getElements().add(elementConfig);
});
groupObject.add("elements", elements);
groups.add(groupObject);
config.getGroups().add(groupConfig);
});
simulatorObject.add("groups", groups);
File file = new File(directory, simulator.getName() + ".sim");
try {
simulatorObject.toJSONLossy(new FileOutput(file)).close();
SimulatorConfig.STORE.save(file, config);
} catch (IOException e) {
// Ignore
}
@@ -19,6 +19,7 @@
package de.steamwar.bausystem.features.simulator.storage;
import de.steamwar.bausystem.config.legacy.YapionReader;
import de.steamwar.bausystem.features.simulator.data.Simulator;
import de.steamwar.bausystem.features.simulator.data.SimulatorGroup;
import de.steamwar.bausystem.features.simulator.data.tnt.TNTElement;
@@ -26,18 +27,23 @@ import de.steamwar.bausystem.features.simulator.data.tnt.TNTPhase;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import yapion.exceptions.YAPIONException;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.config.legacy.YapionReader.*;
/**
* Loads the legacy per player .yapion files containing multiple simulators, they are
* deleted after loading and saved in the current format by the {@link SimulatorSaver}.
*/
@Deprecated
public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
@Override
@@ -46,10 +52,10 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
return Optional.empty();
}
YAPIONObject yapionObject;
Map<String, Object> yapionObject;
try {
yapionObject = YAPIONParser.parse(file);
} catch (YAPIONException | IOException e) {
yapionObject = YapionReader.parse(Files.readString(file.toPath(), StandardCharsets.UTF_8));
} catch (IllegalArgumentException | IOException e) {
return Optional.empty();
}
@@ -57,28 +63,28 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
SteamwarUser steamwarUser = SteamwarUser.byId(Integer.parseInt(name));
List<Simulator> simulators = new ArrayList<>();
for (String s : yapionObject.getKeys()) {
for (String s : yapionObject.keySet()) {
String newName = steamwarUser.getUserName() + (s.isEmpty() ? "" : "_" + s);
YAPIONArray content = yapionObject.getArray(s);
List<Map<?, ?>> content = getObjects(yapionObject, s);
if (content.isEmpty()) continue;
Simulator simulator = new Simulator(newName);
for (YAPIONObject element : content.streamObject().collect(Collectors.toList())) {
for (Map<?, ?> element : content) {
SimulatorGroup simulatorGroup = new SimulatorGroup();
simulator.add(simulatorGroup);
TNTElement tntElement = new TNTElement(new Vector(element.getDouble("positionX"), element.getDouble("positionY"), element.getDouble("positionZ")));
tntElement.setDisabled(element.getBooleanOrDefault("disabled", false)); // IDK if this existed back then?
tntElement.setMaterial(Material.valueOf(element.getStringOrDefault("material", "TNT")));
TNTElement tntElement = new TNTElement(new Vector(requireDouble(element, "positionX"), requireDouble(element, "positionY"), requireDouble(element, "positionZ")));
tntElement.setDisabled(getBoolean(element, "disabled", false)); // IDK if this existed back then?
tntElement.setMaterial(Material.valueOf(getString(element, "material", "TNT")));
simulatorGroup.add(tntElement);
TNTPhase tntPhase = new TNTPhase();
tntPhase.setLifetime(element.getIntOrDefault("fuseTicks", 80));
tntPhase.setCount(element.getIntOrDefault("count", 1));
tntPhase.setTickOffset(element.getIntOrDefault("tickOffset", 0));
tntPhase.setXJump(element.getBooleanOrDefault("xVelocity", false));
tntPhase.setYJump(element.getBooleanOrDefault("yVelocity", false));
tntPhase.setZJump(element.getBooleanOrDefault("zVelocity", false));
tntPhase.setOrder(element.getBooleanOrDefault("comparator", false) ? 2 : 0);
tntPhase.setLifetime(getInt(element, "fuseTicks", 80));
tntPhase.setCount(getInt(element, "count", 1));
tntPhase.setTickOffset(getInt(element, "tickOffset", 0));
tntPhase.setXJump(getBoolean(element, "xVelocity", false));
tntPhase.setYJump(getBoolean(element, "yVelocity", false));
tntPhase.setZJump(getBoolean(element, "zVelocity", false));
tntPhase.setOrder(getBoolean(element, "comparator", false) ? 2 : 0);
tntElement.add(tntPhase);
}
simulators.add(simulator);
@@ -87,4 +93,8 @@ public class YAPIONFormatSimulatorLoader implements SimulatorLoader {
file.delete();
return Optional.of(simulators);
}
private double requireDouble(Map<?, ?> element, String key) {
return ((Number) element.get(key)).doubleValue();
}
}
@@ -0,0 +1,64 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.features.slaves.laufbau.converter.LaufbauConfigV1ToV2Converter;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.util.HashSet;
import java.util.Set;
/**
* The current laufbau config of a player. It is stored as JSON, fields missing
* in the stored JSON keep the default value of their initializer, so adding a
* new field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to LaufbauConfigV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to
* this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class LaufbauConfig implements VersionedConfig {
public static final int VERSION = 2;
/**
* LaufbauConfigV1 is stored as Base64 of gzipped YAPION, the current version as JSON
*/
public static final ConfigStore<LaufbauConfig> STORE = new ConfigStore<>(LaufbauConfig.class, VERSION, LaufbauConfig::new)
.legacy(s -> !s.startsWith("{"), LaufbauConfigV1::parse)
.converter(new LaufbauConfigV1ToV2Converter())
.userConfig("bausystem-laufbau")
.removeIf(config -> config.getDeactivated().isEmpty());
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Identifiers of the deactivated {@link BlockBoundingBox}es, see {@link LaufbauUtils#identifier(BlockBoundingBox)}
*/
protected Set<String> deactivated = new HashSet<>();
}
@@ -0,0 +1,65 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* The legacy laufbau config that was stored as Base64 of the gzipped YAPION,
* an object with the deactivated identifiers as keys.
*/
@Getter
@Setter
@Deprecated
public class LaufbauConfigV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
protected Set<String> deactivated = new HashSet<>();
public static LaufbauConfigV1 parse(String base64) {
String yapion;
try (InputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(base64)))) {
yapion = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
LaufbauConfigV1 config = new LaufbauConfigV1();
config.deactivated.addAll(YapionReader.parse(yapion).keySet());
return config;
}
}
@@ -20,63 +20,22 @@
package de.steamwar.bausystem.features.slaves.laufbau;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.core.SWPlayer;
import de.steamwar.inventory.SWItem;
import de.steamwar.sql.UserConfig;
import lombok.Cleanup;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.ItemMeta;
import yapion.hierarchy.output.StreamOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.YAPIONParser;
import yapion.parser.options.StreamOptions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@UtilityClass
public class LaufbauUtils {
public static class LaufbauComponent implements SWPlayer.Component {
private YAPIONObject yapionObject = new YAPIONObject();
@Override
@SneakyThrows
public void onMount(SWPlayer player) {
String config = UserConfig.getConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau");
if (config == null) {
return;
}
byte[] bytes = Base64.getDecoder().decode(config);
@Cleanup GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(bytes));
yapionObject = new YAPIONParser(gzipInputStream, new StreamOptions().stopOnStreamEnd(true)).parse().result();
}
@Override
@SneakyThrows
public void onUnmount(SWPlayer player) {
if (yapionObject.isEmpty()) {
UserConfig.removePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau");
return;
}
@Cleanup ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@Cleanup GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
yapionObject.toYAPION(new StreamOutput(gzipOutputStream)).close();
UserConfig.updatePlayerConfig(player.getPlayer().getUniqueId(), "bausystem-laufbau", Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
}
}
public static Cuboid pixelCuboid(double pixelX, double pixelY, double pixelZ, double pixelDX, double pixelDY, double pixelDZ) {
return new Cuboid(pixelToBlock(pixelX), pixelToBlock(pixelY), pixelToBlock(pixelZ), pixelToBlock(pixelDX), pixelToBlock(pixelDY), pixelToBlock(pixelDZ));
}
@@ -123,7 +82,7 @@ public class LaufbauUtils {
return false;
}
String identifier = identifier(bb);
return SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject.containsKey(identifier);
return LaufbauConfig.STORE.get(p).getDeactivated().contains(identifier);
}
@SneakyThrows
@@ -132,11 +91,9 @@ public class LaufbauUtils {
return;
}
String identifier = identifier(bb);
YAPIONObject yapionObject = SWPlayer.of(p).getComponentOrDefault(LaufbauComponent.class, LaufbauComponent::new).yapionObject;
if (yapionObject.containsKey(identifier)) {
yapionObject.remove(identifier);
} else {
yapionObject.put(identifier, new YAPIONObject());
Set<String> deactivated = LaufbauConfig.STORE.get(p).getDeactivated();
if (!deactivated.remove(identifier)) {
deactivated.add(identifier);
}
}
}
@@ -0,0 +1,40 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.slaves.laufbau.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfig;
import de.steamwar.bausystem.features.slaves.laufbau.LaufbauConfigV1;
@Deprecated
public class LaufbauConfigV1ToV2Converter implements ConfigConverter<LaufbauConfigV1, LaufbauConfig> {
@Override
public int version() {
return LaufbauConfigV1.VERSION;
}
@Override
public LaufbauConfig convert(LaufbauConfigV1 old) {
LaufbauConfig config = new LaufbauConfig();
config.getDeactivated().addAll(old.getDeactivated());
return config;
}
}
@@ -35,8 +35,8 @@ public class SmartPlaceCommand extends SWCommand {
@Register(description = {"SMART_PLACE_HELP", "SMART_PLACE_INFO"})
public void genericToggle(Player p) {
boolean smartPlace = Config.getInstance().get(p).getPlainValueOrDefault("smartPlace", false);
Config.getInstance().get(p).put("smartPlace", !smartPlace);
boolean smartPlace = Config.STORE.get(p).isSmartPlace();
Config.STORE.get(p).setSmartPlace(!smartPlace);
if (!smartPlace) {
SWUtils.sendToActionbar(p, BauSystem.MESSAGE.parse("SMART_PLACE_ENABLE", p));
BauSystem.MESSAGE.send("SMART_PLACE_INFO", p);
@@ -25,6 +25,7 @@ import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.configplayer.Config;
import de.steamwar.linkage.Linked;
import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket;
import net.minecraft.world.InteractionHand;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
@@ -87,7 +88,9 @@ public class SmartPlaceListener implements Listener {
public SmartPlaceListener() {
TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> {
if (!Permission.BUILD.hasPermission(player)) return packet;
if (!Config.getInstance().get(player).getPlainValueOrDefault("smartPlace", false)) return packet;
if (!Config.STORE.get(player).isSmartPlace()) return packet;
ItemStack usedItem = packet.getHand() == InteractionHand.MAIN_HAND ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand();
if (usedItem.getType() == Material.DEBUG_STICK) return packet;
RayTraceResult rayTraceResult = player.rayTraceBlocks(6);
Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null;
BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null;
@@ -127,7 +130,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return;
if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return;
WAS_EXECUTED.add(event.getPlayer());
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return;
@@ -146,7 +149,7 @@ public class SmartPlaceListener implements Listener {
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (!Config.getInstance().get(event.getPlayer()).getPlainValueOrDefault("smartPlace", false)) return;
if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return;
if (!SMART_PLACING.contains(event.getPlayer())) {
if (CONTAINERS.contains(event.getBlockAgainst().getType())) {
SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup();
@@ -35,18 +35,15 @@ public class BlockCounter {
public final Map<Region, BlockCount> blockCountMap = new HashMap<>();
public boolean isActive(final Player p) {
if (!Config.getInstance().get(p).containsKey("blockCounter")) {
return false;
}
return Config.getInstance().get(p).getPlainValue("blockCounter");
return Config.STORE.get(p).isBlockCounter();
}
public void enable(final Player p) {
Config.getInstance().get(p).put("blockCounter", true);
Config.STORE.get(p).setBlockCounter(true);
}
public void disable(final Player p) {
Config.getInstance().get(p).put("blockCounter", false);
Config.STORE.get(p).setBlockCounter(false);
}
public String getMessage(Player player, int count, int tntCount, long tick, long lastTick) {
@@ -36,13 +36,10 @@ import org.bukkit.entity.Player;
import org.bukkit.util.Transformation;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import yapion.hierarchy.types.YAPIONValue;
import java.util.List;
import java.util.stream.Collectors;
import static de.steamwar.bausystem.features.util.TNTClickListener.TNT_CLICK_DETAILS;
/**
* Wrapper for the rendering of a record bundle
*/
@@ -102,7 +99,7 @@ public class TraceEntity extends RBlockDisplay {
* @param player the player the message should be printed for
*/
public void printIntoChat(Player player) {
if (!Config.getInstance().get(player).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
if (!Config.STORE.get(player).isTntClickDetails()) {
return;
}
TNTPoint representative = records.get(0);
@@ -30,7 +30,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import yapion.hierarchy.types.YAPIONObject;
@Linked
public class NightVisionCommand extends SWCommand implements Listener {
@@ -41,15 +40,15 @@ public class NightVisionCommand extends SWCommand implements Listener {
@Register(description = "NIGHT_VISION_HELP")
public void genericCommand(Player p) {
YAPIONObject yapionObject = Config.getInstance().get(p);
boolean value = !yapionObject.getBooleanOrDefault("nightvision", false);
yapionObject.put("nightvision", value);
Config config = Config.STORE.get(p);
boolean value = !config.isNightVision();
config.setNightVision(value);
setNightVision(p, value);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
setNightVision(event.getPlayer(), Config.getInstance().get(event.getPlayer()).getBooleanOrDefault("nightvision", false));
setNightVision(event.getPlayer(), Config.STORE.get(event.getPlayer()).isNightVision());
}
private void setNightVision(Player p, boolean value) {
@@ -53,7 +53,7 @@ public class NoClipCommand extends SWCommand implements Listener {
@Override
public void onUnmount(SWPlayer player) {
player.setGameMode(GameMode.CREATIVE);
player.getPlayer().setGameMode(GameMode.CREATIVE);
}
}
@@ -115,6 +115,7 @@ public class NoClipCommand extends SWCommand implements Listener {
Abilities abilities = (((CraftPlayer) player).getHandle()).getAbilities();
abilities.mayBuild = true;
abilities.mayfly = true;
abilities.instabuild = true;
swPlayer.setComponent(new NoClipData());
BauSystem.MESSAGE.send("OTHER_NOCLIP_SLOT_INFO", player);
@@ -32,26 +32,22 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.inventory.EquipmentSlot;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONValue;
@Linked
public class TNTClickListener extends SWCommand implements Listener {
public static final String TNT_CLICK_DETAILS = "tnt_click_details";
public TNTClickListener() {
super("tntdetails");
}
@Register(description = "TNT_DETAILS_COMMAND")
public void toggle(Player player) {
YAPIONObject yapionObject = Config.getInstance().get(player);
if (yapionObject.getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
yapionObject.put(TNT_CLICK_DETAILS, false);
Config config = Config.STORE.get(player);
if (config.isTntClickDetails()) {
config.setTntClickDetails(false);
BauSystem.MESSAGE.send("TNT_DETAILS_OFF", player);
} else {
yapionObject.put(TNT_CLICK_DETAILS, true);
config.setTntClickDetails(true);
BauSystem.MESSAGE.send("TNT_DETAILS_ON", player);
}
}
@@ -60,7 +56,7 @@ public class TNTClickListener extends SWCommand implements Listener {
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!Permission.BUILD.hasPermission(event.getPlayer())) return;
if (event.getHand() != EquipmentSlot.HAND) return;
if (!Config.getInstance().get(event.getPlayer()).getOrSetDefault(TNT_CLICK_DETAILS, new YAPIONValue<>(true)).asBoolean().orElse(true)) {
if (!Config.STORE.get(event.getPlayer()).isTntClickDetails()) {
return;
}
@@ -25,7 +25,6 @@ import lombok.Getter;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent;
import yapion.hierarchy.types.YAPIONObject;
import java.util.*;
@@ -56,27 +55,22 @@ public class Warp {
warpMap.put(name, this);
}
public Warp(String name, YAPIONObject object) {
public Warp(String name, WorldData.WarpEntry warp) {
this.name = name;
double x = object.getPlainValue("x");
double y = object.getPlainValue("y");
double z = object.getPlainValue("z");
float yaw = object.getPlainValue("yaw");
float pitch = object.getPlainValue("pitch");
location = new Location(Bukkit.getWorlds().get(0), x, y, z, yaw, pitch);
mat = Material.getMaterial(object.getPlainValue("material"));
location = new Location(Bukkit.getWorlds().get(0), warp.getX(), warp.getY(), warp.getZ(), warp.getYaw(), warp.getPitch());
mat = Material.getMaterial(warp.getMaterial());
warpMap.put(name, this);
}
public YAPIONObject output() {
YAPIONObject yapionObject = new YAPIONObject();
yapionObject.add("x", location.getX());
yapionObject.add("y", location.getY());
yapionObject.add("z", location.getZ());
yapionObject.add("yaw", location.getYaw());
yapionObject.add("pitch", location.getPitch());
yapionObject.add("material", mat.toString());
return yapionObject;
public WorldData.WarpEntry output() {
WorldData.WarpEntry warp = new WorldData.WarpEntry();
warp.setX(location.getX());
warp.setY(location.getY());
warp.setZ(location.getZ());
warp.setYaw(location.getYaw());
warp.setPitch(location.getPitch());
warp.setMaterial(mat.toString());
return warp;
}
public static Set<String> getWarpNames() {
@@ -101,7 +95,7 @@ public class Warp {
public void delete() {
warpMap.remove(name);
WorldData.getWarpData().remove(name);
WorldData.STORE.get().getWarps().remove(name);
}
public void teleport(Player player) {
@@ -34,9 +34,9 @@ import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.entity.Player;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.Map;
@Linked
public class WarpCommand extends SWCommand implements Disable, Enable {
@@ -135,17 +135,15 @@ public class WarpCommand extends SWCommand implements Disable, Enable {
@Override
public void enable() {
WorldData.getWarpData().forEach((name, yapionAnyType) -> {
new Warp(name, (YAPIONObject) yapionAnyType);
});
WorldData.STORE.get().getWarps().forEach(Warp::new);
Warp.enable();
}
@Override
public void disable() {
YAPIONObject yapionObject = WorldData.getWarpData();
Map<String, WorldData.WarpEntry> warps = WorldData.STORE.get().getWarps();
for (Warp warp : Warp.getWarps()) {
yapionObject.add(warp.getName(), warp.output());
warps.put(warp.getName(), warp.output());
}
}
}
@@ -131,6 +131,7 @@ public class BauScoreboard implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
SWScoreboard.impl.removeScoreboard(event.getPlayer());
ADDITIONAL_SCOREBOARD_LINES.remove(event.getPlayer());
}
}
@@ -20,78 +20,39 @@
package de.steamwar.bausystem.region;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.sql.SchematicNode;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public abstract class RegionData {
private final List<Property<?, ?>> properties = new ArrayList<>();
protected final YAPIONObject data;
protected final YAPIONObject flagData;
protected final WorldData.RegionEntry data;
protected final Map<String, String> flagData;
protected final Runnable onChange;
protected final Map<Flag<?>, Flag.Value<?>> flagMap = new HashMap<>();
private final class Property<T, K> {
private final String field;
private final Function<K, T> loader;
private final Function<T, K> writer;
private SchematicNode testblockSchematic;
private T value;
public Property(String field, Function<K, T> loader, Function<T, K> writer) {
this.field = field;
this.loader = loader;
this.writer = writer;
properties.add(this);
}
public void load() {
if (flagData.containsKey(field)) {
value = loader.apply(flagData.getPlainValue(field));
} else {
value = null;
}
}
public T get() {
return value;
}
public void set(T value) {
this.value = value;
if (value == null) {
flagData.remove(field);
} else {
flagData.put(field, writer.apply(value));
}
}
}
private Property<SchematicNode, Integer> testblockSchematic = new Property<>("testblockSchematic", SchematicNode::byId, SchematicNode::getId);
protected RegionData(YAPIONObject data, Runnable onChange) {
protected RegionData(WorldData.RegionEntry data, Runnable onChange) {
this.data = data;
this.flagData = data.getObjectOrSetDefault("flagStorage", new YAPIONObject());
this.flagData = data.getFlags();
this.onChange = onChange;
initialize();
for (final Flag flag : Flag.getFlags()) {
if (!has(flag).isWritable()) continue;
try {
String s = flagData.getPlainValue(flag.name());
String s = flagData.get(flag.name());
flagMap.put(flag, flag.valueOfValue(s));
} catch (Exception e) {
flagMap.put(flag, (Flag.Value<?>) flag.getDefaultValue());
}
}
properties.forEach(Property::load);
if (data.getTestblockSchematic() != null) {
testblockSchematic = SchematicNode.byId(data.getTestblockSchematic());
}
}
protected void initialize() {
@@ -126,7 +87,8 @@ public abstract class RegionData {
flagData.remove(flag.name());
}
}
properties.forEach(property -> property.set(null));
testblockSchematic = null;
data.setTestblockSchematic(null);
onChange.run();
}
@@ -135,11 +97,12 @@ public abstract class RegionData {
}
public SchematicNode getTestblockSchematic() {
return testblockSchematic.get();
return testblockSchematic;
}
public void setTestblockSchematic(SchematicNode schematic) {
testblockSchematic.set(schematic);
testblockSchematic = schematic;
data.setTestblockSchematic(schematic == null ? null : schematic.getId());
onChange.run();
}
@@ -1,101 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.worlddata;
import de.steamwar.sql.SteamwarUser;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONArray;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.InputStreamCharsets;
import yapion.parser.YAPIONParser;
import yapion.parser.options.FileOptions;
import java.io.File;
import java.util.List;
@UtilityClass
public class SimulatorData {
private final File simulatorsDir = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "simulators");
static {
simulatorsDir.mkdirs();
}
@SneakyThrows
public YAPIONObject getSimulator(Player player) {
File file = getFile(player);
if (!file.exists()) {
return new YAPIONObject();
}
return YAPIONParser.parse(file, new FileOptions().charset(InputStreamCharsets.UTF_8));
}
@SneakyThrows
private void internalSaveSimulator(Player player, YAPIONObject yapionObject) {
File file = getFile(player);
yapionObject.toYAPION(new FileOutput(file)).close();
}
@SneakyThrows
void saveSimulator(SteamwarUser steamwarUser, YAPIONObject yapionObject) {
File file = new File(simulatorsDir, steamwarUser.getId() + ".yapion");
yapionObject.toYAPION(new FileOutput(file)).close();
}
public List<String> listSimulator(Player player) {
List<String> strings = getSimulator(player).getKeys();
strings.remove("");
return strings;
}
public void saveSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put(name, yapionObject.getArray("").copy());
internalSaveSimulator(player, yapionObject);
}
public void saveSimulator(Player player, YAPIONArray simulator) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put("", simulator);
internalSaveSimulator(player, yapionObject);
}
public void loadSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.put("", yapionObject.getArray(name).copy());
internalSaveSimulator(player, yapionObject);
}
public void removeSimulator(Player player, String name) {
YAPIONObject yapionObject = getSimulator(player);
yapionObject.remove(name);
internalSaveSimulator(player, yapionObject);
}
private File getFile(Player player) {
SteamwarUser steamwarUser = SteamwarUser.get(player.getUniqueId());
return new File(simulatorsDir, steamwarUser.getId() + ".yapion");
}
}
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -19,58 +19,83 @@
package de.steamwar.bausystem.worlddata;
import lombok.experimental.UtilityClass;
import com.google.gson.GsonBuilder;
import de.steamwar.bausystem.config.ConfigStore;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.worlddata.converter.WorldDataV1ToV2Converter;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Bukkit;
import yapion.hierarchy.output.FileOutput;
import yapion.hierarchy.types.YAPIONObject;
import yapion.parser.InputStreamCharsets;
import yapion.parser.YAPIONParser;
import yapion.parser.options.StreamOptions;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@UtilityClass
public class WorldData {
/**
* The current world data. It is stored as JSON, fields missing in the
* stored JSON keep the default value of their initializer, so adding a new
* field does not need a new version.
* <p>
* When a field gets removed, renamed or its meaning changes, copy this class
* to WorldDataV{@link #VERSION} (without the {@link #STORE}), increase {@link #VERSION}, write a
* {@link de.steamwar.bausystem.config.ConfigConverter} from the copy to this class and register it in the {@link #STORE}.
*/
@Getter
@Setter
public class WorldData implements VersionedConfig {
private final File optionsFile = new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion");
private YAPIONObject worldData;
public static final int VERSION = 2;
public YAPIONObject getWorldData() {
if (worldData == null) {
read();
}
return worldData;
public static final ConfigStore<WorldData> STORE = new ConfigStore<>(WorldData.class, VERSION, WorldData::new)
.gson(new GsonBuilder().setPrettyPrinting().create())
.converter(new WorldDataV1ToV2Converter())
.file(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.json"))
.legacyFile(new File(Bukkit.getWorlds().get(0).getWorldFolder(), "options.yapion"), WorldDataV1::parse);
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Region name to the data of the region
*/
protected Map<String, RegionEntry> regions = new HashMap<>();
protected RegionEntry globalRegion = new RegionEntry();
/**
* Warp name to warp
*/
protected Map<String, WarpEntry> warps = new HashMap<>();
@Getter
@Setter
public static class RegionEntry {
/**
* Overrides the prototype of the region config, null if not overridden
*/
protected String prototype = null;
/**
* Flag name to flag value name
*/
protected Map<String, String> flags = new HashMap<>();
/**
* Id of the testblock SchematicNode, null if the default testblock is used
*/
protected Integer testblockSchematic = null;
}
public YAPIONObject getRegionsData() {
return getWorldData().getYAPIONObjectOrSetDefault("regions", new YAPIONObject());
}
@Getter
@Setter
public static class WarpEntry {
public YAPIONObject getWarpData() {
return getWorldData().getYAPIONObjectOrSetDefault("warps", new YAPIONObject());
}
private void read() {
worldData = new YAPIONObject();
if (optionsFile.length() != 0) {
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(optionsFile))) {
worldData = YAPIONParser.parse(bufferedInputStream, new StreamOptions().charset(InputStreamCharsets.UTF_8));
} catch (IOException e) {
// Ignored
}
} else {
write();
}
}
public void write() {
try {
worldData.toYAPION(new FileOutput(optionsFile)).close();
} catch (IOException e) {
e.printStackTrace();
}
protected double x;
protected double y;
protected double z;
protected float yaw;
protected float pitch;
protected String material;
}
}
@@ -0,0 +1,111 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.worlddata;
import de.steamwar.bausystem.config.VersionedConfig;
import de.steamwar.bausystem.config.legacy.YapionReader;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
/**
* The legacy world data that was stored in the YAPION format in options.yapion.
*/
@Getter
@Setter
@Deprecated
public class WorldDataV1 implements VersionedConfig {
public static final int VERSION = 1;
@Setter(AccessLevel.NONE)
protected int version = VERSION;
/**
* Region name to the data of the region, the global region is stored with the name "global"
*/
protected Map<String, RegionEntry> regions = new HashMap<>();
/**
* Warp name to warp
*/
protected Map<String, WarpEntry> warps = new HashMap<>();
@Getter
@Setter
public static class RegionEntry {
protected String prototype = null;
/**
* Flag name to flag value name and "testblockSchematic" to the SchematicNode id
*/
protected Map<String, Object> flagStorage = new HashMap<>();
}
@Getter
@Setter
public static class WarpEntry {
protected double x;
protected double y;
protected double z;
protected float yaw;
protected float pitch;
protected String material;
}
public static WorldDataV1 parse(String yapion) {
Map<String, Object> object = YapionReader.parse(yapion);
WorldDataV1 worldData = new WorldDataV1();
if (object.get("regions") instanceof Map<?, ?> regions) {
regions.forEach((name, value) -> {
if (!(value instanceof Map<?, ?> region)) return;
RegionEntry entry = new RegionEntry();
if (region.get("prototype") instanceof String prototype) {
entry.prototype = prototype;
}
if (region.get("flagStorage") instanceof Map<?, ?> flagStorage) {
flagStorage.forEach((key, flag) -> entry.flagStorage.put((String) key, flag));
}
worldData.regions.put((String) name, entry);
});
}
if (object.get("warps") instanceof Map<?, ?> warps) {
warps.forEach((name, value) -> {
if (!(value instanceof Map<?, ?> warp)) return;
WarpEntry entry = new WarpEntry();
entry.x = ((Number) warp.get("x")).doubleValue();
entry.y = ((Number) warp.get("y")).doubleValue();
entry.z = ((Number) warp.get("z")).doubleValue();
entry.yaw = ((Number) warp.get("yaw")).floatValue();
entry.pitch = ((Number) warp.get("pitch")).floatValue();
entry.material = (String) warp.get("material");
worldData.warps.put((String) name, entry);
});
}
return worldData;
}
}
@@ -0,0 +1,75 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.worlddata.converter;
import de.steamwar.bausystem.config.ConfigConverter;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.bausystem.worlddata.WorldDataV1;
@Deprecated
public class WorldDataV1ToV2Converter implements ConfigConverter<WorldDataV1, WorldData> {
private static final String GLOBAL_REGION = "global";
private static final String TESTBLOCK_SCHEMATIC = "testblockSchematic";
@Override
public int version() {
return WorldDataV1.VERSION;
}
@Override
public WorldData convert(WorldDataV1 old) {
WorldData worldData = new WorldData();
old.getRegions().forEach((name, region) -> {
if (name.equals(GLOBAL_REGION)) {
worldData.setGlobalRegion(convert(region));
} else {
worldData.getRegions().put(name, convert(region));
}
});
old.getWarps().forEach((name, warp) -> worldData.getWarps().put(name, convert(warp)));
return worldData;
}
private WorldData.RegionEntry convert(WorldDataV1.RegionEntry old) {
WorldData.RegionEntry region = new WorldData.RegionEntry();
region.setPrototype(old.getPrototype());
old.getFlagStorage().forEach((key, value) -> {
if (value == null) return;
if (key.equals(TESTBLOCK_SCHEMATIC)) {
region.setTestblockSchematic(((Number) value).intValue());
} else {
region.getFlags().put(key, value.toString());
}
});
return region;
}
private WorldData.WarpEntry convert(WorldDataV1.WarpEntry old) {
WorldData.WarpEntry warp = new WorldData.WarpEntry();
warp.setX(old.getX());
warp.setY(old.getY());
warp.setZ(old.getZ());
warp.setYaw(old.getYaw());
warp.setPitch(old.getPitch());
warp.setMaterial(old.getMaterial());
return warp;
}
}
@@ -25,12 +25,12 @@ import de.steamwar.bausystem.region.flags.ColorMode;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.ProtectMode;
import de.steamwar.bausystem.region.flags.TNTMode;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedGlobalRegionData extends RegionData {
public FixedGlobalRegionData(YAPIONObject data, Runnable onChange) {
public FixedGlobalRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, onChange);
}
@@ -26,6 +26,7 @@ import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.region.flags.TestblockMode;
import de.steamwar.bausystem.utils.FlatteningWrapper;
import de.steamwar.bausystem.utils.PasteBuilder;
import de.steamwar.bausystem.worlddata.WorldData;
import de.steamwar.core.Core;
import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.SchematicType;
@@ -139,7 +140,7 @@ public class FixedRegion implements Region {
}
}
public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, YAPIONObject regionData) {
public FixedRegion(String name, FixedRegionData flagStorage, Prototype prototype, YAPIONObject regionConfig, WorldData.RegionEntry regionData) {
this.name = name;
uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
this.flagStorage = flagStorage;
@@ -325,7 +326,7 @@ public class FixedRegion implements Region {
}
floorLevel = prototype.getFloorOffset() != 0 ? minPoint.getY() + prototype.getFloorOffset() : 0;
waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getX() + prototype.getWaterOffset() : 0;
waterLevel = prototype.getWaterOffset() != 0 ? minPoint.getY() + prototype.getWaterOffset() : 0;
File found = null;
File baseFile = new File(BauSystem.getInstance().getDataFolder().getParentFile(), "FightSystem");
@@ -22,12 +22,12 @@ package de.steamwar.bausystem.region.fixed;
import de.steamwar.bausystem.region.RegionData;
import de.steamwar.bausystem.region.RegionFlagPolicy;
import de.steamwar.bausystem.region.flags.Flag;
import de.steamwar.bausystem.worlddata.WorldData;
import lombok.NonNull;
import yapion.hierarchy.types.YAPIONObject;
public class FixedRegionData extends RegionData {
public FixedRegionData(YAPIONObject data, Runnable onChange) {
public FixedRegionData(WorldData.RegionEntry data, Runnable onChange) {
super(data, onChange);
}
@@ -210,14 +210,14 @@ public class Prototype {
}
}
public static void generateRegion(String name, YAPIONObject regionConfig, YAPIONObject regionData) {
public static void generateRegion(String name, YAPIONObject regionConfig, WorldData.RegionEntry regionData) {
Prototype prototype;
if (regionData.containsKey("prototype", String.class)) {
prototype = PROTOTYPE_MAP.get(regionData.getPlainValue("prototype"));
if (regionData.getPrototype() != null) {
prototype = PROTOTYPE_MAP.get(regionData.getPrototype());
} else {
prototype = PROTOTYPE_MAP.get(regionConfig.getPlainValue("prototype"));
}
FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData::write);
FixedRegionData flagStorage = new FixedRegionData(regionData, WorldData.STORE::save);
FixedRegionSystem.addRegion(new FixedRegion(name, flagStorage, prototype, regionConfig, regionData));
}
}
@@ -28,7 +28,6 @@ import org.bukkit.Bukkit;
import yapion.hierarchy.diff.DiffChange;
import yapion.hierarchy.diff.YAPIONDiff;
import yapion.hierarchy.types.YAPIONObject;
import yapion.hierarchy.types.YAPIONType;
import yapion.parser.YAPIONParser;
import java.io.BufferedInputStream;
@@ -55,7 +54,7 @@ public class RegionLoader {
}
loaded = yapionObject;
YAPIONObject optionsYapionObject = WorldData.getRegionsData();
WorldData worldData = WorldData.STORE.get();
yapionObject.forEach((key, yapionAnyType) -> {
if (key.equals("global")) {
return;
@@ -65,21 +64,10 @@ public class RegionLoader {
}
YAPIONObject regionConfig = (YAPIONObject) yapionAnyType;
YAPIONObject regionData = new YAPIONObject();
if (optionsYapionObject.containsKey(key, YAPIONType.OBJECT)) {
regionData = optionsYapionObject.getObject(key);
} else {
optionsYapionObject.add(key, regionData);
}
WorldData.RegionEntry regionData = worldData.getRegions().computeIfAbsent(key, k -> new WorldData.RegionEntry());
Prototype.generateRegion(key, regionConfig, regionData);
});
YAPIONObject globalOptions = optionsYapionObject.getObject("global");
if (globalOptions == null) {
globalOptions = new YAPIONObject();
optionsYapionObject.add("global", globalOptions);
}
FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(globalOptions, WorldData::write));
FixedGlobalRegion.setFLAG_STORAGE(new FixedGlobalRegionData(worldData.getGlobalRegion(), WorldData.STORE::save));
}
}
@@ -0,0 +1,40 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.network.packets.server;
import de.steamwar.network.packets.NetworkPacket;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* A chat message written on an arena. The arena only records it for the replay and never displays it.
*/
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Getter
public class ChatRecordPacket extends NetworkPacket {
private static final long serialVersionUID = -4718360932583127458L;
private int playerId;
private String message;
}
@@ -19,6 +19,7 @@
package de.steamwar.sql;
import de.steamwar.sql.internal.SQLConfig;
import lombok.ToString;
import java.io.File;
@@ -250,6 +251,11 @@ public final class GameModeConfig<M, W> {
Times = new TimesConfig(loader.with("Times"));
// Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards
Schematic = new SchematicConfig<>(loader.with("Schematic"));
for (int i = 0; i < Schematic.Limited.size(); i++) {
if (Schematic.Limited.get(i).Materials.isEmpty()) {
SQLConfig.impl.getLogger().warning(file.getName() + ": Schematic.Limited[" + i + "] matches no blocks, check its 'Materials' entry");
}
}
GameName = loader.getString("GameName", defaultGameName.apply(file));
ActiveMonths = loader.getIntList("ActiveMonths");
TeamChatPrefix = loader.getString("TeamChatPrefix", "+");
@@ -719,7 +725,10 @@ public final class GameModeConfig<M, W> {
public final double MaxDesignBlastResistance;
/**
* List of limited block materials
* List of limited block materials<br/>
* Every inventory block is counted towards exactly one rule: a filled block towards a rule whose
* {@link BlockRule#Content} allows all of its items, an empty block towards a rule without
* {@link BlockRule#Content}. Empty inventory blocks without such a rule are unlimited.
*
* @implSpec {@link SQLWrapper#getInventoryMaterials)} are all disallowed by default to have any content but are unlimited in number.
* @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0.
@@ -775,18 +784,13 @@ public final class GameModeConfig<M, W> {
}
public boolean isInventory(M material) {
return Limited.stream()
.filter(rule -> rule.Materials.contains(material))
.anyMatch(rule -> rule.Content.loaded);
return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material));
}
public Integer getItemAmount(M material, M item) {
public List<BlockRule<M>> getRules(M material) {
return Limited.stream()
.filter(rule -> rule.Materials.contains(material))
.filter(rule -> rule.Content.Items.contains(item))
.mapToInt(rule -> rule.Content.Amount)
.max()
.orElse(0);
.collect(Collectors.toList());
}
@ToString
@@ -46,7 +46,7 @@ public interface SQLWrapper<M> {
}
/**
* Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s
* Every inventory material - the {@code inventory} group usable in {@code GameModeConfig}'s
* {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}.
*/
default Set<M> getInventoryMaterials() {
@@ -307,12 +307,12 @@ class SteamwarUser(id: EntityID<Int>) : IntEntity(id) {
punishments[punishment] = Punishment.createPunishment(this@SteamwarUser.id.value, from, punishment, reason, time, perma)
}
fun setLocale(locale: Locale?, manualeLocale: Boolean) {
fun setLocale(locale: Locale?, manualLocale: Boolean) {
if (locale == null || (this.manualLocale && !manualLocale)) return
useDb {
this@SteamwarUser.locale = locale
this@SteamwarUser.manualLocale = manualeLocale
this@SteamwarUser.manualLocale = manualLocale
}
}
@@ -21,8 +21,8 @@ package de.steamwar.fightsystem;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.Core;
import de.steamwar.core.WorldIdentifier;
import de.steamwar.core.worldedit.SteamEdit;
import de.steamwar.core.worldedit.WorldEditRendererCUIEditor;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.listener.ClickAnalyzer;
@@ -79,6 +79,11 @@ public class FightSystem extends JavaPlugin {
Core.setServerName("Replay");
}
String arenaMap = System.getProperty("arenaMap");
if (arenaMap != null) {
WorldIdentifier.setArena(arenaMap);
}
message = new Message("de.steamwar.fightsystem.FightSystem", FightSystem.class.getClassLoader());
spigotLinker = new SpigotLinker(this, message) {
@@ -109,7 +109,6 @@ TEAM_READY = §aTeam ready
TEAM_NOT_READY = §c§mTeam ready
SKIP_READY = §aSkipping to next event
SKIP_NOT_READY = §c§mSkipping to next event
TEAM_CHAT = {0}{1}§8» {0}{2}
CHOOSE_KIT = §eChoose kit
RESPAWN = §eRespawn
MANAGE_PLAYERS = §cManage players
@@ -256,10 +256,6 @@ public class FightTeam {
broadcast(player -> FightSystem.getMessage().send(message, player, params));
}
public void broadcastChat(Player sender, String message) {
broadcast(player -> FightSystem.getMessage().sendPrefixless("TEAM_CHAT", player, ChatMessageType.CHAT, prefix, sender.getName(), message));
}
private void broadcast(Consumer<Player> f) {
players.forEach((uuid, fightPlayer) -> {
Player player = Bukkit.getPlayer(uuid);
@@ -19,50 +19,41 @@
package de.steamwar.fightsystem.listener;
import de.steamwar.fightsystem.ArenaMode;
import de.steamwar.fightsystem.Config;
import de.steamwar.fightsystem.FightSystem;
import de.steamwar.fightsystem.fight.Fight;
import de.steamwar.fightsystem.fight.FightTeam;
import de.steamwar.fightsystem.record.GlobalRecorder;
import de.steamwar.fightsystem.states.FightState;
import de.steamwar.fightsystem.states.StateDependentListener;
import de.steamwar.linkage.Linked;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.ChatRecordPacket;
import de.steamwar.sql.SteamwarUser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.logging.Level;
/**
* Chat is handled by Velocity. Messages written on this server are only sent here to be recorded for the replay.
*/
@Linked
public class Chat implements Listener {
public class Chat extends PacketHandler {
public Chat() {
new StateDependentListener(ArenaMode.All, FightState.All, this);
}
@Handler
public void handleChatRecord(ChatRecordPacket packet) {
SteamwarUser user = SteamwarUser.byId(packet.getPlayerId());
String message = packet.getMessage();
FightSystem.getPlugin().getLogger().log(Level.INFO, user.getUserName() + "» " + message);
@EventHandler
public void handlePlayerChat(AsyncPlayerChatEvent event) {
Player player = event.getPlayer();
String message = event.getMessage();
FightSystem.getPlugin().getLogger().log(Level.INFO, player.getName() + "» " + message);
FightTeam fightTeam = Fight.getPlayerTeam(player);
Player player = Bukkit.getPlayer(user.getUUID());
FightTeam fightTeam = player != null ? Fight.getPlayerTeam(player) : null;
if (fightTeam != null) {
String teamName = fightTeam.getColoredName();
if (message.startsWith(Config.GameModeConfig.TeamChatPrefix)) {
fightTeam.broadcastChat(player, message.substring(1));
} else {
broadcastChat("PARTICIPANT_CHAT", teamName, player.getName(), message);
}
} else if (Config.isReferee(player)) {
broadcastChat("FIGHTLEADER_CHAT", player.getName(), message);
GlobalRecorder.getInstance().chat("PARTICIPANT_CHAT", fightTeam.getColoredName(), user.getUserName(), message);
} else if (player != null && Config.isReferee(player)) {
GlobalRecorder.getInstance().chat("FIGHTLEADER_CHAT", user.getUserName(), message);
} else {
broadcastChat("SPECTATOR_CHAT", player.getName(), message);
GlobalRecorder.getInstance().chat("SPECTATOR_CHAT", user.getUserName(), message);
}
event.setCancelled(true);
}
public static void broadcastChat(String message, Object... params) {
@@ -19,6 +19,7 @@
package de.steamwar.misslewars;
import de.steamwar.core.WorldIdentifier;
import de.steamwar.linkage.AbstractLinker;
import de.steamwar.linkage.SpigotLinker;
import de.steamwar.misslewars.items.CustomItem;
@@ -56,6 +57,11 @@ public class MissileWars extends JavaPlugin {
redTeam = new MWTeam(Config.TeamRedColor, Config.RedSpawn, Config.TeamRedName, Config.RedPortalZ);
blueTeam = new MWTeam(Config.TeamBlueColor, Config.BlueSpawn, Config.TeamBlueName, Config.BluePortalZ);
String arenaMap = System.getProperty("arenaMap");
if (arenaMap != null) {
WorldIdentifier.setArena(arenaMap);
}
SpigotLinker spigotLinker = new SpigotLinker(this, null) {
@Override
protected void linkObject(Object any) {
@@ -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_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2}
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_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_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}]
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_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2}
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_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_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}]
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.world.block.BaseBlock;
import de.steamwar.sql.GameModeConfig;
import de.steamwar.sql.GameModeConfig.SchematicConfig.BlockRule;
import lombok.Getter;
import lombok.ToString;
import org.bukkit.Material;
@@ -62,6 +63,7 @@ public class AutoChecker {
public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig<Material, String> type) {
AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult();
Map<BlockPos, List<BlockRule<Material>>> ruleCandidates = new HashMap<>();
BlockVector3 min = clipboard.getMinimumPoint();
BlockVector3 max = clipboard.getMaximumPoint();
for (int x = min.x(); x <= max.x(); x++) {
@@ -75,13 +77,13 @@ public class AutoChecker {
BlockPos pos = new BlockPos(x, y, z);
result.getBlockCounts().merge(material, 1, Integer::sum);
if (type.Schematic.isInventory(material)) {
checkInventory(result, block, material, pos, type);
if (!result.getInventoryItemCounts().getOrDefault(pos, Collections.emptyMap()).isEmpty()) {
result.getBlockCounts().merge(material, 1, Integer::sum);
result.getInventoryBlockType().put(pos, material);
List<BlockRule<Material>> candidates = checkInventory(result, block, material, pos, type);
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()) {
@@ -90,28 +92,28 @@ public class AutoChecker {
}
}
}
assignRules(result, ruleCandidates);
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();
if (nbt == null) {
result.getDefunctNbt().add(pos);
return;
return Collections.emptyList();
}
if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) {
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);
int windChargeCount = 0;
for (CompoundTag item : items) {
for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) {
if (!item.containsKey("id")) {
result.getDefunctNbt().add(pos);
continue;
@@ -121,22 +123,69 @@ public class AutoChecker {
if (itemType == null) // Leere Slots
continue;
if (type.Schematic.Type.getName().equals("wargearseason26") && material == Material.DISPENSER && itemType == Material.WIND_CHARGE) {
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);
}
itemCounts.merge(itemType, item.getInt("count"), Integer::sum);
if (item.containsKey("tag")) {
result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType);
}
}
if (!itemCounts.isEmpty()) {
result.getInventoryItemCounts().put(pos, itemCounts);
result.getInventoryBlockType().put(pos, material);
List<BlockRule<Material>> rules = type.Schematic.getRules(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
@@ -148,8 +197,11 @@ public class AutoChecker {
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, 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>> mixedItems = 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.List;
import java.util.Map;
import java.util.stream.Collectors;
@Builder
@Getter
@@ -46,12 +47,12 @@ public class AutoCheckerResult {
public boolean isOk() {
return blockScanResult.getRecords().isEmpty() &&
blockScanResult.getForbiddenItems().isEmpty() &&
blockScanResult.getMixedItems().isEmpty() &&
blockScanResult.getForbiddenNbt().isEmpty() &&
isSizeOk() &&
isBlockCountOk() &&
isLimitedBlocksOK() &&
isInventoryItemsOK() &&
isWindchargeCountOK() &&
!type.isAfterDeadline() &&
entities.isEmpty() &&
isDesignBlastResistanceOK();
@@ -62,23 +63,8 @@ public class AutoCheckerResult {
!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() {
return blockScanResult.getInventoryItemCounts().entrySet().stream().allMatch(posEntry -> {
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;
});
});
return blockScanResult.getTooManyItems().isEmpty();
}
public boolean hasWarnings() {
@@ -107,7 +93,10 @@ public class AutoCheckerResult {
public boolean isLimitedBlocksOK() {
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() {
@@ -132,6 +121,7 @@ public class AutoCheckerResult {
}
if (!isLimitedBlocksOK()) {
blockScanResult.getBlockCounts().forEach((mat, count) -> {
if (type.Schematic.isInventory(mat)) return;
int maxCount = type.Schematic.getMaxCount(mat);
if (count > maxCount) {
if (maxCount == 0) {
@@ -141,30 +131,25 @@ public class AutoCheckerResult {
}
}
});
}
if (!isWindchargeCountOK()) {
int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0);
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES", p, windChargesCount, 2048);
blockScanResult.getWindChargeCount().entrySet().stream().filter(blockVector3IntegerEntry -> blockVector3IntegerEntry.getValue() > 0).forEach(blockVector3IntegerEntry -> {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3IntegerEntry.getKey()),
blockVector3IntegerEntry.getKey().getBlockX(),
blockVector3IntegerEntry.getKey().getBlockY(),
blockVector3IntegerEntry.getKey().getBlockZ(),
blockVector3IntegerEntry.getValue());
blockScanResult.getRuleCounts().forEach((rule, count) -> {
if (count <= rule.Amount) return;
String blocks = blockScanResult.getRuleBlockTypes().get(rule).stream().map(Material::name).collect(Collectors.joining(", "));
if (rule.Amount == 0) {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK", p, blocks);
} else {
SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_BLOCK", p, blocks, count, rule.Amount);
}
});
}
blockScanResult.getInventoryItemCounts().forEach((pos, itemCounts) -> {
blockScanResult.getTooManyItems().forEach((pos, caps) -> {
Material inventory = blockScanResult.getInventoryBlockType().get(pos);
itemCounts.forEach((itemType, count) -> {
int cap = type.Schematic.getItemAmount(inventory, itemType);
if (count <= cap) return;
caps.forEach((itemType, cap) -> {
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.getBlockY(),
pos.getBlockZ(),
count,
blockScanResult.getInventoryItemCounts().get(pos).get(itemType),
cap,
itemType.name(),
inventory.name());
@@ -177,6 +162,9 @@ public class AutoCheckerResult {
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));
});
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) -> {
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.Function;
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.SchematicCommandUtils.togglePublic;
@@ -171,8 +172,8 @@ public class SchematicCommand extends SWCommand {
Map<BlockPos, Set<Material>> toBeCheckedInvs = new HashMap<>();
toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenItems());
toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenNbt());
Stream.of(result.getBlockScanResult().getForbiddenItems(), result.getBlockScanResult().getMixedItems(), 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()) {
BlockPos pos = entry.getKey();
@@ -197,16 +198,13 @@ public class SchematicCommand extends SWCommand {
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();
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()) {
Material itemType = itemEntry.getKey();
Integer cap = type.Schematic.getItemAmount(inventory, itemType);
if (itemEntry.getValue() <= cap) {
continue;
}
int cap = itemEntry.getValue();
BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ());
BaseBlock block = clipboard.getFullBlock(vector);
@@ -215,7 +213,7 @@ public class SchematicCommand extends SWCommand {
List<CompoundTag> items = new ArrayList<>(tag.getList("Items", CompoundTag.class));
Collections.reverse(items); // To let the first item be in the inventory
List<CompoundTag> list = new ArrayList<>();
int diff = itemEntry.getValue() - cap;
int diff = itemCounts.get(itemType) - cap;
for (CompoundTag item : items) {
if (item == null) {
continue;
@@ -16,7 +16,6 @@
# 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/>.
#
LOCAL_CHAT = §eLocal §r{0}§8»§7 {1}
COMMAND_SYSTEM_ERROR = §cError executing the command!
@@ -16,7 +16,6 @@
# 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/>.
#
LOCAL_CHAT = §eLokal §r{0}§8»§7 {1}
COMMAND_SYSTEM_ERROR = §cFehler beim Ausführen des Befehls!
@@ -21,8 +21,8 @@ package de.steamwar.core;
import de.steamwar.message.Message;
import lombok.Data;
import lombok.Getter;
import lombok.NonNull;
import lombok.experimental.Delegate;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.ClickEvent;
import org.bukkit.Bukkit;
@@ -99,11 +99,11 @@ public class SWPlayer {
@Data
public static final class SWPlayerWithComponent<T extends Component> {
private final SWPlayer player;
private final SWPlayer swPlayer;
private final T component;
}
@Delegate
@Getter
private final Player player;
private final Map<Class<? extends Component>, Component> components = new HashMap<>();
@@ -27,6 +27,9 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
@Linked
public class WorldIdentifier {
@@ -36,6 +39,10 @@ public class WorldIdentifier {
resourceKey = new ResourceKey<>(new ResourceLocation("minecraft", "dimension"), new ResourceLocation("steamwar", name));
}
public static void setArena(String map) {
set("arena/" + Core.getVersion() + "/" + HexFormat.of().formatHex(map.getBytes(StandardCharsets.UTF_8)));
}
public WorldIdentifier() {
TinyProtocol.instance.addFilter(ClientboundLoginPacket.class, (player, packet) -> {
if (resourceKey == null) return packet;
@@ -20,6 +20,7 @@
package de.steamwar.core.worldedit;
import com.comphenix.tinyprotocol.TinyProtocol;
import com.sk89q.worldedit.IncompleteRegionException;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
@@ -29,16 +30,13 @@ import com.sk89q.worldedit.regions.selector.limit.PermissiveSelectorLimits;
import com.sk89q.worldedit.world.World;
import de.steamwar.core.Core;
import de.steamwar.core.SWPlayer;
import de.steamwar.entity.CWireframe;
import de.steamwar.entity.REntityServer;
import de.steamwar.inventory.SWItem;
import net.minecraft.core.BlockPos;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.*;
import org.bukkit.Bukkit;
import org.bukkit.FluidCollisionMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -46,6 +44,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.util.RayTraceResult;
@@ -64,6 +63,16 @@ public class SteamEditWand implements Listener {
};
public static final ItemStack ITEM = new SWItem(MATERIAL, NAME).setLore(LORE).getItemStack();
static {
ItemMeta itemMeta = ITEM.getItemMeta();
itemMeta.addAttributeModifier(Attribute.BLOCK_INTERACTION_RANGE, new AttributeModifier(
NamespacedKey.fromString("range", Core.getInstance()),
35.0,
AttributeModifier.Operation.ADD_NUMBER
));
ITEM.setItemMeta(itemMeta);
}
private boolean noWand(Player player) {
return player.getInventory().getItemInMainHand().getType() != SteamEditWand.MATERIAL;
}
@@ -86,15 +95,6 @@ public class SteamEditWand implements Listener {
TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, this::rightClick);
TinyProtocol.instance.addFilter(ServerboundPickItemFromBlockPacket.class, this::middleClick);
TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, this::swapHands);
TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, this::movePlayerPacket);
TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Rot.class, this::movePlayerPacket);
TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, this::movePlayerPacket);
Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> {
for (Player player : Bukkit.getOnlinePlayers()) {
if (noWand(player)) SWPlayer.of(player).removeComponent(SWEditCursor.class);
}
}, 1, 1);
}
@EventHandler
@@ -103,35 +103,6 @@ public class SteamEditWand implements Listener {
event.setCancelled(true);
}
public static class SWEditCursor implements SWPlayer.Component {
private Player player;
private REntityServer entityServer = new REntityServer();
private CWireframe wireframe;
@Override
public void onMount(SWPlayer player) {
this.player = player.getPlayer();
entityServer.addPlayer(player.getPlayer());
wireframe = new CWireframe(entityServer);
}
@Override
public void onUnmount(SWPlayer player) {
entityServer.close();
}
public void setLocation(Location location) {
float distance = (float) player.getLocation().distanceSquared(location);
wireframe.setPos1And2(location, location);
float width = distance / 1600;
width = ((int) (width * 16)) / 16F;
width = Math.max(width, 1 / 16F);
width = Math.min(width, 1 / 2F);
wireframe.setWidth(width);
wireframe.setBlock(WorldEditRendererCUIEditor.Type.CURSOR.getMaterial(player).createBlockData());
}
}
private Packet<?> leftClick(Player player, Packet<?> packet) {
if (noWand(player)) return packet;
if (player.getOpenInventory().getType() != InventoryType.CREATIVE) return packet;
@@ -338,7 +309,11 @@ public class SteamEditWand implements Listener {
}
private void selectPos1(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos1) {
if (!pos1.equals(regionSelector.getPrimaryPosition())) {
try {
if (!pos1.equals(regionSelector.getPrimaryPosition())) {
SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1));
}
} catch (IncompleteRegionException e) {
SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1));
}
regionSelector.selectPrimary(pos1, PermissiveSelectorLimits.getInstance());
@@ -362,7 +337,11 @@ public class SteamEditWand implements Listener {
}
private void selectPos2(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos2) {
if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) {
try {
if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) {
SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2));
}
} catch (IncompleteRegionException e) {
SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2));
}
regionSelector.selectSecondary(pos2, PermissiveSelectorLimits.getInstance());
@@ -377,30 +356,4 @@ public class SteamEditWand implements Listener {
});
return null;
}
public ServerboundMovePlayerPacket movePlayerPacket(Player player, ServerboundMovePlayerPacket packet) {
SWPlayer swPlayer = SWPlayer.of(player);
if (noWand(player)) {
swPlayer.removeComponent(SWEditCursor.class);
return packet;
}
double distance = WorldEditRendererCUIEditor.Type.CURSOR.getWidth(player).blocks;
RayTraceResult result = player.rayTraceBlocks(distance, FluidCollisionMode.NEVER);
Block block = result == null ? null : result.getHitBlock();
if (result == null || block == null) {
swPlayer.removeComponent(SWEditCursor.class);
return packet;
}
if (!swPlayer.hasComponent(SWEditCursor.class)) {
swPlayer.setComponent(new SWEditCursor());
}
swPlayer.getComponent(SWEditCursor.class).ifPresent(cursor -> {
cursor.setLocation(block.getLocation());
});
return packet;
}
}
@@ -67,7 +67,9 @@ public class SWScoreboard {
}
public void removeScoreboard(Player player) {
player.getScoreboard().getObjective(SIDEBAR).unregister();
if (player.getScoreboard().getObjective(SIDEBAR) != null) {
player.getScoreboard().getObjective(SIDEBAR).unregister();
}
playerBoards.remove(player);
}
}
@@ -19,6 +19,7 @@
package de.steamwar.tntleague
import de.steamwar.core.WorldIdentifier
import de.steamwar.message.Message
import de.steamwar.tntleague.command.*
import de.steamwar.tntleague.events.GlobalListener
@@ -43,6 +44,11 @@ class TNTLeague : JavaPlugin() {
logger.info("TNTLeague enabled")
val arenaMap = System.getProperty("arenaMap")
if (arenaMap != null) {
WorldIdentifier.setArena(arenaMap)
}
ReadyCommand.register()
InviteCommand.register()
AcceptCommand.register()
@@ -53,6 +53,3 @@ DEALER_REDSTONE = §eRedstone
DEALER_BLOCKS = §eBlocks
DEALER_TOOLS = §eTools
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.game.TNTLeagueGame
import de.steamwar.tntleague.game.TNTLeagueTeam
import de.steamwar.tntleague.message
import de.steamwar.tntleague.plugin
import org.bukkit.GameMode
import org.bukkit.Material
@@ -90,20 +89,4 @@ object GlobalListener : Listener {
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!!
COMMAND_START = §aThe game will start soon§8!
PARTICIPANT_CHAT = {0} {1}§8» §7{2}
SPECTATOR_CHAT = §7{0}§8» §7{1}
@@ -20,6 +20,7 @@
package de.steamwar.towerrun;
import de.steamwar.command.SWCommand;
import de.steamwar.core.WorldIdentifier;
import de.steamwar.linkage.AbstractLinker;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.linkage.SpigotLinker;
@@ -56,6 +57,11 @@ public class TowerRun extends JavaPlugin {
towerGenerator = new TowerGenerator(WorldConfig.TOWER_GENERATOR_CONFIG);
}
String arenaMap = System.getProperty("arenaMap");
if (arenaMap != null) {
WorldIdentifier.setArena(arenaMap);
}
SpigotLinker spigotLinker = new SpigotLinker(this, message) {
@Override
protected void linkObject(Object any) {
@@ -64,6 +70,7 @@ public class TowerRun extends JavaPlugin {
}
}
};
try {
spigotLinker.link();
} catch (AbstractLinker.LinkException e) {
@@ -20,19 +20,15 @@
package de.steamwar.towerrun.listener;
import de.steamwar.linkage.Linked;
import de.steamwar.towerrun.TowerRun;
import de.steamwar.towerrun.config.WorldConfig;
import de.steamwar.towerrun.game.TowerRunGame;
import de.steamwar.towerrun.game.TowerRunPlayer;
import de.steamwar.towerrun.state.GameState;
import de.steamwar.towerrun.state.GameStateBukkitListener;
import de.steamwar.towerrun.state.GameStates;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
@@ -71,25 +67,4 @@ public class GlobalListener extends GameStateBukkitListener implements Listener
public void onPlayerRespawn(PlayerRespawnEvent event) {
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);
}
}
@@ -138,14 +138,16 @@ public class Subserver {
writer.println(command);
}
private ProcessHandle serverProcess() {
if (checkpoint) {
return process.children().findAny().orElseGet(process::toHandle);
}
return process.toHandle();
}
public void sleep() {
try {
long pid = process.pid();
if (checkpoint) {
pid = process.children().findAny().map(ProcessHandle::pid).orElse(pid);
}
Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(pid)});
Runtime.getRuntime().exec(new String[]{"kill", "-SIGUSR1", Long.toString(serverProcess().pid())});
} catch (IOException e) {
logger.log(Level.SEVERE, "Failed to send SIGUSR1 to subserver.", e);
}
@@ -162,7 +164,7 @@ public class Subserver {
public void stop() {
try {
process.destroy();
serverProcess().destroy();
if (!process.waitFor(1, TimeUnit.MINUTES)) {
forceStop();
} else if (thread.isAlive()) {
@@ -177,6 +179,7 @@ public class Subserver {
public void forceStop() {
try {
logger.log(Level.SEVERE, () -> serverName + " did not stop correctly, forcibly stopping!");
serverProcess().destroyForcibly();
process.destroyForcibly();
if (thread.isAlive()) thread.join();
@@ -430,7 +430,6 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Change your team name.
TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Change your team color.
TEAM_HELP_LEADER = §8/§7team promote §8- §7Promote someone to leader.
TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Demote yourself from leader.
TEAM_HELP_SERVER = §8/§7team server §8[§eIP/address§8] §8(§7port§8) §8- §7Set the address for your teamserver.
#Team Create
TEAM_CREATE_USAGE = §8/§7team create §8[§eteam shortcut§8] §8[§eteam name§8]
@@ -574,12 +573,59 @@ CHAT_BC_USAGE = §8/§7bc §8[§emessage§8]
CHAT_NO_RECEIVER = §cNobody receives your message
CHAT_EMPTY = §cDon\'t write meaningless empty messages.
CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2}
CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2}
CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2}
CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2}
CHAT_TEAM = §8TC §e{0}§8» §f{2}
CHAT_MSG = §e{0}§8»§e{1} §7{2}
CHAT_SERVERTEAM_SENDER = §e{0}
CHAT_SERVERTEAM = §8» §f{2}
CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0}
CHAT_GLOBAL = §8» {7}{2}
CHAT_TEAM_SENDER = §e{0}
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_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
CHECK_UNCHECKED = §7You still have §e{0} §7unchecked schematic§8(§7s§8)!
@@ -399,7 +399,6 @@ TEAM_HELP_NAME = §8/§7team changename §8- §7Ändere deinen Teamnamen.
TEAM_HELP_COLOR = §8/§7team changecolor §8- §7Ändere deine Teamfarbe.
TEAM_HELP_LEADER = §8/§7team promote §8- §7Ernenne jemanden zum Teamleader.
TEAM_HELP_STEP_BACK = §8/§7team stepback §8- §7Tritt als Leader zurück.
TEAM_HELP_SERVER = §8/§7team server §8[§eIP/Adresse§8] §8(§7Port§8) §8- §7Setzt Adresse des Teamservers.
#Team Create
TEAM_CREATE_USAGE = §8/§7team create §8[§eTeamkürzel§8] §8[§eTeamname§8]
@@ -539,12 +538,59 @@ CHAT_BC_USAGE = §8/§7bc §8[§eNachricht§8]
CHAT_NO_RECEIVER = §cNiemand empfängt deine Nachricht
CHAT_EMPTY = §cSchreibe keine inhaltslosen Nachrichten.
CHAT_SERVERTEAM = §8STC §e{0}§8» §f{2}
CHAT_DISCORD_SERVERTEAM = §8STC §e{0}§8» §f{2}
CHAT_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2}
CHAT_DISCORD_GLOBAL = {3}{4}{5}{6}{0}§8» {7}{2}
CHAT_TEAM = §8TC §e{0}§8» §f{2}
CHAT_MSG = §e{0}§8»§e{1} §7{2}
CHAT_SERVERTEAM_SENDER = §e{0}
CHAT_SERVERTEAM = §8» §f{2}
CHAT_GLOBAL_SENDER = {3}{4}{5}{6}{0}
CHAT_GLOBAL = §8» {7}{2}
CHAT_TEAM_SENDER = §e{0}
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_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
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.connection.LoginEvent;
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.UserPerm;
import de.steamwar.velocitycore.VelocityCore;
@@ -61,13 +59,6 @@ public interface Chatter {
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() {
return new ChatterGroup(allStream()
.filter(player -> player.user().hasPerm(UserPerm.TEAM))
@@ -31,8 +31,10 @@ import net.kyori.adventure.text.event.ClickEvent;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static de.steamwar.persistent.Storage.eventServer;
@@ -43,8 +45,15 @@ public class EventStarter {
@Getter
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);
spectateServers.add(serverName);
}
public static boolean isSpectateServer(String serverName) {
return spectateServers.contains(serverName);
}
public EventStarter() {
@@ -91,6 +91,7 @@ public class ServerStarter {
gameMode = mode.configFile.getName().replace(".yml", "");
directory = new File(SERVER_PATH, mode.Server.Folder);
arguments.put("config", mode.configFile.getName());
arguments.put("arenaMap", map);
tempWorld(SERVER_PATH + mode.Server.Folder + "/arenas/" + map);
startCondition = () -> {
if (playersToSend.stream().anyMatch(player -> Subserver.isArena(Subserver.getSubserver(player)))) {
@@ -236,7 +236,7 @@ public class VelocityCore implements ReloadablePlugin {
String cmd = cmds.remove(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]));
@@ -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,158 @@
/*
* 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.HoverEvent;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class ChatFormat {
private static final Pattern MENTION = Pattern.compile("@(\\w{1,16})");
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();
String teamPrefix = "";
if (user.hasPerm(UserPerm.TEAM) && useSwPrefix(user)) {
teamPrefix = "§eS§8W ";
} else if (user.getTeam() != 0) {
Team team = Team.byId(user.getTeam());
teamPrefix = "§" + team.getTeamColor() + team.getTeamKuerzel() + " ";
}
String chatColorCode = user.hasPerm(UserPerm.TEAM) ? "§f" : "§7";
Object[] params = {
user,
msgReceiver == null ? receiver : msgReceiver,
highlightMentions(message, chatColorCode, receiver),
teamPrefix,
"",
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(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 = team.hoverEvent(HoverEvent.showText(teamHover(receiver, eventTeam)));
}
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) {
if (useSwPrefix(user)) {
return component.hoverEvent(HoverEvent.showText(receiver.parse(new Message("CHAT_HOVER_TEAM", "SteamWar", "§e", "S§8W"))));
}
if (user.getTeam() == 0) return component;
return component.hoverEvent(HoverEvent.showText(teamHover(receiver, Team.byId(user.getTeam()))));
}
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));
}
}

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