package tc.oc.commons.bukkit.respack; import java.time.Duration; import java.time.Instant; import java.util.Objects; import java.util.logging.Logger; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import com.google.common.eventbus.Subscribe; 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.PlayerResourcePackStatusEvent; import org.bukkit.plugin.Plugin; import tc.oc.api.bukkit.users.Users; import tc.oc.api.docs.virtual.UserDoc; import tc.oc.api.minecraft.MinecraftService; import tc.oc.api.minecraft.servers.LocalServerReconfigureEvent; import tc.oc.api.users.UserService; import tc.oc.commons.bukkit.util.OnlinePlayerMapAdapter; import tc.oc.commons.core.logging.Loggers; import tc.oc.commons.core.plugin.PluginFacet; @Singleton public class ResourcePackListener implements ResourcePackManager, Listener, PluginFacet { // Minimum time a player must be connected before sending them a res pack. // If we send it too soon, the client behaves badly. private static final Duration JOIN_DELAY = Duration.ofSeconds(1); private final Logger logger; private final Plugin plugin; private final MinecraftService minecraftService; private final UserService userService; private boolean enabled = true; private final OnlinePlayerMapAdapter<String> lastSentSha1; private final OnlinePlayerMapAdapter<Instant> joinTime; @Inject ResourcePackListener(Loggers loggers, Plugin plugin, MinecraftService minecraftService, UserService userService, OnlinePlayerMapAdapter<String> lastSentSha1, OnlinePlayerMapAdapter<Instant> joinTime) { this.logger = loggers.get(getClass()); this.userService = userService; this.lastSentSha1 = lastSentSha1; this.joinTime = joinTime; this.minecraftService = minecraftService; this.plugin = plugin; } @Override public void enable() { lastSentSha1.enable(); joinTime.enable(); } @Override public void disable() { joinTime.disable(); lastSentSha1.disable(); } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { if(this.enabled != enabled) { this.enabled = enabled; if(enabled && isFastUpdate()) { refreshAll(); } } } @Override public boolean isFastUpdate() { return minecraftService.getLocalServer().resource_pack_fast_update(); } @Override public @Nullable String getUrl() { return minecraftService.getLocalServer().resource_pack_url(); } @Override public @Nullable String getSha1() { return minecraftService.getLocalServer().resource_pack_sha1(); } @Override public void refreshPlayer(final Player player) { if(!enabled) return; if(!player.isOnline()) return; String url = getUrl(); String sha1 = getSha1(); if(url == null || sha1 == null) return; if(!Objects.equals(lastSentSha1.get(player), sha1)) { Instant joined = joinTime.get(player); if(joined == null) return; long delayMillis = Duration.between(Instant.now(), joined.plus(JOIN_DELAY)).toMillis(); if(delayMillis <= 0) { logger.fine("Sending resource pack " + url + " with SHA1 " + sha1 + " to player " + player.getName()); lastSentSha1.put(player, sha1); player.setResourcePack(url, sha1); } else { plugin.getServer().getScheduler().runTaskLater(plugin, new Runnable() { @Override public void run() { refreshPlayer(player); } }, delayMillis / 50 + 1); } } } @Override public void refreshAll() { for(Player player : plugin.getServer().getOnlinePlayers()) { refreshPlayer(player); } } @EventHandler public void join(PlayerJoinEvent event) { this.joinTime.put(event.getPlayer(), Instant.now()); refreshPlayer(event.getPlayer()); } @Subscribe public void reconfigure(LocalServerReconfigureEvent event) { if(event.getNewConfig().resource_pack_fast_update()) { String oldSha1 = event.getOldConfig() == null ? null : event.getOldConfig().resource_pack_sha1(); String newSha1 = event.getNewConfig().resource_pack_sha1(); if(!Objects.equals(oldSha1, newSha1)) refreshAll(); } } @EventHandler public void confirm(final PlayerResourcePackStatusEvent event) { logger.fine("Player " + event.getPlayer().getName() + " sent res pack status " + event.getStatus()); final UserDoc.ResourcePackStatus status; switch(event.getStatus()) { case ACCEPTED: status = UserDoc.ResourcePackStatus.ACCEPTED; break; case DECLINED: status = UserDoc.ResourcePackStatus.DECLINED; break; case SUCCESSFULLY_LOADED: status = UserDoc.ResourcePackStatus.LOADED; break; case FAILED_DOWNLOAD: status = UserDoc.ResourcePackStatus.FAILED; break; default: throw new IllegalStateException("Unknown status " + event.getStatus()); } userService.update( Users.playerId(event.getPlayer()), (UserDoc.ResourcePackResponse) () -> status ); } }