From 388086303f692de1434755485f47fe27dacd6ef1 Mon Sep 17 00:00:00 2001 From: RealizedMC Date: Sat, 15 Sep 2018 16:20:12 -0700 Subject: [PATCH] v3.2.1 - LeaderHeads support, revamped paged gui system, autosave, bug fixes --- build.gradle | 2 +- .../api/event/request/RequestAcceptEvent.java | 58 +++++++++++++++++ .../api/event/request/RequestDenyEvent.java | 34 ++++++++++ .../duels/api/event/request/RequestEvent.java | 59 ++++++++++++++++++ .../api/event/request/RequestSendEvent.java | 55 ++++------------ .../duels/api/event/user/UserCreateEvent.java | 5 ++ duels-plugin/build.gradle | 1 + .../java/me/realized/duels/DuelsPlugin.java | 5 ++ .../java/me/realized/duels/api/DuelsAPI.java | 10 +-- .../java/me/realized/duels/arena/Arena.java | 2 +- .../me/realized/duels/arena/ArenaManager.java | 22 ++++++- .../me/realized/duels/arena/Countdown.java | 7 ++- .../duel/subcommands/AcceptCommand.java | 12 +++- .../duel/subcommands/DenyCommand.java | 9 ++- .../duels/subcommands/ReloadCommand.java | 2 +- .../java/me/realized/duels/config/Config.java | 13 ++++ .../me/realized/duels/duel/DuelManager.java | 1 - .../duels/extension/ExtensionClassLoader.java | 7 +-- .../me/realized/duels/hook/HookManager.java | 2 + .../duels/hook/hooks/LeaderHeadsHook.java | 55 ++++++++++++++++ .../me/realized/duels/kit/KitManager.java | 22 ++++++- .../duels/player/PlayerInfoManager.java | 6 +- .../java/me/realized/duels/queue/Queue.java | 10 ++- .../me/realized/duels/queue/QueueManager.java | 25 +++++++- .../duels/queue/sign/QueueSignManager.java | 20 +++++- .../duels/request/RequestManager.java | 14 +++-- .../me/realized/duels/spectate/Spectator.java | 10 ++- .../me/realized/duels/util/TextBuilder.java | 2 +- .../java/me/realized/duels/util/UUIDUtil.java | 4 -- duels-plugin/src/main/resources/config.yml | 19 +++++- duels-plugin/src/main/resources/plugin.yml | 2 +- libs/LeaderHeadsAPI.jar | Bin 0 -> 29080 bytes 32 files changed, 404 insertions(+), 91 deletions(-) create mode 100644 duels-api/src/main/java/me/realized/duels/api/event/request/RequestAcceptEvent.java create mode 100644 duels-api/src/main/java/me/realized/duels/api/event/request/RequestDenyEvent.java create mode 100644 duels-api/src/main/java/me/realized/duels/api/event/request/RequestEvent.java create mode 100644 duels-plugin/src/main/java/me/realized/duels/hook/hooks/LeaderHeadsHook.java create mode 100644 libs/LeaderHeadsAPI.jar diff --git a/build.gradle b/build.gradle index 7ccf9fc2..717ba2d6 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { allprojects { group 'me.realized' - version '3.2.1-SNAPSHOT' + version '3.2.1' } subprojects { diff --git a/duels-api/src/main/java/me/realized/duels/api/event/request/RequestAcceptEvent.java b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestAcceptEvent.java new file mode 100644 index 00000000..5337d502 --- /dev/null +++ b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestAcceptEvent.java @@ -0,0 +1,58 @@ +package me.realized.duels.api.event.request; + +import javax.annotation.Nonnull; +import me.realized.duels.api.request.Request; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; + +/** + * Called when a {@link Player} accepts a {@link Request} from a {@link Player}. + * + * @since 3.2.1 + */ +public class RequestAcceptEvent extends RequestEvent implements Cancellable { + + private static final HandlerList handlers = new HandlerList(); + + private boolean cancelled; + + /** + * @param source {@link Player} who is accepting this {@link Request}. + * @param target {@link Player} who sent this {@link Request}. + * @param request {@link Request} that is being handled. + */ + public RequestAcceptEvent(@Nonnull final Player source, @Nonnull final Player target, @Nonnull final Request request) { + super(source, target, request); + } + + /** + * Whether or not this event has been cancelled. + * + * @return True if this event has been cancelled. False otherwise. + */ + @Override + public boolean isCancelled() { + return cancelled; + } + + /** + * Whether or not to cancel this event. + * When cancelled, the request will not be removed and remain as unhandled. + * + * @param cancelled True to cancel this event. + */ + @Override + public void setCancelled(final boolean cancelled) { + this.cancelled = cancelled; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/duels-api/src/main/java/me/realized/duels/api/event/request/RequestDenyEvent.java b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestDenyEvent.java new file mode 100644 index 00000000..42e6240d --- /dev/null +++ b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestDenyEvent.java @@ -0,0 +1,34 @@ +package me.realized.duels.api.event.request; + +import javax.annotation.Nonnull; +import me.realized.duels.api.request.Request; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; + +/** + * Called when a {@link Player} denies a {@link Request} from a {@link Player}. + * + * @since 3.2.1 + */ +public class RequestDenyEvent extends RequestEvent { + + private static final HandlerList handlers = new HandlerList(); + + /** + * @param source {@link Player} who is denying this {@link Request}. + * @param target {@link Player} who sent this {@link Request}. + * @param request {@link Request} that is being handled. + */ + public RequestDenyEvent(@Nonnull final Player source, @Nonnull final Player target, @Nonnull final Request request) { + super(source, target, request); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/duels-api/src/main/java/me/realized/duels/api/event/request/RequestEvent.java b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestEvent.java new file mode 100644 index 00000000..568b8b8e --- /dev/null +++ b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestEvent.java @@ -0,0 +1,59 @@ +package me.realized.duels.api.event.request; + +import java.util.Objects; +import javax.annotation.Nonnull; +import me.realized.duels.api.event.SourcedEvent; +import me.realized.duels.api.request.Request; +import org.bukkit.entity.Player; + +/** + * Represents an event caused by a {@link Request}. + * + * @since 3.2.1 + */ +public abstract class RequestEvent extends SourcedEvent { + + private final Player source, target; + private final Request request; + + RequestEvent(@Nonnull final Player source, @Nonnull final Player target, @Nonnull final Request request) { + super(source); + Objects.requireNonNull(source, "source"); + Objects.requireNonNull(target, "target"); + Objects.requireNonNull(request, "request"); + this.source = source; + this.target = target; + this.request = request; + } + + /** + * {@link Player} who is the source of this event. + * + * @return Never-null {@link Player} who is the source of this event. + */ + @Nonnull + @Override + public Player getSource() { + return source; + } + + /** + * {@link Player} who is the target of this event. + * + * @return Never-null {@link Player} who is the target of this event. + */ + @Nonnull + public Player getTarget() { + return target; + } + + /** + * {@link Request} instance associated with this event. + * + * @return Never-null {@link Request} instance associated with this event. + */ + @Nonnull + public Request getRequest() { + return request; + } +} diff --git a/duels-api/src/main/java/me/realized/duels/api/event/request/RequestSendEvent.java b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestSendEvent.java index 3c16df43..660ab665 100644 --- a/duels-api/src/main/java/me/realized/duels/api/event/request/RequestSendEvent.java +++ b/duels-api/src/main/java/me/realized/duels/api/event/request/RequestSendEvent.java @@ -1,8 +1,6 @@ package me.realized.duels.api.event.request; -import java.util.Objects; import javax.annotation.Nonnull; -import me.realized.duels.api.event.SourcedEvent; import me.realized.duels.api.request.Request; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; @@ -11,73 +9,42 @@ /** * Called when a {@link Player} sends a {@link Request} to a {@link Player}. */ -public class RequestSendEvent extends SourcedEvent implements Cancellable { +public class RequestSendEvent extends RequestEvent implements Cancellable { private static final HandlerList handlers = new HandlerList(); - private final Player source; - private final Player target; - private final Request request; - private boolean cancelled; public RequestSendEvent(@Nonnull final Player source, @Nonnull final Player target, @Nonnull final Request request) { - super(source); - Objects.requireNonNull(source, "source"); - Objects.requireNonNull(target, "target"); - Objects.requireNonNull(request, "request"); - this.source = source; - this.target = target; - this.request = request; + super(source, target, request); } /** - * {@link Player} who is sending the {@link Request}. + * Whether or not this event has been cancelled. * - * @return Never-null {@link Player} who is sending the {@link Request}. + * @return True if this event has been cancelled. False otherwise. */ - @Nonnull @Override - public Player getSource() { - return source; - } - - /** - * {@link Player} who is receiving the {@link Request}. - * - * @return Never-null {@link Player} who is receiving the {@link Request}. - */ - @Nonnull - public Player getTarget() { - return target; + public boolean isCancelled() { + return cancelled; } /** - * The {@link Request} that will be sent. + * Whether or not to cancel this event. * - * @return Never-null {@link Request} that will be sent. + * @param cancelled True to cancel this event. */ - @Nonnull - public Request getRequest() { - return request; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - @Override public void setCancelled(final boolean cancelled) { this.cancelled = cancelled; } - public static HandlerList getHandlerList() { + @Override + public HandlerList getHandlers() { return handlers; } - @Override - public HandlerList getHandlers() { + public static HandlerList getHandlerList() { return handlers; } } diff --git a/duels-api/src/main/java/me/realized/duels/api/event/user/UserCreateEvent.java b/duels-api/src/main/java/me/realized/duels/api/event/user/UserCreateEvent.java index 4781a0bd..70dfcc13 100644 --- a/duels-api/src/main/java/me/realized/duels/api/event/user/UserCreateEvent.java +++ b/duels-api/src/main/java/me/realized/duels/api/event/user/UserCreateEvent.java @@ -20,6 +20,11 @@ public UserCreateEvent(@Nonnull final User user) { this.user = user; } + /** + * The {@link User} that was created. + * + * @return Never-null {@link User} that was created. + */ @Nonnull public User getUser() { return user; diff --git a/duels-plugin/build.gradle b/duels-plugin/build.gradle index 66bbdd44..63bebdbb 100644 --- a/duels-plugin/build.gradle +++ b/duels-plugin/build.gradle @@ -32,6 +32,7 @@ dependencies { compile name: 'BountyHunters' compile name: 'SimpleClans' compile name: 'CombatLogX' + compile name: 'LeaderHeadsAPI' compile project(':duels-api') compile 'com.google.code.gson:gson:2.8.2' diff --git a/duels-plugin/src/main/java/me/realized/duels/DuelsPlugin.java b/duels-plugin/src/main/java/me/realized/duels/DuelsPlugin.java index 54f2026e..120213dd 100644 --- a/duels-plugin/src/main/java/me/realized/duels/DuelsPlugin.java +++ b/duels-plugin/src/main/java/me/realized/duels/DuelsPlugin.java @@ -218,6 +218,11 @@ private boolean load() { loadable.handleLoad(); lastLoad = loadables.indexOf(loadable); } catch (Exception ex) { + // Handles the case of exceptions from LogManager not being logged in file + if (loadable instanceof LogSource) { + ex.printStackTrace(); + } + Log.error("There was an error while loading " + loadable.getClass().getSimpleName() + "! If you believe this is an issue from the plugin, please contact the developer.", ex); return false; diff --git a/duels-plugin/src/main/java/me/realized/duels/api/DuelsAPI.java b/duels-plugin/src/main/java/me/realized/duels/api/DuelsAPI.java index 2a26b8db..713a5cea 100644 --- a/duels-plugin/src/main/java/me/realized/duels/api/DuelsAPI.java +++ b/duels-plugin/src/main/java/me/realized/duels/api/DuelsAPI.java @@ -9,9 +9,9 @@ /** * A static API for Duels. - * (This is an old API for Duels v2 and below.) + * (This is an old, deprecated API for Duels v2 and below.) * - * @deprecated as of v3.0.0, use {@link Duels} instead. + * @deprecated As of v3.0.0, use {@link Duels} instead. */ public class DuelsAPI { @@ -21,7 +21,7 @@ public class DuelsAPI { * @deprecated As of v3.0.0, use {@link UserManager#get(UUID)} instead. */ @Deprecated - public static UserData getUser(UUID uuid, boolean force) { + public static UserData getUser(final UUID uuid, boolean force) { return DuelsPlugin.getInstance().getUserManager().get(uuid); } @@ -30,7 +30,7 @@ public static UserData getUser(UUID uuid, boolean force) { * @deprecated As of v3.0.0, use {@link UserManager#get(Player)} instead. */ @Deprecated - public static UserData getUser(Player player, boolean force) { + public static UserData getUser(final Player player, boolean force) { return DuelsPlugin.getInstance().getUserManager().get(player); } @@ -39,7 +39,7 @@ public static UserData getUser(Player player, boolean force) { * @deprecated As of v3.0.0, use {@link ArenaManager#isInMatch(Player)} instead. */ @Deprecated - public static boolean isInMatch(Player player) { + public static boolean isInMatch(final Player player) { return DuelsPlugin.getInstance().getArenaManager().isInMatch(player); } diff --git a/duels-plugin/src/main/java/me/realized/duels/arena/Arena.java b/duels-plugin/src/main/java/me/realized/duels/arena/Arena.java index 6e2b604e..52dc1bf8 100644 --- a/duels-plugin/src/main/java/me/realized/duels/arena/Arena.java +++ b/duels-plugin/src/main/java/me/realized/duels/arena/Arena.java @@ -34,7 +34,7 @@ public class Arena extends BaseButton implements me.realized.duels.api.arena.Are @Getter private final String name; @Getter - private Map positions = new HashMap<>(); + private final Map positions = new HashMap<>(); @Getter private boolean disabled; @Getter diff --git a/duels-plugin/src/main/java/me/realized/duels/arena/ArenaManager.java b/duels-plugin/src/main/java/me/realized/duels/arena/ArenaManager.java index 0ef0a292..f9d780cc 100644 --- a/duels-plugin/src/main/java/me/realized/duels/arena/ArenaManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/arena/ArenaManager.java @@ -43,6 +43,8 @@ public class ArenaManager implements Loadable, me.realized.duels.api.arena.ArenaManager, Listener { + private static final long AUTO_SAVE_INTERVAL = 20L * 60 * 5; + private final DuelsPlugin plugin; private final Config config; private final Lang lang; @@ -52,6 +54,7 @@ public class ArenaManager implements Loadable, me.realized.duels.api.arena.Arena @Getter private MultiPageGui gui; + private int autoSaveTask; public ArenaManager(final DuelsPlugin plugin) { this.plugin = plugin; @@ -91,14 +94,29 @@ public void handleLoad() throws IOException { Log.info(this, "Loaded " + arenas.size() + " arena(s)."); gui.calculatePages(); + + this.autoSaveTask = plugin.doSyncRepeat(() -> { + try { + saveArenas(); + } catch (IOException ex) { + Log.error(this, ex.getMessage(), ex); + } + }, AUTO_SAVE_INTERVAL, AUTO_SAVE_INTERVAL).getTaskId(); } @Override public void handleUnload() throws IOException { + plugin.cancelTask(autoSaveTask); + if (gui != null) { plugin.getGuiListener().removeGui(gui); } + saveArenas(); + arenas.clear(); + } + + private void saveArenas() throws IOException { final List data = new ArrayList<>(); for (final Arena arena : arenas) { @@ -110,11 +128,9 @@ public void handleUnload() throws IOException { } try (Writer writer = new OutputStreamWriter(new FileOutputStream(file))) { - writer.write(plugin.getGson().toJson(data)); + plugin.getGson().toJson(data, writer); writer.flush(); } - - arenas.clear(); } @Nullable diff --git a/duels-plugin/src/main/java/me/realized/duels/arena/Countdown.java b/duels-plugin/src/main/java/me/realized/duels/arena/Countdown.java index 0ff2fe73..5fdf391c 100644 --- a/duels-plugin/src/main/java/me/realized/duels/arena/Countdown.java +++ b/duels-plugin/src/main/java/me/realized/duels/arena/Countdown.java @@ -49,7 +49,12 @@ public void run() { final OpponentInfo info = this.info.get(player.getUniqueId()); if (info != null) { - player.sendMessage(message.replace("%opponent%", info.getName()).replace("%opponent_rating%", String.valueOf(info.getRating())).replace("%kit%", kit)); + player.sendMessage(message + .replace("%opponent%", info.getName()) + .replace("%opponent_rating%", String.valueOf(info.getRating())) + .replace("%kit%", kit) + .replace("%arena%", arena.getName()) + ); } else { player.sendMessage(message); } diff --git a/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/AcceptCommand.java b/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/AcceptCommand.java index 34307c3c..7d4048c0 100644 --- a/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/AcceptCommand.java +++ b/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/AcceptCommand.java @@ -1,6 +1,7 @@ package me.realized.duels.command.commands.duel.subcommands; import me.realized.duels.DuelsPlugin; +import me.realized.duels.api.event.request.RequestAcceptEvent; import me.realized.duels.command.BaseCommand; import me.realized.duels.hook.hooks.CombatLogXHook; import me.realized.duels.hook.hooks.CombatTagPlusHook; @@ -76,13 +77,22 @@ protected void execute(final CommandSender sender, final String label, final Str return; } - final Request request = requestManager.remove(target, player); + final Request request = requestManager.get(target, player); if (request == null) { lang.sendMessage(sender, "ERROR.duel.no-request", "name", target.getName()); return; } + final RequestAcceptEvent event = new RequestAcceptEvent(player, target, request); + plugin.getServer().getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + + requestManager.remove(target, player); + if (arenaManager.isInMatch(target)) { lang.sendMessage(sender, "ERROR.duel.already-in-match.target", "name", target.getName()); return; diff --git a/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/DenyCommand.java b/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/DenyCommand.java index 18dbd0cd..6ac24c2e 100644 --- a/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/DenyCommand.java +++ b/duels-plugin/src/main/java/me/realized/duels/command/commands/duel/subcommands/DenyCommand.java @@ -1,7 +1,9 @@ package me.realized.duels.command.commands.duel.subcommands; import me.realized.duels.DuelsPlugin; +import me.realized.duels.api.event.request.RequestDenyEvent; import me.realized.duels.command.BaseCommand; +import me.realized.duels.request.Request; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -22,11 +24,16 @@ protected void execute(final CommandSender sender, final String label, final Str return; } - if (requestManager.remove(target, player) == null) { + final Request request; + + if ((request = requestManager.remove(target, player)) == null) { lang.sendMessage(sender, "ERROR.duel.no-request", "name", target.getName()); return; } + final RequestDenyEvent event = new RequestDenyEvent(player, target, request); + plugin.getServer().getPluginManager().callEvent(event); + lang.sendMessage(player, "COMMAND.duel.request.deny.receiver", "name", target.getName()); lang.sendMessage(target, "COMMAND.duel.request.deny.sender", "name", player.getName()); } diff --git a/duels-plugin/src/main/java/me/realized/duels/command/commands/duels/subcommands/ReloadCommand.java b/duels-plugin/src/main/java/me/realized/duels/command/commands/duels/subcommands/ReloadCommand.java index 3ba0d1b8..34a2d85f 100644 --- a/duels-plugin/src/main/java/me/realized/duels/command/commands/duels/subcommands/ReloadCommand.java +++ b/duels-plugin/src/main/java/me/realized/duels/command/commands/duels/subcommands/ReloadCommand.java @@ -22,7 +22,7 @@ protected void execute(final CommandSender sender, final String label, final Str if (args.length > getLength()) { final Loadable target = plugin.find(args[1]); - if (target == null || !(target instanceof Reloadable)) { + if (!(target instanceof Reloadable)) { sender .sendMessage(ChatColor.RED + "Invalid module. The following modules are available for a reload: " + StringUtils.join(plugin.getReloadables(), ", ")); return; diff --git a/duels-plugin/src/main/java/me/realized/duels/config/Config.java b/duels-plugin/src/main/java/me/realized/duels/config/Config.java index 99d8ff2b..d56a2295 100644 --- a/duels-plugin/src/main/java/me/realized/duels/config/Config.java +++ b/duels-plugin/src/main/java/me/realized/duels/config/Config.java @@ -52,6 +52,14 @@ public class Config extends AbstractConfiguration { private boolean preventBountyLoss; @Getter private boolean preventAddDeath; + @Getter + private String lhWinsCmd; + @Getter + private String lhWinsTitle; + @Getter + private String lhLossesCmd; + @Getter + private String lhLossesTitle; @Getter private boolean requiresClearedInventory; @@ -237,6 +245,11 @@ protected void loadValues(FileConfiguration configuration) throws Exception { myPetDespawn = configuration.getBoolean("supported-plugins.MyPet.despawn-pet-in-duel", false); preventBountyLoss = configuration.getBoolean("supported-plugins.BountyHunters.prevent-bounty-loss-in-duel", true); preventAddDeath = configuration.getBoolean("supported-plugins.SimpleClans.prevent-add-death-in-duel", true); + lhWinsCmd = configuration.getString("supported-plugins.LeaderHeads.wins.menu.command", "openwins"); + lhWinsTitle = configuration.getString("supported-plugins.LeaderHeads.wins.menu.title", "Duel Wins"); + lhLossesCmd = configuration.getString("supported-plugins.LeaderHeads.losses.menu.command", "openlosses"); + lhLossesTitle = configuration.getString("supported-plugins.LeaderHeads.losses.menu.title", "Duel Losses"); + requiresClearedInventory = configuration.getBoolean("request.requires-cleared-inventory", true); preventCreativeMode = configuration.getBoolean("request.prevent-creative-mode", false); arenaSelectingEnabled = configuration.getBoolean("request.arena-selecting.enabled", true); diff --git a/duels-plugin/src/main/java/me/realized/duels/duel/DuelManager.java b/duels-plugin/src/main/java/me/realized/duels/duel/DuelManager.java index 0f3bdaa0..77782928 100644 --- a/duels-plugin/src/main/java/me/realized/duels/duel/DuelManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/duel/DuelManager.java @@ -631,7 +631,6 @@ public void on(final PlayerDeathEvent event) { handleEndUsers(match, userDataManager.get(winner), userDataManager.get(player), matchData, "winner", winner.getName(), "loser", player.getName(), "health", health, "kit", kit, "arena", arena.getName()); handleInventories(match); - plugin.doSyncAfter(() -> { handleWinner(winner, player, arena, match); diff --git a/duels-plugin/src/main/java/me/realized/duels/extension/ExtensionClassLoader.java b/duels-plugin/src/main/java/me/realized/duels/extension/ExtensionClassLoader.java index df224d52..d03538c1 100644 --- a/duels-plugin/src/main/java/me/realized/duels/extension/ExtensionClassLoader.java +++ b/duels-plugin/src/main/java/me/realized/duels/extension/ExtensionClassLoader.java @@ -24,9 +24,8 @@ public class ExtensionClassLoader extends URLClassLoader { private final Manifest manifest; private final URL url; private final Map> classes = new ConcurrentHashMap<>(); - @Getter - private DuelsExtension extension; + private final DuelsExtension extension; ExtensionClassLoader(final File file, final ExtensionInfo info, final ClassLoader parent) throws Exception { super(new URL[] {file.toURI().toURL()}, parent); @@ -34,7 +33,7 @@ public class ExtensionClassLoader extends URLClassLoader { this.manifest = jar.getManifest(); this.url = file.toURI().toURL(); - Class mainClass = Class.forName(info.getMain(), true, this); + final Class mainClass = Class.forName(info.getMain(), true, this); if (!DuelsExtension.class.isAssignableFrom(mainClass)) { throw new RuntimeException(mainClass.getName() + " does not extend DuelsExtension"); @@ -102,7 +101,7 @@ public URL getResource(final String name) { @Override public Enumeration getResources(final String name) throws IOException { @SuppressWarnings("unchecked") - Enumeration[] tmp = (Enumeration[]) new Enumeration[2]; + final Enumeration[] tmp = (Enumeration[]) new Enumeration[2]; tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp); } diff --git a/duels-plugin/src/main/java/me/realized/duels/hook/HookManager.java b/duels-plugin/src/main/java/me/realized/duels/hook/HookManager.java index 2b302d9b..92247f69 100644 --- a/duels-plugin/src/main/java/me/realized/duels/hook/HookManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/hook/HookManager.java @@ -6,6 +6,7 @@ import me.realized.duels.hook.hooks.CombatTagPlusHook; import me.realized.duels.hook.hooks.EssentialsHook; import me.realized.duels.hook.hooks.FactionsHook; +import me.realized.duels.hook.hooks.LeaderHeadsHook; import me.realized.duels.hook.hooks.MVdWPlaceholderHook; import me.realized.duels.hook.hooks.McMMOHook; import me.realized.duels.hook.hooks.MyPetHook; @@ -25,6 +26,7 @@ public HookManager(final DuelsPlugin plugin) { register("CombatTagPlus", CombatTagPlusHook.class); register("Essentials", EssentialsHook.class); register("Factions", FactionsHook.class); + register("LeaderHeads", LeaderHeadsHook.class); register("mcMMO", McMMOHook.class); register("MVdWPlaceholderAPI", MVdWPlaceholderHook.class); register("MyPet", MyPetHook.class); diff --git a/duels-plugin/src/main/java/me/realized/duels/hook/hooks/LeaderHeadsHook.java b/duels-plugin/src/main/java/me/realized/duels/hook/hooks/LeaderHeadsHook.java new file mode 100644 index 00000000..2b814f12 --- /dev/null +++ b/duels-plugin/src/main/java/me/realized/duels/hook/hooks/LeaderHeadsHook.java @@ -0,0 +1,55 @@ +package me.realized.duels.hook.hooks; + +import java.util.Arrays; +import me.realized.duels.DuelsPlugin; +import me.realized.duels.config.Config; +import me.realized.duels.data.UserData; +import me.realized.duels.data.UserManager; +import me.realized.duels.util.hook.PluginHook; +import me.robin.leaderheads.datacollectors.OnlineDataCollector; +import me.robin.leaderheads.objects.BoardType; +import org.bukkit.entity.Player; + +public class LeaderHeadsHook extends PluginHook { + + private final Config config; + private final UserManager userManager; + + public LeaderHeadsHook(final DuelsPlugin plugin) { + super(plugin, "LeaderHeads"); + this.config = plugin.getConfiguration(); + this.userManager = plugin.getUserManager(); + + // Wait for config to load + plugin.doSyncAfter(() -> { + new DuelWinsCollector(config.getLhWinsTitle(), config.getLhWinsCmd()); + new DuelLossesCollector(config.getLhLossesTitle(), config.getLhLossesCmd()); + }, 1L); + } + + public class DuelWinsCollector extends OnlineDataCollector { + + DuelWinsCollector(final String title, final String command) { + super("duels-wins", "Duels", BoardType.DEFAULT, title, command, Arrays.asList(null, null, null, null)); + } + + @Override + public Double getScore(final Player player) { + final UserData user; + return (user = userManager.get(player)) != null ? user.getWins() : 0.0; + } + } + + public class DuelLossesCollector extends OnlineDataCollector { + + DuelLossesCollector(final String title, final String command) { + super("duels-losses", "Duels", BoardType.DEFAULT, title, command, Arrays.asList(null, null, null, null)); + } + + @Override + public Double getScore(final Player player) { + final UserData user; + return (user = userManager.get(player)) != null ? user.getLosses() : 0.0; + } + } +} diff --git a/duels-plugin/src/main/java/me/realized/duels/kit/KitManager.java b/duels-plugin/src/main/java/me/realized/duels/kit/KitManager.java index 8ce71d21..821e2266 100644 --- a/duels-plugin/src/main/java/me/realized/duels/kit/KitManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/kit/KitManager.java @@ -35,6 +35,8 @@ public class KitManager implements Loadable, me.realized.duels.api.kit.KitManager { + private static final long AUTO_SAVE_INTERVAL = 20L * 60 * 5; + private final DuelsPlugin plugin; private final Config config; private final Lang lang; @@ -43,6 +45,7 @@ public class KitManager implements Loadable, me.realized.duels.api.kit.KitManage @Getter private MultiPageGui gui; + private int autoSaveTask; public KitManager(final DuelsPlugin plugin) { this.plugin = plugin; @@ -78,14 +81,29 @@ public void handleLoad() throws IOException { Log.info(this, "Loaded " + kits.size() + " kit(s)."); gui.calculatePages(); + + this.autoSaveTask = plugin.doSyncRepeat(() -> { + try { + saveKits(); + } catch (IOException ex) { + Log.error(this, ex.getMessage(), ex); + } + }, AUTO_SAVE_INTERVAL, AUTO_SAVE_INTERVAL).getTaskId(); } @Override public void handleUnload() throws IOException { + plugin.cancelTask(autoSaveTask); + if (gui != null) { plugin.getGuiListener().removeGui(gui); } + saveKits(); + kits.clear(); + } + + private void saveKits() throws IOException { final Map data = new HashMap<>(); for (final Map.Entry entry : kits.entrySet()) { @@ -97,11 +115,9 @@ public void handleUnload() throws IOException { } try (Writer writer = new OutputStreamWriter(new FileOutputStream(file))) { - writer.write(plugin.getGson().toJson(data)); + plugin.getGson().toJson(data, writer); writer.flush(); } - - kits.clear(); } @Nullable diff --git a/duels-plugin/src/main/java/me/realized/duels/player/PlayerInfoManager.java b/duels-plugin/src/main/java/me/realized/duels/player/PlayerInfoManager.java index 8a8511af..008dd716 100644 --- a/duels-plugin/src/main/java/me/realized/duels/player/PlayerInfoManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/player/PlayerInfoManager.java @@ -48,9 +48,9 @@ public void handleLoad() { } if (lobby == null || lobby.getWorld() == null) { - final World defaultWorld = Bukkit.getWorlds().iterator().next(); - this.lobby = defaultWorld.getSpawnLocation(); - Log.info(this, "Lobby location was not set, using " + defaultWorld.getName() + final World world = Bukkit.getWorlds().get(0); + this.lobby = world.getSpawnLocation(); + Log.info(this, "Lobby location was not set, using " + world.getName() + "'s spawn location as default. Use the command /duels setlobby in-game to set the lobby location."); } } diff --git a/duels-plugin/src/main/java/me/realized/duels/queue/Queue.java b/duels-plugin/src/main/java/me/realized/duels/queue/Queue.java index 083a44e4..6f3bb59f 100644 --- a/duels-plugin/src/main/java/me/realized/duels/queue/Queue.java +++ b/duels-plugin/src/main/java/me/realized/duels/queue/Queue.java @@ -56,11 +56,13 @@ public List getQueuedPlayers() { void addPlayer(final QueueEntry entry) { players.add(entry); update(); + queueManager.getGui().calculatePages(); } boolean removePlayer(final Player player) { if (players.removeIf(entry -> entry.getPlayer().equals(player))) { update(); + queueManager.getGui().calculatePages(); return true; } @@ -68,7 +70,12 @@ boolean removePlayer(final Player player) { } boolean removeAll(final Set players) { - return this.players.removeAll(players); + if (this.players.removeAll(players)) { + update(); + return true; + } + + return false; } private void update() { @@ -76,7 +83,6 @@ private void update() { "kit", kit != null ? kit.getName() : "none", "bet_amount", bet, "players", players.size())); setLore(lang.getMessage("GUI.queues.buttons.queue.lore", "kit", kit != null ? kit.getName() : "none", "bet_amount", bet, "players", players.size()).split("\n")); - queueManager.getGui().calculatePages(); } @Override diff --git a/duels-plugin/src/main/java/me/realized/duels/queue/QueueManager.java b/duels-plugin/src/main/java/me/realized/duels/queue/QueueManager.java index 2a433ae3..bae95ac7 100644 --- a/duels-plugin/src/main/java/me/realized/duels/queue/QueueManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/queue/QueueManager.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; @@ -53,6 +54,8 @@ public class QueueManager implements Loadable, DQueueManager, Listener { + private static final long AUTO_SAVE_INTERVAL = 20L * 60 * 5; + private final DuelsPlugin plugin; private final Config config; private final Lang lang; @@ -71,6 +74,7 @@ public class QueueManager implements Loadable, DQueueManager, Listener { @Getter private MultiPageGui gui; + private int autoSaveTask; public QueueManager(final DuelsPlugin plugin) { this.plugin = plugin; @@ -116,6 +120,13 @@ public void handleLoad() throws Exception { Log.info(this, "Loaded " + queues.size() + " queue(s)."); gui.calculatePages(); + this.autoSaveTask = plugin.doSyncRepeat(() -> { + try { + saveQueues(); + } catch (IOException ex) { + Log.error(this, ex.getMessage(), ex); + } + }, AUTO_SAVE_INTERVAL, AUTO_SAVE_INTERVAL).getTaskId(); this.queueTask = plugin.doSyncRepeat(() -> { boolean update = false; @@ -183,8 +194,18 @@ private boolean canFight(final Kit kit, final UserData first, final UserData sec @Override public void handleUnload() throws Exception { + plugin.cancelTask(autoSaveTask); plugin.cancelTask(queueTask); + if (gui != null) { + plugin.getGuiListener().removeGui(gui); + } + + saveQueues(); + queues.clear(); + } + + private void saveQueues() throws IOException { final List data = new ArrayList<>(); for (final Queue queue : queues) { @@ -196,11 +217,9 @@ public void handleUnload() throws Exception { } try (Writer writer = new OutputStreamWriter(new FileOutputStream(file))) { - writer.write(plugin.getGson().toJson(data)); + plugin.getGson().toJson(data, writer); writer.flush(); } - - queues.clear(); } @Nullable diff --git a/duels-plugin/src/main/java/me/realized/duels/queue/sign/QueueSignManager.java b/duels-plugin/src/main/java/me/realized/duels/queue/sign/QueueSignManager.java index 004b1197..2daea442 100644 --- a/duels-plugin/src/main/java/me/realized/duels/queue/sign/QueueSignManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/queue/sign/QueueSignManager.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; @@ -39,6 +40,8 @@ public class QueueSignManager implements Loadable, me.realized.duels.api.queue.sign.QueueSignManager, Listener { + private static final long AUTO_SAVE_INTERVAL = 20L * 60 * 5; + private final DuelsPlugin plugin; private final Lang lang; private final QueueManager queueManager; @@ -46,6 +49,7 @@ public class QueueSignManager implements Loadable, me.realized.duels.api.queue.s private final Map signs = new HashMap<>(); + private int autoSaveTask; private int updateTask; public QueueSignManager(final DuelsPlugin plugin) { @@ -78,6 +82,13 @@ public void handleLoad() throws Exception { Log.info(this, "Loaded " + signs.size() + " queue sign(s)."); + this.autoSaveTask = plugin.doSyncRepeat(() -> { + try { + saveQueueSigns(); + } catch (IOException ex) { + Log.error(this, ex.getMessage(), ex); + } + }, AUTO_SAVE_INTERVAL, AUTO_SAVE_INTERVAL).getTaskId(); this.updateTask = plugin.doSyncRepeat(() -> signs.entrySet().removeIf(entry -> { entry.getValue().updateCount(); return entry.getValue().getQueue().isRemoved(); @@ -86,8 +97,13 @@ public void handleLoad() throws Exception { @Override public void handleUnload() throws Exception { + plugin.cancelTask(autoSaveTask); plugin.cancelTask(updateTask); + saveQueueSigns(); + signs.clear(); + } + private void saveQueueSigns() throws IOException { final List data = new ArrayList<>(); for (final QueueSign sign : signs.values()) { @@ -103,11 +119,9 @@ public void handleUnload() throws Exception { } try (Writer writer = new OutputStreamWriter(new FileOutputStream(file))) { - writer.write(plugin.getGson().toJson(data)); + plugin.getGson().toJson(data, writer); writer.flush(); } - - signs.clear(); } @Nullable diff --git a/duels-plugin/src/main/java/me/realized/duels/request/RequestManager.java b/duels-plugin/src/main/java/me/realized/duels/request/RequestManager.java index 281184f5..c8cab26e 100644 --- a/duels-plugin/src/main/java/me/realized/duels/request/RequestManager.java +++ b/duels-plugin/src/main/java/me/realized/duels/request/RequestManager.java @@ -84,25 +84,29 @@ public void send(final Player sender, final Player target, final Settings settin TextBuilder.of(lang.getMessage(path + "extra.text"), null, null, Action.SHOW_TEXT, lang.getMessage(path + "extra.hover-text")).send(target); } - public boolean has(final Player sender, final Player target) { + public Request get(final Player sender, final Player target) { final Map cached = get(sender, false); if (cached == null) { - return false; + return null; } final Request request = cached.get(target.getUniqueId()); if (request == null) { - return false; + return null; } if (System.currentTimeMillis() - request.getCreation() >= config.getExpiration() * 1000L) { cached.remove(target.getUniqueId()); - return false; + return null; } - return true; + return request; + } + + public boolean has(final Player sender, final Player target) { + return get(sender, target) != null; } public Request remove(final Player sender, final Player target) { diff --git a/duels-plugin/src/main/java/me/realized/duels/spectate/Spectator.java b/duels-plugin/src/main/java/me/realized/duels/spectate/Spectator.java index 978ccbc6..d67a20e4 100644 --- a/duels-plugin/src/main/java/me/realized/duels/spectate/Spectator.java +++ b/duels-plugin/src/main/java/me/realized/duels/spectate/Spectator.java @@ -27,8 +27,14 @@ public class Spectator { @Override public boolean equals(final Object other) { - if (this == other) { return true; } - if (other == null || getClass() != other.getClass()) { return false; } + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + final Spectator spectator = (Spectator) other; return Objects.equals(owner, spectator.owner); } diff --git a/duels-plugin/src/main/java/me/realized/duels/util/TextBuilder.java b/duels-plugin/src/main/java/me/realized/duels/util/TextBuilder.java index fb92a388..825bf50f 100644 --- a/duels-plugin/src/main/java/me/realized/duels/util/TextBuilder.java +++ b/duels-plugin/src/main/java/me/realized/duels/util/TextBuilder.java @@ -120,7 +120,7 @@ public TextBuilder setHoverEvent(final HoverEvent.Action action, final String va } public void send(final Collection players) { - final BaseComponent[] message = list.toArray(new BaseComponent[list.size()]); + final BaseComponent[] message = list.toArray(new BaseComponent[0]); players.forEach(player -> { if (player.isOnline()) { player.spigot().sendMessage(message); diff --git a/duels-plugin/src/main/java/me/realized/duels/util/UUIDUtil.java b/duels-plugin/src/main/java/me/realized/duels/util/UUIDUtil.java index ecda1356..4037821f 100644 --- a/duels-plugin/src/main/java/me/realized/duels/util/UUIDUtil.java +++ b/duels-plugin/src/main/java/me/realized/duels/util/UUIDUtil.java @@ -16,8 +16,4 @@ public static UUID parseUUID(final String s) { return UUID.fromString(s); } - - public static boolean isUUID(final String s) { - return UUID_PATTERN.matcher(s).matches(); - } } diff --git a/duels-plugin/src/main/resources/config.yml b/duels-plugin/src/main/resources/config.yml index b138ec6a..99041f85 100644 --- a/duels-plugin/src/main/resources/config.yml +++ b/duels-plugin/src/main/resources/config.yml @@ -1,5 +1,5 @@ # DO NOT EDIT THIS VALUE! -config-version: 5 +config-version: 6 # If set to 'true', a notification and a download link will be printed on the console whenever an update is available. # default: true @@ -74,6 +74,23 @@ supported-plugins: # If set to 'true', player will not be added a death when they die in a duel. # default: true prevent-add-death-in-duel: true + LeaderHeads: + wins: + menu: + # Menu title for the leaderboard 'duelsre-wins'. + # default: 'Duel Wins' + title: 'Duel Wins' + # Menu open command for the leaderboard 'duelsre-wins'. + # default: 'openwins' + open-command: 'openwins' + losses: + menu: + # Menu title for the leaderboard 'duelsre-losses'. + # default: 'Duel Losses' + title: 'Duel Losses' + # Menu open command for the leaderboard 'duelsre-losses'. + # default: 'openlosses' + open-command: 'openlosses' request: diff --git a/duels-plugin/src/main/resources/plugin.yml b/duels-plugin/src/main/resources/plugin.yml index d4b8c0d1..eaa70453 100644 --- a/duels-plugin/src/main/resources/plugin.yml +++ b/duels-plugin/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ name: Duels main: me.realized.duels.DuelsPlugin version: @VERSION@ -softdepend: [BountyHunters, CombatLogX, CombatTagPlus, Essentials, Factions, mcMMO, MVdWPlaceholderAPI, MyPet, PlaceholderAPI, PvPManager, SimpleClans, Vault, WorldGuard, Multiverse-Core] +softdepend: [BountyHunters, CombatLogX, CombatTagPlus, Essentials, Factions, LeaderHeads, mcMMO, MVdWPlaceholderAPI, MyPet, PlaceholderAPI, PvPManager, SimpleClans, Vault, WorldGuard, Multiverse-Core] # Multiverse-Core added as soft-depend due to locations being null when Duels is loaded first. # Add any world management plugins to softdepend list if arena doesn't load properly in the generated world. api-version: 1.13 diff --git a/libs/LeaderHeadsAPI.jar b/libs/LeaderHeadsAPI.jar new file mode 100644 index 0000000000000000000000000000000000000000..e612e96ded2d0feaf069aa24c2a6ebfdd84eeff7 GIT binary patch literal 29080 zcma&NWl&wg*Y24>fB*r4TR6D8I|Kq8+}+*XodklrySsC6hl9Hw4lcpnUBcx5-aAz@ zbLYKvyQ;godp&FQm#)2b|8{Rh8Q4!)A3l8k{9z)yQu4#U0`s5dUxEEs?aY{AKPdjU z62$)@adt4avj5)-vHzb!TQeh5GiS?xt;_$`3ID%yGIF%~UmSPxRL~&sA3n5v{qTYD zf9ELsU#m<0XLS)pDJBzJBNvx0^%ECNHLUk_Dzi-2aTS_iobk#cX0q+X5NldBBq=Zz zj<^!{WCoL5gONuN6{}rZE z!OEi1QhwZ`5IA3Y>jV_}Zn$ndxvnhrJqp=2a`9HbWD_{ZF?oI~2rWxk6^6aDFw@ zSI{k0ZSz&36=K`L(6LI-2~9Pvl?N8|4rP!uD9M4Xv7am4l|jhp;gu@i<0T*k0dnH8Ne(}l$0dd!MV`PSz8uavZlwT{R2h5~zDdcJ$MNBR5q)L4ullk z{jd;p-&hRExC%LFuvjU#sexvrzdI}baF{51DV9Gy;n)oh0WTkM5->Z>cRtFKzfBTC zz#7(pciBnbU9;P9`f2`#^ovYaKa5XGza-AEp;ZOR)bF(9?l;eJJn$?F22XRDhi4^{ z1nu7Job$aSmxK0mo;fqN=bY4^4VHwhCnO2^5!u@j?3=Vx+K3IxB)DTRGp|QbVRh1hK<4`WCtn8h(v6&|BM80xig=Qt z)k!H+N*K}v%bojJjNQUF0eYAq>uCRnV@rI-eWKytSKjJbM`YAZVK*%T*y-QD@KY7dLfO#49ng3%q+KSEozO9?xC@BWKF*u%wH|KgCE{A4@*`# zQH)Ta%2bouJVicET7Gc3d3~$rH5XfY2F+9JnFiQN((`sTfUh}&$Go-~{V6WkLIm`- zwZIOC)#Fuoi&-VBg}sYc0OMozmX7Vg{N2$RlrywOx*v7((vQHM8L!z%t_JF%H*hYU zc-K9Oz!AO;FHEyXpz~aW@^Gl)w z3kl2I3(7f8nn)Bg@KZO?*r1M*C)B5Gc>;6}Y;5|phdfL-CiVRIMU+v5E_+FU{k`7{ zH+-NuxES43T6Aet7NPD}Ll=c}f=AMjrmt9b=By(ksS{#F;2Q!DQ@%cUOTg4#Z}XGq&(g22kX#Z~r7cRcVxgW}x%)4LdEez?~@Hiq6d^QSPSG5=CK zT5{4N+&uq-AAU>06&l|Kbptcp@@tY!P%!}zd0K7q;crwHPS403GNaNlm)}lvXTB4} z6glNb4Z%|h_Hs1dL=|}+o*%qY6esinI2aTUzt4o$T!y3uM0s)j5k_-qHR_oteK`X37(`_ zk`1h80(X6W`wCx24)yTxb*4E6#)xr6H&CS?ZGr?Ae)z!se;7mTU&tT-cgX*dYzI>j zJ=GSPhTmT%xIZoPd*cUH~v%ItU=7^Ga7fdQCn%vEk2m5ROhb+Or585`l zUFYKlLLCUbECMx-&wZZycJ%^Ei5f()YC|>$SK6r>_?5A06qS`b*mt8V?VTXsZ!TN2 zp~`rU0_(>Awi*&X(3O}?xS6LUuSS)}NnYwPPJ^hjd@NJlAhWETU^Igau9ie8v5Z-@ zmHyMW_Tl1fV`mmRv7of);DE^p9Iv!OqYDot>Vx!^?^-u@ilfM&y1!l$mm&E{jy zM977`9cy+CNz%)kcZ7)&eYi&$iG1QwB0Xu9N!m(T0W$Ce>}|JiY4Oiwv-rkW!m*z&xp|CE-DQCqv7=Hh) zPrMY5Y<=CzUyt7%X|jNoKf_P&BmYuqsB#SW4YnE)sS&L;X}pOCE{juA&PP&5s~mb& zp(_l}d3${ONj9`w@c*%py+8*60rMb0q zD62yU@}V67Spb6w5_}9|({_12JGYPlN8$&CABa>=PkTsu4H1zIv8YF`vS2tX-D}l$ zu4YT6&JtzPmTV#y*UjcOo&gS7G>Z&ydf$kxLyA@_4TmjlW)^{AjHqI+ZS|{l72wpF zl?f5HjNw~*l+eXUvrwx*rWw+YdOi*ve)?^l0*osT)vfv+zeUC?)>s+MteB@i&5dsB{e(iDVN|y$)1=X%DoUN zka#(bPF4uKu84~ojVo`3KW)ty6i>Bd#uk<~9-(L#7edoy3Y*I7F+v;$Nyz*rHt1Qb z-rUsbai0JpqW3a>9BA|7<2b$-9QJ}8u%_8pYOe@O)IU(O} zl4F9g6XhSJJ1LH(oZIU6WofvTj0IO&RokbmN1D;A{Tcjof52#Gjuw1@(2fo)z6I*x z-)A;9CNlj@%PvcMfHgvQty05D{GA=4c6ytsGwZ{fgfBm zbA6;k6!bg!xEP#QqbSJZJ5BQ@0>oupjoww=`6&;v?*XOMaKIKC0X{R@5n9>o{?UL% z+Rd6M$DzJL`K>_m{`U9QjOUeNI9?#}K=7-|p+I!)Z#df}^S4H;(U6sh$44=@wWA-4 z{IaHufVU{bjk_=8ft%=MEaE1UI6~R|FQ0qZUEoos&i>pw`gkh!0P8T1cst@)C6_y9 z&S;_Z0uwe8))SaUOVmi2ZG(u9>ee_?(JNhcklD_M$MwiU`{B{@Uqo&bBJ+8(>4+M9 z!8s7FLv`REtgJEj>gm|a8nox@cFHD_?k|1Lce`Lvrz+yS?2CoXp&dm1#1aecSzV|*F7A1Ch$by2efCL7=L4qf zcWy3Y9UYEFnCA7)Ts>_2*K_@LeZ{X_DhBNIVFnVuU@Zt zvCd_>wV{^QaWbT`vDZ=Ux)2H-3U?P7LMcwnsH;%)81<;~ZJAXIDzvwPu%+0sz)U{V3p2$2TEkEfm+gh+Q(MNl` zgaQT6n32xF0ws4lC5=HAf{p~E)Nn;3n75psQgK#}9eI`DLYQhG5+DAn#Fn+gU)T;GlDa%_B zEOdj0&BEg4;*3oGd}*%!xGqW$n1!f!uFr_V5-XjSL_AFcf?}(#QPn|HLK4|84rMOg z=E82K*#TuW%Wn1$9$feJ*SM^Qmvo%Gyx(s>yF-is@N4 zXxx@sDM&FR*Ug8I7><1Cas@@yyur8B%M`I$A`%H?ePzzlBf-05&aCAmHzT6Tz?3+f z5hQ2ww%{t#hd7j7(msup8@y!+_c)8un_cm1B(LaB@eR_8_{{{g(cxW{YhKvQ_`x$& z?rhVz^Ig-qSj7&RvvWshF5b{6@vW8 zd|nz`45Td`a_{f09C|_&JElFa%GV8~?8*Ka*Z=zcZudL>XURoK|2ML~hB2yoWX2qd znAW@s5ZU-oXwWvhy6;d#5AkO_`i~}*d6Td|$^;L^pW}W}q<$fX0HVka5^fNi5Fh|W zpYLZMO$77qf35_1*C#RjGb;Kx5*k%~YV`V#g>owf8SmiZj(}m-f2n(U(DbSQr&|+3 z0Led9CPJgv?^1aRl5E5s%0AEVTdJs=&u4)Ab@E1^T?V#{ne%u=Q>1kHGC^gDZBrpF zF}#EL%3C%k!dnU_+#~#LrWx#Q{gQ^k=%eoKDbB~rR(2Hk2%lfz*uiYW%eYn(lntyF z#aN67hM@Pa9%u8_P-|Bcv%N>q`nLJu&EQ$pa5&+iwv(M-Igi1y z%fM48xNG}Wjrr?=f_FO)@nOyOtAqi?R0p3*Rtu5S#$0LmO*@!)z)ReKeCh$kpK1yj zzBcTpDu;239lj-^LC%13s_@y>3Bc8=pB4Sg<^&}?#jvP`ZdXCwwyTg7_)BNE*qu_2 zzBr{2bW+J@gIfl)EH81LDqO{~oP(;j9p7TZ*;Nckrc{H3!`WvIi+Of#Rh_0lgS0ai zk?f)#xE5tKSk#M#i9E0<>S6Q1Se($(EX!gLL@@E$(U4?IEm&hRD#jb(B zs*7Q7ATWY)fMuQhsb71~(gnV+%4rKqH`a?>&^y-4uy@*$Fvnx|lFywZ=mD>j@}4P( ze{iG5Wzb*rAiQf~%L&Ade~Rf71kN4oUfp^z-UQ@a-qY!k+~2_~MEF&M0id?O9fSMN zo7kBr@5xJQ@E2i3?5*Obyzse^g8f^m#tO42#`EvQ5rXQ>*glOy3a1Sy8>yFyUh1c$ z-UUm`@Y9j&(_3aBpOf#l&jC$K_?`UVtyNnizlnWv;9aBXaLfSG=X~t?XF?lTvAOgB<;-hDG|4D@J+TUUM?XRItI^bP z%h1ehLO=N!)MUI~sep4LA9QBCZau>QjfO{ybjt?Y8S9xj7HXov2Vl}$T?T3*25Agd zuq=^42jLCEIL8e`HC6mTRQSFOP%rrLLAp7Y0lJy_0fPMLuQrxxyjnqSF`BpU-k|40 zkg?e@2B`gr$=K`=bMUw-G;B~~+Ra!C{vmwD;gU!xm*7M4UKa+|>X` zj_vl1k4ps_#JehdBD}Sj(eDbn6(PvpzsuI)g)s&nd!!&-L8W4X1wLUb1qv8VO3eQz zH|edb&#A1Nzz<^UMWGSKufjCe=|XA{ldQ9Y-nXPOAX^m`!PdE*CSo{3ZO@yv`tL zmR0GVw9ofPSAh?%U0XzYbe=M~v|E1zeq2U? zCBTvGEk9r0dp=E<%t45rtv@cl=WLE;U;^4Q(rKUK@W3IMD6Rp$+C5eHti$L5V};e#dp+DHrx zNO|;dk>CjhNbwBoBpfSps1MC9F-24rjMVKen@Qw={Z-!P-ShOa0|oMC-Gpx3B-dR- zEa^vsRdNU#p@aGm8^-&zv%QzVaHUPXB;s8Ozmi7*UHS0#iOIqm4>t%(3WLSdCH9}R zC#Lx)%d{4tAk+&8@rv$_cy99nSTCbfg-&eCxac%IT9un#jRr~i{zB?@gb&p!Nqlbo zGmQA2eYwf&*p&UF_z$J8HKUW(qWV_ry^-6WN$}N*5W-7I#)=&~ptZ={1VrJh>gJZewL?9WsDq3B=e?j+i9pP3pqc6`02S|gQ%w3)bxjx`u^Aq%=j!8C3c^I0CW zBP8y(#pFpVrQ$j=U6miz_@DFT2);F{Nk&SfKqRb$QD4?71C&DZCnU1_DVBKe+Djgkp74O_?&2MB$eRXN$7u19acEnR#rzQPM7|B zvubZZAu8gAd+!({XLJ1GU6PLwV^+u(d=9ixP1hJ58~)?j7y%J>CM@Bq)n zXEzyi{Tb`gh0u!w8I%$_dmDH`Nyht&)rtpu?F_7`MyHW);$G=CY}8rnD%$WsWbK7k zN_%*0S;Y4(I%%K}G_oyeCTr9?Tl`2QE614@kOzmrN0d0H^xHUNV3VNQiWgQzl;c%3 zxCVGJ*6$juC`SPmV68EsH$MEr&caSaxuunWu7yKe87*G2AW<;>hQrt>9}9t~4@i@B z4>ho?I9T?=z)}Z52=|yv6GoLv(6{*f> zU$gOYSR&x~2)gsqj0^d~AREnDQ{k=;Cpi8`bBE1Q21-7!9Y? zM)m&wg*E9tkR-=MVsr32#R)d&UPG?URllol&`HMIL7X!hw<9G^h|N?UvYBDmg}_PO zpUZ5oUdw|ms-C_{)eYmAwU2wCJ+E>tuU2u=Y(4pyfxf?PTg;C1=VAuyQ9AP=#1<#A z?%EVJ@gt$PhR9gxU6}g%F6!D5akPh%>OEUSeO9X8>}$V6J5}m>M8_NH1@8d8%KVMv z#jHX{T`2KYQrb>#)as8`w1-ks%v26$c`r46DGF^s4NVWr9vm877Q|ZP&+&B%_hMSP zIKbT*#k!(^^|@KJ_H_G4r{4hjgB!g2pLh;|D4{0jXa4KC{p<1VKwjZA+(E&XzFg8ax;D}w zqmhCd^e1azGTcs3g1RQ!;q!##sq`8LH5TE0TRadU%8C!MZG>By3ijgb?F3CY6*Jq1wi zJo|zUa$B2jtP@lwdlg=h>j4T4;>&%GuCfenU$My??@E>+50==x9@zYaj6n;sDWM877XwoYM}D5 zH=?8QTxa$Qa`u`ND@exo9Phb1zG_C{;Hr=8t9V*NJ&W+t&r<5})TiJh&k`{BLAi%a z*ALPTXs~^CK*)syLh+KnB(G8^C_FnP9xDL{vf)CXy)LejL5J6YS>!E`G=F#C)Rff- z86#zBYQuQCEL}p0IdIyaBf+ILf0bF>L}0#cK5>d z{t4Ab%7C|u?+%vkxAyhp51Iu=%6gxKj8aL>9hQRa?+(>MF^^|jJWV&dpcR56*+#_yn#kK=X|$KCj_9S+vM zKehyt_pYnSXaBD1yGogH83qTE*m;uBD~8H6WI?S#tI3QucV@w;MSArK&TmByuuL~6 zzO$;pODC7yzatr@uhhW7J4T?Mn<2!kzbk-y!BG9u=SxO~+frV(4ytz^^+maNm0j?H(r&t0;l!YUvm2pf$KbXr(}a${1Fx1oQS+moQyQjZEktqs zm4bWRrk5+Y%FtQ-tx78}ra!2o&IFU~^$+nS`||S2n)j<`=}I`JBmViG8RVma5wpJG zGr#X|g&C1z+G@VbH%(g&!ABug?6O(eg9t#BH#Pv41&=lsyFt4q*j^2%BbD|`Bs7nQB~{X0HUb= zNVC^Ks|2Q>t2k5|u)KvnYx@nm2DOh~3}@J>X(_qZm1tS$6w6X;u~#>wnd=l*gQ^~U z?#iE|j{NVa?{G^LDpMCKWgp5c7H(u6)E?+oes!&vH`nXsV>hIb&1>6NH`f%|k8hqH z^JKvjcFm;ZMxMnFs}!OM!^Re3BjF>_s~VG}TvN1tfy=TI*5(xJKrJ!z8Zx1&ju%1B z{Z9B$lsvqp;#x3RuSy`C#35fWsGy#XSKMwaP@PlhZCr31En-b0Y>xbR#!jFI!TX|! z`RK%(YG8a5H@a|Gfs-exwx!yHAqXMxSFaAPt^u>S!N!!j%`Le5I|@f}HE!{!4J$kg z;^p7x_jnWfB|?31*CGC`$HbPU-TN9Z9+Erd<_x&J>R*3U*}67M7zV0^+1g-)=p1Zw z(8I^#jJ2ffxSr5`v(ZDgtKKPpre8Utm$fS*g#j?c&I#;#(_G;ZnuG&QaBdD%yJH>< zhAxfBh+*W~C@0$}N7tmv*9uH)-nS0e_K#*HSG_{fwltLASM>?ow1Lw5JA2W(S6k?m z43en#0U6R;GMbwtyGDBCM8{zwX%|lBY5Ji5}xRqJAD88*HB$te#XTX7v|S^tJF zkifv<-M6CY+|krUhh=M%W7_Kz^D`d+JRZnc?6$=HGOElktjIUqMu==-0Y3#uz@O2- zbEkNhm!a&O`gLk*`{4^2bz;0}6@c`xxboBbou-IIkjE#98-fv*$7&M8%e{&_b`1p8 zqpZo#IeK)_I?@NG@aOhxtq6B6(jGgLe6O9Z>gB@vCMG7@^PYcQl;}1tThx<#0O{Si zLgGP-^u?M%S0NrOKb~Zi0%TXq|2mMbd{bV&RtcCzvofR?>fV1DBVD#<%Y0uhc+VIA z1uH&g@cQnX{_;L^UU%e$Fdk!`U<>a#Y$Q1rFkdSV=VAq?=LM{_exITw_xQ7S9+V?9 zz7dSufmVQ=9dCoiYS~F~#E~zPnR3bBD@7gRO13V=>`Zn9JOoa$uti`ckg)|%P`F5? zWdU*Gt(`w=iKxfZkD?(uh#cTJ^S5QL0Ra@Tao*RTq~dT#DtG1jczJuM{*f!H+=PGx zhok-1D94~8Mk@urM8K}6=)1?!`?;YrQoem^yWTmv2V1)Q6xl)CQ&E;`0zrM&4AZ%F zeTuGvJz3@m(Jnyrj0p|rVR)DI8p|uBQ?hKm*gf?D)RFs|quBAHan1XbA;K*O*is+~ zQuN6~<_)=};x+v+N=-h3#}=>X@v|MaVPx49kUt5FZHoA>ZS?xdg*LPQaYf)ChbrU$ zzW(tT)o}rqAJOfGKVx9P70y@dhXPaDwlrP&8ZyXvH8mmP+hAdjr!X1X*Jor{ zDNhBA=g5D$vh_mu*p;q_|LOVQA$kUALU@wHF1jc2qgwhkb1t$XDA|EfE|>-k`T8sy zCxlrv{ppBZWJO3*)I3ocgY+PgPFaSDf_{|18AS_dPt}sZxT{|ubeZ<$oa*+@FiSu| zpR^X!8g1?z{11WtV|V3(l4m04(_h~N#m-1q1R?RJ=+zSFL2 zpC3Vn$l(R)vrpLH|IGha*Rl;P?PSv4(W(ZI7NJu!KagFk@U;c9Md)Nf_$s}I5i~uR zZCj_k7VaJPkH4K`w)AefIo+V#hx7MtVLQ2exer^_iA8TIjR{{>0}f1bJc_Ql!!w5y zvMn*!RJ#m5O&J#Q7~uIbOl{uUI{hucjkw7NS8u-#Z2_D%77ikBxN4aWIQ1B&=)K!` zrU?b;<=6g!GywObd=_8O_~3FmLZnL%SZ~{QUT?5%+^~fw6Xh?%R5bizxJxz|xUFke zr6&O9aQ*Ur;_>+1wh=u0AB1I``b9VKCsXU?XS_Yl<1n)0sUIYY@o5A`@BaDsU@ zbxNwidLayI8T^~&QM(l2HuzW4fb5?5i!kEZ8z0Gi2YhY#TM9VR_+oy~xTlDHV58C} z{H+mOX?!u8vy}BdX?(%)Sh}=|VvqF#5R^IPzbiL(fYJ|ik8TYc8`N5a`8AzW-($na zM);M34F*eKUlmUYf=UPUZ7=hekiYnoyDx3ofDQ;c>F;HO-2UOWbmLrm$MmJ0FEtoG zNnkl+w0XM$K-Ejwx`78)M=W^Vn3x!=qZHg^Y;p7r#D6?$?D8gz{txD5Ej8!R;VizR zf?EG7MBH)zgJ*t4rD20l4wyDQ;2*|sb6ZxR4G8SON+~enE&)IX)eq_kzr(Z}SSgIf zt&}GwhxWIJYVy1KfCjecLCuFQgY9ymgDaMoJ|O53`#?L?uvF9LOU+lCVrnQ-P|HAj z?-l{50);EmtviQAU5uC-+V%_s;(7n~<)XY{FvA)zT&witk8w12t6bGA{GD;Ov15;B zkQ*-O`NYZiZq_1lVKq;r%EUETQwzQavofe#v#J^X{VToYB?5>coB3ErPzi$T-C5Ri z;;KYU*hw1kHz2ZX(~|L6@C{Z;nEO!`Vgnx=?yZG}b(d9xE&i?$R`7m-p;n44CRoKE zzG8GKqx5hbSH}2ErqpsIUchy1UjQY1)x$b?XIHYhWm8H$QW2}3Stv+3mJt_sQ^Z<) zUo2?-n=J1tM!+_>kR!{-L(WR;i0#I5P)Sz>!=TcY~2`NEXf^EjZSMLKN;Xd5QXLwKjj`$1`6YMGCzJ- z!0dEz?ARje`hFP#HuxmW=_z*E$%IbvrTKHt4AG}Mc;?UU_ySBz`7&??K1j{SB7v(Y zIk>hEGnlD&+(4VVZ{Xy1#kw)W+Mg!@#al3bC5GWsn5u_2pR(RjKOQg#wmp9Xr+)|JcLOTi;7+_G=+$?AAGVZSJv*_#0buZgytG znM?wBdkQxHAA$2fxxFcrU34NE-ZQukot8gdmYUOrs2;8`N@wjK3pFQNCsky~s@P?8 z+&7%6uHx{P`zu$U?soi6({JSZVU`*K*`Y(?(EWTEA-2*UmP z6k-Dj>;7DB`dF=;?|EH5u2J_I>%ZEPJ6)S-ka7P0|G70UYTxB43#@X=FsnVE9Zx+SN@^b)812c7@%-->Mbsh1VXBeM$<_kG281)9>%4 zIo#|IIX>sS2RS||{E6Mq{Vup+hw9ytN(Yqqh)-=d_0Aae>@YX%*X&n1aV)}$c#_kT zQ&W=66O+u6pKs^FSm$uzQs6M^VR?};hbDD?3q@l|Xy<95ErY|%SgmtcNWiKu{z;t| zXalwqa>}*JnW|~I$qkC`3`P(pPVNAszRrTd$Oa@#q&ar>eOxn(UFtO>Zdp_;h7?cU zpHETV&>^|$Nv*i1IvQccwB*dhIud$*aw7>tsr#9gNoNM0h3wHtq=tG-< z{7;)FLJA=B2MSmm7@UvhFe$7NcADq9Itp-z9=q_@2MSD6<8c%zFl-;ZetP{Bm9Vox zPDpNGAV-K`mY1yQPV8s<`1F&k*SXhNLV}Sh)Eoh+$T@Stf$*yZd(OC}Y(Lw|vn=gF)a9XVz5i7GLrq5^X{1Ub^Lkg-6? z0ngNSk??|&SX{9n)xJczQ~% zS{f^bu&^S7*P>-!D!dXiu^W|M4B)iT9-_Lj{*in3bN2D7(yu_-swRp4P7Ym+@ZuWh z!x29H{@DzPCE@(ww(_=d_Ull1q&n;j5vWblN#ShLaWNLb4mnz|r1OsNuFE-<F{`Qcu_^InyqLv&wvhFSEXN}7 zj0k)BMhW4V=3*;an@iHqNc4LloRBruvYzD9;2*bY3GzpDG>A6d$k?wYAe75wc=rB+ zZyvbZNRfntshf+DCcFvsnz~UY7MGge{tZIo*bcQ`otU%_&-(rPZrmW*@1RMVS2*+msss;Oa@qBDJyc+_L^rYxh${F z#H>qECb-gysxYRW57`_M>BRk^pXBac_y7Mqu<5 z%e9(E)c1^HC0<)sBZ)sOr|anV*5EUZd1Nw?EToDgZLb)m$mi2_r6kHwtJ}`hsqsg2 zQk3v{aXbeSW@d7PbI$Hxm;Tl0@U%wlxT8q}45o;PfM-NsTcetwox)TzQ(gsOGYDxj zQznYT{3q`P8H*zeLuwN^*P!SJzIEzM|0$Y3K&pI#H<%Z4er}c?dGU}5H*EMiIECUH?NV+D+khZ7{ngr8i_cT(zXCz{^hkcU9_`FYLZ$CUMf-XA{!Hjm zxkAqfezaHJF!~};y{)(0MsfLT=CshoADe;ZHFld}1yUek?VFt}P}(8?vwp=6f<*W` zJ_^dWc>ei~gD7Rf#^?7W{-N}3v#9dR8y2K=TMF(XrhRjQ#(n{A!jc!O2_KZEjJmI2 zUwxdhK7#3Z92Bd(TPj8Y)+%ySUaAnm6t5epr(3B{sGRokdKtH zO>r4kw8iyO&BjTuJ%ik2_8gHZswP`xHOj5z+_)yr98h?7>(IYW$P}^cHkJHgzZYzV zSA4W|puggA%eU*I;C>a@GPtLnGNW27=_812{W^2>sAUw&Ec=9%@`QR zMx<(Y*iRa{O_8hUBY#Bnw*P7&IU-$Nj(XlmLLw}}oA#EBKwNrhx`*ST9XReYs>=y$ z41W5R6==)TOuFny!P^Sr58a=drishI_fAY)AM^InVs zv9_%3qSoSg3O5_Xa(vlQLTM2^k0;G-Cux1WyUW_|$DmcRG_HjtF_xz_&K5-3H8iy3 zQ_?(DQR!zDyVGgQKp5M}CdkiC-9Jnv&jK^F5nEPWxRq{nw~y;AG-nreA3LxJ-Un zB|z?G5%QnE@*PH4N{yTfO-Oz{OB@?t~V2~=dFc0?h=24 zcFF}B|Ddk94*9a6>aP^D>);GchJWgM8;Ig@!$ zN~+-&$i4w#S81|2rFQlIK|(^ZyKy7m>ZLfwZe8$UTpm6*sw`T$aKpeU?O*05B2;*@ zcF5$kt){4xFA<#S#8nLx0>w*#oVe=coF~kQ#aEIsJ9=iVU5rHe?iKKSzPbtclwE12 z)PNY-_-wE1flGx;{hV`#M-Fu1!3;XfT%(KbRzcTUL1%_oKI;{Oh!pN;1t5%17$g1C z%S@h>>F>+*JXCR*nl$qfsBS7*eBCLnRD{$lccz>I*O@#bh=-#sLG6qBvB%Gh1qJ9fpKW?I+}@=I-OnFBr^jo$J9zm36mfc=F<)*<~=3js_(W{Cw z!(S$NVTAh6U6%J#ljS`Tl2OfmKVkD>{{0ry6{}`tD-rGY0W{;j;7v%fVEAi9$TZ-# zUbrr{^VIJ7E1(F}*FADi5|Cn?k7vEX5;OFdOo3LJP|=9|&g#=kX6jqAA6@^ENE=M< z_)YHqoYLqRjn`?Ilh8dH5YC_-!2#bk6sGM%e~pf4LqNW)`R0-nKl#tcCx=h0w%HfQ zSU#K=s2uk}I|o1utQVgAU1PPZ^;0f-NNZo%v?#_mupX)Ip1oG`-t_bsqUlR35TG-; zPg+LG+OL}a>BX~*J+wAv4|SX|0Xo!=A3W7^VQ0H)9{SWeg~A-WB}cHDCxZz?;6U|5 z0(LB(Fpm4-T5tOkQ4B>+g>i`)c#6#!tu*WjosTZ8$32`iGtUsPVNd9 zOHtTOq?jCyMg~dlsNlhAhVG zZ-#1_vX9PhuXzRJs1Sag+Gc(j8{nH;ZlGM!bN+&M=_R@9**?K-w)+R)J%42edENBI z%u-*;sLQ(NYI3djd@Ef*<~&9`YGrui67Hv7;0TPjGE|hJIDf%m`SdY~+#t_xtL?KE zYC`XiRi!caD#3tj{5{-Zhcnc;c0+c66sRY~KQ4pDx)*w9MAWpL3473%h%KM1`6_ z|6a1#l(y-2to!0j2pZdeR(|)pts%APC3rz^vVXtk9vZ`9qoQoNp?Xu+#)R2v zAPGX%QBdI0AcX!JnLTfYdbv1MG;MUU^!oPs+9y3hNWwTBMP15l#(hU_Y#%*uu8N^K z9jvEyUaX6JysN$&YrGHvx0c~GT%nB>VOl7kh2Ywnz^ql)6URx$0vLn7DnWc-eNtdk zV0q0}sL9m6V70xc+zag6Uj7sopxN98URfQ`o-Y+-FlRRR?o55EdWvcr=i^q>;Tv94 zHO+2`d9GrF>O0oWuB6)mT4JynGN;HV-PFUIrsyn6ST5KXqw!~#JBcH_=lEcu)i36n zz)4_6+RG$nilvo(>-Z*j^}*}Y{-2NPZFik*P=<>Of&{$Wwh2CU+muWnTpcnvY5n09 zoy^HM=#Tv!&7FTRd3$c+%zShms?uL|*0!(mi%GrQtISI}Ld*C(laO-<=Z1%h-~24c zQgB0oeY#iDL%Wi6Gwum0QaQ0Q)3;(QWMr3am&2Y5;^Jf{W_ggWYd(Bye>ludL!I~K zgSOXts7$!`(n%4pF4+jN+Aja*uU>%0{Hiu|on~yvLDoEhP1`PVD@$xD33eO6fTFs% zJnnK$Do^ z2mLJD8o3U}?mgAX6(W{cS&daAWTIQ0(yDdn)W?Q`h`L}j&C=CO3q6&5nj7@WHGz1u zoMu3)g|0i#jPyZf>E6(0#bi?s7xxMDc(GskVeM5QHz5xCGHv5Q!N35nx(@awTPS~8=tMm_Q-gDH+oXVS0XCON`duUC zwowl5WRuFowZ2Cma)j-Ye_Io^1I0I4{E>6k>ERB0e8XxT0%Q9K5GqPz&c7!G$09?Q zOIF*IkIZYe;{L2t9_>VEs7`6*jjfK& z;%z=uWgpc>?}4mryWs9t^~Ary_{tXVh|gyIQMHl{$okM2#7N9dkHuI$z1E=9S zX}dGm`l+?mO+}E&W?b^d@};DqdZD32J2Y2$;?*zoDVV_`J|^W{l|k`;deBk z;XWqxkAMZ>OYpGg5FgJhpHRp02N#Endw_rJ>l_I{oUcd2pV?@$8r0Jlk8ppl)I_oB zhBH#Lb3(a{QjbMgaV2+c@>A}z;@3)uR; z54>K@Ou}Es$42pV8cp>j{+eom*^n9xNXbl4g^1sK-qzKFHlOuw`gHw))GC3b(Y|G@ zTLcx+0SoRvZ;dT~6t1?duePw7?mb5bTWvNL-&+iBP}-#;qFDwlW#9Vi-mm9{FN)R* zUT?p6G1NAb%hRDhU7b*&cvQU7OmutGLb{&=r;V^;=u?Tq(05{&9^h9p6;(QVw7Tvk zt?gS{@>1>>h>Paw!icd?WB>hH>Mvz1+)@rKtrm-Ixo*!%3a|J@DmXZcHX1t&eOO09;~xWoezmcY4B&Kk2{uB`_+T`@-uN@iuL2roBJ`5nsx*=mE7#aqVUnR;y0zpdZc zHHSpj;mol3!GsiNGbUNv>W(&XWF5ww03aY$daP23_I$jjoi;H*kG4iB=8k@zahX5m zt=ol~-(4Un{Y20xaUf{`nw&t?;5(|h6QV3oQP|R)je{#OBEPcM^5eSwNij368b`*O zGTj$_dt^NYauoL!<66{+HzM`nAe)WJ(Jy-x){7C4dW%$>=>8S8c%LrdY4k+5>=t}$dZe6+8Vi1a8*~X3_qXaPegm*N)xdf_LJ0#Eu zd4%wV91qDiuT+H4(_R(Triy<3P?KPtg(3|BpoImGZ5BW9z-SaVYW#tK_*;1%?${9} zEi#iF6Ys;06Dt!gUMMdA`rhrBmN%mt$=VU2X5Q5Q)!0`+#nmiZ4^D7`I|O&v;4Z-} zxVt;SVUPsZ;4-+o1(zT}f(L>V2r@VfZa?|&{a)_PyYgz*In(E?>0M{dRCia`bnl{` z6_58}uhxU>G?4C2xSFCq(%gM?;P2!`okuU0O4ahG8Rq2a%6fbo;~l&Dd5kPZNJiWF z!7UN29WzQWXlGh%I{VLk)Z6~}<<;Fv0Y*tge zt+E%15-!oqXecN|Luyuj2JISoLgh|Z0C4mu(bz#NA+sTH6YT|;m zp&0kM1eVfFs??0#%Zd~TeF4>Yjpj67w7hj?$6dx0a&qF+=YZK+l zR1rAf9W6p&zcnj&Q?9<^BdY$p*AAVTQmGo*J-6C729w_WZi*F#-imS%NuS1tM!IPj z`5Cd6TnAMv>*qaDA~zMeltO3KiDAw@>AJVv{P|!q9H!+2sH5szv%3B; z8P`*9=Ox8ty}S_aFpWI%aX_17yCjI`E{967es}(B{k9y%BgR0h;D~IUL;MX+xdJuA zm;Eaq$$?C4HM|B7Q#xmO>2NKxj{<48L7ykRH(kr8wh-QcG-_jiGU1|1g(n8X@6l#8 z+jib;^$j;6okzgUgIhb0t2J9imRpJ0Mzl<8UHIo!9+}2-Ja}1HxzJYac#DYwnJXNL zO*$fDZp4$6N4_(2?{dT1CzJc2LUGYZ?Pfn<;xJ+MHPTx>H2OTnpbEcwg=L23Pki=6KR6JH}sg6dW^hQ)*brJz#{qa`@Iy?EeDklXaY& ztt;~fslk%KY6{GQD$jW+{d>i%YWN)^TVew}&bR0;q3yWLOS_$IkHpWLKIC30dIUl(Jz)mNq{fAlO@-A$pTg{zC*v7HT*_I^nfcTF&fBli ztqt~N%QfR~1C|N28bJhl@M6a-X2{CbUd@&e@G(`=Y|(aS_B$pVnNS(B{!s zj9R<%Ib!UDnW42?EUm}MYqHJiVz5PHo8WaPZ3ttE!jjPy8^c)MSZ|W(en>Xs)||Aw z}$y9K9+ka7XO{P5w+{#iPt&y zZH8>Y(? z?#U~Ger*-su0*Z?GJn`r${Uu9;lOajQ>g48>q;BRWn4wy<-G)oAt2@vy4xlWprP8C zO>li|K=DDiLO=%eNK~W5I~?j6+Od+@0@hjajNMTVQu(XS2rRUEv2k9< ziHw>%Xw#_Qai~c*B(#=mPUg}sR83_P8cgj99tqQyi(isXt7zBShk`b(*c4J)zMSDc zeC|$p=ROKAV<`4YV2YHp*~c3vaavkssyWSOGfiNH^ws`STDXfvQ$I00E_!Nm#CkDO z^DM@t=?Bz}TUBey&iR6c>onUo_N{i7*ajqEy2Qu2*tH~+0)NA>hdiUeN+3Sd1hD|6 zt*Qhg(LfhPQzX1>IWLum6pV0^eM=z?Zw#x#=ZfNFQvE?PUcW}FjmQd> zGj^uZke{Vi>g6hEybtVKw$W|6S!6=D<&w3ttxu*MNUVpB0TJU7ci?SQ$83=Ic6`dx zonxt4p+3BJd?IPxj0WHlOrZi)B zMS4PWF8UCq8SdO+T`J{yO=3!xX}iL3drn>H3|HeL`#24ugoqUJfs7Pa=uwGhBRcyD zxlr6RWqN>I+%yhYX-$m?SIJGsiqI@qlq>blv)dp1BWsu)7BnYZsg?vb!|u{2)iz1_ zDBc3Ir6wvPms%Fwz{nBAW^b{ z3JdyA$(t^%Q38~jMi&I@$YoA~BbVh)70P}L-{qvu+7L#g7LIzsrl$+n@}46(B{1aJ zdBLSVg5`zaLnJN7d1~(d`tk^zxEKUWD!`3ln1kAZ@8o=qdZe1@H!e5%axk3geOLD8ld{>rRp`0_6W6ATlYLCw$E%bDiPR?a&3IR=f zPi;RjWM44Aq$`Y^W>AxwklC#%tMAHX42sv)K1nR?kqRhf@9NwibKlbX%jUA9ADTH3 z5DN+onASZYsA4OC4w;hr-m(mQR|rVUuPBvq{*pe3hHmBMqAP1U7C2)AvKo{AjLR>u_kLuN<`DhZr zsIO^0qUb{MqK5s`_AORgYF>dii-hh4bae`Z^OnDwb((dTS-+-BRz=lIQWctMPYf*4 zUna65-7;fAC~Y8Cg#y{0G53KhTQ@Z30zQ4hn(Yp)dnDoa!rEHK)A22@SL6WdYN^gt z(Aa$05_hF`n&Mk_luuAROBkK)Z5Eo7%UD=?DxOspGxyLxq0#(-jwu zMhX9-$FUUtr)spP9KqVSC)*rq(a0eFY!IkhZm^sOVkljL{O?bV~P34!PRwj_Vq>{N-jtgR!Lv1ZV_G%{1;x=K$U zO8xGqK_yd$5lxK<)fYxO!3B*A93P30AGa)Zi~&`;mkI|4f$#kx63kS|_1a3ON7ngu zm-}^u9`#A=&4Z3Y0Ok8v$H7aN)U(SqIh!qGC%*TKIsp}KqEt6`vbGp>Q*Bp(rsT+` zDREnnW{o1sF~%xVZkld(a~r%GKaCH@$`_+%cYDEQ3W=zCyWS z1-~}Tr|b*mZ;oYd_1otWyyB6o@r7^I-s-E6DPqc-F)W123ybFHG$*i1m1~9o@PIoo zDSn6rFtfTZiCAc*tYGt+I?-?h2Po!ic*+!JFNnz)8(JzH5R;mdSJN8&LJrAql5IR_ zqbr0=#`uL5)%XCxS*o1bstv_e+6z8-Q7kvYOsyI#@^rVvxt#s6Ah3=w@0YMf820pu zowJ3|zMo5k>m!2sdi+Mt-0E$i4mEvdO$8SVAK-t?LdoYd9h~KGCDEvmW*6Z!MjRD` zAlN^ar3$5KLXLeGFv%vBLd!+id2in0Kg8^D1xd}we+wSbL3gbals6^Xy&AURe=Q$q zj zhJ|)*qeZ>FUS)s`yX;kWfg{UoE4}R)pJH*)MsK9x`zpDsj8|rE`^GmqB@&y5r~8R+ z#ia!Y(-GXUR_fvx*&(y9q)aEwB@*lk>-VCigVpCRBH#(ghIV~zH?RvnaOj2_S*g@c zC&(c6#I(K8*TWb77&fxtidD$}3MOE|+kFVFIl{9%pvG5oPqrwWV{5tKuo0Y@fwz1{ zt;=PEAC2!tF`p2K?cIKU=Az+V<-owh1pY(N#I-Xn00xcMCw_7ltna9Qo%(>MjS?^_OB5AeGjw}YoWboRUX^_vkw-{ZQ2c4= zY3COPQ6DSgcAu{1Suo@*^7!n1!tcGU#wR+oi`YxWf`O(z{ zbw3Yx*0hNQ@1hnl&5FX_&`Knh4RpzJygZl;n0%;1KrIC~sWW39t1v<;I13=976>{2YK;& zdj+Ci^ZLJ?rA~r^X+LVfdlyYEWhQ>B%9~-%q>C zFnU)#--Py|uArI$%~0}C>q{_QLX#~Bx0j-8IXlmnZZA#<*HeSm04y-Vu=fNf^Z5mU#)6Nm2*6&*oQ|A;4ULU}4F;fxo{rul zj0$y3u`X1&kY|LX4HCP0p&cuh8p^27ic#$jj76R&1`1lHL zzzZ?VD&8818`w>9iW}HW;(%Bcs3qGe8KT0@~=9U?%_fDkb2CPysPA z##s#@6DXE#H%|qD$Q+6LOCGBDVU-Lbx7ac5EMI53%4-4$I;fd_R|s^ADnLD3>{G4M zf6*n5%D#&XV({rw37AYp{7EFFMQ20^@df!=PHKzayj8{=A-g~Ub=0uF9BIGB2HndW z(IxYu=rSj3L&QNHgVDZNLzuLbn#EV&o{b}sFf$MMod>QMxcSD*dzKtPh^YYRWUksh$JZ5 z3z=H;m?Al07&rMfLCVapfDutiq=CU%IFT~POxOc?+OSWCxwO~aC~b7tc()wAB6GBek9A^C*!L3t#p}m=QE-7WPAu1Y z@D7;w9>8xe9yvJh9l;WCB2JSNup)MoPH-YNlWTAymXohT$hb=NPdedKUZC9s(q3zI zX0M}DZfsl;K#JD)QEpq-ukaymy{Y?hAv;8knkV*(CvSw*ofn?YLnfx$5rtRs@ZfSw zz3}{^#o}HFPg7p209g>xZdzWf$~aMavvwA&pTa*#pRnBe^-?^kJ|FD8nM9Dr^)|6M z#`DfY#wTZBGwmlOLSndg>x~N_day+0 zUN7zA#aLf_!_%Q|pUazo1Kv-(_$GBs{|qL|HvCj?JpQ~09tB8>!K7hCpSF}22S@2& zm={Rot~;rHYb+MXyIY&rjSm5m4+knu-og97+9l~sA+LN=hU={Mg?-Q5>l0)FpY3$8VHT2?Rbe z?eaW0inZ*zgtQSfDyz`?C+9Wax{ImavWbmfe0)Zo4-vok^88*b$t}Nak^uI5SKpc` zZm1jQ_c7o6Td}X!h^JTKl8qPTioz`0yTa`e1;}R^TiH8IgGI@<_&S-kWkyPa9Gr@& zRo^PAjNO;mBzqtI9U-6gA)n^$nAD=WPt4~C(urTS#i`6Wi74KrJl3$_3gRS z&$%I3RX)1pR5*n+mVbADi;{0pL`DTB*NWPFzoB+)yr%9p*j0cFz;iuyd_Uy*IG&}y z++}xDRT-K&%9?T5;9zJ=_M`0){h!G%o>$!e53e)vzgvE9;c4OM{=aD-ji^I0te;7t zec=EAzW-A9Z{*72PA0DAn$FL3k`_|GF#ooUze(Ll8A}Y~$-x)0s761{?t!H- zL!?L&#bz&3q~rrixFI%zhXK0qHUj- zT%wjym+dA(FPOm8+#9oz+W*6z)0@WS+7KiQnV+CdLt?nX5A~MK_SKQfo3(pfHgZ~u zdIvs-ZO%q#ZBidUWNgJqfbDCaPM*L<=dz`%p5YA6hi>>a{5eY)6N(DPAKFT%0;^4w zxKkSA2^j*55}+l%%ww4s)KLnFH&Yc4H_NxTKnWWO48@Q-YROkvrO{Vqn|N;Emyy;F zP4<(4Y)Xc6t~KnaW`t!+c*Psp7vnaBbrKPuB+=f`iU^v3$ZrJ(^m|^(;czN2s`FpP z-$^o_(^GaaOS-=Bf}RNLajS$7a008r8KZ#-KgbGA5*ddPw2x@BN!&XtQ^g=&2}r}K za~`_{Pmz85ZrI8RuL+*rFtY2(I-`GY`6U0&dYw#dpE*zex9KaQBOQEu=0P_>_z$~Q z{;K@{tzP4uv&G-dT|-fsNE=(oNpQ1{1cs*<`{i&%5=}FO%O;f)G8$a8%}Sce7g2L! z8v!15XWQpR05UQ4jHL$ds`ODT3tSGklueo8vNX0D*c%2dDQ&xPyUL}$Yxt_0k_nn+ zl-*uyojc2MUj9}AcXz2m0PO_R*4pZ@7`5uWh^UOVd;+`1)c&GnA!Z}`h!TbJy807i ziKJfqLA9&dJKNS$aK%Qbp+Q`cfE#HpzSXWUm{EdiiQ*@R3_;yR_1OvH|Z3d?xuO=4ZbOgc$I;E2D6hr~4S5lG z{dhLnt5_U}xJ3x)i93Y~sNk6O8Dj^Hlh5?#2hGAy5(qA3- zDsZthQ1i($u>CQ6p~XqK(n-KMHW8QGzp36bOs?&ehoyCWNGg2uyLMDOrn&RSicj@L z=kiHk)++>grP)R{_48qGxaZQ!C*5Hads~T2I{FhBQHB_b_=Zp<{ChTxKdx7_4sPtD zi1V;)!at-0+jqRXx#HuHra<Bwz8pkSf;a29V zGVN#-2Tmt3>@FUHeUvzHXY~`9zPg)eYBOpAzZ&dCW<1#|t?>2e@2SlV+ckvc^SlfD zAqzo^N&*Vvh)%yFH_-P{I{ChZQtt&KLLrOaru{-Tk0!`{ZP<6mX@f#G?byQh%)H^f zAJ6($OS=ZcCl-s9qVe3}a~>ur`&>E6k&-?_I^5F*Y(u0YJ}vJOE-$Fqo+7zG86$6L zb=ch(IEuS3l$%~b!M5=}Qe0y(aiLUse#a6r$_nWC=C zMBxWF!+Q(U1uSm%^?_6xpUwrIjmsbCSe~q1k3Xhl*u>A-iyV7XaWe~^^R)#VTM zR9_!;O1P?hMCUa&c#n*jF4oR$x-_uG`G!}M{*^BHjXDlxV$oMMqR?mugqgkQR$#ij z^kT35ErFnN)%L098U0;S*B1!EfXr-L#0l3r&_MdVeSox)o!^@Xc%@(ziaarGhcGx6 zqTS$v0a+)ixEn`cA6b*a$LxSDw=&cQGE>+{C+MQ5q4RFw;e7HF+}dOol+RA(5SR(h zMyUz4g~{d#;h#Gf%!8)#3s?YP66rrMBLCGtNH{q-+go`5Vv|yLcl9uH_i+7tSCOry zr>?Gz{piU;{8EYh1v$A2Y+bly%r~D3*)ne^sVev;6$jtN!wYFGxd|snLQMA zYvgYNGkZzs58T;bAt2VGwbdPG1r9XNw1&%7yPAipi>jtew7~njeJ6lYK9zI6xn;#= zzUEh4#mxjAgM9b;%O7T(#{%E!+coGj)(koHDR;E&-nD3Pb0Y>{n{C=r>T!*P9WYx| zP|!`zk3FVw;UK2OB+|!ctq99uRp|3g2<@dvR9U*b_od;UXRth8&HhA%s$Z~<=zg&9 z$wZ^o#kR$=1G7_=v)7GzQ|r4cEi78erK|Ztjwl`UU$Z>C&ck%mz z5m$R_V-fSGcf^8ciRvsLhGl z2%yTq`eY7je*MNpOi9G^)!Ga{=0qTNQxaRJV@Hakim?15v;1d|$>k(wLa*CTiEO44~uMxAs@tlM=Ss+G%X0cXjgNL~5@37)7P2XQ*~TrP4> zQ;X73{Uxg!GSMKNfQQ90O6ZQ>yMS_QzSzpAufxfS!3Kjk@1Ux|vXodB&nwT;`ikgc zf3(FSr^UAsaa`_8nz<{eUs9@v@lwV&RfJWlvY#nf@;If+dwxG%$FtK)xY5#NS z;$Fr?EU~ogXy?U~Li3XAFVz_1VD$S$5c9poU0mzx^h0P4-ut%8?dDiz7rk;awwkY& z>IM#;P>eV{j%YNbTXd*2{n$r%el*2P zz3q)YgetBfEzV6GppnHR^P;H9ML+L};Qve&T@om5^rQ~ymafREE~b|W?EMnPUAA^@ z=Mm+vG3OTK+-ADWOyKoObWr?EI8sjyv7`H^O+01hOOuex8b`Q={Ny`2<&8$CjLV7@ zrJ0AT43!6nw4C7se74Y4*#W;U^F zl*Ia9?qhods=pvrnt8784dyN@F`M-)wbFm-{9FYnRbrwf)_pO(>lE;0kC3B*y4t&T zPT_ImIxG8<^1Xen9x$U)Q+AF4o3cVK7S2V}SeN4#zvZD;x$G;&jP~&O5Y{OOuXRVN znS67-Ha3FMV@!`;q<=^Fvshc|#k9HIC2z8e2-cF^IG$d%>*9oMpIHVTxk6nAm)BLU zq~lO&K2E6hPC9F*JCVAMQ97N4s!7-r%s=;}?ZUt?yk}2q=6RCA_TSvJUtOr0g^P!U zo4bYU-@L6VjZrl$F>E2;H|H0A7#gHq`ELiOpGC>KzlRN&6e}eR z7udtx21uB8{n;1ivai%3)cX^Oc;s}NKEk@lBHwv4&GN_ zrzntf9Cdt~mMEp^DHHngYHKu-iIG5uyzi3!RKB$fQU1s~%N!IuSx-@?y@OHO^E3qO z)`Kj_Eo&|7CG1=a7sG?H09!$o!a|j$xWn@^A!>79;OKa4Ku`-wb z0_MQ%p(9L!$z3h^c$go@_j;k%gxE$LA@*f7dAL^*uj<}9pVDpz1b(|^;9zP>R7w+> zKzv9*F04LMQgtu1gO_uEw5jnZ!>Gl4POQeT9+^eKY&ZCXak{sR&^tp6XIpa%u1JJ` zPG#F2*Z=l@!l6}x(xhlz0U7V@K4a(hHFDpnyZ^&2wVhOlIqm}wi)oPaD+4D9A+`2! zAde5*=Z}#ux)9oV%VxEVdNuv|`t2{A`6ynD^Rvc#F>t~Wtl)KsfY^^ypg|0nL#$65 zH0WDA{8_=>m>k0*ecw7>4%k%dZ=TIyI(yS~(kl26XQq+9h-VJ4;;gZogTl1lC728L z%P$3K9)d{K1uNfqB=zdKB;UXI=SC)q&^bu-Jd?+HZe%R~t&#o1RQ|8|`>5lLD7FyR zQu?CHZ9bfSo{F`?ODMyj1y~AsiGjYL&>7s+IVu7iJYM={pvGgN(ImHtDr^Yz%kDn( z+E>eb8ZT5~%V5vOF2+uUyuOYR2R@u`a0X=#b8dMSk?=4Cte_Q^emQ{~7RNLy6+JN$$lr!nfB>aZ+&_xKf2ii})L;;j#pzzea$#bn z@9s0V!Ux%38raA(^Pt9zDml|sm##xm9#h)-cy+bKwjC&OMuZYJ2Zk+P9?3Zs(fv6Hc%R2Wr|zkF}6PDbh+!8+AkoT zSx2_OXhL^M8SA!%g2^9%&B^pL=QXgIVLO|&EJ=D)zo#Skymk2GkoCvy0pB9?gIg}d z-RYvJ68=@B($?OQ11o;opan@;EZppzv?9kFz5oMZ$0IHCx40=m@s&hT;87XVLCH&#G9Q%zbtLbyzvdQ`)F%pl-J^X ziIGvHV-t0*oOX_FH+G?+;3YK9q0j1Iv3%89SidjRTF-AsYFr*R0OR}v%Bjo~ zX%u@1G@0}Y6-mDFRkix=9tHBO_fGiPv-EP>4XM@hL{n z_{Bb{f@+w4zT(}pnL$Bg1O9y)v1eQR2YF5^_Ma;JN-FkSD4&bZ75_1iem169- zym>DEPqlwcIQE|)ekC0HEy2&le+ls`{n(!{f2JS%4b%3YVE$DyvOl4JK|MFx|1ZB% zxBQmx&tZvw`Gn7rw|`9J@+ab-iClgok|6%y5Pyxpf24K!Q^}uvyT6qz;r`#0{9E?j zKUMunF7jKI4#WRV)n8O2e`R`HGKau{7qW(t0*8R^&|IiWt zubAqeT7IGa^}N5LUB6}enRw-2+R9(k(?6m9jD7uvLO1v?P=7?i{zU$B*8Cfp)#Se* z|NA-gpGy98D1R$?XZc^0{0sN;PxL=W(%i+?q#|2LS literal 0 HcmV?d00001