First commit of group-ironmen-tracker-master

This commit is contained in:
2025-10-27 08:28:05 +08:00
parent a8467389ef
commit b80f09a4ea
32 changed files with 2249 additions and 2 deletions

22
.gitignore vendored
View File

@@ -2,5 +2,25 @@ group-ironmen-master/.github
group-ironmen-master/backup
group-ironmen-master/.gitignore
group-ironmen-master/.ignore
group-ironmen-tracker-master/*
group-ironmen-master/.#*
group-ironmen-master/.env
group-ironmen-tracker-master/gradle
group-ironmen-tracker-master/.gradle
group-ironmen-tracker-master/build
group-ironmen-tracker-master/.idea/
group-ironmen-tracker-master/.project
group-ironmen-tracker-master/.settings/
group-ironmen-tracker-master/.classpath
group-ironmen-tracker-master/nbactions.xml
group-ironmen-tracker-master/nb-configuration.xml
group-ironmen-tracker-master/nbproject/
group-ironmen-tracker-master/.#*
group-ironmen-tracker-master/bin/
group-ironmen-tracker-master/spotless/
tasks-tracker-plugin-master/*

12
group-ironmen-tracker-master/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.gradle
build
.idea/
.project
.settings/
.classpath
nbactions.xml
nb-configuration.xml
nbproject/
.#*
bin/
spotless/

View File

@@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2022, Christopher Brown
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.

View File

@@ -0,0 +1,23 @@
# Group Ironmen Tracker Plugin
Website: [groupiron.men](https://groupiron.men)
Demo: [grouiron.men/demo](https://groupiron.men/demo)
Source for frontend and server: [https://github.com/christoabrown/group-ironmen](https://github.com/christoabrown/group-ironmen)
This plugin tracks information about your group ironman player and sends it to a server where you and your other group members can view it. Currently it tracks:
* Inventory, equipment, bank, rune pouch, seed vault, and shared bank
* Skill XP
* World position, viewable in an interactive map
* HP, prayer, energy, and world
* Quest completion status
* Health and position of npcs the player is interacting with
* Achievement Diaries
* Collection Log
![](https://i.imgur.com/1Mdz8RU.png)
![](https://i.imgur.com/Rs0ruRE.png)
## Setup
Each group member that you want to track will need to install the plugin. Just one person will go to the site and create a group, the credentials it provides is shared with your group.
![](https://i.imgur.com/Dyi8LXL.png)

View File

@@ -0,0 +1,32 @@
plugins {
id 'java'
}
repositories {
mavenLocal()
maven {
url = 'https://repo.runelite.net'
}
mavenCentral()
}
def runeLiteVersion = 'latest.release'
dependencies {
compileOnly group: 'net.runelite', name:'client', version: runeLiteVersion
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
testImplementation 'junit:junit:4.12'
testImplementation group: 'net.runelite', name:'client', version: runeLiteVersion
testImplementation group: 'net.runelite', name:'jshell', version: runeLiteVersion
}
group = 'men.groupiron'
version = '1.5.3'
sourceCompatibility = '1.8'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}

234
group-ironmen-tracker-master/gradlew vendored Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,6 @@
displayName=Group Ironmen Tracker
author=Christopher Brown
support=
description=Tracks information (skills, inventory, bank, etc.) about a group ironman player and sends it to a website for other group members to view
tags=group,ironman,tracker
plugins=men.groupiron.GroupIronmenTrackerPlugin

View File

@@ -0,0 +1 @@
rootProject.name = 'GroupIronmenTracker'

View File

@@ -0,0 +1,78 @@
package men.groupiron;
import net.runelite.api.Client;
public class AchievementDiaryState implements ConsumableState {
private final transient String playerName;
private static final int[] diaryVarbits = new int[]{
/* Karamja Easy */
3566, 3567, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575,
/* Karamja Medium */
3579, 3580, 3581, 3582, 3583, 3584, 3596, 3586, 3587, 3588, 3589, 3590, 3591, 3592, 3593, 3594, 3595, 3597, 3585,
/* Karamja Hard */
3600, 3601, 3602, 3603, 3604, 3605, 3606, 3607, 3608, 3609
};
private static final int[] diaryVarps = new int[]{
/* Ardougne */
1196, 1197,
/* Desert */
1198, 1199,
/* Falador */
1186, 1187,
/* Fremennik */
1184, 1185,
/* Kandarin */
1178, 1179,
/* Karamja Elite */
1200,
/* Kourend & Kebos */
2085, 2086,
/* Lumbridge & Draynor */
1194, 1195,
/* Morytania */
1180, 1181,
/* Varrock */
1176, 1177,
/* Western Provinces */
1182, 1183,
/* Wilderness */
1192, 1193
};
private final int[] diaryVarValues = new int[diaryVarbits.length + diaryVarps.length];
public AchievementDiaryState(String playerName, Client client) {
this.playerName = playerName;
for (int i = 0; i < diaryVarps.length; ++i) {
diaryVarValues[i] = client.getVarpValue(diaryVarps[i]);
}
for (int i = 0; i < diaryVarbits.length; ++i) {
diaryVarValues[i + diaryVarps.length] = client.getVarbitValue(diaryVarbits[i]);
}
}
@Override
public Object get() {
return diaryVarValues;
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof AchievementDiaryState)) return false;
AchievementDiaryState other = (AchievementDiaryState) o;
for (int i = 0; i < diaryVarValues.length; ++i) {
if (diaryVarValues[i] != other.diaryVarValues[i]) return false;
}
return true;
}
}

View File

@@ -0,0 +1,174 @@
package men.groupiron;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.EnumComposition;
import net.runelite.api.ItemComposition;
import net.runelite.api.StructComposition;
import net.runelite.api.widgets.Widget;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.client.game.ItemManager;
import net.runelite.client.util.Text;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Singleton
public class CollectionLogManager {
@Inject
Client client;
@Inject
ItemManager itemManager;
private final Map<String, DataState> collections = new HashMap<>();
private String playerName;
private Set<String> consumedNewItems = null;
private Set<String> newItems = null;
private static final int collectionLogTabVarbit = 6905;
private static final int collectionLogPageVarbit = 6906;
static final Pattern COLLECTION_LOG_COUNT_PATTERN = Pattern.compile(".+:(.+)");
static Map<String, Set<Integer>> pageItems;
static Map<Integer, Map<Integer, String>> pageNameLookup;
private static final List<Integer> COLLECTION_LOG_TAB_STRUCT_IDS = ImmutableList.of(
471, // Bosses
472, // Raids
473, // Clues
474, // Minigames
475 // Other
);
private static final int COLLECTION_LOG_PAGE_NAME_PARAM_ID = 689;
private static final int COLLECTION_LOG_TAB_ENUM_PARAM_ID = 683;
private static final int COLLECTION_LOG_PAGE_ITEMS_ENUM_PARAM_ID = 690;
public void initCollectionLog()
{
// NOTE: varbit 6905 gives us the selected collection log tab index and 6906 is the selected page index.
// In here we build a lookup map which will give us the page name with the tab index and the page index.
// This should be better than pulling the page name from the widget since that value can be changed by
// other runelite plugins.
// We also create a lookup of the item ids to the page name which should be better than using the item id
// in the collection log window as these will match the ids in the container state change.
pageItems = new HashMap<>();
pageNameLookup = new HashMap<>();
int tabIdx = 0;
for (Integer structId : COLLECTION_LOG_TAB_STRUCT_IDS) {
StructComposition tabStruct = client.getStructComposition(structId);
int tabEnumId = tabStruct.getIntValue(COLLECTION_LOG_TAB_ENUM_PARAM_ID);
EnumComposition tabEnum = client.getEnum(tabEnumId);
Map<Integer, String> pageIdToName = pageNameLookup.computeIfAbsent(tabIdx, k -> new HashMap<>());
int pageIdx = 0;
for (Integer pageStructId : tabEnum.getIntVals()) {
StructComposition pageStruct = client.getStructComposition(pageStructId);
String pageName = pageStruct.getStringValue(COLLECTION_LOG_PAGE_NAME_PARAM_ID);
int pageItemsEnumId = pageStruct.getIntValue(COLLECTION_LOG_PAGE_ITEMS_ENUM_PARAM_ID);
EnumComposition pageItemsEnum = client.getEnum(pageItemsEnumId);
pageIdToName.put(pageIdx, pageName);
Set<Integer> items = pageItems.computeIfAbsent(pageName, k -> new HashSet<>());
for (Integer pageItemId : pageItemsEnum.getIntVals()) {
ItemComposition itemComposition = itemManager.getItemComposition(pageItemId);
items.add(itemComposition.getId());
}
++pageIdx;
}
++tabIdx;
}
}
public void updateCollection(ItemContainerState containerState) {
Widget collectionLogHeader = client.getWidget(InterfaceID.Collection.HEADER_TEXT);
if (collectionLogHeader == null || collectionLogHeader.isHidden()) return;
Widget[] collectionLogHeaderChildren = collectionLogHeader.getChildren();
if (collectionLogHeaderChildren == null || collectionLogHeaderChildren.length == 0) return;
// Get the completion count information from the lines in the collection log header
List<Integer> completionCounts = new ArrayList<>();
for (int i = 2; i < collectionLogHeaderChildren.length; ++i) {
String text = Text.removeTags(collectionLogHeaderChildren[i].getText());
Matcher matcher = COLLECTION_LOG_COUNT_PATTERN.matcher(text);
if (matcher.find()) {
try {
Integer count = Integer.valueOf(matcher.group(1).trim());
completionCounts.add(count);
} catch(Exception ignored) {}
}
}
int tabIdx = client.getVarbitValue(collectionLogTabVarbit);
int pageIdx = client.getVarbitValue(collectionLogPageVarbit);
String pageName = getPageName(tabIdx, pageIdx);
if (!StringUtils.isBlank(pageName)) {
// Sending the tab index just in case the page name is not unique across them
DataState pageDataState = collections.computeIfAbsent(pageName + tabIdx, k -> new DataState());
pageDataState.update(new CollectionPageState(tabIdx, pageName, containerState, completionCounts));
}
}
private String getPageName(int tabIdx, int pageIdx) {
Map<Integer, String> x = pageNameLookup.get(tabIdx);
if (x != null) return x.get(pageIdx);
return null;
}
public synchronized void updateNewItem(String item) {
String playerName = client.getLocalPlayer().getName();
if (playerName != null) {
if (!playerName.equals(this.playerName) || newItems == null) {
this.playerName = playerName;
newItems = new HashSet<>();
}
newItems.add(item.trim());
}
}
public synchronized void consumeNewItems(Map<String, Object> output) {
if (newItems != null && output.get("name").equals(this.playerName)) {
output.put("collection_log_new", newItems);
}
consumedNewItems = newItems;
newItems = null;
}
public void consumeCollections(Map<String, Object> output) {
if (collections.isEmpty()) return;
List<Object> collectionLogOutput = new ArrayList<>();
String whoIsUpdating = (String) output.get("name");
for (DataState pageDataState : collections.values()) {
Object result = pageDataState.consumeState(whoIsUpdating);
if (result != null) {
collectionLogOutput.add(result);
}
}
// log.info("collectionLogOutput={}", collectionLogOutput);
if (!collectionLogOutput.isEmpty()) {
output.put("collection_log", collectionLogOutput);
}
}
public void restoreCollections() {
for (DataState pageDataState : collections.values()) {
pageDataState.restoreState();
}
}
public synchronized void restoreNewCollections() {
if (consumedNewItems == null) return;
for (String item : consumedNewItems) {
updateNewItem(item);
}
consumedNewItems = null;
}
}

View File

@@ -0,0 +1,42 @@
package men.groupiron;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.GameState;
import net.runelite.api.events.GameStateChanged;
import net.runelite.client.eventbus.Subscribe;
@Slf4j
@Singleton
public class CollectionLogV2Manager {
// item id -> quantity
private final Map<Integer, Integer> clogItems = new HashMap<>();
public synchronized void storeClogItem(int itemId, int quantity) {
if (quantity <= 0) return;
clogItems.put(itemId, quantity);
}
public synchronized void consumeClogItems(Map<String, Object> updates) {
if (clogItems.isEmpty()) return;
updates.put("collection_log_v2", new HashMap<>(clogItems));
}
public synchronized void clearClogItems() {
clogItems.clear();
}
@Subscribe
public synchronized void onGameStateChanged(GameStateChanged ev) {
if (ev.getGameState() != GameState.LOGGED_IN) {
clogItems.clear();
}
}
public synchronized Map<Integer, Integer> snapshotItems() {
return Collections.unmodifiableMap(new HashMap<>(clogItems));
}
}

View File

@@ -0,0 +1,95 @@
package men.groupiron;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.GameState;
import net.runelite.api.MenuAction;
import net.runelite.api.events.GameStateChanged;
import net.runelite.api.events.GameTick;
import net.runelite.api.events.ScriptPostFired;
import net.runelite.api.events.ScriptPreFired;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.api.gameval.VarbitID;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.eventbus.Subscribe;
@Slf4j
@Singleton
public class CollectionLogWidgetSubscriber {
@Inject
private EventBus eventBus;
@Inject
private Client client;
@Inject
private CollectionLogV2Manager collectionLogV2Manager;
private boolean searchTriggered = false;
private int searchTriggeredTick = -1;
public void startUp() {
eventBus.register(this);
}
public void shutDown() {
eventBus.unregister(this);
}
@Subscribe
public void onGameStateChanged(GameStateChanged e) {
GameState s = e.getGameState();
if (s != GameState.HOPPING && s != GameState.LOGGED_IN) {
searchTriggered = false;
searchTriggeredTick = -1;
}
}
@Subscribe
public void onGameTick(GameTick tick) {
if (searchTriggeredTick != -1) {
int currentTick = client.getTickCount();
if (currentTick - searchTriggeredTick >= 500) {
searchTriggered = false;
searchTriggeredTick = -1;
}
}
}
@Subscribe
public void onScriptPreFired(ScriptPreFired pre) {
// Script 4100 fires when collection log items are enumerated via search
if (pre.getScriptId() == 4100) {
// Arguments: [widgetId, itemId, qty]
Object[] args = pre.getScriptEvent().getArguments();
if (args != null && args.length >= 3) {
try {
int itemId = (int) args[1];
int quantity = (int) args[2];
collectionLogV2Manager.storeClogItem(itemId, quantity);
} catch (Exception ignored) {
//
}
}
}
}
@Subscribe
public void onScriptPostFired(ScriptPostFired post) {
final int COLLECTION_LOG_SETUP = 7797;
if (post.getScriptId() == COLLECTION_LOG_SETUP) {
if (searchTriggered) return;
boolean isAdventureLog = client.getVarbitValue(VarbitID.COLLECTION_POH_HOST_BOOK_OPEN) == 1;
if (isAdventureLog) return;
searchTriggered = true;
searchTriggeredTick = client.getTickCount();
client.menuAction(-1, InterfaceID.Collection.SEARCH_TOGGLE, MenuAction.CC_OP, 1, -1, "Search", null);
final int COLLECTION_INIT = 2240;
client.runScript(COLLECTION_INIT);
}
}
}

View File

@@ -0,0 +1,48 @@
package men.groupiron;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CollectionPageState implements ConsumableState {
private final ItemContainerState items;
private final String pageName;
private final int tabIdx;
private final List<Integer> completionCounts;
public CollectionPageState(int tabIdx, String pageName, ItemContainerState items, List<Integer> completionCounts) {
this.tabIdx = tabIdx;
this.pageName = pageName;
this.items = items;
this.completionCounts = completionCounts;
}
@Override
public Object get() {
Map<String, Object> result = new HashMap<>();
result.put("tab", tabIdx);
result.put("page_name", pageName);
result.put("items", items.get());
result.put("completion_counts", completionCounts);
return result;
}
@Override
public String whoOwnsThis() { return items.whoOwnsThis(); }
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof CollectionPageState)) return false;
CollectionPageState other = (CollectionPageState) o;
boolean completionCountsEqual = completionCounts.size() == other.completionCounts.size();
if (completionCountsEqual) {
for (int i = 0; i < completionCounts.size(); ++i) {
completionCountsEqual = completionCounts.get(i).equals(other.completionCounts.get(i));
if (!completionCountsEqual) break;
}
}
return (completionCountsEqual && tabIdx == other.tabIdx && pageName.equals(other.pageName) && items.equals(other.items));
}
}

View File

@@ -0,0 +1,7 @@
package men.groupiron;
public interface ConsumableState {
Object get();
String whoOwnsThis();
}

View File

@@ -0,0 +1,199 @@
package men.groupiron;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.WorldType;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Singleton
public class DataManager {
@Inject
Client client;
@Inject
GroupIronmenTrackerConfig config;
@Inject
private CollectionLogManager collectionLogManager;
@Inject
private CollectionLogV2Manager collectionLogV2Manager;
@Inject
private HttpRequestService httpRequestService;
private boolean isMemberInGroup = false;
private int skipNextNAttempts = 0;
@Getter
private final DataState inventory = new DataState("inventory", false);
@Getter
private final DataState bank = new DataState("bank", false);
@Getter
private final DataState equipment = new DataState("equipment", false);
@Getter
private final DataState sharedBank = new DataState("shared_bank", true);
@Getter
private final DataState resources = new DataState("stats", false);
@Getter
private final DataState skills = new DataState("skills", false);
@Getter
private final DataState quests = new DataState("quests", false);
@Getter
private final DataState position = new DataState("coordinates", false);
@Getter
private final DataState runePouch = new DataState("rune_pouch", false);
@Getter
private final DataState quiver = new DataState("quiver", false);
@Getter
private final DataState interacting = new DataState("interacting", false);
@Getter
private final DataState seedVault = new DataState("seed_vault", false);
@Getter
private final DataState achievementDiary = new DataState("diary_vars", false);
@Getter
private final DepositedItems deposited = new DepositedItems();
public void submitToApi() {
if (client.getLocalPlayer() == null || client.getLocalPlayer().getName() == null || isBadWorldType()) return;
if (skipNextNAttempts-- > 0) return;
String playerName = client.getLocalPlayer().getName();
String groupToken = config.authorizationToken().trim();
if (groupToken.length() > 0) {
// NOTE: We do this check so characters who are not authorized won't waste time serializing and sending
// their data. It is OK if the user switches characters or is removed from the group since the update call
// below will return a 401 where we set isMemberOfGroup = false again.
if (!isMemberInGroup) {
boolean isMember = checkIfPlayerIsInGroup(groupToken, playerName);
if (!isMember) {
// NOTE: We don't really need to check this everytime I don't think.
// Waiting for a game state event is not what we really want either
// since membership can change at anytime from the website.
skipNextNAttempts = 10;
return;
}
isMemberInGroup = true;
}
String url = getUpdateGroupMemberUrl();
if (url == null) return;
Map<String, Object> updates = new HashMap<>();
updates.put("name", playerName);
inventory.consumeState(updates);
bank.consumeState(updates);
equipment.consumeState(updates);
sharedBank.consumeState(updates);
resources.consumeState(updates);
skills.consumeState(updates);
quests.consumeState(updates);
position.consumeState(updates);
runePouch.consumeState(updates);
quiver.consumeState(updates);
interacting.consumeState(updates);
deposited.consumeState(updates);
seedVault.consumeState(updates);
achievementDiary.consumeState(updates);
collectionLogManager.consumeCollections(updates);
collectionLogManager.consumeNewItems(updates);
collectionLogV2Manager.consumeClogItems(updates);
if (updates.size() > 1) {
HttpRequestService.HttpResponse response = httpRequestService.post(url, groupToken, updates);
if (!response.isSuccessful()) {
skipNextNAttempts = 10;
if (response.getCode() == 401) {
isMemberInGroup = false;
}
restoreStateIfNothingUpdated();
} else {
collectionLogV2Manager.clearClogItems();
}
} else {
log.debug("Skip POST: no changes to send (fields={})", updates.size());
}
}
}
private boolean checkIfPlayerIsInGroup(String groupToken, String playerName) {
String url = amIMemberOfGroupUrl(playerName);
if (url == null) return false;
HttpRequestService.HttpResponse response = httpRequestService.get(url, groupToken);
return response.isSuccessful();
}
// NOTE: These states should only be restored if a new update did not come in at some point before calling this
private void restoreStateIfNothingUpdated() {
inventory.restoreState();
bank.restoreState();
equipment.restoreState();
sharedBank.restoreState();
resources.restoreState();
skills.restoreState();
quests.restoreState();
position.restoreState();
runePouch.restoreState();
quiver.restoreState();
interacting.restoreState();
deposited.restoreState();
seedVault.restoreState();
achievementDiary.restoreState();
// collectionLogManager.restoreCollections();
// collectionLogManager.restoreNewCollections();
}
private String baseUrl() {
return httpRequestService.getBaseUrl();
}
private String groupName() {
String groupName = config.groupName().trim();
if (groupName.length() == 0) {
return null;
}
return groupName;
}
private String getUpdateGroupMemberUrl() {
String baseUrl = baseUrl();
String groupName = groupName();
if (baseUrl == null || groupName == null) return null;
return String.format("%s/api/group/%s/update-group-member", baseUrl, groupName);
}
private String amIMemberOfGroupUrl(String playerName) {
String baseUrl = baseUrl();
String groupName = groupName();
if (baseUrl == null || groupName == null) return null;
return String.format("%s/api/group/%s/am-i-in-group?member_name=%s", baseUrl, groupName, playerName);
}
private boolean isBadWorldType() {
EnumSet<WorldType> worldTypes = client.getWorldType();
for (WorldType worldType : worldTypes) {
if (worldType == WorldType.SEASONAL ||
worldType == WorldType.DEADMAN ||
worldType == WorldType.TOURNAMENT_WORLD ||
worldType == WorldType.PVP_ARENA ||
worldType == WorldType.BETA_WORLD ||
worldType == WorldType.QUEST_SPEEDRUNNING) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,66 @@
package men.groupiron;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public class DataState {
private final AtomicReference<ConsumableState> state = new AtomicReference<>();
private ConsumableState previousState;
private final String key;
private final boolean transactionBased;
DataState() {
key = "";
transactionBased = false;
}
DataState(String key, boolean transactionBased) {
this.key = key;
this.transactionBased = transactionBased;
}
public void update(ConsumableState o) {
if (!o.equals(previousState)) {
previousState = o;
if (!transactionBased) {
state.set(o);
}
}
}
public Object consumeState(String whoIsUpdating) {
return consumeState(whoIsUpdating, new HashMap<>());
}
public Object consumeState(Map<String, Object> output) {
return consumeState((String) output.get("name"), output);
}
public Object consumeState(String whoIsUpdating, Map<String, Object> output) {
final ConsumableState consumedState = state.getAndSet(null);
if (consumedState != null) {
final String whoOwnsThis = consumedState.whoOwnsThis();
if (whoOwnsThis != null && whoOwnsThis.equals(whoIsUpdating)) {
Object c = consumedState.get();
output.put(key, c);
return c;
}
}
return null;
}
public ConsumableState mostRecentState() {
return this.previousState;
}
public void restoreState() {
state.compareAndSet(null, previousState);
}
public void commitTransaction() {
state.set(previousState);
}
}

View File

@@ -0,0 +1,45 @@
package men.groupiron;
import java.util.Map;
public class DepositedItems {
private ItemContainerState items = null;
private ItemContainerState consumedItems = null;
public DepositedItems() {
}
public synchronized void update(ItemContainerState deposited) {
if (deposited == null) return;
if (items == null || !deposited.whoOwnsThis().equals(items.whoOwnsThis())) {
items = deposited;
} else {
items = items.add(deposited);
}
}
public synchronized void consumeState(Map<String, Object> output) {
if (items != null) {
final String whoOwnsThis = items.whoOwnsThis();
final String whoIsUpdating = (String) output.get("name");
if (whoOwnsThis != null && whoOwnsThis.equals(whoIsUpdating)) output.put("deposited", items.get());
}
consumedItems = items;
items = null;
}
public synchronized void restoreState() {
if (consumedItems == null) return;
if (items != null) {
items = items.add(consumedItems);
} else {
items = consumedItems;
}
consumedItems = null;
}
public synchronized void reset() {
items = null;
consumedItems = null;
}
}

View File

@@ -0,0 +1,55 @@
package men.groupiron;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.ConfigSection;
@ConfigGroup("GroupIronmenTracker")
public interface GroupIronmenTrackerConfig extends Config {
@ConfigSection(
name = "Group Config",
description = "Enter the group details you created on the website here",
position = 0
)
String groupSection = "GroupSection";
@ConfigSection(
name = "Self Hosted Config",
description = "Configure your connection to a self hosted server",
position = 1,
closedByDefault = true
)
String connectionSection = "ConnectionSection";
@ConfigItem(
keyName = "groupName",
name = "Group Name (on the website)",
description = "This is the group name you provided on the website when creating your group",
section = groupSection
)
default String groupName() {
return "";
}
@ConfigItem(
keyName = "groupToken",
name = "Group Token",
description = "Secret token for your group provided by the website. Get this from the member which created the group on the site, or create a new one by visiting the site.",
secret = true,
section = groupSection
)
default String authorizationToken() {
return "";
}
@ConfigItem(
keyName = "baseUrlOverride",
name = "Server base URL override (leave blank to use public server)",
description = "Overrides the public server URL used to send data. Only change this if you are hosting your own server.",
section = connectionSection
)
default String baseUrlOverride() {
return "";
}
}

View File

@@ -0,0 +1,282 @@
package men.groupiron;
import com.google.inject.Provides;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.*;
import net.runelite.api.coords.LocalPoint;
import net.runelite.api.coords.WorldPoint;
import net.runelite.api.events.*;
import net.runelite.api.gameval.InventoryID;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.api.gameval.VarClientID;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.api.widgets.Widget;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.game.ItemManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.task.Schedule;
import net.runelite.client.util.Text;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@PluginDescriptor(
name = "Group Ironmen Tracker"
)
public class GroupIronmenTrackerPlugin extends Plugin {
@Inject
private Client client;
@Inject
private GroupIronmenTrackerConfig config;
@Inject
private DataManager dataManager;
@Inject
private ItemManager itemManager;
@Inject
private CollectionLogManager collectionLogManager;
@Inject
private CollectionLogV2Manager collectionLogV2Manager;
@Inject
private CollectionLogWidgetSubscriber collectionLogWidgetSubscriber;
@Inject
private HttpRequestService httpRequestService;
@Inject
ClientThread clientThread;
private int itemsDeposited = 0;
private static final int SECONDS_BETWEEN_UPLOADS = 1;
private static final int SECONDS_BETWEEN_INFREQUENT_DATA_CHANGES = 60;
private static final int DEPOSIT_ITEM = 12582914;
private static final int DEPOSIT_INVENTORY = 12582916;
private static final int DEPOSIT_EQUIPMENT = 12582918;
private static final int CHATBOX_ENTERED = 681;
private static final int GROUP_STORAGE_LOADER = 293;
private static final int COLLECTION_LOG_INVENTORYID = 620;
private static final Pattern COLLECTION_LOG_ITEM_PATTERN = Pattern.compile("New item added to your collection log: (.*)");
private boolean notificationStarted = false;
@Override
protected void startUp() throws Exception {
clientThread.invokeLater(() -> {
collectionLogManager.initCollectionLog();
});
collectionLogWidgetSubscriber.startUp();
log.info("Group Ironmen Tracker started!");
}
@Override
protected void shutDown() throws Exception {
collectionLogWidgetSubscriber.shutDown();
log.info("Group Ironmen Tracker stopped!");
}
@Schedule(
period = SECONDS_BETWEEN_UPLOADS,
unit = ChronoUnit.SECONDS,
asynchronous = true
)
public void submitToApi() {
dataManager.submitToApi();
}
@Schedule(
period = SECONDS_BETWEEN_UPLOADS,
unit = ChronoUnit.SECONDS
)
public void updateThingsThatDoChangeOften() {
if (doNotUseThisData())
return;
Player player = client.getLocalPlayer();
String playerName = player.getName();
dataManager.getResources().update(new ResourcesState(playerName, client));
LocalPoint localPoint = player.getLocalLocation();
WorldPoint worldPoint = WorldPoint.fromLocalInstance(client, localPoint);
dataManager.getPosition().update(new LocationState(playerName, worldPoint));
dataManager.getRunePouch().update(new RunePouchState(playerName, client));
dataManager.getQuiver().update(new QuiverState(playerName, client, itemManager));
}
@Schedule(
period = SECONDS_BETWEEN_INFREQUENT_DATA_CHANGES,
unit = ChronoUnit.SECONDS
)
public void updateThingsThatDoNotChangeOften() {
if (doNotUseThisData())
return;
String playerName = client.getLocalPlayer().getName();
dataManager.getQuests().update(new QuestState(playerName, client));
dataManager.getAchievementDiary().update(new AchievementDiaryState(playerName, client));
}
@Subscribe
public void onVarbitChanged(VarbitChanged event) {
if (doNotUseThisData()) return;
final int varpId = event.getVarpId();
if (varpId == VarPlayerID.DIZANAS_QUIVER_TEMP_AMMO || varpId == VarPlayerID.DIZANAS_QUIVER_TEMP_AMMO_AMOUNT) {
String playerName = client.getLocalPlayer().getName();
dataManager.getQuiver().update(new QuiverState(playerName, client, itemManager));
}
}
@Subscribe
public void onGameTick(GameTick gameTick) {
--itemsDeposited;
updateInteracting();
Widget groupStorageLoaderText = client.getWidget(GROUP_STORAGE_LOADER, 1);
if (groupStorageLoaderText != null) {
if (groupStorageLoaderText.getText().equalsIgnoreCase("saving...")) {
dataManager.getSharedBank().commitTransaction();
}
}
}
@Subscribe
public void onStatChanged(StatChanged statChanged) {
if (doNotUseThisData())
return;
String playerName = client.getLocalPlayer().getName();
dataManager.getSkills().update(new SkillState(playerName, client));
}
@Subscribe
public void onItemContainerChanged(ItemContainerChanged event) {
if (doNotUseThisData())
return;
String playerName = client.getLocalPlayer().getName();
final int id = event.getContainerId();
ItemContainer container = event.getItemContainer();
if (id == InventoryID.BANK) {
dataManager.getDeposited().reset();
dataManager.getBank().update(new ItemContainerState(playerName, container, itemManager));
} else if (id == InventoryID.SEED_VAULT) {
dataManager.getSeedVault().update(new ItemContainerState(playerName, container, itemManager));
} else if (id == InventoryID.INV) {
ItemContainerState newInventoryState = new ItemContainerState(playerName, container, itemManager, 28);
if (itemsDeposited > 0) {
updateDeposited(newInventoryState, (ItemContainerState) dataManager.getInventory().mostRecentState());
}
dataManager.getInventory().update(newInventoryState);
} else if (id == InventoryID.WORN) {
ItemContainerState newEquipmentState = new ItemContainerState(playerName, container, itemManager, 14);
if (itemsDeposited > 0) {
updateDeposited(newEquipmentState, (ItemContainerState) dataManager.getEquipment().mostRecentState());
}
dataManager.getEquipment().update(newEquipmentState);
} else if (id == InventoryID.INV_GROUP_TEMP) {
dataManager.getSharedBank().update(new ItemContainerState(playerName, container, itemManager));
} else if (id == COLLECTION_LOG_INVENTORYID) {
collectionLogManager.updateCollection(new ItemContainerState(playerName, container, itemManager));
}
}
@Subscribe
private void onScriptPostFired(ScriptPostFired event) {
if (event.getScriptId() == CHATBOX_ENTERED && client.getWidget(InterfaceID.BankDepositbox.INVENTORY) != null) {
itemsMayHaveBeenDeposited();
}
}
@Subscribe
private void onMenuOptionClicked(MenuOptionClicked event) {
final int param1 = event.getParam1();
final MenuAction menuAction = event.getMenuAction();
if (menuAction == MenuAction.CC_OP) {
if (param1 == DEPOSIT_ITEM || param1 == DEPOSIT_INVENTORY || param1 == DEPOSIT_EQUIPMENT) {
itemsMayHaveBeenDeposited();
}
}
}
@Subscribe
private void onInteractingChanged(InteractingChanged event) {
if (event.getSource() != client.getLocalPlayer()) return;
updateInteracting();
}
@Subscribe
private void onChatMessage(ChatMessage chatMessage) {
if (doNotUseThisData())
return;
if (chatMessage.getType() != ChatMessageType.GAMEMESSAGE) return;
Matcher matcher = COLLECTION_LOG_ITEM_PATTERN.matcher(chatMessage.getMessage());
if (matcher.find()) {
String itemName = Text.removeTags(matcher.group(1));
if (!StringUtils.isBlank(itemName)) {
collectionLogManager.updateNewItem(itemName);
}
}
}
@Subscribe
public void onScriptPreFired(ScriptPreFired scriptPreFired)
{
switch (scriptPreFired.getScriptId())
{
case ScriptID.NOTIFICATION_START:
notificationStarted = true;
break;
case ScriptID.NOTIFICATION_DELAY:
if (!notificationStarted) return;
String topText = client.getVarcStrValue(VarClientID.NOTIFICATION_TITLE);
String bottomText = client.getVarcStrValue(VarClientID.NOTIFICATION_MAIN);
if (topText.equalsIgnoreCase("Collection log")) {
String entry = Text.removeTags(bottomText).substring("New item:".length());
collectionLogManager.updateNewItem(entry);
}
notificationStarted = false;
break;
}
}
private void itemsMayHaveBeenDeposited() {
// NOTE: In order to determine if an item has gone through the deposit box we first detect if any of the menu
// actions were performed OR a custom amount was entered while the deposit box inventory widget was opened.
// Then we allow up to two game ticks were an inventory changed event can occur and at that point we assume
// it must have been caused by the action detected just before. We can't check the inventory at the time of
// either interaction since the inventory may have not been updated yet. We also cannot just check that the deposit
// box window is open in the item container event since it is possible for a player to close the widget before
// the event handler is called.
itemsDeposited = 2;
}
private void updateInteracting() {
Player player = client.getLocalPlayer();
if (player != null) {
Actor actor = player.getInteracting();
if (actor != null) {
String playerName = player.getName();
dataManager.getInteracting().update(new InteractingState(playerName, actor, client));
}
}
}
private void updateDeposited(ItemContainerState newState, ItemContainerState previousState) {
ItemContainerState deposited = newState.whatGotRemoved(previousState);
dataManager.getDeposited().update(deposited);
}
private boolean doNotUseThisData() {
return client.getGameState() != GameState.LOGGED_IN || client.getLocalPlayer() == null;
}
@Provides
GroupIronmenTrackerConfig provideConfig(ConfigManager configManager) {
return configManager.getConfig(GroupIronmenTrackerConfig.class);
}
}

View File

@@ -0,0 +1,120 @@
package men.groupiron;
import com.google.gson.Gson;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.RuneLiteProperties;
import okhttp3.*;
@Slf4j
@Singleton
public class HttpRequestService {
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String USER_AGENT =
"GroupIronmenTracker/1.5.3 " + "RuneLite/" + RuneLiteProperties.getVersion();
private static final String PUBLIC_BASE_URL = "https://groupiron.men";
@Inject
private OkHttpClient okHttpClient;
@Inject
private GroupIronmenTrackerConfig config;
@Inject
private Gson gson;
public HttpResponse get(String url, String authToken) {
Request request = buildRequest(url, authToken).get().build();
return executeRequest(request, "GET", url, null);
}
public HttpResponse post(String url, String authToken, Object requestBody) {
String requestJson = gson.toJson(requestBody);
RequestBody body = RequestBody.create(JSON, requestJson);
Request request = buildRequest(url, authToken).post(body).build();
return executeRequest(request, "POST", url, requestJson);
}
private Request.Builder buildRequest(String url, String authToken) {
Request.Builder requestBuilder = new Request.Builder().url(url).header("User-Agent", USER_AGENT);
if (isInternalUrl(url)) {
if (authToken != null && !authToken.trim().isEmpty()) {
requestBuilder.header("Authorization", authToken);
}
requestBuilder.header("Accept", "application/json");
}
return requestBuilder;
}
private boolean isInternalUrl(String url) {
return url.startsWith(getBaseUrl());
}
private HttpResponse executeRequest(Request request, String method, String url, String requestBody) {
Call call = okHttpClient.newCall(request);
try (Response response = call.execute()) {
String responseBody = readBodySafe(response);
logRequest(method, url, requestBody, response, responseBody);
return new HttpResponse(response.isSuccessful(), response.code(), responseBody);
} catch (IOException ex) {
log.warn("{} {} failed: {}", method, url, ex.toString());
return new HttpResponse(false, -1, ex.getMessage());
}
}
private void logRequest(String method, String url, String requestBody, Response response, String responseBody) {
if (!log.isDebugEnabled()) {
return;
}
switch (method) {
case "GET":
log.debug("GET {} -> {}\nResponse: {}", url, response.code(), responseBody);
break;
case "POST":
log.debug("POST {}\nRequest: {}\nResponse({}): {}", url, requestBody, response.code(), responseBody);
break;
default:
log.debug("{} {} -> {}\nResponse: {}", method, url, response.code(), responseBody);
}
}
private static String readBodySafe(Response response) {
try {
ResponseBody responseBody = response.body();
return responseBody != null ? responseBody.string() : "<no body>";
} catch (Exception e) {
return "<unavailable: " + e.getMessage() + ">";
}
}
public String getBaseUrl() {
String baseUrlOverride = config.baseUrlOverride().trim();
if (!baseUrlOverride.isEmpty()) {
return baseUrlOverride;
}
return PUBLIC_BASE_URL;
}
@Getter
public static class HttpResponse {
private final boolean successful;
private final int code;
private final String body;
public HttpResponse(boolean successful, int code, String body) {
this.successful = successful;
this.code = code;
this.body = body;
}
}
}

View File

@@ -0,0 +1,49 @@
package men.groupiron;
import lombok.Getter;
import net.runelite.api.Actor;
import net.runelite.api.Client;
import net.runelite.api.coords.WorldPoint;
public class InteractingState implements ConsumableState {
private final transient String playerName;
@Getter
private final String name;
@Getter
private final int scale;
@Getter
private final int ratio;
@Getter
private final LocationState location;
public InteractingState(String playerName, Actor actor, Client client) {
this.playerName = playerName;
this.scale = actor.getHealthScale();
this.ratio = actor.getHealthRatio();
this.name = actor.getName();
WorldPoint worldPoint = WorldPoint.fromLocalInstance(client, actor.getLocalLocation());
this.location = new LocationState(playerName, worldPoint);
}
@Override
public Object get() {
return this;
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof InteractingState)) return false;
// NOTE: For interactions, we want to keep sending the data until the player stops interacting
// even if nothing changed about what is being interacted with. The UI will handle not showing
// the interaction once it goes stale from the player not interacting with anything.
return false;
}
}

View File

@@ -0,0 +1,27 @@
package men.groupiron;
import lombok.Getter;
public class ItemContainerItem {
@Getter
private final int id;
@Getter
private int quantity;
ItemContainerItem(int id, int quantity) {
this.id = id;
this.quantity = quantity;
}
public void addQuantity(int quantity) {
this.quantity += quantity;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof ItemContainerItem)) return false;
ItemContainerItem other = (ItemContainerItem) o;
return other.id == id && other.quantity == quantity;
}
}

View File

@@ -0,0 +1,161 @@
package men.groupiron;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Item;
import net.runelite.api.ItemContainer;
import net.runelite.client.game.ItemManager;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class ItemContainerState implements ConsumableState {
private final List<ItemContainerItem> items;
private transient final String playerName;
public ItemContainerState(String playerName, ItemContainer container, ItemManager itemManager) {
this.playerName = playerName;
items = new ArrayList<>();
Item[] contents = container.getItems();
for (final Item item : contents) {
if (isItemValid(item, itemManager)) {
items.add(new ItemContainerItem(itemManager.canonicalize(item.getId()), item.getQuantity()));
}
}
}
// NOTE: This is for when we care about the order of the items in the container
// like the player inventory and equipment.
public ItemContainerState(String playerName, ItemContainer container, ItemManager itemManager, int containerSize) {
this.playerName = playerName;
items = new ArrayList<>();
for (int i = 0; i < containerSize; i++) {
Item item = container.getItem(i);
if (!isItemValid(item, itemManager)) {
items.add(new ItemContainerItem(0, 0));
} else {
items.add(new ItemContainerItem(itemManager.canonicalize(item.getId()), item.getQuantity()));
}
}
}
public ItemContainerState(String playerName, List<ItemContainerItem> items) {
this.playerName = playerName;
this.items = items;
}
@Nullable
public ItemContainerState add(ItemContainerState itemsToAdd) {
if (itemsToAdd == null || !itemsToAdd.whoOwnsThis().equals(whoOwnsThis())) return null;
Map<Integer, ItemContainerItem> thisItems = getItemMap();
Map<Integer, ItemContainerItem> otherItems = itemsToAdd.getItemMap();
List<ItemContainerItem> result = new ArrayList<>();
for (Integer itemId : thisItems.keySet()) {
ItemContainerItem item = thisItems.get(itemId);
if (otherItems.containsKey(itemId)) {
item.addQuantity(otherItems.get(itemId).getQuantity());
}
result.add(item);
}
for (Integer itemId : otherItems.keySet()) {
if (!thisItems.containsKey(itemId)) {
result.add(otherItems.get(itemId));
}
}
return new ItemContainerState(whoOwnsThis(), result);
}
@Nullable
public ItemContainerState whatGotRemoved(ItemContainerState other) {
if (other == null || !other.whoOwnsThis().equals(whoOwnsThis())) return null;
Map<Integer, ItemContainerItem> thisItems = getItemMap();
Map<Integer, ItemContainerItem> otherItems = other.getItemMap();
List<ItemContainerItem> result = new ArrayList<>();
for (Integer itemId : otherItems.keySet()) {
ItemContainerItem otherItem = otherItems.get(itemId);
if (otherItem.getId() == 0) continue;
if (thisItems.containsKey(itemId)) {
ItemContainerItem thisItem = thisItems.get(itemId);
int quantityDifference = otherItem.getQuantity() - thisItem.getQuantity();
if (quantityDifference > 0) {
result.add(new ItemContainerItem(itemId, quantityDifference));
}
} else {
result.add(new ItemContainerItem(itemId, otherItem.getQuantity()));
}
}
return new ItemContainerState(playerName, result);
}
public Map<Integer, ItemContainerItem> getItemMap() {
Map<Integer, ItemContainerItem> itemMap = new HashMap<>();
for (ItemContainerItem itemContainerItem : items) {
Integer id = itemContainerItem.getId();
if (itemMap.containsKey(id)) {
itemMap.get(id).addQuantity(itemContainerItem.getQuantity());
} else {
itemMap.put(id, new ItemContainerItem(id, itemContainerItem.getQuantity()));
}
}
return itemMap;
}
public boolean isEmpty() {
return items.isEmpty();
}
private boolean isItemValid(Item item, ItemManager itemManager) {
if (item == null) return false;
final int id = item.getId();
final int quantity = item.getQuantity();
if (itemManager != null) {
final boolean isPlaceholder = itemManager.getItemComposition(id).getPlaceholderTemplateId() != -1;
return id >= 0 && quantity >= 0 && !isPlaceholder;
}
return false;
}
public List<Integer> asFlatList() {
List<Integer> result = new ArrayList<>(items.size() * 2);
for (ItemContainerItem item : items) {
result.add(item.getId());
result.add(item.getQuantity());
}
return result;
}
@Override
public Object get() {
return asFlatList();
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof ItemContainerState)) return false;
ItemContainerState other = (ItemContainerState) o;
if (other.items.size() != items.size()) return false;
for (int i = 0; i < items.size(); i++) {
if (!items.get(i).equals(other.items.get(i))) return false;
}
return true;
}
}

View File

@@ -0,0 +1,45 @@
package men.groupiron;
import lombok.Getter;
import net.runelite.api.coords.WorldPoint;
public class LocationState implements ConsumableState {
@Getter
private final int x;
@Getter
private final int y;
@Getter
private final int plane;
private transient final String playerName;
LocationState(String playerName, WorldPoint worldPoint) {
this.playerName = playerName;
x = worldPoint.getX();
y = worldPoint.getY();
plane = worldPoint.getPlane();
}
@Override
public Object get() {
return new int[] { x, y, plane };
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof LocationState)) return false;
LocationState other = (LocationState) o;
return (x == other.x) && (y == other.y) && (plane == other.plane);
}
@Override
public String toString() {
return String.format("{ x: %d, y: %d, plane: %d }", x, y, plane);
}
}

View File

@@ -0,0 +1,52 @@
package men.groupiron;
import net.runelite.api.Client;
import net.runelite.api.Quest;
import java.util.*;
import java.util.stream.Collectors;
public class QuestState implements ConsumableState {
private final Map<Integer, net.runelite.api.QuestState> questStateMap;
private transient final String playerName;
private List<Integer> sortedQuestIds = Arrays.stream(Quest.values()).map(Quest::getId).sorted().collect(Collectors.toList());
public QuestState(String playerName, Client client) {
this.playerName = playerName;
this.questStateMap = new HashMap<>();
for (Quest quest : Quest.values()) {
questStateMap.put(quest.getId(), quest.getState(client));
}
}
@Override
public Object get() {
List<Integer> result = new ArrayList<>(questStateMap.size());
for (Integer questId : sortedQuestIds) {
result.add(questStateMap.get(questId).ordinal());
}
return result;
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof QuestState)) return false;
QuestState other = (QuestState) o;
for (Quest quest : Quest.values()) {
Integer questId = quest.getId();
if (questStateMap.get(questId) != other.questStateMap.get(questId)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,48 @@
package men.groupiron;
import net.runelite.api.Client;
import net.runelite.api.gameval.VarPlayerID;
import net.runelite.client.game.ItemManager;
public class QuiverState implements ConsumableState {
private final ItemContainerItem ammo;
private final String playerName;
public QuiverState(String playerName, Client client, ItemManager itemManager) {
this.playerName = playerName;
int id = client.getVarpValue(VarPlayerID.DIZANAS_QUIVER_TEMP_AMMO);
int qty = client.getVarpValue(VarPlayerID.DIZANAS_QUIVER_TEMP_AMMO_AMOUNT);
if (id <= 0 || qty <= 0) {
this.ammo = new ItemContainerItem(0, 0);
} else {
int canonId = itemManager.canonicalize(id);
this.ammo = new ItemContainerItem(canonId, qty);
}
}
@Override
public Object get() {
return new int[] {ammo.getId(), ammo.getQuantity()};
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof QuiverState)) {
return false;
}
QuiverState other = (QuiverState) o;
return this.ammo.equals(other.ammo);
}
}

View File

@@ -0,0 +1,79 @@
package men.groupiron;
import lombok.Getter;
import net.runelite.api.Client;
import net.runelite.api.Skill;
public class ResourcesState implements ConsumableState {
private static class CurrentMax {
@Getter
private final int current;
@Getter
private final int max;
CurrentMax(int current, int max) {
this.current = current;
this.max = max;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof CurrentMax)) return false;
CurrentMax other = (CurrentMax) o;
return other.getCurrent() == current && other.getMax() == max;
}
}
@Getter
private final CurrentMax hitpoints;
@Getter
private final CurrentMax prayer;
@Getter
private final CurrentMax energy;
@Getter
int world;
private transient final String playerName;
ResourcesState(String playerName, Client client) {
this.playerName = playerName;
hitpoints = new CurrentMax(
client.getBoostedSkillLevel(Skill.HITPOINTS),
client.getRealSkillLevel(Skill.HITPOINTS)
);
prayer = new CurrentMax(
client.getBoostedSkillLevel(Skill.PRAYER),
client.getRealSkillLevel(Skill.PRAYER)
);
energy = new CurrentMax(
client.getEnergy(),
100
);
world = client.getWorld();
}
@Override
public Object get() {
return new int[] {
hitpoints.current, hitpoints.max,
prayer.current, prayer.max,
energy.current, energy.max,
world
};
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof ResourcesState)) return false;
ResourcesState other = (ResourcesState) o;
return other.world == world && other.hitpoints.equals(hitpoints) && other.prayer.equals(prayer) && other.energy.equals(energy);
}
}

View File

@@ -0,0 +1,51 @@
package men.groupiron;
import net.runelite.api.Client;
import net.runelite.api.EnumComposition;
import net.runelite.api.EnumID;
import net.runelite.api.gameval.VarbitID;
public class RunePouchState implements ConsumableState {
private final ItemContainerItem rune1;
private final ItemContainerItem rune2;
private final ItemContainerItem rune3;
private final ItemContainerItem rune4;
private final transient String playerName;
public RunePouchState(String playerName, Client client) {
this.playerName = playerName;
final EnumComposition runepouchEnum = client.getEnum(EnumID.RUNEPOUCH_RUNE);
rune1 = itemForRune(client.getVarbitValue(VarbitID.RUNE_POUCH_TYPE_1), client.getVarbitValue(VarbitID.RUNE_POUCH_QUANTITY_1), runepouchEnum);
rune2 = itemForRune(client.getVarbitValue(VarbitID.RUNE_POUCH_TYPE_2), client.getVarbitValue(VarbitID.RUNE_POUCH_QUANTITY_2), runepouchEnum);
rune3 = itemForRune(client.getVarbitValue(VarbitID.RUNE_POUCH_TYPE_3), client.getVarbitValue(VarbitID.RUNE_POUCH_QUANTITY_3), runepouchEnum);
rune4 = itemForRune(client.getVarbitValue(VarbitID.RUNE_POUCH_TYPE_4), client.getVarbitValue(VarbitID.RUNE_POUCH_QUANTITY_4), runepouchEnum);
}
private ItemContainerItem itemForRune(int runeId, int amount, EnumComposition runepouchEnum) {
return new ItemContainerItem(runepouchEnum.getIntValue(runeId), amount);
}
@Override
public Object get() {
return new int[] {
rune1.getId(), rune1.getQuantity(),
rune2.getId(), rune2.getQuantity(),
rune3.getId(), rune3.getQuantity(),
rune4.getId(), rune4.getQuantity()
};
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof RunePouchState)) return false;
RunePouchState other = (RunePouchState) o;
return rune1.equals(other.rune1) && rune2.equals(other.rune2) && rune3.equals(other.rune3) && rune4.equals(other.rune4);
}
}

View File

@@ -0,0 +1,69 @@
package men.groupiron;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.Client;
import net.runelite.api.Skill;
import java.util.*;
@Slf4j
public class SkillState implements ConsumableState {
private final Map<String, Integer> skillXpMap;
private transient final String playerName;
public SkillState(String playerName, Client client) {
this.playerName = playerName;
skillXpMap = new HashMap<>();
for (Skill skill : Skill.values()) {
skillXpMap.put(skill.getName(), client.getSkillExperience(skill));
}
}
@Override
public Object get() {
return new int[] {
skillXpMap.get("Agility"),
skillXpMap.get("Attack"),
skillXpMap.get("Construction"),
skillXpMap.get("Cooking"),
skillXpMap.get("Crafting"),
skillXpMap.get("Defence"),
skillXpMap.get("Farming"),
skillXpMap.get("Firemaking"),
skillXpMap.get("Fishing"),
skillXpMap.get("Fletching"),
skillXpMap.get("Herblore"),
skillXpMap.get("Hitpoints"),
skillXpMap.get("Hunter"),
skillXpMap.get("Magic"),
skillXpMap.get("Mining"),
skillXpMap.get("Prayer"),
skillXpMap.get("Ranged"),
skillXpMap.get("Runecraft"),
skillXpMap.get("Slayer"),
skillXpMap.get("Smithing"),
skillXpMap.get("Strength"),
skillXpMap.get("Thieving"),
skillXpMap.get("Woodcutting")
};
}
@Override
public String whoOwnsThis() {
return playerName;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof SkillState)) return false;
SkillState other = (SkillState) o;
for (Skill skill : Skill.values()) {
String skillName = skill.getName();
if (!skillXpMap.get(skillName).equals(other.skillXpMap.get(skillName))) return false;
}
return true;
}
}

View File

@@ -0,0 +1,13 @@
package men.groupiron;
import net.runelite.client.RuneLite;
import net.runelite.client.externalplugins.ExternalPluginManager;
public class GroupIronmenTrackerPluginTest
{
public static void main(String[] args) throws Exception
{
ExternalPluginManager.loadBuiltin(GroupIronmenTrackerPlugin.class);
RuneLite.main(args);
}
}