/* * This file is part of HuskSync, licensed under the Apache License 2.0. * * Copyright (c) William278 * Copyright (c) contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.william278.husksync; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.gson.Gson; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import net.kyori.adventure.platform.AudienceProvider; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.william278.desertwell.util.Version; import net.william278.husksync.adapter.DataAdapter; import net.william278.husksync.adapter.GsonAdapter; import net.william278.husksync.adapter.SnappyGsonAdapter; import net.william278.husksync.api.BukkitHuskSyncAPI; import net.william278.husksync.command.BukkitCommand; import net.william278.husksync.config.Locales; import net.william278.husksync.config.Server; import net.william278.husksync.config.Settings; import net.william278.husksync.data.*; import net.william278.husksync.database.Database; import net.william278.husksync.database.MongoDbDatabase; import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.database.PostgresDatabase; import net.william278.husksync.event.BukkitEventDispatcher; import net.william278.husksync.hook.PlanHook; import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.migrator.LegacyMigrator; import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.MpdbMigrator; import net.william278.husksync.redis.RedisManager; import net.william278.husksync.sync.DataSyncer; import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.OnlineUser; import net.william278.husksync.util.BukkitLegacyConverter; import net.william278.husksync.util.BukkitMapPersister; import net.william278.husksync.util.BukkitTask; import net.william278.husksync.util.LegacyConverter; import org.bstats.bukkit.Metrics; import org.bukkit.entity.Player; import org.bukkit.map.MapView; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import space.arim.morepaperlib.MorePaperLib; import space.arim.morepaperlib.commands.CommandRegistration; import space.arim.morepaperlib.scheduling.AsynchronousScheduler; import space.arim.morepaperlib.scheduling.AttachedScheduler; import space.arim.morepaperlib.scheduling.GracefulScheduling; import space.arim.morepaperlib.scheduling.RegionalScheduler; import java.nio.file.Path; import java.util.*; import java.util.logging.Level; import java.util.stream.Collectors; @Getter @NoArgsConstructor public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, BukkitEventDispatcher, BukkitMapPersister { /** * Metrics ID for HuskSync on Bukkit. */ private static final int METRICS_ID = 13140; private static final String PLATFORM_TYPE_ID = "bukkit"; private final Map> serializers = Maps.newLinkedHashMap(); private final Map> playerCustomDataStore = Maps.newConcurrentMap(); private final Map mapViews = Maps.newConcurrentMap(); private final List availableMigrators = Lists.newArrayList(); private final Set lockedPlayers = Sets.newConcurrentHashSet(); private boolean disabling; private Gson gson; private AudienceProvider audiences; private MorePaperLib paperLib; private Database database; private RedisManager redisManager; private BukkitEventListener eventListener; private DataAdapter dataAdapter; private DataSyncer dataSyncer; private LegacyConverter legacyConverter; private AsynchronousScheduler asyncScheduler; private RegionalScheduler regionalScheduler; @Setter private Settings settings; @Setter private Locales locales; @Setter @Getter(AccessLevel.NONE) private Server serverName; @Override public void onLoad() { // Initial plugin setup this.disabling = false; this.gson = createGson(); this.paperLib = new MorePaperLib(this); // Load settings and locales initialize("plugin config & locale files", (plugin) -> { loadSettings(); loadLocales(); loadServer(); }); this.eventListener = createEventListener(); eventListener.onLoad(); } @Override public void onEnable() { this.audiences = BukkitAudiences.create(this); // Prepare data adapter initialize("data adapter", (plugin) -> { if (settings.getSynchronization().isCompressData()) { dataAdapter = new SnappyGsonAdapter(this); } else { dataAdapter = new GsonAdapter(this); } }); // Prepare serializers initialize("data serializers", (plugin) -> { registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this)); registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this)); registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this)); registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class)); registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class)); registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class)); registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class)); registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class)); registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class)); registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this)); registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class)); registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class)); registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this)); }); // Setup available migrators initialize("data migrators/converters", (plugin) -> { availableMigrators.add(new LegacyMigrator(this)); if (isDependencyLoaded("MySqlPlayerDataBridge")) { availableMigrators.add(new MpdbMigrator(this)); } legacyConverter = new BukkitLegacyConverter(this); }); // Initialize the database initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> { this.database = switch (settings.getDatabase().getType()) { case MYSQL, MARIADB -> new MySqlDatabase(this); case POSTGRES -> new PostgresDatabase(this); case MONGO -> new MongoDbDatabase(this); }; this.database.initialize(); }); // Prepare redis connection initialize("Redis server connection", (plugin) -> { this.redisManager = new RedisManager(this); this.redisManager.initialize(); }); // Prepare data syncer initialize("data syncer", (plugin) -> { dataSyncer = getSettings().getSynchronization().getMode().create(this); dataSyncer.initialize(); }); // Register events initialize("events", (plugin) -> eventListener.onEnable()); // Register commands initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this)); // Register plugin hooks initialize("hooks", (plugin) -> { if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) { new PlanHook(this).hookIntoPlan(); } }); // Register API initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this)); // Hook into bStats and check for updates initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID)); this.checkForUpdates(); } @Override public void onDisable() { // Handle shutdown this.disabling = true; // Close the event listener / data syncer if (this.dataSyncer != null) { this.dataSyncer.terminate(); } if (this.eventListener != null) { this.eventListener.handlePluginDisable(); } // Unregister API and cancel tasks BukkitHuskSyncAPI.unregister(); this.cancelTasks(); // Complete shutdown log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion()); } @NotNull protected BukkitEventListener createEventListener() { return new BukkitEventListener(this); } @Override @NotNull public Set getOnlineUsers() { return getServer().getOnlinePlayers().stream() .map(player -> BukkitUser.adapt(player, this)) .collect(Collectors.toSet()); } @Override @NotNull public Optional getOnlineUser(@NotNull UUID uuid) { final Player player = getServer().getPlayer(uuid); if (player == null) { return Optional.empty(); } return Optional.of(BukkitUser.adapt(player, this)); } @Override public void setDataSyncer(@NotNull DataSyncer dataSyncer) { log(Level.INFO, String.format("Switching data syncer to %s", dataSyncer.getClass().getSimpleName())); this.dataSyncer = dataSyncer; } @NotNull @Override public Map getPlayerCustomDataStore(@NotNull OnlineUser user) { return playerCustomDataStore.compute( user.getUuid(), (uuid, data) -> data == null ? Maps.newHashMap() : data ); } @Override @NotNull public String getServerName() { return serverName == null ? "server" : serverName.getName(); } @Override public boolean isDependencyLoaded(@NotNull String name) { return getServer().getPluginManager().getPlugin(name) != null; } // Register bStats metrics public void registerMetrics(int metricsId) { if (!getPluginVersion().getMetadata().isBlank()) { return; } try { new Metrics(this, metricsId); } catch (Throwable e) { log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")"); } } @Override public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) { if (throwable.length > 0) { getLogger().log(level, message, throwable[0]); } else { getLogger().log(level, message); } } @NotNull @Override public Version getPluginVersion() { return Version.fromString(getDescription().getVersion(), "-"); } @NotNull @Override public Version getMinecraftVersion() { return Version.fromString(getServer().getBukkitVersion()); } @NotNull @Override public String getPlatformType() { return PLATFORM_TYPE_ID; } @Override public Optional getLegacyConverter() { return Optional.of(legacyConverter); } @NotNull public GracefulScheduling getScheduler() { return paperLib.scheduling(); } @NotNull public AsynchronousScheduler getAsyncScheduler() { return asyncScheduler == null ? asyncScheduler = getScheduler().asyncScheduler() : asyncScheduler; } @NotNull public RegionalScheduler getSyncScheduler() { return regionalScheduler == null ? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler; } @NotNull public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) { return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer()); } @NotNull public CommandRegistration getCommandRegistrar() { return paperLib.commandRegistration(); } @Override @NotNull public Path getConfigDirectory() { return getDataFolder().toPath(); } @Override @NotNull public BukkitHuskSync getPlugin() { return this; } }