Skip to content

Commit 6fc3fc8

Browse files
committed
Add clan rank updates highlighting
1 parent f74269a commit 6fc3fc8

File tree

5 files changed

+383
-4
lines changed

5 files changed

+383
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright (c) 2021, Ferrariic, Seltzer Bro, Cyborger1
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice, this
9+
* list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
package com.botdetector;
27+
28+
import com.botdetector.model.CaseInsensitiveString;
29+
import com.google.common.collect.ImmutableMap;
30+
import com.google.common.collect.ImmutableSet;
31+
import java.util.HashMap;
32+
import java.util.Map;
33+
import java.util.regex.Matcher;
34+
import java.util.regex.Pattern;
35+
import javax.inject.Inject;
36+
import net.runelite.api.Client;
37+
import net.runelite.api.clan.ClanMember;
38+
import net.runelite.api.clan.ClanRank;
39+
import net.runelite.api.clan.ClanSettings;
40+
import net.runelite.api.clan.ClanTitle;
41+
import net.runelite.api.widgets.Widget;
42+
import net.runelite.client.util.Text;
43+
44+
public class BotDetectorClanHighlighter
45+
{
46+
private static final String CLAN_NAME = "Bot Detector";
47+
48+
private static final int HIGHLIGHT_COLOR = 0x00ff00;
49+
50+
private static final int NAME_OFFSET = 1;
51+
private static final int SPRITE_OFFSET = 2;
52+
private static final int WIDGETS_PER_NAME = 3;
53+
54+
private static final Pattern POPUP_TITLE_PLAYER_NAME_PATTERN = Pattern.compile("^Set rank for ([\\w\\-\\s]{1,12}):$");
55+
56+
private static final ImmutableSet<ClanRank> EXCLUDE_RANKS = ImmutableSet.of(
57+
ClanRank.GUEST, ClanRank.OWNER, ClanRank.JMOD
58+
);
59+
60+
@Inject
61+
private Client client;
62+
63+
private Map<CaseInsensitiveString, ClanRank> toHighlight;
64+
65+
protected void startUp()
66+
{
67+
toHighlight = null;
68+
}
69+
70+
protected void shutDown()
71+
{
72+
toHighlight = null;
73+
}
74+
75+
/**
76+
* Gets the left-hand side name list widgets from the clan members interface.
77+
* @return An array of widgets, or {@code null} if the clan members interface is not currently loaded.
78+
*/
79+
private Widget[] getNameWidgets()
80+
{
81+
Widget members = client.getWidget(693, 10);
82+
if (members == null)
83+
{
84+
return null;
85+
}
86+
87+
Widget[] dyn = members.getDynamicChildren();
88+
if (dyn.length % WIDGETS_PER_NAME != 0)
89+
{
90+
return null;
91+
}
92+
93+
return dyn;
94+
}
95+
96+
/**
97+
* Gets the current ranks for the members in the clan. The caller must be in {@link #CLAN_NAME}.
98+
* @return A map of clan member names and their current rank, or {@code null} if the caller is not currently in the correct clan.
99+
*/
100+
public ImmutableMap<CaseInsensitiveString, ClanRank> getClanMemberRanks()
101+
{
102+
ClanSettings cs = client.getClanSettings();
103+
if (cs == null || !CLAN_NAME.equals(cs.getName()))
104+
{
105+
return null;
106+
}
107+
108+
return cs.getMembers().stream().collect(ImmutableMap.toImmutableMap(
109+
cm -> BotDetectorPlugin.normalizeAndWrapPlayerName(cm.getName()), ClanMember::getRank));
110+
}
111+
112+
/**
113+
* Sets {@link #toHighlight}, then calls {@link #updateHighlight()}.
114+
* @param toHighlight The map of clan members to highlight and the rank they should be.
115+
*/
116+
public void setHighlight(Map<CaseInsensitiveString, ClanRank> toHighlight)
117+
{
118+
this.toHighlight = toHighlight;
119+
updateHighlight();
120+
}
121+
122+
/**
123+
* Highlights the players that need their ranks changed according to {@link #toHighlight},
124+
* assuming the clan members interface is currently loaded. If the rank changer popup is up,
125+
* the correct rank to set will also be highlighted.
126+
* The caller must be in {@link #CLAN_NAME}, also see {@link #setHighlight(Map)}.
127+
*/
128+
public void updateHighlight()
129+
{
130+
if (toHighlight == null)
131+
{
132+
return;
133+
}
134+
135+
ClanSettings cs = client.getClanSettings();
136+
if (cs == null || !CLAN_NAME.equals(cs.getName()))
137+
{
138+
return;
139+
}
140+
141+
ImmutableMap<CaseInsensitiveString, ClanRank> currentRanks = getClanMemberRanks();
142+
if (currentRanks == null)
143+
{
144+
return;
145+
}
146+
147+
Widget[] nameWidgets = getNameWidgets();
148+
if (nameWidgets == null)
149+
{
150+
return;
151+
}
152+
153+
Map<CaseInsensitiveString, String> checkPopupNames = new HashMap<>();
154+
for (int i = 0; i < nameWidgets.length; i += WIDGETS_PER_NAME)
155+
{
156+
Widget nameWidget = nameWidgets[i + NAME_OFFSET];
157+
CaseInsensitiveString name = BotDetectorPlugin.normalizeAndWrapPlayerName(nameWidget.getText());
158+
ClanRank newRank = toHighlight.get(name);
159+
if (newRank == null || newRank == currentRanks.get(name) || EXCLUDE_RANKS.contains(newRank))
160+
{
161+
continue;
162+
}
163+
164+
ClanTitle title = cs.titleForRank(newRank);
165+
if (title == null)
166+
{
167+
continue;
168+
}
169+
170+
nameWidget.setTextColor(HIGHLIGHT_COLOR);
171+
checkPopupNames.put(name, title.getName());
172+
}
173+
174+
// Highlight correct rank in popup if present
175+
Widget popupTitle = client.getWidget(289, 4);
176+
Widget popupRanks = client.getWidget(289, 6);
177+
if (popupTitle == null || popupRanks == null)
178+
{
179+
return;
180+
}
181+
182+
Widget[] popupRanksDyn = popupRanks.getDynamicChildren();
183+
if (popupRanks.getDynamicChildren().length % WIDGETS_PER_NAME != 0)
184+
{
185+
return;
186+
}
187+
188+
Matcher match = POPUP_TITLE_PLAYER_NAME_PATTERN.matcher(popupTitle.getChild(1).getText());
189+
if (!match.matches())
190+
{
191+
return;
192+
}
193+
194+
CaseInsensitiveString popupName = BotDetectorPlugin.normalizeAndWrapPlayerName(match.group(1));
195+
196+
String highlightRank = checkPopupNames.get(popupName);
197+
if (highlightRank == null)
198+
{
199+
return;
200+
}
201+
202+
for (int i = 0; i < popupRanksDyn.length; i += WIDGETS_PER_NAME)
203+
{
204+
Widget w = popupRanksDyn[i + NAME_OFFSET];
205+
if (highlightRank.equals(Text.removeTags(w.getText())))
206+
{
207+
w.setTextColor(HIGHLIGHT_COLOR);
208+
break;
209+
}
210+
}
211+
}
212+
}

src/main/java/com/botdetector/BotDetectorPlugin.java

+72
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import net.runelite.api.MenuEntry;
7575
import net.runelite.api.Player;
7676
import net.runelite.api.WorldType;
77+
import net.runelite.api.clan.ClanRank;
7778
import net.runelite.api.coords.WorldPoint;
7879
import net.runelite.api.events.ChatMessage;
7980
import net.runelite.api.events.CommandExecuted;
@@ -82,9 +83,11 @@
8283
import net.runelite.api.events.MenuOpened;
8384
import net.runelite.api.events.MenuOptionClicked;
8485
import net.runelite.api.events.PlayerSpawned;
86+
import net.runelite.api.events.ScriptPostFired;
8587
import net.runelite.api.events.WorldChanged;
8688
import net.runelite.api.kit.KitType;
8789
import net.runelite.api.widgets.WidgetInfo;
90+
import net.runelite.client.callback.ClientThread;
8891
import net.runelite.client.chat.ChatColorType;
8992
import net.runelite.client.chat.ChatCommandManager;
9093
import net.runelite.client.chat.ChatMessageBuilder;
@@ -158,6 +161,7 @@ public class BotDetectorPlugin extends Plugin
158161
private static final String CLEAR_AUTH_TOKEN_COMMAND = COMMAND_PREFIX + "ClearToken";
159162
private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND = COMMAND_PREFIX + "ToggleShowDiscordVerificationErrors";
160163
private static final String TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS = COMMAND_PREFIX + "ToggleDVE";
164+
private static final String GET_CLAN_RANK_UPDATES_COMMAND = COMMAND_PREFIX + "GetRankUpdates";
161165

162166
/** Command to method map to be used in {@link #onCommandExecuted(CommandExecuted)}. **/
163167
private final ImmutableMap<CaseInsensitiveString, Consumer<String[]>> commandConsumerMap =
@@ -171,6 +175,7 @@ public class BotDetectorPlugin extends Plugin
171175
.put(wrap(CLEAR_AUTH_TOKEN_COMMAND), s -> clearAuthTokenCommand())
172176
.put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND), s -> toggleShowDiscordVerificationErrors())
173177
.put(wrap(TOGGLE_SHOW_DISCORD_VERIFICATION_ERRORS_COMMAND_ALIAS), s -> toggleShowDiscordVerificationErrors())
178+
.put(wrap(GET_CLAN_RANK_UPDATES_COMMAND), s -> getClanRankUpdatesCommand())
174179
.build();
175180

176181
private static final int MANUAL_FLUSH_COOLDOWN_SECONDS = 60;
@@ -184,6 +189,9 @@ public class BotDetectorPlugin extends Plugin
184189
@Inject
185190
private Client client;
186191

192+
@Inject
193+
private ClientThread clientThread;
194+
187195
@Inject
188196
private MenuManager menuManager;
189197

@@ -208,6 +216,9 @@ public class BotDetectorPlugin extends Plugin
208216
@Inject
209217
private BotDetectorClient detectorClient;
210218

219+
@Inject
220+
private BotDetectorClanHighlighter clanHighlighter;
221+
211222
private BotDetectorPanel panel;
212223
private NavigationButton navButton;
213224

@@ -336,6 +347,8 @@ protected void startUp()
336347
previousTwoGameStates.offer(client.getGameState());
337348

338349
chatCommandManager.registerCommand(VERIFY_DISCORD_COMMAND, this::verifyDiscord);
350+
351+
clanHighlighter.startUp();
339352
}
340353

341354
@Override
@@ -366,6 +379,8 @@ protected void shutDown()
366379
previousTwoGameStates.clear();
367380

368381
chatCommandManager.unregisterCommand(VERIFY_DISCORD_COMMAND);
382+
383+
clanHighlighter.shutDown();
369384
}
370385

371386
/**
@@ -881,6 +896,17 @@ private void onWorldChanged(WorldChanged event)
881896
processCurrentWorld();
882897
}
883898

899+
@Subscribe
900+
private void onScriptPostFired(ScriptPostFired event)
901+
{
902+
// If clan names list updates (4253)
903+
// or clan rank selection pops up (4316)
904+
if (event.getScriptId() == 4253 || event.getScriptId() == 4316)
905+
{
906+
clanHighlighter.updateHighlight();
907+
}
908+
}
909+
884910
/**
885911
* Opens the plugin panel and sends over {@code playerName} to {@link BotDetectorPanel#predictPlayer(String)} for prediction.
886912
* @param playerName The player name to predict.
@@ -1182,6 +1208,52 @@ private void toggleShowDiscordVerificationErrors()
11821208
}
11831209
}
11841210

1211+
/**
1212+
* Gets the current clan members and their ranks, sends them over to {@link BotDetectorClient#requestClanRankUpdates(String, Map)}
1213+
* to get players that need their ranks updated and then highlights them on the clan members interface using {@link #clanHighlighter}.
1214+
*/
1215+
private void getClanRankUpdatesCommand()
1216+
{
1217+
if (!authToken.getTokenType().getPermissions().contains(AuthTokenPermission.GET_CLAN_RANK_UPDATES))
1218+
{
1219+
sendChatStatusMessage("The currently set auth token does not permit this command.", true);
1220+
return;
1221+
}
1222+
1223+
Map<CaseInsensitiveString, ClanRank> ranks = clanHighlighter.getClanMemberRanks();
1224+
if (ranks == null || ranks.size() == 0)
1225+
{
1226+
sendChatStatusMessage("Could not get members/rank list from clan settings.", true);
1227+
return;
1228+
}
1229+
1230+
detectorClient.requestClanRankUpdates(authToken.getToken(), ranks).whenComplete((newRanks, ex) ->
1231+
{
1232+
if (ex == null)
1233+
{
1234+
int size = newRanks != null ? newRanks.size() : 0;
1235+
if (size == 0)
1236+
{
1237+
sendChatStatusMessage("No clan ranks to update.", true);
1238+
clientThread.invokeLater(() -> clanHighlighter.setHighlight(null));
1239+
}
1240+
else
1241+
{
1242+
sendChatStatusMessage("Received " + size + " clan rank updates from the API.", true);
1243+
clientThread.invokeLater(() -> clanHighlighter.setHighlight(newRanks));
1244+
}
1245+
}
1246+
else if (ex instanceof UnauthorizedTokenException)
1247+
{
1248+
sendChatStatusMessage("Invalid token for getting clan rank updates.", true);
1249+
}
1250+
else
1251+
{
1252+
sendChatStatusMessage("Error getting clan rank updates from the API.", true);
1253+
}
1254+
});
1255+
}
1256+
11851257
//endregion
11861258

11871259

0 commit comments

Comments
 (0)