Skip to content

Add clan rank updates highlighting #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions src/main/java/com/botdetector/BotDetectorClanHighlighter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.botdetector;

import com.botdetector.model.CaseInsensitiveString;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.clan.ClanMember;
import net.runelite.api.clan.ClanRank;
import net.runelite.api.clan.ClanSettings;
import net.runelite.api.clan.ClanTitle;
import net.runelite.api.widgets.Widget;
import net.runelite.client.util.Text;

public class BotDetectorClanHighlighter
{
private static final String CLAN_NAME = "Bot Detector";

private static final int HIGHLIGHT_COLOR = 0x00ff00;

private static final int NAME_OFFSET = 1;
private static final int SPRITE_OFFSET = 2;
private static final int WIDGETS_PER_NAME = 3;

private static final Pattern POPUP_TITLE_PLAYER_NAME_PATTERN = Pattern.compile("^Set rank for ([\\w\\-\\s]{1,12}):$");

private static final ImmutableSet<ClanRank> EXCLUDE_RANKS = ImmutableSet.of(
ClanRank.GUEST, ClanRank.OWNER, ClanRank.JMOD
);

@Inject
private Client client;

private Map<CaseInsensitiveString, ClanRank> toHighlight;

protected void startUp()
{
toHighlight = null;
}

protected void shutDown()
{
toHighlight = null;
}

/**
* Gets the left-hand side name list widgets from the clan members interface.
* @return An array of widgets, or {@code null} if the clan members interface is not currently loaded.
*/
private Widget[] getNameWidgets()
{
Widget members = client.getWidget(693, 10);
if (members == null)
{
return null;
}

Widget[] dyn = members.getDynamicChildren();
if (dyn.length % WIDGETS_PER_NAME != 0)
{
return null;
}

return dyn;
}

/**
* Gets the current ranks for the members in the clan. The caller must be in {@link #CLAN_NAME}.
* @return A map of clan member names and their current rank, or {@code null} if the caller is not currently in the correct clan.
*/
public ImmutableMap<CaseInsensitiveString, ClanRank> getClanMemberRanks()
{
ClanSettings cs = client.getClanSettings();
if (cs == null || !CLAN_NAME.equals(cs.getName()))
{
return null;
}

return cs.getMembers().stream().collect(ImmutableMap.toImmutableMap(
cm -> BotDetectorPlugin.normalizeAndWrapPlayerName(cm.getName()), ClanMember::getRank));
}

/**
* Sets {@link #toHighlight}, then calls {@link #updateHighlight()}.
* @param toHighlight The map of clan members to highlight and the rank they should be.
*/
public void setHighlight(Map<CaseInsensitiveString, ClanRank> toHighlight)
{
this.toHighlight = toHighlight;
updateHighlight();
}

/**
* Highlights the players that need their ranks changed according to {@link #toHighlight},
* assuming the clan members interface is currently loaded. If the rank changer popup is up,
* the correct rank to set will also be highlighted.
* The caller must be in {@link #CLAN_NAME}, also see {@link #setHighlight(Map)}.
*/
public void updateHighlight()
{
if (toHighlight == null)
{
return;
}

ClanSettings cs = client.getClanSettings();
if (cs == null || !CLAN_NAME.equals(cs.getName()))
{
return;
}

ImmutableMap<CaseInsensitiveString, ClanRank> currentRanks = getClanMemberRanks();
if (currentRanks == null)
{
return;
}

Widget[] nameWidgets = getNameWidgets();
if (nameWidgets == null)
{
return;
}

Map<CaseInsensitiveString, String> checkPopupNames = new HashMap<>();
for (int i = 0; i < nameWidgets.length; i += WIDGETS_PER_NAME)
{
Widget nameWidget = nameWidgets[i + NAME_OFFSET];
CaseInsensitiveString name = BotDetectorPlugin.normalizeAndWrapPlayerName(nameWidget.getText());
ClanRank newRank = toHighlight.get(name);
if (newRank == null || newRank == currentRanks.get(name) || EXCLUDE_RANKS.contains(newRank))
{
continue;
}

ClanTitle title = cs.titleForRank(newRank);
if (title == null)
{
continue;
}

nameWidget.setTextColor(HIGHLIGHT_COLOR);
checkPopupNames.put(name, title.getName());
}

// Highlight correct rank in popup if present
Widget popupTitle = client.getWidget(289, 4);
Widget popupRanks = client.getWidget(289, 6);
if (popupTitle == null || popupRanks == null)
{
return;
}

Widget[] popupRanksDyn = popupRanks.getDynamicChildren();
if (popupRanks.getDynamicChildren().length % WIDGETS_PER_NAME != 0)
{
return;
}

Matcher match = POPUP_TITLE_PLAYER_NAME_PATTERN.matcher(popupTitle.getChild(1).getText());
if (!match.matches())
{
return;
}

CaseInsensitiveString popupName = BotDetectorPlugin.normalizeAndWrapPlayerName(match.group(1));

String highlightRank = checkPopupNames.get(popupName);
if (highlightRank == null)
{
return;
}

for (int i = 0; i < popupRanksDyn.length; i += WIDGETS_PER_NAME)
{
Widget w = popupRanksDyn[i + NAME_OFFSET];
if (highlightRank.equals(Text.removeTags(w.getText())))
{
w.setTextColor(HIGHLIGHT_COLOR);
break;
}
}
}
}
71 changes: 71 additions & 0 deletions src/main/java/com/botdetector/BotDetectorPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import net.runelite.api.MessageNode;
import net.runelite.api.Player;
import net.runelite.api.WorldType;
import net.runelite.api.clan.ClanRank;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.ChatMessage;
import net.runelite.api.events.CommandExecuted;
Expand All @@ -89,6 +90,7 @@
import net.runelite.api.events.MenuOpened;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.PlayerSpawned;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.WorldChanged;
import net.runelite.api.kit.KitType;
import net.runelite.api.widgets.WidgetInfo;
Expand Down Expand Up @@ -168,6 +170,7 @@ public class BotDetectorPlugin extends Plugin
private static final String CLEAR_AUTH_TOKEN_COMMAND = COMMAND_PREFIX + "ClearToken";
private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND = COMMAND_PREFIX + "ToggleShowDiscordVerificationErrors";
private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS = COMMAND_PREFIX + "ToggleDVE";
private static final String GET_CLAN_RANK_UPDATES_COMMAND = COMMAND_PREFIX + "GetRankUpdates";

/** Command to method map to be used in {@link #onCommandExecuted(CommandExecuted)}. **/
private final ImmutableMap<CaseInsensitiveString, Consumer<String[]>> commandConsumerMap =
Expand All @@ -181,6 +184,7 @@ public class BotDetectorPlugin extends Plugin
.put(wrap(CLEAR_AUTH_TOKEN_COMMAND), s -> clearAuthTokenCommand())
.put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND), s -> toggleShowDiscordVerificationErrors())
.put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS), s -> toggleShowDiscordVerificationErrors())
.put(wrap(GET_CLAN_RANK_UPDATES_COMMAND), s -> getClanRankUpdatesCommand())
.build();

private static final int MANUAL_FLUSH_COOLDOWN_SECONDS = 60;
Expand Down Expand Up @@ -225,6 +229,9 @@ public class BotDetectorPlugin extends Plugin
@Inject
private BotDetectorClient detectorClient;

@Inject
private BotDetectorClanHighlighter clanHighlighter;

private BotDetectorPanel panel;
private NavigationButton navButton;

Expand Down Expand Up @@ -365,6 +372,8 @@ protected void startUp()

chatCommandManager.registerCommand(VERIFY_DISCORD_COMMAND, this::verifyDiscord);
chatCommandManager.registerCommand(STATS_CHAT_COMMAND, this::statsChatCommand);

clanHighlighter.startUp();
}

@Override
Expand Down Expand Up @@ -395,6 +404,8 @@ protected void shutDown()

chatCommandManager.unregisterCommand(VERIFY_DISCORD_COMMAND);
chatCommandManager.unregisterCommand(STATS_CHAT_COMMAND);

clanHighlighter.shutDown();
}

/**
Expand Down Expand Up @@ -1044,6 +1055,17 @@ private void onWorldChanged(WorldChanged event)
processCurrentWorld();
}

@Subscribe
private void onScriptPostFired(ScriptPostFired event)
{
// If clan names list updates (4253)
// or clan rank selection pops up (4316)
if (event.getScriptId() == 4253 || event.getScriptId() == 4316)
{
clanHighlighter.updateHighlight();
}
}

/**
* Opens the plugin panel and sends over {@code playerName} to {@link BotDetectorPanel#predictPlayer(String)} for prediction.
* @param playerName The player name to predict.
Expand Down Expand Up @@ -1339,6 +1361,55 @@ private void toggleShowDiscordVerificationErrors()
}
}

/**
* Gets the current clan members and their ranks, sends them over to {@link BotDetectorClient#requestClanRankUpdates(String, Map)}
* to get players that need their ranks updated and then highlights them on the clan members interface using {@link #clanHighlighter}.
*/
private void getClanRankUpdatesCommand()
{
if (!authToken.getTokenType().getPermissions().contains(AuthTokenPermission.GET_CLAN_RANK_UPDATES))
{
sendChatStatusMessage("The currently set auth token does not permit this command.", true);
return;
}

Map<CaseInsensitiveString, ClanRank> ranks = clanHighlighter.getClanMemberRanks();
int numMembers = ranks != null ? ranks.size() : 0;
if (numMembers == 0)
{
sendChatStatusMessage("Could not get members/rank list from clan settings.", true);
return;
}

sendChatStatusMessage("Checking rank updates for " + numMembers + " clan members. Please wait, this may take some time.", true);

detectorClient.requestClanRankUpdates(authToken.getToken(), ranks).whenComplete((newRanks, ex) ->
{
if (ex == null)
{
int numUpdates = newRanks != null ? newRanks.size() : 0;
if (numUpdates == 0)
{
sendChatStatusMessage("No clan ranks to update.", true);
clientThread.invokeLater(() -> clanHighlighter.setHighlight(null));
}
else
{
sendChatStatusMessage("Received " + numUpdates + " clan rank updates from the API.", true);
clientThread.invokeLater(() -> clanHighlighter.setHighlight(newRanks));
}
}
else if (ex instanceof UnauthorizedTokenException)
{
sendChatStatusMessage("Invalid token for getting clan rank updates.", true);
}
else
{
sendChatStatusMessage("Error getting clan rank updates from the API.", true);
}
});
}

//endregion


Expand Down
Loading