forked from pajlads/DinkPlugin
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMetaNotifier.java
More file actions
343 lines (296 loc) · 13.4 KB
/
MetaNotifier.java
File metadata and controls
343 lines (296 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package dinkplugin.notifiers;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import dinkplugin.domain.AchievementDiary;
import dinkplugin.message.NotificationBody;
import dinkplugin.message.NotificationType;
import dinkplugin.message.templating.Replacements;
import dinkplugin.message.templating.Template;
import dinkplugin.notifiers.data.AmascutPurpleNotificationData;
import dinkplugin.notifiers.data.GroupBankContentsNotificationData;
import dinkplugin.notifiers.data.LoginNotificationData;
import dinkplugin.notifiers.data.Progress;
import dinkplugin.notifiers.data.SerializedItemStack;
import dinkplugin.util.ConfigUtil;
import dinkplugin.util.ItemUtils;
import dinkplugin.util.SerializedPet;
import dinkplugin.util.Utils;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Experience;
import net.runelite.api.GameState;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.api.Skill;
import net.runelite.api.WallObject;
import net.runelite.api.annotations.Varbit;
import net.runelite.api.events.VarbitChanged;
import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.events.WallObjectSpawned;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.api.gameval.VarbitID;
import net.runelite.api.gameval.ObjectID;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.chatcommands.ChatCommandsPlugin;
import net.runelite.client.util.QuantityFormatter;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.VisibleForTesting;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Singleton
public class MetaNotifier extends BaseNotifier {
static final @VisibleForTesting String RL_CHAT_CMD_PLUGIN_NAME = ChatCommandsPlugin.class.getSimpleName().toLowerCase();
static final @VisibleForTesting int INIT_TICKS = 10; // 6 seconds after login
private static final int SARCOPHAGUS_WALL_ID = ObjectID.TOA_VAULT_BARRIER_PARENT;
private boolean isToaPurple = false;
private static final int[] TOA_CHEST_VARBS;
private final AtomicInteger loginTicks = new AtomicInteger(-1);
private String cachedPlayerName;
@Inject
private ClientThread clientThread;
@Inject
private ConfigManager configManager;
@Inject
private ItemManager itemManager;
@Inject
private Gson gson;
@Override
public boolean isEnabled() {
return StringUtils.isNotBlank(config.metadataWebhook()) && super.isEnabled();
}
@Override
protected String getWebhookUrl() {
return config.metadataWebhook();
}
public void onGameState(GameState oldState, GameState newState) {
// inspect oldState because we don't want a notification on each world hop
if (oldState == GameState.LOGGING_IN && newState == GameState.LOGGED_IN) {
loginTicks.set(INIT_TICKS);
}
// check if the oldState is any that can be considered "in game", and if the new state is "LOGIN_SCREEN"
if ((oldState == GameState.LOGGED_IN || oldState == GameState.CONNECTION_LOST || oldState == GameState.HOPPING)
&& newState == GameState.LOGIN_SCREEN && isEnabled()) {
notifyLogout();
}
}
public void onWallObjectSpawned(WallObjectSpawned event) {
final WallObject wallObject = event.getWallObject();
if (wallObject.getId() != SARCOPHAGUS_WALL_ID) {
return;
}
if (isEnabled()) {
clientThread.invokeAtTickEnd(this::notifyPurpleAmascut);
}
}
public void onTick() {
if (cachedPlayerName == null) {
cachedPlayerName = Utils.getPlayerName(client);
}
if (loginTicks.getAndUpdate(i -> Math.max(-1, i - 1)) == 0 && isEnabled()) {
clientThread.invokeLater(this::notifyLogin); // just 20ms later to be able to run client scripts cleanly
}
}
public void reset() {
cachedPlayerName = null;
}
public void onVarbit(VarbitChanged event) {
if (event.getVarbitId() == VarbitID.TOA_VAULT_SARCOPHAGUS) {
isToaPurple = event.getValue() % 2 == 1;
}
}
public void onWidget(WidgetLoaded event) {
if (event.getGroupId() == InterfaceID.SHARED_BANK && isEnabled()) {
clientThread.invokeLater(this::notifyGroupStorage);
}
}
private void notifyGroupStorage() {
ItemContainer bank = client.getItemContainer(InventoryID.INV_GROUP_TEMP);
if (bank == null) return;
Item[] array = bank.getItems();
List<SerializedItemStack> items = new ArrayList<>(array.length);
long totalValue = 0;
for (Item i : array) {
if (i == null || i.getId() < 0) continue;
int itemId = ItemUtils.canonicalizeItem(itemManager, i.getId());
SerializedItemStack item = ItemUtils.stackFromItem(itemManager, itemId, i.getQuantity());
items.add(item);
totalValue += item.getTotalPrice();
}
// Fire notification
int slots = client.getVarpValue(VarPlayerID.IF3);
String playerName = Utils.getPlayerName(client);
Template message = Template.builder()
.replacementBoundary("%")
.template("%USERNAME% opened the GIM shared bank containing %ITEM_COUNT% items worth %TOTAL_VALUE% with %SLOT_COUNT% slots unlocked")
.replacement("%USERNAME%", Replacements.ofText(playerName))
.replacement("%ITEM_COUNT%", Replacements.ofText(String.valueOf(items.size())))
.replacement("%TOTAL_VALUE%", Replacements.ofText(QuantityFormatter.quantityToStackSize(totalValue)))
.replacement("%SLOT_COUNT%", Replacements.ofText(String.valueOf(slots)))
.build();
var extra = new GroupBankContentsNotificationData(items, slots);
createMessage(false, NotificationBody.builder()
.type(NotificationType.GROUP_BANK_CONTENTS)
.text(message)
.extra(extra)
.playerName(playerName)
.build()
);
}
private void notifyPurpleAmascut() {
if (isToaPurple) {
isToaPurple = false; // prevent multiple notifications
} else {
return; // not notification worthy
}
// inspect multiloc to ensure local player is the recipient of the purple drop (s/o @rdutta)
for (@Varbit int varbitId : TOA_CHEST_VARBS) {
if (client.getVarbitValue(varbitId) == 2) {
// someone else in the party received the purple drop
return;
}
}
// Gather relevant data
var party = Utils.getAmascutTombsParty(client);
int rewardPoints = client.getVarbitValue(VarbitID.RAIDS_CLIENT_PARTYSCORE);
int raidLevels = client.getVarbitValue(VarbitID.TOA_CLIENT_RAID_LEVEL);
// Calculate probability based on https://oldschool.runescape.wiki/w/Chest_(Tombs_of_Amascut)#Uniques
int x = Math.min(raidLevels, 400);
int y = Math.max(Math.min(raidLevels, 550) - 400, 0);
int partySize = Math.max(party.size(), 1);
double probability = Math.min(0.01 * rewardPoints / (10_500 - 20 * (x + y / 3.0)), 0.55) / partySize;
// Fire notification
String playerName = Utils.getPlayerName(client);
Template message = Template.builder()
.replacementBoundary("%")
.template("%USERNAME% rolled a purple (unique) drop from Tombs of Amascut!")
.replacement("%USERNAME%", Replacements.ofText(playerName))
.build();
var extra = new AmascutPurpleNotificationData(party, rewardPoints, raidLevels, probability);
createMessage(false, NotificationBody.builder()
.type(NotificationType.TOA_UNIQUE)
.text(message)
.extra(extra)
.playerName(playerName)
.build()
);
}
private void notifyLogin() {
// Gather data points
int world = client.getWorld();
int collectionCompleted = client.getVarpValue(VarPlayerID.COLLECTION_COUNT);
int collectionTotal = client.getVarpValue(VarPlayerID.COLLECTION_COUNT_MAX);
int combatAchievementPoints = client.getVarbitValue(VarbitID.CA_POINTS);
int combatAchievementPointsTotal = client.getVarbitValue(VarbitID.CA_THRESHOLD_GRANDMASTER);
int diaryCompleted = AchievementDiary.DIARIES.keySet()
.stream()
.mapToInt(id -> DiaryNotifier.isComplete(id, client.getVarbitValue(id)) ? 1 : 0)
.sum();
int diaryTotal = AchievementDiary.DIARIES.size();
client.runScript(DiaryNotifier.COMPLETED_TASKS_SCRIPT_ID);
int diaryTaskCompleted = client.getIntStack()[0];
client.runScript(DiaryNotifier.TOTAL_TASKS_SCRIPT_ID);
int diaryTaskTotal = client.getIntStack()[0];
int gambleCount = client.getVarbitValue(VarbitID.BARBASSAULT_GAMBLECOUNT);
long experienceTotal = client.getOverallExperience();
int levelTotal = client.getTotalLevel();
Map<String, Integer> skillLevels = new HashMap<>(32);
Map<String, Integer> skillExperience = new HashMap<>(32);
for (Skill skill : Skill.values()) {
int xp = client.getSkillExperience(skill);
int lvl = client.getRealSkillLevel(skill);
int virtualLevel = lvl < 99 ? lvl : Experience.getLevelForXp(xp);
skillExperience.put(skill.getName(), xp);
skillLevels.put(skill.getName(), virtualLevel);
}
int questsCompleted = client.getVarbitValue(VarbitID.QUESTS_COMPLETED_COUNT);
int questsTotal = client.getVarbitValue(VarbitID.QUESTS_TOTAL_COUNT);
int questPoints = client.getVarpValue(VarPlayerID.QP);
int questPointsTotal = client.getVarbitValue(VarbitID.QP_MAX);
int slayerPoints = client.getVarbitValue(VarbitID.SLAYER_POINTS);
int slayerStreak = client.getVarbitValue(VarbitID.SLAYER_TASKS_COMPLETED);
// Fire notification
String playerName = Utils.getPlayerName(client);
cachedPlayerName = playerName;
Template message = Template.builder()
.replacementBoundary("%")
.template("%USERNAME% logged into World %WORLD%")
.replacement("%USERNAME%", Replacements.ofText(playerName))
.replacement("%WORLD%", Replacements.ofText(String.valueOf(world)))
.build();
LoginNotificationData extra = new LoginNotificationData(
world,
Progress.of(collectionCompleted, collectionTotal),
Progress.of(combatAchievementPoints, combatAchievementPointsTotal),
Progress.of(diaryCompleted, diaryTotal),
Progress.of(diaryTaskCompleted, diaryTaskTotal),
new LoginNotificationData.BarbarianAssault(gambleCount),
new LoginNotificationData.SkillData(experienceTotal, levelTotal, skillLevels, skillExperience),
Progress.of(questsCompleted, questsTotal),
Progress.of(questPoints, questPointsTotal),
new LoginNotificationData.SlayerData(slayerPoints, slayerStreak),
getPets()
);
createMessage(false, NotificationBody.builder()
.type(NotificationType.LOGIN)
.text(message)
.extra(extra)
.playerName(playerName)
.build()
);
}
private void notifyLogout() {
String playerName = Utils.getPlayerName(client);
// Fallback if playerName is null.
// It will use a cached player name which is set in notifyLogin.
if (playerName == null) {
playerName = cachedPlayerName;
}
Template message = Template.builder()
.replacementBoundary("%")
.template("%USERNAME% logged out")
.replacement("%USERNAME%", Replacements.ofText(playerName))
.build();
createMessage(false, NotificationBody.builder()
.type(NotificationType.LOGOUT)
.text(message)
.playerName(playerName)
.build()
);
cachedPlayerName = null;
}
@VisibleForTesting
List<SerializedPet> getPets() {
if (ConfigUtil.isPluginDisabled(configManager, RL_CHAT_CMD_PLUGIN_NAME))
return null;
String json = configManager.getRSProfileConfiguration("chatcommands", "pets2");
if (json == null || json.isEmpty())
return null;
int[] petItemIds;
try {
petItemIds = gson.fromJson(json, int[].class);
} catch (JsonSyntaxException e) {
log.info("Failed to deserialize owned pet IDs", e);
return null;
}
List<SerializedPet> pets = new ArrayList<>(petItemIds.length);
for (int itemId : petItemIds) {
pets.add(new SerializedPet(itemId, client.getItemDefinition(itemId).getMembersName()));
}
return pets;
}
static {
TOA_CHEST_VARBS = new int[] {
VarbitID.TOA_VAULT_CHEST_0, VarbitID.TOA_VAULT_CHEST_1, VarbitID.TOA_VAULT_CHEST_2, VarbitID.TOA_VAULT_CHEST_3,
VarbitID.TOA_VAULT_CHEST_4, VarbitID.TOA_VAULT_CHEST_5, VarbitID.TOA_VAULT_CHEST_6, VarbitID.TOA_VAULT_CHEST_7
};
}
}