forked from SteamWar/SteamWar
Remove YAPION from BauSystem_Main module
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 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.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 ConfigStoreListener implements Listener {
|
||||
|
||||
@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();
|
||||
}
|
||||
+95
@@ -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);
|
||||
}
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user