diff --git a/pom.xml b/pom.xml index 5c69674..f283fc7 100755 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,13 @@ testDelombok false src/test/java --> + + maven-surefire-plugin + + false + + + diff --git a/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java b/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java new file mode 100644 index 0000000..994c8ed --- /dev/null +++ b/src/main/java/buttondevteam/discordplugin/ChannelconBroadcast.java @@ -0,0 +1,15 @@ +package buttondevteam.discordplugin; + +public enum ChannelconBroadcast { + JOINLEAVE, + AFK, + RESTART, + DEATH, + BROADCAST; + + public final int flag; + + ChannelconBroadcast() { + this.flag = 1 << this.ordinal(); + } +} diff --git a/src/main/java/buttondevteam/discordplugin/ChromaBot.java b/src/main/java/buttondevteam/discordplugin/ChromaBot.java index 2db1a17..79a0514 100755 --- a/src/main/java/buttondevteam/discordplugin/ChromaBot.java +++ b/src/main/java/buttondevteam/discordplugin/ChromaBot.java @@ -9,6 +9,7 @@ import sx.blah.discord.api.internal.json.objects.EmbedObject; import sx.blah.discord.handle.obj.IChannel; import sx.blah.discord.util.EmbedBuilder; +import javax.annotation.Nullable; import java.awt.*; import java.util.Arrays; import java.util.stream.Collectors; @@ -62,9 +63,10 @@ public class ChromaBot { * * @param message The message to send, duh * @param embed Custom fancy stuff, use {@link EmbedBuilder} to create one + * @param toggle The toggle type for channelcon */ - public void sendMessageCustomAsWell(String message, EmbedObject embed) { - MCChatListener.forCustomAndAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed)); + public void sendMessageCustomAsWell(String message, EmbedObject embed, @Nullable ChannelconBroadcast toggle) { + MCChatListener.forCustomAndAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed), toggle, false); } /** diff --git a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java index 431f212..be7e8a9 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordPlugin.java @@ -38,6 +38,7 @@ import java.awt.*; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -149,30 +150,30 @@ public class DiscordPlugin extends JavaPlugin implements IListener { val mcch = Channel.getChannels().stream().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny(); val ch = dc.getChannelByID(chcon.getLong("chid")); val did = chcon.getLong("did"); - val dp = DiscordPlayer.getUser(Long.toString(did), DiscordPlayer.class); val user = dc.fetchUser(did); val dcp = new DiscordConnectedPlayer(user, ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname")); val groupid = chcon.getString("groupid"); + val toggles = chcon.getInt("toggles"); if (!mcch.isPresent() || ch == null || user == null || groupid == null) continue; - MCChatListener.addCustomChat(ch, groupid, mcch.get(), dp, user, dcp); + MCChatListener.addCustomChat(ch, groupid, mcch.get(), user, dcp, toggles); } } DiscordCommandBase.registerCommands(); if (ResetMCCommand.resetting) ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.CYAN) - .withTitle("Discord plugin restarted - chat connected.").build()); //Really important to note the chat, hmm + .withTitle("Discord plugin restarted - chat connected.").build(), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm else if (getConfig().getBoolean("serverup", false)) { ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.YELLOW) - .withTitle("Server recovered from a crash - chat connected.").build()); + .withTitle("Server recovered from a crash - chat connected.").build(), ChannelconBroadcast.RESTART); val thr = new Throwable( "The server shut down unexpectedly. See the log of the previous run for more details."); thr.setStackTrace(new StackTraceElement[0]); TBMCCoreAPI.SendException("The server crashed!", thr); } else ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.GREEN) - .withTitle("Server started - chat connected.").build()); + .withTitle("Server started - chat connected.").build(), ChannelconBroadcast.RESTART); ResetMCCommand.resetting = false; //This is the last event handling this flag @@ -220,6 +221,12 @@ public class DiscordPlugin extends JavaPlugin implements IListener { TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this); TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class); TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class); + ChromaGamerBase.addConverter(sender -> { + //System.out.println("Discord converter queried: "+sender+" "+sender.getName()); - TODO: Remove + //System.out.println(((DiscordSenderBase) sender).getChromaUser().channel().get().ID); //TODO: TMP + return Optional.ofNullable(sender instanceof DiscordSenderBase + ? ((DiscordSenderBase) sender).getChromaUser() : null); + }); new Thread(this::AnnouncementGetterThreadMethod).start(); setupProviders(); } catch (Exception e) { @@ -261,31 +268,33 @@ public class DiscordPlugin extends JavaPlugin implements IListener { chconc.set("mcuid", chcon.dcp.getUniqueId().toString()); chconc.set("mcname", chcon.dcp.getName()); chconc.set("groupid", chcon.groupID); + chconc.set("toggles", chcon.toggles); } saveConfig(); + EmbedObject embed; + if (ResetMCCommand.resetting) + embed = new EmbedBuilder().withColor(Color.ORANGE).withTitle("Discord plugin restarting").build(); + else + embed = new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) + .withTitle(Restart ? "Server restarting" : "Server stopping") + .withDescription( + Bukkit.getOnlinePlayers().size() > 0 + ? (DPUtils + .sanitizeString(Bukkit.getOnlinePlayers().stream() + .map(Player::getDisplayName).collect(Collectors.joining(", "))) + + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") + + "kicked the hell out.") //TODO: Make configurable + : "") //If 'restart' is disabled then this isn't shown even if joinleave is enabled + .build(); MCChatListener.forCustomAndAllMCChat(ch -> { try { - if (ResetMCCommand.resetting) DiscordPlugin.sendMessageToChannelWait(ch, "", - new EmbedBuilder().withColor(Color.ORANGE).withTitle("Discord plugin restarting").build()); - else - DiscordPlugin.sendMessageToChannelWait(ch, "", - new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED) - .withTitle(Restart ? "Server restarting" : "Server stopping") - .withDescription( - Bukkit.getOnlinePlayers().size() > 0 - ? (DPUtils - .sanitizeString(Bukkit.getOnlinePlayers().stream() - .map(Player::getDisplayName).collect(Collectors.joining(", "))) - + (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ") - + "asked *politely* to leave the server for a bit.") - : "") - .build(), 5, TimeUnit.SECONDS); + embed, 5, TimeUnit.SECONDS); } catch (TimeoutException | InterruptedException e) { e.printStackTrace(); } - }); + }, ChannelconBroadcast.RESTART, false); ChromaBot.getInstance().updatePlayerList(); try { SafeMode = true; // Stop interacting with Discord @@ -415,10 +424,7 @@ public class DiscordPlugin extends JavaPlugin implements IListener { .warning("Message was too long to send to discord and got truncated. In " + channel.getName()); } try { - if (channel == chatchannel) - MCChatListener.resetLastMessage(); // If this is a chat message, it'll be set again - else if (channel.isPrivate()) - MCChatListener.resetLastMessage(channel); + MCChatListener.resetLastMessage(channel); // If this is a chat message, it'll be set again final String content = message; RequestBuffer.IRequest r = () -> embed == null ? channel.sendMessage(content) : channel.sendMessage(content, embed, false); diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSender.java b/src/main/java/buttondevteam/discordplugin/DiscordSender.java index f2f829b..8ad7445 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSender.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSender.java @@ -16,10 +16,11 @@ import java.util.Set; public class DiscordSender extends DiscordSenderBase implements CommandSender { private PermissibleBase perm = new PermissibleBase(this); - private String name = null; + private String name; public DiscordSender(IUser user, IChannel channel) { super(user, channel); + name = user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer); } public DiscordSender(IUser user, IChannel channel, String name) { @@ -85,12 +86,12 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender { } @Override - public boolean isOp() { // TODO: Connect with TBMC acc + public boolean isOp() { return false; } @Override - public void setOp(boolean value) { // TODO: Connect with TBMC acc + public void setOp(boolean value) { } @Override @@ -100,7 +101,7 @@ public class DiscordSender extends DiscordSenderBase implements CommandSender { @Override public String getName() { - return name == null ? user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer) : name; + return name; } @Override diff --git a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java index 0a6e392..14c7fc4 100755 --- a/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java +++ b/src/main/java/buttondevteam/discordplugin/DiscordSenderBase.java @@ -1,26 +1,18 @@ package buttondevteam.discordplugin; import buttondevteam.lib.TBMCCoreAPI; -import buttondevteam.lib.chat.Channel; import buttondevteam.lib.chat.IDiscordSender; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.scheduler.BukkitTask; import sx.blah.discord.handle.obj.IChannel; import sx.blah.discord.handle.obj.IUser; -import java.util.Arrays; -import java.util.stream.Collectors; - public abstract class DiscordSenderBase implements IDiscordSender { /** * May be null. */ protected IUser user; protected IChannel channel; - private @Getter @Setter @NonNull Channel mcchannel = Channel.GlobalChat; protected DiscordSenderBase(IUser user, IChannel channel) { this.user = user; @@ -43,6 +35,18 @@ public abstract class DiscordSenderBase implements IDiscordSender { return channel; } + private DiscordPlayer chromaUser; + + /** + * Loads the user data on first query. + * + * @return A Chroma user of Discord or a Discord user of Chroma + */ + public DiscordPlayer getChromaUser() { + if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getStringID(), DiscordPlayer.class); + return chromaUser; + } + @Override public void sendMessage(String message) { try { @@ -65,6 +69,6 @@ public abstract class DiscordSenderBase implements IDiscordSender { @Override public void sendMessage(String[] messages) { - sendMessage(Arrays.stream(messages).collect(Collectors.joining("\n"))); + sendMessage(String.join("\n", messages)); } } diff --git a/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java b/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java index c2c878c..432b75c 100755 --- a/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java +++ b/src/main/java/buttondevteam/discordplugin/PlayerListWatcher.java @@ -30,7 +30,7 @@ public class PlayerListWatcher extends DedicatedPlayerList { if (packet instanceof PacketPlayOutChat) { Field msgf = PacketPlayOutChat.class.getDeclaredField("a"); msgf.setAccessible(true); - MCChatListener.sendSystemMessageToChat(((IChatBaseComponent) msgf.get(packet)).toPlainText()); + MCChatListener.forAllMCChat(MCChatListener.send(((IChatBaseComponent) msgf.get(packet)).toPlainText())); } } catch (Exception e) { TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e); diff --git a/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java b/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java index d9a1c8f..73eefd1 100644 --- a/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/ChannelconCommand.java @@ -1,9 +1,9 @@ package buttondevteam.discordplugin.commands; +import buttondevteam.discordplugin.ChannelconBroadcast; import buttondevteam.discordplugin.DiscordConnectedPlayer; import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.listeners.MCChatListener; -import buttondevteam.lib.TBMCChannelConnectFakeEvent; import buttondevteam.lib.chat.Channel; import buttondevteam.lib.player.TBMCPlayer; import lombok.val; @@ -13,6 +13,8 @@ import sx.blah.discord.handle.obj.Permissions; import sx.blah.discord.util.PermissionUtils; import java.util.Arrays; +import java.util.function.Supplier; +import java.util.stream.Collectors; public class ChannelconCommand extends DiscordCommandBase { @Override @@ -29,13 +31,38 @@ public class ChannelconCommand extends DiscordCommandBase { return true; } if (MCChatListener.hasCustomChat(message.getChannel())) { - if (args.equalsIgnoreCase("remove")) { + if (args.toLowerCase().startsWith("remove")) { if (MCChatListener.removeCustomChat(message.getChannel())) message.reply("channel connection removed."); else message.reply("wait what, couldn't remove channel connection."); return true; } + if (args.toLowerCase().startsWith("toggle")) { + val cc = MCChatListener.getCustomChat(message.getChannel()); + Supplier togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n")); + String[] argsa = args.split(" "); + if (argsa.length < 2) { + message.reply("toggles:\n" + togglesString.get()); + return true; + } + String arg = argsa[1].toUpperCase(); + val b = Arrays.stream(ChannelconBroadcast.values()).filter(t -> t.toString().equals(arg)).findAny(); + if (!b.isPresent()) { + message.reply("cannot find toggle. Toggles:\n" + togglesString.get()); + return true; + } + //A B | F + //------- A: original - B: mask - F: new + //0 0 | 0 + //0 1 | 1 + //1 0 | 1 + //1 1 | 0 + // XOR + cc.toggles ^= b.get().flag; + message.reply("'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled")); + return true; + } message.reply("this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it."); return true; } @@ -51,18 +78,17 @@ public class ChannelconCommand extends DiscordCommandBase { return true; } DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor(), message.getChannel(), chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName()); - val ev = new TBMCChannelConnectFakeEvent(dcp, chan.get()); //Using a fake player with no login/logout, should be fine for this event - String groupid = ev.getGroupID(ev.getSender()); //We're not trying to send in a specific group, we want to know which group the user belongs to (so not getGroupID()) + String groupid = chan.get().getGroupID(dcp); if (groupid == null) { message.reply("sorry, that didn't work. You cannot use that Minecraft channel."); return true; } - if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { + /*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) { message.reply("sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm."); return true; - } - MCChatListener.addCustomChat(message.getChannel(), groupid, ev.getChannel(), dp, message.getAuthor(), dcp); + }*/ //TODO: "Channel admins" that can connect channels? + MCChatListener.addCustomChat(message.getChannel(), groupid, chan.get(), message.getAuthor(), dcp, 0); message.reply("alright, connection made to group `" + groupid + "`!"); return true; } diff --git a/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java b/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java index dbe8988..3978c6a 100755 --- a/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java +++ b/src/main/java/buttondevteam/discordplugin/commands/RoleCommand.java @@ -59,19 +59,19 @@ public class RoleCommand extends DiscordCommandBase { DiscordPlugin.sendMessageToChannel(message.getChannel(), usage + "\nUsage: " + argsa[0] + " "); return null; } - String rolename = argsa[1]; + StringBuilder rolename = new StringBuilder(argsa[1]); for (int i = 2; i < argsa.length; i++) - rolename += " " + argsa[i]; - if (!DiscordPlugin.GameRoles.contains(rolename)) { + rolename.append(" ").append(argsa[i]); + if (!DiscordPlugin.GameRoles.contains(rolename.toString())) { DiscordPlugin.sendMessageToChannel(message.getChannel(), "That game role cannot be found."); listRoles(message); return null; } - final List roles = DiscordPlugin.mainServer.getRolesByName(rolename); + final List roles = DiscordPlugin.mainServer.getRolesByName(rolename.toString()); if (roles.size() == 0) { DiscordPlugin.sendMessageToChannel(message.getChannel(), "The specified role cannot be found on Discord! Removing from the list."); - DiscordPlugin.GameRoles.remove(rolename); + DiscordPlugin.GameRoles.remove(rolename.toString()); return null; } if (roles.size() > 1) { diff --git a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java index 9384c6e..f46abff 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/CommandListener.java @@ -1,6 +1,5 @@ package buttondevteam.discordplugin.listeners; -import buttondevteam.discordplugin.DiscordPlayer; import buttondevteam.discordplugin.DiscordPlugin; import buttondevteam.discordplugin.commands.DiscordCommandBase; import buttondevteam.lib.TBMCCoreAPI; @@ -99,8 +98,7 @@ public class CommandListener { && !(event.getMessage().getContent().startsWith("/") && event.getChannel().getStringID().equals(DiscordPlugin.botchannel.getStringID()))) // return; - if (DiscordPlayer.getUser(event.getAuthor().getStringID(), DiscordPlayer.class) - .isMinecraftChatEnabled()) + if (MCChatListener.isMinecraftChatEnabled(event.getAuthor().toString())) if (!event.getMessage().getContent().equalsIgnoreCase("mcchat")) return; if (event.getMessage().getAuthor().isBot()) diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java index d1838be..4d73c83 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCChatListener.java @@ -2,7 +2,10 @@ package buttondevteam.discordplugin.listeners; import buttondevteam.discordplugin.*; import buttondevteam.discordplugin.playerfaker.VanillaCommandListener; -import buttondevteam.lib.*; +import buttondevteam.lib.TBMCChatEvent; +import buttondevteam.lib.TBMCChatPreprocessEvent; +import buttondevteam.lib.TBMCCoreAPI; +import buttondevteam.lib.TBMCSystemChatEvent; import buttondevteam.lib.chat.Channel; import buttondevteam.lib.chat.ChatMessage; import buttondevteam.lib.chat.ChatRoom; @@ -33,6 +36,7 @@ import sx.blah.discord.util.DiscordException; import sx.blah.discord.util.EmbedBuilder; import sx.blah.discord.util.MissingPermissionsException; +import javax.annotation.Nullable; import java.awt.*; import java.time.Instant; import java.util.*; @@ -124,14 +128,14 @@ public class MCChatListener implements Listener, IListener Predicate isdifferentchannel = ch -> !(e.getSender() instanceof DiscordSenderBase) || ((DiscordSenderBase) e.getSender()).getChannel().getLongID() != ch.getLongID(); - if ((e.getChannel() == Channel.GlobalChat || e.getChannel().ID.equals("rp")) + if (e.getChannel().isGlobal() && (e.isFromcmd() || isdifferentchannel.test(DiscordPlugin.chatchannel))) doit.accept(lastmsgdata == null - ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null, null) + ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null) : lastmsgdata); for (LastMsgData data : lastmsgPerUser) { - if (data.dp.isMinecraftChatEnabled() && (e.isFromcmd() || isdifferentchannel.test(data.channel)) + if ((e.isFromcmd() || isdifferentchannel.test(data.channel)) && e.shouldSendTo(getSender(data.channel, data.user))) doit.accept(data); } @@ -166,20 +170,21 @@ public class MCChatListener implements Listener, IListener public final IChannel channel; public Channel mcchannel; public final IUser user; - public final DiscordPlayer dp; } public static class CustomLMD extends LastMsgData { public final String groupID; public final Channel mcchannel; public final DiscordConnectedPlayer dcp; + public int toggles; - public CustomLMD(@NonNull IChannel channel, @NonNull IUser user, @NonNull DiscordPlayer dp, - @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp) { - super(channel, user, dp); + private CustomLMD(@NonNull IChannel channel, @NonNull IUser user, + @NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles) { + super(channel, user); groupID = groupid; this.mcchannel = mcchannel; this.dcp = dcp; + this.toggles = toggles; } } @@ -238,7 +243,7 @@ public class MCChatListener implements Listener, IListener if (!start) lastmsgfromd.remove(channel.getLongID()); return start // - ? lastmsgPerUser.add(new LastMsgData(channel, user, dp)) // Doesn't support group DMs + ? lastmsgPerUser.add(new LastMsgData(channel, user)) // Doesn't support group DMs : lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID()); } @@ -293,8 +298,8 @@ public class MCChatListener implements Listener, IListener .anyMatch(lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(did)); } - public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, DiscordPlayer dp, IUser user, DiscordConnectedPlayer dcp) { - val lmd = new CustomLMD(channel, user, dp, groupid, mcchannel, dcp); + public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, IUser user, DiscordConnectedPlayer dcp, int toggles) { + val lmd = new CustomLMD(channel, user, groupid, mcchannel, dcp, toggles); lastmsgCustom.add(lmd); } @@ -302,9 +307,10 @@ public class MCChatListener implements Listener, IListener return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getLongID() == channel.getLongID()); } - public static CustomLMD getCustomChat(IChannel channel) { - return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null); - } + @Nullable + public static CustomLMD getCustomChat(IChannel channel) { + return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null); + } public static boolean removeCustomChat(IChannel channel) { lastmsgfromd.remove(channel.getLongID()); @@ -326,33 +332,25 @@ public class MCChatListener implements Listener, IListener public static final HashMap> OnlineSenders = new HashMap<>(); public static short ListC = 0; - public static void resetLastMessage() { - (lastmsgdata == null ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null, null) - : lastmsgdata).message = null; - } // Don't set the whole object to null, the player and channel information should be preserved - - public static void resetLastMessage(IChannel channel) { - for (LastMsgData data : lastmsgPerUser) - if (data.channel.getLongID() == channel.getLongID()) - data.message = null; // Since only private channels are stored, only those will work anyways - } - - public static void resetLastMessageCustom(IChannel channel) { - for (LastMsgData data : lastmsgCustom) - if (data.channel.getLongID() == channel.getLongID()) - data.message = null; - } - /** - * This overload sends it to the global chat. + * Resets the last message, so it will start a new one instead of appending to it. + * This is used when someone (even the bot) sends a message to the channel. + * + * @param channel The channel to reset in - the process is slightly different for the public, private and custom chats */ - public static void sendSystemMessageToChat(String msg) { - forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(msg))); - } - - public static void sendSystemMessageToChat(TBMCSystemChatEvent event) { - forAllowedMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(event.getMessage())), - event); + public static void resetLastMessage(IChannel channel) { + if (channel.getLongID() == DiscordPlugin.chatchannel.getLongID()) { + (lastmsgdata == null ? lastmsgdata = new LastMsgData(DiscordPlugin.chatchannel, null) + : lastmsgdata).message = null; + return; + } // Don't set the whole object to null, the player and channel information should be preserved + for (LastMsgData data : channel.isPrivate() ? lastmsgPerUser : lastmsgCustom) { + if (data.channel.getLongID() == channel.getLongID()) { + data.message = null; + return; + } + } + //If it gets here, it's sending a message to a non-chat channel } public static void forAllMCChat(Consumer action) { @@ -362,27 +360,70 @@ public class MCChatListener implements Listener, IListener // lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat } - public static void forCustomAndAllMCChat(Consumer action) { - forAllMCChat(action); - lastmsgCustom.forEach(cc -> action.accept(cc.channel)); + /** + * For custom and all MC chat + * + * @param action The action to act + * @param toggle The toggle to check + * @param hookmsg Whether the message is also sent from the hook + */ + public static void forCustomAndAllMCChat(Consumer action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { + if (!DiscordPlugin.hooked || !hookmsg) + forAllMCChat(action); + final Consumer customLMDConsumer = cc -> action.accept(cc.channel); + if (toggle == null) + lastmsgCustom.forEach(customLMDConsumer); + else + lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).forEach(customLMDConsumer); } - public static void forAllowedCustomMCChat(Consumer action, CommandSender sender) { + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + */ + public static void forAllowedCustomMCChat(Consumer action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) { lastmsgCustom.stream().filter(clmd -> { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple - val e = new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel); - return clmd.groupID.equals(e.getGroupID(sender)); - }).forEach(cc -> action.accept(cc.channel)); //TODO: Use getScore and getGroupID in fake event constructor - This should also send error messages on channel connect + if (toggle != null && (clmd.toggles & toggle.flag) == 0) + return false; //If null then allow + if (sender == null) + return true; + return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender)); + }).forEach(cc -> action.accept(cc.channel)); //TODO: Send error messages on channel connect } - private static void forAllowedMCChat(Consumer action, TBMCSystemChatEvent event) { - if (Channel.GlobalChat.ID.equals(event.getChannel().ID)) + /** + * Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled. + * + * @param action The action to do + * @param sender The sender to check perms of or null to send to all that has it toggled + * @param toggle The toggle to check or null to send to all allowed + * @param hookmsg Whether the message is also sent from the hook + */ + public static void forAllowedCustomAndAllMCChat(Consumer action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) { + if (!DiscordPlugin.hooked || !hookmsg) + forAllMCChat(action); + forAllowedCustomMCChat(action, sender, toggle); + } + + public static Consumer send(String message) { + return ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(message)); + } + + public static void forAllowedMCChat(Consumer action, TBMCSystemChatEvent event) { + if (event.getChannel().isGlobal()) action.accept(DiscordPlugin.chatchannel); for (LastMsgData data : lastmsgPerUser) if (event.shouldSendTo(getSender(data.channel, data.user))) action.accept(data.channel); - lastmsgCustom.stream().filter(data -> event.shouldSendTo(data.dcp)) - .map(data -> data.channel).forEach(action); + lastmsgCustom.stream().filter(clmd -> { + if ((clmd.toggles & ChannelconBroadcast.BROADCAST.flag) == 0) + return false; + return event.shouldSendTo(clmd.dcp); + }).map(clmd -> clmd.channel).forEach(action); } /** @@ -438,12 +479,7 @@ public class MCChatListener implements Listener, IListener return; // Race condition: If it gets here after it enabled mcchat it says it - I might as well allow disabling with this (CommandListener) if (CommandListener.runCommand(ev.getMessage(), true)) return; - if (!ev.getMessage().getChannel().isPrivate()) - resetLastMessage(); - else if (hasCustomChat) - resetLastMessageCustom(ev.getChannel()); - else - resetLastMessage(ev.getMessage().getChannel()); + resetLastMessage(ev.getChannel()); lastlist++; recevents.add(ev); if (rectask != null) @@ -467,10 +503,10 @@ public class MCChatListener implements Listener, IListener return; } val sender = event.getMessage().getAuthor(); - val user = DiscordPlayer.getUser(sender.getStringID(), DiscordPlayer.class); String dmessage = event.getMessage().getContent(); try { final DiscordSenderBase dsender = getSender(event.getMessage().getChannel(), sender); + val user = dsender.getChromaUser(); for (IUser u : event.getMessage().getMentions()) { dmessage = dmessage.replace(u.mention(false), "@" + u.getName()); // TODO: IG Formatting @@ -486,7 +522,7 @@ public class MCChatListener implements Listener, IListener Function getChatMessage = msg -> // msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage() - .getAttachments().stream().map(a -> a.getUrl()).collect(Collectors.joining("\n")) + .getAttachments().stream().map(IMessage.Attachment::getUrl).collect(Collectors.joining("\n")) : ""); CustomLMD clmd = getCustomChat(event.getChannel()); @@ -498,7 +534,6 @@ public class MCChatListener implements Listener, IListener if (!event.getMessage().isDeleted() && !event.getChannel().isPrivate()) event.getMessage().delete(); }); - //preprocessChat(dsender, dmessage); - Same is done below final String cmd = dmessage.substring(1); final String cmdlowercased = cmd.toLowerCase(); if (dsender instanceof DiscordSender && Arrays.stream(UnconnectedCmds) @@ -530,38 +565,51 @@ public class MCChatListener implements Listener, IListener .filter(c -> c.ID.equalsIgnoreCase(topcmd) || (c.IDs != null && c.IDs.length > 0 && Arrays.stream(c.IDs).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny(); - if (!ch.isPresent()) - Bukkit.getScheduler().runTask(DiscordPlugin.plugin, - () -> { + if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one + Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync + () -> { //TODO: Better handling... + val channel = user.channel(); + val chtmp = channel.get(); + //System.out.println("1: "+chtmp.ID); + //System.out.println("clmd: "+clmd); + if (clmd != null) { + channel.set(clmd.mcchannel); //Hack to send command in the channel + //System.out.println("clmd chan: "+clmd.mcchannel.ID); + } //TODO: Permcheck isn't implemented for commands + //System.out.println("2: "+channel.get().ID); VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd); Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased); + if (clmd != null) + channel.set(chtmp); + //System.out.println("3: "+channel.get().ID); - TODO: Remove }); else { Channel chc = ch.get(); - if (!chc.ID.equals(Channel.GlobalChat.ID) && !chc.ID.equals("rp") && !event.getMessage().getChannel().isPrivate()) + if (!chc.isGlobal() && !event.getMessage().getChannel().isPrivate()) dsender.sendMessage( - "You can only talk in global in the public chat. DM `mcchat` to enable private chat to talk in the other channels."); + "You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels."); else { if (spi == -1) // Switch channels { - val oldch = dsender.getMcchannel(); + val channel = dsender.getChromaUser().channel(); + val oldch = channel.get(); if (oldch instanceof ChatRoom) ((ChatRoom) oldch).leaveRoom(dsender); if (!oldch.ID.equals(chc.ID)) { - dsender.setMcchannel(chc); + channel.set(chc); if (chc instanceof ChatRoom) ((ChatRoom) chc).joinRoom(dsender); } else - dsender.setMcchannel(Channel.GlobalChat); + channel.set(Channel.GlobalChat); dsender.sendMessage("You're now talking in: " - + DPUtils.sanitizeString(dsender.getMcchannel().DisplayName)); + + DPUtils.sanitizeString(channel.get().DisplayName)); } else { // Send single message final String msg = cmd.substring(spi + 1); - val cmb = ChatMessage.builder(chc, dsender, user, getChatMessage.apply(msg)).fromCommand(true); + val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true); if (clmd == null) - TBMCChatAPI.SendChatMessage(cmb.build()); + TBMCChatAPI.SendChatMessage(cmb.build(), chc); else - TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build()); + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc); react = true; } } @@ -570,14 +618,16 @@ public class MCChatListener implements Listener, IListener lastlistp = (short) Bukkit.getOnlinePlayers().size(); } else {// Not a command if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0 - && !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) - TBMCChatAPI.SendSystemMessage(Channel.GlobalChat, 0, "everyone", - (dsender instanceof Player ? ((Player) dsender).getDisplayName() - : dsender.getName()) + " pinned a message on Discord."); + && !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) { + val rtr = clmd != null ? clmd.mcchannel.filteranderrormsg.apply(clmd.dcp) : dsender.getChromaUser().channel().get().filteranderrormsg.apply(dsender); + TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr.score, rtr.groupID, + (dsender instanceof Player ? ((Player) dsender).getDisplayName() + : dsender.getName()) + " pinned a message on Discord."); + } else { - val cmb = ChatMessage.builder(dsender.getMcchannel(), dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); + val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false); if (clmd != null) - TBMCChatAPI.SendChatMessage(cmb.channel(clmd.mcchannel).permCheck(clmd.dcp).build()); + TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel); else TBMCChatAPI.SendChatMessage(cmb.build()); react = true; @@ -601,55 +651,17 @@ public class MCChatListener implements Listener, IListener } } - private boolean preprocessChat(DiscordSenderBase dsender, String dmessage) { - if (dmessage.length() < 2) - return false; - int index = dmessage.indexOf(" "); - String cmd; - if (index == -1) { // Only the command is run - cmd = dmessage; - for (Channel channel : Channel.getChannels()) { - if (cmd.equalsIgnoreCase(channel.ID) || (channel.IDs != null && Arrays.stream(channel.IDs).anyMatch(cmd::equalsIgnoreCase))) { - Channel oldch = dsender.getMcchannel(); - if (oldch instanceof ChatRoom) - ((ChatRoom) oldch).leaveRoom(dsender); - if (oldch.equals(channel)) - dsender.setMcchannel(Channel.GlobalChat); - else { - dsender.setMcchannel(channel); - if (channel instanceof ChatRoom) - ((ChatRoom) channel).joinRoom(dsender); - } - dsender.sendMessage("You are now talking in: " + dsender.getMcchannel().DisplayName); - return true; - } - } - } else { // We have arguments - cmd = dmessage.substring(0, index); - for (Channel channel : Channel.getChannels()) { - if (cmd.equalsIgnoreCase(channel.ID) || (channel.IDs != null && Arrays.stream(channel.IDs).anyMatch(cmd::equalsIgnoreCase))) { - val dp = DiscordPlayer.getUser(dsender.getUser().getStringID(), DiscordPlayer.class); - TBMCChatAPI.SendChatMessage(ChatMessage.builder(channel, dsender, dp, dmessage.substring(index + 1)).build()); - return true; - } - } - // TODO: Target selectors - } - return false; - } - /** * This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc. */ private static DiscordSenderBase getSender(IChannel channel, final IUser author) { - val key = author.getStringID(); + //noinspection OptionalGetWithoutIsPresent return Stream.>>of( // https://stackoverflow.com/a/28833677/2703239 () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null () -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it - () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), () -> { - return Optional.of(addSender(UnconnectedSenders, author, - new DiscordSender(author, channel))); - }).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); + () -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // + () -> Optional.of(addSender(UnconnectedSenders, author, + new DiscordSender(author, channel)))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get(); } @FunctionalInterface diff --git a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java index d6e64e3..6f9b3c9 100755 --- a/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java +++ b/src/main/java/buttondevteam/discordplugin/listeners/MCListener.java @@ -63,9 +63,7 @@ public class MCListener implements Listener { p.sendMessage("§bIf it wasn't you, do /discord decline"); } final String message = e.GetPlayer().PlayerName().get() + " joined the game"; - if (!DiscordPlugin.hooked) - MCChatListener.sendSystemMessageToChat(message); - MCChatListener.forAllowedCustomMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message), e.getPlayer()); + MCChatListener.forAllowedCustomAndAllMCChat(MCChatListener.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); //System.out.println("Does this appear more than once?"); //No MCChatListener.ListC = 0; ChromaBot.getInstance().updatePlayerList(); @@ -85,16 +83,14 @@ public class MCListener implements Listener { Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, ChromaBot.getInstance()::updatePlayerList, 5); final String message = e.GetPlayer().PlayerName().get() + " left the game"; - if (!DiscordPlugin.hooked) - MCChatListener.sendSystemMessageToChat(message); //TODO: Probably double sends if kicked and unhooked - MCChatListener.forAllowedCustomMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message), e.getPlayer()); + MCChatListener.forAllowedCustomAndAllMCChat(MCChatListener.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true); } @EventHandler(priority = EventPriority.HIGHEST) public void onPlayerKick(PlayerKickEvent e) { - if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting") + /*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting") && !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special - MCChatListener.sendSystemMessageToChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook + MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/ } @EventHandler @@ -113,16 +109,17 @@ public class MCListener implements Listener { @EventHandler(priority = EventPriority.LOW) public void onPlayerDeath(PlayerDeathEvent e) { - if (!DiscordPlugin.hooked) - MCChatListener.sendSystemMessageToChat(e.getDeathMessage()); + MCChatListener.forAllowedCustomAndAllMCChat(MCChatListener.send(e.getDeathMessage()), e.getEntity(), ChannelconBroadcast.DEATH, true); } @EventHandler - public void onPlayerAFK(AfkStatusChangeEvent e) { //TODO: Add AFK to custom chats? - if (e.isCancelled() || !e.getAffected().getBase().isOnline()) + public void onPlayerAFK(AfkStatusChangeEvent e) { + final Player base = e.getAffected().getBase(); + if (e.isCancelled() || !base.isOnline()) return; - MCChatListener.sendSystemMessageToChat(e.getAffected().getBase().getDisplayName() - + " is " + (e.getValue() ? "now" : "no longer") + " AFK."); + final String msg = base.getDisplayName() + + " is " + (e.getValue() ? "now" : "no longer") + " AFK."; + MCChatListener.forAllowedCustomAndAllMCChat(MCChatListener.send(msg), base, ChannelconBroadcast.AFK, false); } @EventHandler @@ -140,7 +137,7 @@ public class MCListener implements Listener { return; final IUser user = DiscordPlugin.dc.getUserByID( Long.parseLong(TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class) - .getAs(DiscordPlayer.class).getDiscordID())); // TODO: Use long + .getAs(DiscordPlayer.class).getDiscordID())); if (e.getValue()) user.addRole(role); else @@ -154,18 +151,21 @@ public class MCListener implements Listener { @EventHandler public void onChatSystemMessage(TBMCSystemChatEvent event) { - MCChatListener.sendSystemMessageToChat(event); + MCChatListener.forAllowedMCChat(MCChatListener.send(event.getMessage()), event); } @EventHandler public void onBroadcastMessage(BroadcastMessageEvent event) { - MCChatListener.sendSystemMessageToChat(event.getMessage()); + MCChatListener.forCustomAndAllMCChat(MCChatListener.send(event.getMessage()), ChannelconBroadcast.BROADCAST, false); } - /*@EventHandler - public void onYEEHAW(TBMCYEEHAWEvent event) { - MCChatListener.forAllowedCustomMCChat();event.getSender().getName()+" <:YEEHAW:"+DiscordPlugin.mainServer.getEmojiByName("YEEHAW").getStringID()+">s"//TODO: :YEEHAW:s - Change from broadcastMessage() in ButtonChat - }*/ + @EventHandler + public void onYEEHAW(TBMCYEEHAWEvent event) { //TODO: Inherit from the chat event base to have channel support + String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName() + : event.getSender().getName(); + //Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO + MCChatListener.forAllMCChat(MCChatListener.send(name + " <:YEEHAW:" + DiscordPlugin.mainServer.getEmojiByName("YEEHAW").getStringID() + ">s")); + } private static final String[] EXCLUDED_PLUGINS = {"ProtocolLib", "LibsDisguises"}; diff --git a/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java b/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java index fce8d04..1f4855f 100644 --- a/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java +++ b/src/main/java/buttondevteam/discordplugin/mccommands/ResetMCCommand.java @@ -1,6 +1,7 @@ package buttondevteam.discordplugin.mccommands; import buttondevteam.discordplugin.DiscordPlugin; +import buttondevteam.discordplugin.DiscordSenderBase; import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.TBMCCommandBase; import org.bukkit.Bukkit; @@ -15,9 +16,11 @@ public class ResetMCCommand extends TBMCCommandBase { //Not player-only, so not resetting = true; //Turned off after sending enable message (ReadyEvent) sender.sendMessage("§bDisabling DiscordPlugin..."); Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin); - sender.sendMessage("§bEnabling DiscordPlugin..."); + if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors + sender.sendMessage("§bEnabling DiscordPlugin..."); Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin); - sender.sendMessage("§bReset finished!"); + if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors + sender.sendMessage("§bReset finished!"); }); return true; } diff --git a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java index a97f715..365f968 100755 --- a/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java +++ b/src/main/java/buttondevteam/discordplugin/playerfaker/DiscordInventory.java @@ -46,13 +46,13 @@ public class DiscordInventory implements Inventory { @Override public HashMap addItem(ItemStack... items) throws IllegalArgumentException { // Can't add anything return new HashMap<>( - IntStream.range(0, items.length).mapToObj(i -> i).collect(Collectors.toMap(i -> i, i -> items[i]))); + IntStream.range(0, items.length).boxed().collect(Collectors.toMap(i -> i, i -> items[i]))); } @Override public HashMap removeItem(ItemStack... items) throws IllegalArgumentException { return new HashMap<>( - IntStream.range(0, items.length).mapToObj(i -> i).collect(Collectors.toMap(i -> i, i -> items[i]))); + IntStream.range(0, items.length).boxed().collect(Collectors.toMap(i -> i, i -> items[i]))); } @Override