├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icon.png ├── runelite-plugin.properties ├── settings.gradle ├── src ├── main │ └── java │ │ └── com │ │ └── andmcadams │ │ └── wikisync │ │ ├── Manifest.java │ │ ├── PlayerData.java │ │ ├── PlayerDataSubmission.java │ │ ├── PlayerProfile.java │ │ ├── SyncButtonManager.java │ │ ├── WikiSyncConfig.java │ │ ├── WikiSyncPlugin.java │ │ └── dps │ │ ├── DpsDataFetcher.java │ │ ├── WebSocketManager.java │ │ ├── messages │ │ ├── Request.java │ │ ├── RequestType.java │ │ └── response │ │ │ ├── GetPlayer.java │ │ │ └── UsernameChanged.java │ │ └── ws │ │ ├── WSHandler.java │ │ └── WSWebsocketServer.java └── test │ └── java │ └── com │ └── andmcadams │ └── wikisync │ ├── WikiSyncLogPlugin.java │ └── WikiSyncPluginTest.java └── stub_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | .idea/ 4 | .project 5 | .settings/ 6 | .classpath 7 | nbactions.xml 8 | nb-configuration.xml 9 | nbproject/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, andmcadams 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WikiSync 2 | Pushes up bits of data about your player to a server hosted by the wiki. 3 | 4 | The varbits/varplayers passed up to the server are determined by the manifest returned by the server. 5 | This allows us to handle updates quickly without having to push a change to the plugin itself. 6 | Your player's stats are passed up to the server. 7 | For details about what is passed to the server, see the [wiki page](https://oldschool.runescape.wiki/w/RuneScape:WikiSync). 8 | 9 | Keep in mind that this data is public, similar to the official HiScores. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | } 5 | 6 | repositories { 7 | mavenLocal() 8 | maven { 9 | url = 'https://repo.runelite.net' 10 | } 11 | mavenCentral() 12 | } 13 | 14 | def runeLiteVersion = 'latest.release' 15 | 16 | dependencies { 17 | compileOnly group: 'net.runelite', name: 'client', version: runeLiteVersion 18 | 19 | compileOnly 'org.projectlombok:lombok:1.18.30' 20 | annotationProcessor 'org.projectlombok:lombok:1.18.30' 21 | 22 | implementation "org.java-websocket:Java-WebSocket:1.5.6" 23 | 24 | testImplementation 'junit:junit:4.12' 25 | testImplementation group: 'net.runelite', name: 'client', version: runeLiteVersion 26 | testImplementation group: 'net.runelite', name: 'jshell', version: runeLiteVersion 27 | 28 | testImplementation "ch.qos.logback:logback-classic:1.4.14" 29 | } 30 | 31 | group = 'com.andmcadams.wikisync' 32 | version = '3.0-SNAPSHOT' 33 | 34 | tasks.withType(JavaCompile) { 35 | options.encoding = 'UTF-8' 36 | options.release.set(11) 37 | } 38 | 39 | idea { 40 | module { 41 | downloadJavadoc = true 42 | downloadSources = true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weirdgloop/WikiSync/4d9ebe27be9104ec9f80aeb733a7a4e72579a52b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weirdgloop/WikiSync/4d9ebe27be9104ec9f80aeb733a7a4e72579a52b/icon.png -------------------------------------------------------------------------------- /runelite-plugin.properties: -------------------------------------------------------------------------------- 1 | displayName=WikiSync 2 | author=andmcadams 3 | support=https://oldschool.runescape.wiki/w/RuneScape:WikiSync 4 | description=Send off bits of your player's data so the wiki can personalize your experience 5 | tags=wiki,sync,quest 6 | plugins=com.andmcadams.wikisync.WikiSyncPlugin -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'WikiSync' 2 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/Manifest.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.ArrayList; 6 | 7 | @Data 8 | public class Manifest 9 | { 10 | final int version = -1; 11 | final int[] varbits = new int[0]; 12 | final int[] varps = new int[0]; 13 | final ArrayList collections = new ArrayList<>(); 14 | } -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/PlayerData.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PlayerData 14 | { 15 | Map varb = new HashMap<>(); 16 | Map varp = new HashMap<>(); 17 | Map level = new HashMap<>(); 18 | Integer collectionLogItemCount = null; 19 | String collectionLogSlots = ""; 20 | 21 | public boolean isEmpty() 22 | { 23 | return varb.isEmpty() && varp.isEmpty() && level.isEmpty() && collectionLogSlots.isEmpty() && collectionLogItemCount == null; 24 | } 25 | 26 | public void clearCollectionLog() 27 | { 28 | collectionLogSlots = ""; 29 | collectionLogItemCount = null; 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/PlayerDataSubmission.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class PlayerDataSubmission 9 | { 10 | private String username; 11 | private String profile; 12 | private PlayerData data; 13 | } -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/PlayerProfile.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import lombok.Value; 4 | import net.runelite.client.config.RuneScapeProfileType; 5 | 6 | @Value 7 | public class PlayerProfile 8 | { 9 | String username; 10 | RuneScapeProfileType profileType; 11 | } -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/SyncButtonManager.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import com.google.inject.Inject; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.extern.slf4j.Slf4j; 8 | import net.runelite.api.*; 9 | import net.runelite.api.annotations.Component; 10 | import net.runelite.api.events.ScriptPostFired; 11 | import net.runelite.api.gameval.InterfaceID; 12 | import net.runelite.api.widgets.*; 13 | import net.runelite.client.callback.ClientThread; 14 | import net.runelite.client.eventbus.EventBus; 15 | import net.runelite.client.eventbus.Subscribe; 16 | 17 | @Slf4j 18 | public class SyncButtonManager { 19 | 20 | private static final int COLLECTION_LOG_SETUP = 7797; 21 | private static final int[] SPRITE_IDS_INACTIVE = { 22 | SpriteID.DIALOG_BACKGROUND, 23 | SpriteID.WORLD_MAP_BUTTON_METAL_CORNER_TOP_LEFT, 24 | SpriteID.WORLD_MAP_BUTTON_METAL_CORNER_TOP_RIGHT, 25 | SpriteID.WORLD_MAP_BUTTON_METAL_CORNER_BOTTOM_LEFT, 26 | SpriteID.WORLD_MAP_BUTTON_METAL_CORNER_BOTTOM_RIGHT, 27 | SpriteID.WORLD_MAP_BUTTON_EDGE_LEFT, 28 | SpriteID.WORLD_MAP_BUTTON_EDGE_TOP, 29 | SpriteID.WORLD_MAP_BUTTON_EDGE_RIGHT, 30 | SpriteID.WORLD_MAP_BUTTON_EDGE_BOTTOM, 31 | }; 32 | 33 | private static final int[] SPRITE_IDS_ACTIVE = { 34 | SpriteID.RESIZEABLE_MODE_SIDE_PANEL_BACKGROUND, 35 | SpriteID.EQUIPMENT_BUTTON_METAL_CORNER_TOP_LEFT_HOVERED, 36 | SpriteID.EQUIPMENT_BUTTON_METAL_CORNER_TOP_RIGHT_HOVERED, 37 | SpriteID.EQUIPMENT_BUTTON_METAL_CORNER_BOTTOM_LEFT_HOVERED, 38 | SpriteID.EQUIPMENT_BUTTON_METAL_CORNER_BOTTOM_RIGHT_HOVERED, 39 | SpriteID.EQUIPMENT_BUTTON_EDGE_LEFT_HOVERED, 40 | SpriteID.EQUIPMENT_BUTTON_EDGE_TOP_HOVERED, 41 | SpriteID.EQUIPMENT_BUTTON_EDGE_RIGHT_HOVERED, 42 | SpriteID.EQUIPMENT_BUTTON_EDGE_BOTTOM_HOVERED, 43 | }; 44 | 45 | private static final int FONT_COLOUR_INACTIVE = 0xd6d6d6; 46 | private static final int FONT_COLOUR_ACTIVE = 0xffffff; 47 | private static final int CLOSE_BUTTON_OFFSET = 28; 48 | private static final int BUTTON_WIDTH = 71; 49 | private static final int BUTTON_OFFSET = CLOSE_BUTTON_OFFSET + 5; 50 | 51 | private final Client client; 52 | private final ClientThread clientThread; 53 | private final EventBus eventBus; 54 | 55 | @Getter 56 | @Setter 57 | private boolean syncAllowed; 58 | 59 | @Inject 60 | private SyncButtonManager( 61 | Client client, 62 | ClientThread clientThread, 63 | EventBus eventBus 64 | ) 65 | { 66 | this.client = client; 67 | this.clientThread = clientThread; 68 | this.eventBus = eventBus; 69 | } 70 | 71 | public void startUp() 72 | { 73 | setSyncAllowed(false); 74 | eventBus.register(this); 75 | clientThread.invokeLater(() -> tryAddButton(this::onButtonClick)); 76 | } 77 | 78 | public void shutDown() 79 | { 80 | eventBus.unregister(this); 81 | clientThread.invokeLater(this::removeButton); 82 | } 83 | 84 | @Getter 85 | @RequiredArgsConstructor 86 | enum Screen 87 | { 88 | // First number is col log container (inner) and second is search button id 89 | COLLECTION_LOG(InterfaceID.Collection.UNIVERSE, InterfaceID.Collection.SEARCH_TOGGLE, InterfaceID.Collection.INFINITY), 90 | ; 91 | 92 | @Getter(onMethod_ = @Component) 93 | private final int parentId; 94 | 95 | @Getter(onMethod_ = @Component) 96 | private final int searchButtonId; 97 | 98 | @Getter(onMethod_ = @Component) 99 | private final int collectionLogContainer; 100 | } 101 | 102 | void tryAddButton(Runnable onClick) 103 | { 104 | for (Screen screen : Screen.values()) 105 | { 106 | addButton(screen, onClick); 107 | } 108 | } 109 | @Subscribe 110 | public void onScriptPostFired(ScriptPostFired scriptPostFired) 111 | { 112 | if (scriptPostFired.getScriptId() == COLLECTION_LOG_SETUP) 113 | { 114 | removeButton(); 115 | addButton(Screen.COLLECTION_LOG, this::onButtonClick); 116 | } 117 | } 118 | 119 | void onButtonClick() { 120 | setSyncAllowed(true); 121 | client.menuAction(-1, InterfaceID.Collection.SEARCH_TOGGLE, MenuAction.CC_OP, 1, -1, "Search", null); 122 | client.runScript(2240); 123 | client.addChatMessage(ChatMessageType.CONSOLE, "WikiSync", "Your collection log data is being sent to WikiSync...", "WikiSync"); 124 | } 125 | 126 | void addButton(Screen screen, Runnable onClick) 127 | { 128 | Widget parent = client.getWidget(screen.getParentId()); 129 | Widget searchButton = client.getWidget(screen.getSearchButtonId()); 130 | Widget collectionLogContainer = client.getWidget(screen.getCollectionLogContainer()); 131 | Widget[] containerChildren; 132 | Widget draggableTopbar; 133 | if (parent == null || searchButton == null || collectionLogContainer == null || 134 | (containerChildren = collectionLogContainer.getChildren()) == null || 135 | (draggableTopbar = containerChildren[0]) == null) 136 | { 137 | return; 138 | } 139 | 140 | final int w = BUTTON_WIDTH; 141 | final int h = searchButton.getOriginalHeight(); 142 | final int x = BUTTON_OFFSET; 143 | final int y = searchButton.getOriginalY(); 144 | final int cornerDim = 9; 145 | 146 | final Widget[] spriteWidgets = new Widget[9]; 147 | 148 | spriteWidgets[0] = parent.createChild(-1, WidgetType.GRAPHIC) 149 | .setSpriteId(SPRITE_IDS_INACTIVE[0]) 150 | .setPos(x, y) 151 | .setSize(w, h) 152 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 153 | .setYPositionMode(searchButton.getYPositionMode()); 154 | 155 | spriteWidgets[1] = parent.createChild(-1, WidgetType.GRAPHIC) 156 | .setSpriteId(SPRITE_IDS_INACTIVE[1]) 157 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 158 | .setSize(cornerDim, cornerDim) 159 | .setPos(x + (w - cornerDim), y); 160 | spriteWidgets[2] = parent.createChild(-1, WidgetType.GRAPHIC) 161 | .setSpriteId(SPRITE_IDS_INACTIVE[2]) 162 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 163 | .setSize(cornerDim, cornerDim) 164 | .setPos(x, y); 165 | spriteWidgets[3] = parent.createChild(-1, WidgetType.GRAPHIC) 166 | .setSpriteId(SPRITE_IDS_INACTIVE[3]) 167 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 168 | .setSize(cornerDim, cornerDim) 169 | .setPos(x + (w - cornerDim), y + h - cornerDim); 170 | spriteWidgets[4] = parent.createChild(-1, WidgetType.GRAPHIC) 171 | .setSpriteId(SPRITE_IDS_INACTIVE[4]) 172 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 173 | .setSize(cornerDim, cornerDim) 174 | .setPos(x, y + h - cornerDim); 175 | // Left and right edges 176 | int sideWidth = 9; 177 | int sideHeight = 4; 178 | spriteWidgets[5] = parent.createChild(-1, WidgetType.GRAPHIC) 179 | .setSpriteId(SPRITE_IDS_INACTIVE[5]) 180 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 181 | .setSize(sideWidth, sideHeight) 182 | .setPos(x + (w - sideWidth), y + cornerDim); 183 | spriteWidgets[7] = parent.createChild(-1, WidgetType.GRAPHIC) 184 | .setSpriteId(SPRITE_IDS_INACTIVE[7]) 185 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 186 | .setSize(sideWidth, sideHeight) 187 | .setPos(x, y + cornerDim); 188 | 189 | // Top and bottom edges 190 | int topWidth = 53; 191 | int topHeight = 9; 192 | spriteWidgets[6] = parent.createChild(-1, WidgetType.GRAPHIC) 193 | .setSpriteId(SPRITE_IDS_INACTIVE[6]) 194 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 195 | .setSize(topWidth, topHeight) 196 | .setPos(x + cornerDim, y); 197 | spriteWidgets[8] = parent.createChild(-1, WidgetType.GRAPHIC) 198 | .setSpriteId(SPRITE_IDS_INACTIVE[8]) 199 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 200 | .setSize(topWidth, topHeight) 201 | .setPos(x + cornerDim, y + h - topHeight); 202 | for (int i = 0; i < 9; i++) 203 | { 204 | spriteWidgets[i].revalidate(); 205 | } 206 | 207 | final Widget text = parent.createChild(-1, WidgetType.TEXT) 208 | .setText("WikiSync") 209 | .setTextColor(FONT_COLOUR_INACTIVE) 210 | .setFontId(FontID.PLAIN_11) 211 | .setTextShadowed(true) 212 | .setXPositionMode(WidgetPositionMode.ABSOLUTE_RIGHT) 213 | .setXTextAlignment(WidgetTextAlignment.CENTER) 214 | .setYTextAlignment(WidgetTextAlignment.CENTER) 215 | .setPos(x, y) 216 | .setSize(w, h) 217 | .setYPositionMode(searchButton.getYPositionMode()); 218 | text.revalidate(); 219 | 220 | // We'll give the text layer the listeners since it covers the whole area 221 | text.setHasListener(true); 222 | text.setOnMouseOverListener((JavaScriptCallback) ev -> 223 | { 224 | for (int i = 0; i <= 8; i++) 225 | { 226 | spriteWidgets[i].setSpriteId(SPRITE_IDS_ACTIVE[i]); 227 | } 228 | text.setTextColor(FONT_COLOUR_ACTIVE); 229 | }); 230 | text.setOnMouseLeaveListener((JavaScriptCallback) ev -> 231 | { 232 | for (int i = 0; i <= 8; i++) 233 | { 234 | spriteWidgets[i].setSpriteId(SPRITE_IDS_INACTIVE[i]); 235 | } 236 | text.setTextColor(FONT_COLOUR_INACTIVE); 237 | }); 238 | 239 | // Register a click listener 240 | text.setAction(0, "Sync your collection log with WikiSync"); 241 | text.setOnOpListener((JavaScriptCallback) ev -> onClick.run()); 242 | 243 | 244 | // Shrink the top bar to avoid overlapping the new button 245 | draggableTopbar.setOriginalWidth(draggableTopbar.getOriginalWidth() - (w + (x - CLOSE_BUTTON_OFFSET))); 246 | draggableTopbar.revalidate(); 247 | 248 | // recompute locations / sizes on parent 249 | parent.revalidate(); 250 | } 251 | 252 | void removeButton() 253 | { 254 | for (Screen screen : Screen.values()) 255 | { 256 | Widget parent = client.getWidget(screen.getParentId()); 257 | if (parent != null) 258 | { 259 | parent.deleteAllChildren(); 260 | parent.revalidate(); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/WikiSyncConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, andmcadams 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 | * 2. Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | package com.andmcadams.wikisync; 26 | 27 | import net.runelite.client.config.Config; 28 | import net.runelite.client.config.ConfigGroup; 29 | import net.runelite.client.config.ConfigItem; 30 | 31 | @ConfigGroup(WikiSyncPlugin.CONFIG_GROUP_KEY) 32 | public interface WikiSyncConfig extends Config 33 | { 34 | String WIKISYNC_VERSION_KEYNAME = "version"; 35 | String ENABLE_LOCAL_WEB_SOCKET_SERVER_KEYNAME = "enableLocalWebSocketServer"; 36 | 37 | @ConfigItem(keyName = WIKISYNC_VERSION_KEYNAME, name = "Version", description = "The last version of WikiSync used by the player", hidden = true) 38 | default int wikiSyncVersion() 39 | { 40 | return WikiSyncPlugin.VERSION; 41 | } 42 | 43 | 44 | @ConfigItem(keyName = ENABLE_LOCAL_WEB_SOCKET_SERVER_KEYNAME, 45 | name = "Enable local WebSocket server", 46 | description = "If enabled, a WebSocket server will be served on localhost to be used by the OSRS DPS calculator (and other tools in the future!).") 47 | default boolean enableLocalWebSocketServer() 48 | { 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/WikiSyncPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, andmcadams 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 | * 2. Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | package com.andmcadams.wikisync; 26 | 27 | import com.andmcadams.wikisync.dps.DpsDataFetcher; 28 | import com.andmcadams.wikisync.dps.WebSocketManager; 29 | import com.google.gson.Gson; 30 | import com.google.gson.JsonParseException; 31 | import com.google.inject.Provides; 32 | import lombok.extern.slf4j.Slf4j; 33 | import net.runelite.api.*; 34 | import net.runelite.api.events.GameStateChanged; 35 | import net.runelite.api.events.GameTick; 36 | import net.runelite.api.events.ScriptPreFired; 37 | import net.runelite.client.callback.ClientThread; 38 | import net.runelite.client.config.ConfigManager; 39 | import net.runelite.client.config.RuneScapeProfileType; 40 | import net.runelite.client.eventbus.EventBus; 41 | import net.runelite.client.eventbus.Subscribe; 42 | import net.runelite.client.events.ConfigChanged; 43 | import net.runelite.client.plugins.Plugin; 44 | import net.runelite.client.plugins.PluginDescriptor; 45 | import net.runelite.client.task.Schedule; 46 | import okhttp3.*; 47 | 48 | import javax.inject.Inject; 49 | import java.io.IOException; 50 | import java.io.InputStream; 51 | import java.io.InputStreamReader; 52 | import java.nio.charset.StandardCharsets; 53 | import java.time.temporal.ChronoUnit; 54 | import java.util.*; 55 | import java.util.concurrent.ScheduledExecutorService; 56 | import java.util.concurrent.TimeUnit; 57 | import java.util.stream.Collectors; 58 | 59 | @Slf4j 60 | @PluginDescriptor( 61 | name = "WikiSync" 62 | ) 63 | public class WikiSyncPlugin extends Plugin 64 | { 65 | @Inject 66 | private Client client; 67 | 68 | @Inject 69 | private ClientThread clientThread; 70 | 71 | @Inject 72 | private EventBus eventBus; 73 | 74 | @Inject 75 | private ConfigManager configManager; 76 | 77 | @Inject 78 | private WebSocketManager webSocketManager; 79 | 80 | @Inject 81 | private DpsDataFetcher dpsDataFetcher; 82 | 83 | @Inject 84 | private WikiSyncConfig config; 85 | 86 | @Inject 87 | private Gson gson; 88 | 89 | @Inject 90 | private OkHttpClient okHttpClient; 91 | 92 | @Inject 93 | private SyncButtonManager syncButtonManager; 94 | 95 | @Inject 96 | private ScheduledExecutorService scheduledExecutorService; 97 | 98 | private static final int SECONDS_BETWEEN_UPLOADS = 10; 99 | private static final int SECONDS_BETWEEN_MANIFEST_CHECKS = 1200; 100 | 101 | private static final String MANIFEST_URL = "https://sync.runescape.wiki/runelite/manifest"; 102 | private static final String SUBMIT_URL = "https://sync.runescape.wiki/runelite/submit"; 103 | private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); 104 | 105 | private static final int VARBITS_ARCHIVE_ID = 14; 106 | private Map varbitCompositions = new HashMap<>(); 107 | 108 | public static final String CONFIG_GROUP_KEY = "WikiSync"; 109 | // THIS VERSION SHOULD BE INCREMENTED EVERY RELEASE WHERE WE ADD A NEW TOGGLE 110 | public static final int VERSION = 1; 111 | 112 | private Manifest manifest; 113 | private Map playerDataMap = new HashMap<>(); 114 | private boolean webSocketStarted; 115 | private int cyclesSinceSuccessfulCall = 0; 116 | 117 | // Keeps track of what collection log slots the user has set. 118 | private static final BitSet clogItemsBitSet = new BitSet(); 119 | private static Integer clogItemsCount = null; 120 | // Map item ids to bit index in the bitset 121 | private static final HashMap collectionLogItemIdToBitsetIndex = new HashMap<>(); 122 | private int tickCollectionLogScriptFired = -1; 123 | private final HashSet collectionLogItemIdsFromCache = new HashSet<>(); 124 | 125 | @Provides 126 | WikiSyncConfig getConfig(ConfigManager configManager) 127 | { 128 | return configManager.getConfig(WikiSyncConfig.class); 129 | } 130 | 131 | @Override 132 | public void startUp() 133 | { 134 | clientThread.invoke(() -> { 135 | if (client.getIndexConfig() == null || client.getGameState().ordinal() < GameState.LOGIN_SCREEN.ordinal()) 136 | { 137 | log.debug("Failed to get varbitComposition, state = {}", client.getGameState()); 138 | return false; 139 | } 140 | collectionLogItemIdsFromCache.addAll(parseCacheForClog()); 141 | populateCollectionLogItemIdToBitsetIndex(); 142 | final int[] varbitIds = client.getIndexConfig().getFileIds(VARBITS_ARCHIVE_ID); 143 | for (int id : varbitIds) 144 | { 145 | varbitCompositions.put(id, client.getVarbit(id)); 146 | } 147 | return true; 148 | }); 149 | 150 | checkManifest(); 151 | if (config.enableLocalWebSocketServer()) { 152 | startUpWebSocketManager(); 153 | } 154 | syncButtonManager.startUp(); 155 | } 156 | 157 | private void startUpWebSocketManager() 158 | { 159 | webSocketManager.startUp(); 160 | eventBus.register(webSocketManager); 161 | eventBus.register(dpsDataFetcher); 162 | webSocketStarted = true; 163 | } 164 | 165 | @Override 166 | protected void shutDown() 167 | { 168 | log.debug("WikiSync stopped!"); 169 | clogItemsBitSet.clear(); 170 | clogItemsCount = null; 171 | shutDownWebSocketManager(); 172 | syncButtonManager.shutDown(); 173 | } 174 | 175 | private void shutDownWebSocketManager() 176 | { 177 | webSocketManager.shutDown(); 178 | eventBus.unregister(webSocketManager); 179 | eventBus.unregister(dpsDataFetcher); 180 | webSocketStarted = false; 181 | } 182 | 183 | /** 184 | * Finds the index this itemId is assigned to in the collections mapping. 185 | * @param itemId: The itemId to look up 186 | * @return The index of the bit that represents the given itemId, if it is in the map. -1 otherwise. 187 | */ 188 | private int lookupCollectionLogItemIndex(int itemId) { 189 | // The map has not loaded yet, or failed to load. 190 | if (collectionLogItemIdToBitsetIndex.isEmpty()) { 191 | return -1; 192 | } 193 | Integer result = collectionLogItemIdToBitsetIndex.get(itemId); 194 | if (result == null) { 195 | log.debug("Item id {} not found in the mapping of items", itemId); 196 | return -1; 197 | } 198 | return result; 199 | } 200 | 201 | @Subscribe 202 | public void onGameStateChanged(GameStateChanged event) 203 | { 204 | switch (event.getGameState()) 205 | { 206 | // When hopping, we need to clear any state related to the player 207 | case HOPPING: 208 | case LOGGING_IN: 209 | case CONNECTION_LOST: 210 | clogItemsBitSet.clear(); 211 | clogItemsCount = null; 212 | break; 213 | } 214 | } 215 | 216 | @Subscribe 217 | public void onScriptPreFired(ScriptPreFired preFired) { 218 | if (syncButtonManager.isSyncAllowed() && preFired.getScriptId() == 4100) { 219 | tickCollectionLogScriptFired = client.getTickCount(); 220 | if (collectionLogItemIdToBitsetIndex.isEmpty()) 221 | { 222 | return; 223 | } 224 | clogItemsCount = collectionLogItemIdsFromCache.size(); 225 | Object[] args = preFired.getScriptEvent().getArguments(); 226 | int itemId = (int) args[1]; 227 | int idx = lookupCollectionLogItemIndex(itemId); 228 | // We should never return -1 under normal circumstances 229 | if (idx != -1) 230 | clogItemsBitSet.set(idx); 231 | } 232 | } 233 | 234 | @Subscribe 235 | public void onGameTick(GameTick gameTick) { 236 | // Submit the collection log data two ticks after the first script prefires 237 | if (tickCollectionLogScriptFired != -1 && 238 | tickCollectionLogScriptFired + 2 > client.getTickCount()) { 239 | tickCollectionLogScriptFired = -1; 240 | if (manifest == null) { 241 | client.addChatMessage(ChatMessageType.CONSOLE, "WikiSync", "Failed to sync collection log. Try restarting the WikiSync plugin.", "WikiSync"); 242 | return; 243 | } 244 | scheduledExecutorService.execute(this::submitTask); 245 | } 246 | } 247 | 248 | @Subscribe 249 | public void onConfigChanged(ConfigChanged e) { 250 | if (e.getGroup().equals(CONFIG_GROUP_KEY)){ 251 | if (config.enableLocalWebSocketServer() != webSocketStarted) { 252 | if (config.enableLocalWebSocketServer()) { 253 | startUpWebSocketManager(); 254 | } else { 255 | shutDownWebSocketManager(); 256 | } 257 | } 258 | } 259 | } 260 | 261 | @Schedule( 262 | period = SECONDS_BETWEEN_UPLOADS, 263 | unit = ChronoUnit.SECONDS, 264 | asynchronous = true 265 | ) 266 | public void queueSubmitTask() { 267 | scheduledExecutorService.execute(this::submitTask); 268 | } 269 | 270 | synchronized public void submitTask() 271 | { 272 | // TODO: do we want other GameStates? 273 | if (client.getGameState() != GameState.LOGGED_IN || varbitCompositions.isEmpty()) 274 | { 275 | return; 276 | } 277 | 278 | if (manifest == null || client.getLocalPlayer() == null) 279 | { 280 | log.debug("Skipped due to bad manifest: {}", manifest); 281 | return; 282 | } 283 | 284 | String username = client.getLocalPlayer().getName(); 285 | RuneScapeProfileType profileType = RuneScapeProfileType.getCurrent(client); 286 | PlayerProfile profileKey = new PlayerProfile(username, profileType); 287 | 288 | PlayerData newPlayerData = getPlayerData(); 289 | PlayerData oldPlayerData = playerDataMap.computeIfAbsent(profileKey, k -> new PlayerData()); 290 | 291 | // Subtraction is done in place so newPlayerData becomes a map of only changed fields 292 | subtract(newPlayerData, oldPlayerData); 293 | if (newPlayerData.isEmpty()) 294 | { 295 | return; 296 | } 297 | submitPlayerData(profileKey, newPlayerData, oldPlayerData); 298 | } 299 | 300 | @Schedule( 301 | period = SECONDS_BETWEEN_MANIFEST_CHECKS, 302 | unit = ChronoUnit.SECONDS, 303 | asynchronous = true 304 | ) 305 | public void manifestTask() 306 | { 307 | if (client.getGameState() == GameState.LOGGED_IN) 308 | { 309 | checkManifest(); 310 | } 311 | } 312 | 313 | 314 | private int getVarbitValue(int varbitId) 315 | { 316 | VarbitComposition v = varbitCompositions.get(varbitId); 317 | if (v == null) 318 | { 319 | return -1; 320 | } 321 | 322 | int value = client.getVarpValue(v.getIndex()); 323 | int lsb = v.getLeastSignificantBit(); 324 | int msb = v.getMostSignificantBit(); 325 | int mask = (1 << ((msb - lsb) + 1)) - 1; 326 | return (value >> lsb) & mask; 327 | } 328 | 329 | private PlayerData getPlayerData() 330 | { 331 | PlayerData out = new PlayerData(); 332 | for (int varbitId : manifest.varbits) 333 | { 334 | out.varb.put(varbitId, getVarbitValue(varbitId)); 335 | } 336 | for (int varpId : manifest.varps) 337 | { 338 | out.varp.put(varpId, client.getVarpValue(varpId)); 339 | } 340 | for(Skill s : Skill.values()) 341 | { 342 | out.level.put(s.getName(), client.getRealSkillLevel(s)); 343 | } 344 | out.collectionLogSlots = Base64.getEncoder().encodeToString(clogItemsBitSet.toByteArray()); 345 | out.collectionLogItemCount = clogItemsCount; 346 | return out; 347 | } 348 | 349 | private void subtract(PlayerData newPlayerData, PlayerData oldPlayerData) 350 | { 351 | oldPlayerData.varb.forEach(newPlayerData.varb::remove); 352 | oldPlayerData.varp.forEach(newPlayerData.varp::remove); 353 | oldPlayerData.level.forEach(newPlayerData.level::remove); 354 | if (newPlayerData.collectionLogSlots.equals(oldPlayerData.collectionLogSlots)) 355 | newPlayerData.clearCollectionLog(); 356 | } 357 | 358 | private void merge(PlayerData oldPlayerData, PlayerData delta) 359 | { 360 | oldPlayerData.varb.putAll(delta.varb); 361 | oldPlayerData.varp.putAll(delta.varp); 362 | oldPlayerData.level.putAll(delta.level); 363 | oldPlayerData.collectionLogSlots = delta.collectionLogSlots; 364 | oldPlayerData.collectionLogItemCount = delta.collectionLogItemCount; 365 | } 366 | 367 | private void submitPlayerData(PlayerProfile profileKey, PlayerData delta, PlayerData old) 368 | { 369 | // If cyclesSinceSuccessfulCall is not a perfect square, we should not try to submit. 370 | // This gives us quadratic backoff. 371 | cyclesSinceSuccessfulCall += 1; 372 | if (Math.pow((int) Math.sqrt(cyclesSinceSuccessfulCall), 2) != cyclesSinceSuccessfulCall) 373 | { 374 | return; 375 | } 376 | 377 | PlayerDataSubmission submission = new PlayerDataSubmission( 378 | profileKey.getUsername(), 379 | profileKey.getProfileType().name(), 380 | delta 381 | ); 382 | 383 | Request request = new Request.Builder() 384 | .url(SUBMIT_URL) 385 | .post(RequestBody.create(JSON, gson.toJson(submission))) 386 | .build(); 387 | 388 | Call call = okHttpClient.newCall(request); 389 | call.timeout().timeout(3, TimeUnit.SECONDS); 390 | call.enqueue(new Callback() 391 | { 392 | @Override 393 | public void onFailure(Call call, IOException e) 394 | { 395 | log.debug("Failed to submit: ", e); 396 | } 397 | 398 | @Override 399 | public void onResponse(Call call, Response response) 400 | { 401 | try 402 | { 403 | if (!response.isSuccessful()) { 404 | log.debug("Failed to submit: {}", response.code()); 405 | return; 406 | } 407 | merge(old, delta); 408 | cyclesSinceSuccessfulCall = 0; 409 | } 410 | finally 411 | { 412 | response.close(); 413 | } 414 | } 415 | }); 416 | } 417 | 418 | private void checkManifest() 419 | { 420 | Request request = new Request.Builder() 421 | .url(MANIFEST_URL) 422 | .build(); 423 | okHttpClient.newCall(request).enqueue(new Callback() 424 | { 425 | @Override 426 | public void onFailure(Call call, IOException e) 427 | { 428 | log.debug("Failed to get manifest: ", e); 429 | } 430 | 431 | @Override 432 | public void onResponse(Call call, Response response) 433 | { 434 | try 435 | { 436 | if (!response.isSuccessful()) 437 | { 438 | log.debug("Failed to get manifest: {}", response.code()); 439 | return; 440 | } 441 | InputStream in = response.body().byteStream(); 442 | manifest = gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), Manifest.class); 443 | populateCollectionLogItemIdToBitsetIndex(); 444 | } 445 | catch (JsonParseException e) 446 | { 447 | log.debug("Failed to parse manifest: ", e); 448 | } 449 | finally 450 | { 451 | response.close(); 452 | } 453 | } 454 | }); 455 | } 456 | 457 | @Schedule( 458 | period = 30, 459 | unit = ChronoUnit.SECONDS, 460 | asynchronous = true 461 | ) 462 | public void scheduledEnsureDpsWsActive() 463 | { 464 | log.debug("ensuring active!!"); 465 | if (webSocketStarted) 466 | { 467 | webSocketManager.ensureActive(); 468 | } 469 | } 470 | 471 | private void populateCollectionLogItemIdToBitsetIndex() 472 | { 473 | if (manifest == null) 474 | { 475 | log.debug("Manifest is not present so the collection log bitset index will not be updated"); 476 | return; 477 | } 478 | clientThread.invoke(() -> { 479 | // Add missing keys in order to the map. Order is extremely important here so 480 | // we get a stable map given the same cache data. 481 | List itemIdsMissingFromManifest = collectionLogItemIdsFromCache 482 | .stream() 483 | .filter((t) -> !manifest.collections.contains(t)) 484 | .sorted() 485 | .collect(Collectors.toList()); 486 | 487 | int currentIndex = 0; 488 | collectionLogItemIdToBitsetIndex.clear(); 489 | for (Integer itemId : manifest.collections) 490 | collectionLogItemIdToBitsetIndex.put(itemId, currentIndex++); 491 | for (Integer missingItemId : itemIdsMissingFromManifest) { 492 | collectionLogItemIdToBitsetIndex.put(missingItemId, currentIndex++); 493 | } 494 | }); 495 | } 496 | 497 | /** 498 | * Parse the enums and structs in the cache to figure out which item ids 499 | * exist in the collection log. This can be diffed with the manifest to 500 | * determine the item ids that need to be appended to the end of the 501 | * bitset we send to the WikiSync server. 502 | */ 503 | private HashSet parseCacheForClog() 504 | { 505 | HashSet itemIds = new HashSet<>(); 506 | // 2102 - Struct that contains the highest level tabs in the collection log (Bosses, Raids, etc) 507 | // https://chisel.weirdgloop.org/structs/index.html?type=enums&id=2102 508 | int[] topLevelTabStructIds = client.getEnum(2102).getIntVals(); 509 | for (int topLevelTabStructIndex : topLevelTabStructIds) 510 | { 511 | // The collection log top level tab structs contain a param that points to the enum 512 | // that contains the pointers to sub tabs. 513 | // ex: https://chisel.weirdgloop.org/structs/index.html?type=structs&id=471 514 | StructComposition topLevelTabStruct = client.getStructComposition(topLevelTabStructIndex); 515 | 516 | // Param 683 contains the pointer to the enum that contains the subtabs ids 517 | // ex: https://chisel.weirdgloop.org/structs/index.html?type=enums&id=2103 518 | int[] subtabStructIndices = client.getEnum(topLevelTabStruct.getIntValue(683)).getIntVals(); 519 | for (int subtabStructIndex : subtabStructIndices) { 520 | 521 | // The subtab structs are for subtabs in the collection log (Commander Zilyana, Chambers of Xeric, etc.) 522 | // and contain a pointer to the enum that contains all the item ids for that tab. 523 | // ex subtab struct: https://chisel.weirdgloop.org/structs/index.html?type=structs&id=476 524 | // ex subtab enum: https://chisel.weirdgloop.org/structs/index.html?type=enums&id=2109 525 | StructComposition subtabStruct = client.getStructComposition(subtabStructIndex); 526 | int[] clogItems = client.getEnum(subtabStruct.getIntValue(690)).getIntVals(); 527 | for (int clogItemId : clogItems) itemIds.add(clogItemId); 528 | } 529 | } 530 | 531 | // Some items with data saved on them have replacements to fix a duping issue (satchels, flamtaer bag) 532 | // Enum 3721 contains a mapping of the item ids to replace -> ids to replace them with 533 | EnumComposition replacements = client.getEnum(3721); 534 | for (int badItemId : replacements.getKeys()) 535 | itemIds.remove(badItemId); 536 | for (int goodItemId : replacements.getIntVals()) 537 | itemIds.add(goodItemId); 538 | 539 | return itemIds; 540 | } 541 | 542 | } 543 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/DpsDataFetcher.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps; 2 | 3 | import com.andmcadams.wikisync.dps.messages.response.UsernameChanged; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonObject; 6 | import java.util.Objects; 7 | import javax.annotation.Nullable; 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | import lombok.Getter; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import net.runelite.api.Client; 14 | import net.runelite.api.EquipmentInventorySlot; 15 | import net.runelite.api.GameState; 16 | import net.runelite.api.Item; 17 | import net.runelite.api.ItemContainer; 18 | import net.runelite.api.Player; 19 | import net.runelite.api.Skill; 20 | import net.runelite.api.events.GameStateChanged; 21 | import net.runelite.api.events.GameTick; 22 | import net.runelite.api.gameval.InventoryID; 23 | import net.runelite.api.gameval.ItemID; 24 | import net.runelite.api.gameval.VarPlayerID; 25 | import net.runelite.api.gameval.VarbitID; 26 | import net.runelite.client.eventbus.EventBus; 27 | import net.runelite.client.eventbus.Subscribe; 28 | 29 | @Slf4j 30 | @Singleton 31 | @RequiredArgsConstructor(onConstructor_ = @Inject) 32 | public class DpsDataFetcher 33 | { 34 | 35 | private final Client client; 36 | private final EventBus eventBus; 37 | 38 | @Getter 39 | private String username; 40 | 41 | @Subscribe 42 | public void onGameTick(GameTick e) 43 | { 44 | checkUsername(); 45 | } 46 | 47 | @Subscribe 48 | public void onGameStateChanged(GameStateChanged e) 49 | { 50 | checkUsername(); 51 | } 52 | 53 | private void checkUsername() 54 | { 55 | String currentName = null; 56 | if (client.getGameState() == GameState.LOGGED_IN) 57 | { 58 | Player p = client.getLocalPlayer(); 59 | if (p != null) 60 | { 61 | currentName = p.getName(); 62 | } 63 | } 64 | 65 | if (!Objects.equals(this.username, currentName)) 66 | { 67 | log.debug("WS player name changed prev=[{}] next=[{}]", this.username, currentName); 68 | this.username = currentName; 69 | eventBus.post(new UsernameChanged(this.username)); 70 | } 71 | } 72 | 73 | // TODO: Delete this once the Wiki plugin service exists. See https://github.com/runelite/runelite/pull/17524 74 | // This is directly copied from https://github.com/runelite/runelite/pull/17524/files#diff-141a15aba5d017de9818b5d39722f85f95b330ef96f8eb06103a947c1094b905 75 | @Nullable 76 | private JsonObject createEquipmentObject(ItemContainer itemContainer, EquipmentInventorySlot slot) 77 | { 78 | if (itemContainer == null) 79 | { 80 | return null; 81 | } 82 | 83 | if (slot == EquipmentInventorySlot.BOOTS && itemContainer.count() == 1 && itemContainer.contains(ItemID.CHEFS_HAT)) 84 | { 85 | JsonObject o = new JsonObject(); 86 | o.addProperty("id", ItemID.TEMPLETREK_SNAIL_SHELL); 87 | return o; 88 | } 89 | 90 | Item item = itemContainer.getItem(slot.getSlotIdx()); 91 | if (item != null) 92 | { 93 | JsonObject o = new JsonObject(); 94 | o.addProperty("id", item.getId()); 95 | return o; 96 | } 97 | return null; 98 | } 99 | 100 | // TODO: Delete this once the Wiki plugin service exists. See https://github.com/runelite/runelite/pull/17524 101 | // This is directly copied from https://github.com/runelite/runelite/pull/17524/files#diff-141a15aba5d017de9818b5d39722f85f95b330ef96f8eb06103a947c1094b905 102 | public JsonObject buildShortlinkData() 103 | { 104 | JsonObject j = new JsonObject(); 105 | 106 | // Build the player's loadout data 107 | JsonArray loadouts = new JsonArray(); 108 | ItemContainer eqContainer = client.getItemContainer(InventoryID.WORN); 109 | 110 | JsonObject l = new JsonObject(); 111 | JsonObject eq = new JsonObject(); 112 | 113 | eq.add("ammo", createEquipmentObject(eqContainer, EquipmentInventorySlot.AMMO)); 114 | eq.add("body", createEquipmentObject(eqContainer, EquipmentInventorySlot.BODY)); 115 | eq.add("cape", createEquipmentObject(eqContainer, EquipmentInventorySlot.CAPE)); 116 | eq.add("feet", createEquipmentObject(eqContainer, EquipmentInventorySlot.BOOTS)); 117 | eq.add("hands", createEquipmentObject(eqContainer, EquipmentInventorySlot.GLOVES)); 118 | eq.add("head", createEquipmentObject(eqContainer, EquipmentInventorySlot.HEAD)); 119 | eq.add("legs", createEquipmentObject(eqContainer, EquipmentInventorySlot.LEGS)); 120 | eq.add("neck", createEquipmentObject(eqContainer, EquipmentInventorySlot.AMULET)); 121 | eq.add("ring", createEquipmentObject(eqContainer, EquipmentInventorySlot.RING)); 122 | eq.add("shield", createEquipmentObject(eqContainer, EquipmentInventorySlot.SHIELD)); 123 | eq.add("weapon", createEquipmentObject(eqContainer, EquipmentInventorySlot.WEAPON)); 124 | l.add("equipment", eq); 125 | 126 | JsonObject skills = new JsonObject(); 127 | skills.addProperty("atk", client.getRealSkillLevel(Skill.ATTACK)); 128 | skills.addProperty("def", client.getRealSkillLevel(Skill.DEFENCE)); 129 | skills.addProperty("hp", client.getRealSkillLevel(Skill.HITPOINTS)); 130 | skills.addProperty("magic", client.getRealSkillLevel(Skill.MAGIC)); 131 | skills.addProperty("mining", client.getRealSkillLevel(Skill.MINING)); 132 | skills.addProperty("prayer", client.getRealSkillLevel(Skill.PRAYER)); 133 | skills.addProperty("ranged", client.getRealSkillLevel(Skill.RANGED)); 134 | skills.addProperty("str", client.getRealSkillLevel(Skill.STRENGTH)); 135 | l.add("skills", skills); 136 | 137 | JsonObject buffs = new JsonObject(); 138 | buffs.addProperty("inWilderness", client.getVarbitValue(VarbitID.INSIDE_WILDERNESS) == 1); 139 | buffs.addProperty("kandarinDiary", client.getVarbitValue(VarbitID.KANDARIN_DIARY_HARD_COMPLETE) == 1); 140 | buffs.addProperty("onSlayerTask", client.getVarpValue(VarPlayerID.SLAYER_COUNT) > 0); 141 | buffs.addProperty("chargeSpell", client.getVarpValue(VarPlayerID.MAGEARENA_CHARGE) > 0); 142 | l.add("buffs", buffs); 143 | 144 | l.addProperty("name", client.getLocalPlayer().getName()); 145 | 146 | loadouts.add(l); 147 | j.add("loadouts", loadouts); 148 | 149 | return j; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/WebSocketManager.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps; 2 | 3 | import com.andmcadams.wikisync.dps.messages.response.GetPlayer; 4 | import com.andmcadams.wikisync.dps.messages.Request; 5 | import com.andmcadams.wikisync.dps.messages.response.UsernameChanged; 6 | import com.andmcadams.wikisync.dps.ws.WSHandler; 7 | import com.andmcadams.wikisync.dps.ws.WSWebsocketServer; 8 | import com.google.common.collect.ImmutableSet; 9 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 10 | import com.google.gson.Gson; 11 | import com.google.gson.JsonObject; 12 | import java.net.URI; 13 | import java.net.URISyntaxException; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | import java.util.concurrent.atomic.AtomicBoolean; 19 | import javax.inject.Inject; 20 | import javax.inject.Singleton; 21 | import lombok.RequiredArgsConstructor; 22 | import lombok.extern.slf4j.Slf4j; 23 | import net.runelite.client.callback.ClientThread; 24 | import net.runelite.client.eventbus.Subscribe; 25 | import org.java_websocket.WebSocket; 26 | import org.java_websocket.handshake.ClientHandshake; 27 | 28 | @Slf4j 29 | @Singleton 30 | @RequiredArgsConstructor(onConstructor_ = @Inject) 31 | public class WebSocketManager implements WSHandler 32 | { 33 | 34 | private final static int PORT_MIN = 37767; 35 | private final static int PORT_MAX = 37776; 36 | 37 | private final static Set ALLOWED_ORIGIN_HOSTS = ImmutableSet.of("localhost", "dps.osrs.wiki", "tools.runescape.wiki"); 38 | 39 | private final AtomicBoolean serverActive = new AtomicBoolean(false); 40 | 41 | private final Gson gson; 42 | private final DpsDataFetcher dpsDataFetcher; 43 | 44 | private int nextPort; 45 | 46 | private WSWebsocketServer server; 47 | 48 | @Inject 49 | private ClientThread clientThread; 50 | 51 | private static final ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("wikisync-dps-manager-%d").build()); 52 | 53 | public void startUp() 54 | { 55 | this.nextPort = PORT_MIN; 56 | // Just in case we are in a bad state, let's try to stop any active server. 57 | stopServer(); 58 | ensureActive(); 59 | } 60 | 61 | public void shutDown() 62 | { 63 | log.debug("Shutting down WikiSync Websocket Manager. Server active = {}", serverActive.getPlain()); 64 | stopServer(); 65 | } 66 | 67 | /** 68 | * If a server is not currently running or starting, then try to start a new server. If a server is currently 69 | * running or starting, then do nothing. Then method is meant to be called regularly, so it will happily do nothing 70 | * on any specific run. 71 | */ 72 | public void ensureActive() 73 | { 74 | if (!serverActive.compareAndExchange(false, true)) 75 | { 76 | this.server = new WSWebsocketServer(this.nextPort++, this); 77 | this.server.start(); 78 | log.debug("WSWSS attempting to start at: {}", this.server.getAddress()); 79 | if (this.nextPort > PORT_MAX) { 80 | this.nextPort = PORT_MIN; 81 | } 82 | } 83 | } 84 | 85 | @Subscribe 86 | public void onUsernameChanged(UsernameChanged e) 87 | { 88 | if (serverActive.get()) 89 | { 90 | executorService.submit(()->{ 91 | this.server.broadcast(gson.toJson(e)); 92 | }); 93 | } 94 | } 95 | 96 | @Override 97 | public void onOpen(WebSocket conn, ClientHandshake handshake) 98 | { 99 | // Validate that only trusted sources are allowed to connect. This is not foolproof, but it should catch 100 | // unauthorized access from any major browser. 101 | String requestPath = conn.getResourceDescriptor(); 102 | String origin = handshake.getFieldValue("origin"); 103 | log.debug("Received new WebSocket request. requestPath: {}, origin: {}", requestPath, origin); 104 | if (!Objects.equals(requestPath, "/")) { 105 | log.error("Unknown requestPath: {}", requestPath); 106 | conn.close(); 107 | return; 108 | } 109 | try 110 | { 111 | URI originUri = new URI(origin); 112 | String host = originUri.getHost(); 113 | if (!ALLOWED_ORIGIN_HOSTS.contains(host)) { 114 | log.error("Unauthorized origin: {}", host); 115 | conn.close(); 116 | return; 117 | } 118 | } 119 | catch (URISyntaxException e) 120 | { 121 | log.error("Could not parse origin: {}", (Object) e); 122 | conn.close(); 123 | return; 124 | } 125 | 126 | // This connection appears to be valid! 127 | conn.send(gson.toJson(new UsernameChanged(dpsDataFetcher.getUsername()))); 128 | } 129 | 130 | @Override 131 | public void onMessage(WebSocket conn, String message) 132 | { 133 | Request request = gson.fromJson(message, Request.class); 134 | switch (request.get_wsType()) { 135 | case GetPlayer: 136 | clientThread.invokeLater(() -> { 137 | JsonObject payload = dpsDataFetcher.buildShortlinkData(); 138 | executorService.submit(()->{ 139 | conn.send(gson.toJson(new GetPlayer(request.getSequenceId(), payload))); 140 | }); 141 | }); 142 | break; 143 | default: 144 | log.debug("Got request with no handler."); 145 | break; 146 | } 147 | } 148 | 149 | 150 | @Override 151 | public void onError(WebSocket conn, Exception ex) 152 | { 153 | log.debug("ws error conn=[{}]", conn == null ? null : conn.getLocalSocketAddress(), ex); 154 | // `conn == null` signals the error is related to the whole server, not just a specific connection. 155 | if (conn == null) 156 | { 157 | log.debug("failed to bind to port, trying next"); 158 | stopServer(); 159 | // Immediately trying a new port is okay to do once for each port, but we do not want to continuously try to 160 | // spawn servers in a tight loop if something goes wrong. If the attempted ports have wrapped around back to 161 | // `PORT_MIN`, then we can stop attempting to start servers and wait for the next scheduled call to 162 | // `ensureActive`. 163 | if (this.nextPort != PORT_MIN) 164 | { 165 | ensureActive(); 166 | } 167 | } 168 | } 169 | 170 | @Override 171 | public void onStart() 172 | { 173 | log.debug("Started! Port: {}", server.getPort()); 174 | } 175 | 176 | private void stopServer() 177 | { 178 | try 179 | { 180 | if (this.server != null) 181 | { 182 | try 183 | { 184 | this.server.stop(); 185 | } 186 | catch (InterruptedException e) 187 | { 188 | // ignored 189 | } 190 | finally 191 | { 192 | this.server = null; 193 | } 194 | } 195 | } finally 196 | { 197 | this.serverActive.set(false); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/messages/Request.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.messages; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Request 7 | { 8 | RequestType _wsType; 9 | int sequenceId; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/messages/RequestType.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.messages; 2 | 3 | public enum RequestType 4 | { 5 | UsernameChanged, 6 | GetPlayer 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/messages/response/GetPlayer.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.messages.response; 2 | 3 | import com.andmcadams.wikisync.dps.messages.RequestType; 4 | import com.google.gson.JsonObject; 5 | import lombok.Value; 6 | 7 | @Value 8 | public class GetPlayer 9 | { 10 | RequestType _wsType = RequestType.GetPlayer; 11 | int sequenceId; 12 | JsonObject payload; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/messages/response/UsernameChanged.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.messages.response; 2 | 3 | import com.andmcadams.wikisync.dps.messages.RequestType; 4 | import lombok.Value; 5 | 6 | @Value 7 | public class UsernameChanged 8 | { 9 | 10 | RequestType _wsType = RequestType.UsernameChanged; 11 | String username; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/ws/WSHandler.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.ws; 2 | 3 | import org.java_websocket.WebSocket; 4 | import org.java_websocket.handshake.ClientHandshake; 5 | 6 | public interface WSHandler 7 | { 8 | 9 | default void onOpen(WebSocket conn, ClientHandshake handshake) {} 10 | default void onClose(WebSocket conn, int code, String reason, boolean remote) {} 11 | default void onMessage(WebSocket conn, String message) {} 12 | default void onError(WebSocket conn, Exception ex) {} 13 | default void onStart() {} 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/andmcadams/wikisync/dps/ws/WSWebsocketServer.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync.dps.ws; 2 | 3 | import java.net.InetSocketAddress; 4 | import org.java_websocket.WebSocket; 5 | import org.java_websocket.handshake.ClientHandshake; 6 | import org.java_websocket.server.WebSocketServer; 7 | 8 | public class WSWebsocketServer extends WebSocketServer 9 | { 10 | 11 | private final WSHandler handler; 12 | 13 | public WSWebsocketServer(int port, WSHandler handler) 14 | { 15 | super(new InetSocketAddress("127.0.0.1", port), 1); 16 | this.setDaemon(true); 17 | this.handler = handler; 18 | } 19 | 20 | @Override 21 | public void onOpen(WebSocket conn, ClientHandshake handshake) 22 | { 23 | this.handler.onOpen(conn, handshake); 24 | } 25 | 26 | @Override 27 | public void onClose(WebSocket conn, int code, String reason, boolean remote) 28 | { 29 | this.handler.onClose(conn, code, reason, remote); 30 | } 31 | 32 | @Override 33 | public void onMessage(WebSocket conn, String message) 34 | { 35 | this.handler.onMessage(conn, message); 36 | } 37 | 38 | @Override 39 | public void onError(WebSocket conn, Exception ex) 40 | { 41 | this.handler.onError(conn, ex); 42 | } 43 | 44 | @Override 45 | public void onStart() 46 | { 47 | this.handler.onStart(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/andmcadams/wikisync/WikiSyncLogPlugin.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import javax.inject.Singleton; 5 | import net.runelite.client.plugins.Plugin; 6 | import net.runelite.client.plugins.PluginDescriptor; 7 | import ch.qos.logback.classic.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | @Singleton 11 | @PluginDescriptor( 12 | name = "[Debug] WikiSync Logging" 13 | ) 14 | public class WikiSyncLogPlugin extends Plugin 15 | { 16 | 17 | @Override 18 | protected void startUp() 19 | { 20 | ((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(Level.WARN); 21 | ((Logger) LoggerFactory.getLogger("com.andmcadams.wikisync")).setLevel(Level.DEBUG); 22 | } 23 | 24 | @Override 25 | protected void shutDown() 26 | { 27 | ((Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)).setLevel(Level.DEBUG); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/andmcadams/wikisync/WikiSyncPluginTest.java: -------------------------------------------------------------------------------- 1 | package com.andmcadams.wikisync; 2 | 3 | import net.runelite.client.RuneLite; 4 | import net.runelite.client.externalplugins.ExternalPluginManager; 5 | 6 | public class WikiSyncPluginTest 7 | { 8 | public static void main(String[] args) throws Exception 9 | { 10 | ExternalPluginManager.loadBuiltin(WikiSyncPlugin.class, WikiSyncLogPlugin.class); 11 | RuneLite.main(args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /stub_server.py: -------------------------------------------------------------------------------- 1 | from wsgiref.simple_server import make_server 2 | 3 | import falcon 4 | 5 | 6 | class TestResource: 7 | 8 | def on_get(self, req: falcon.request.Request, resp: falcon.response.Response): 9 | """Handles GET requests""" 10 | # 4101 is the first prayer, toggle it to send data to the server 11 | resp.media = { 12 | 'varbits': [0, 100, 9657, 4101, 5000, 10000, 4104], 13 | 'varps': [1, 3, 5, 6, 7, 10], 14 | 'collections': [], 15 | 'version': 4 16 | } 17 | resp.status = falcon.HTTP_200 18 | return resp 19 | 20 | def on_post(self, req, resp): 21 | print(req.media) 22 | resp.status = falcon.HTTP_200 23 | 24 | def on_get_check(self, req, resp): 25 | resp.media = { 26 | 'version': 4 27 | } 28 | resp.status = falcon.HTTP_200 29 | return resp 30 | 31 | 32 | def create_app(): 33 | app = falcon.App() 34 | # Resources are represented by long-lived class instances 35 | t = TestResource() 36 | app.add_route('/manifest', t) 37 | app.add_route('/check_manifest', t, suffix='check') 38 | app.add_route('/submit', t) 39 | return app 40 | 41 | 42 | if __name__ == '__main__': 43 | port = 8484 44 | with make_server('', port, create_app()) as httpd: 45 | print(f'Serving on port {port}...') 46 | 47 | # Serve until process is killed 48 | httpd.serve_forever() --------------------------------------------------------------------------------