├── .babelrc ├── demo.jpg ├── inspec.jpg ├── jump1.gif ├── .gitignore ├── config.js ├── package.json ├── LICENSE ├── README.md └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | 5 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqs469/jump/HEAD/demo.jpg -------------------------------------------------------------------------------- /inspec.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqs469/jump/HEAD/inspec.jpg -------------------------------------------------------------------------------- /jump1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqs469/jump/HEAD/jump1.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | findNode.js 3 | sharp.js 4 | index.html 5 | tmp.* 6 | screenshot.* 7 | node_modules 8 | *.lock 9 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 2 | // Waiting for you. 3 | 4 | const iphone7 = { b: 55.229, k: 188.815, top: 300 } 5 | const iphoneX = { b: 141, k: 308.205, top:600 } 6 | const iphone6 = {} 7 | const iphone6p = {} 8 | const iphoneSE = { b: 46.811, k: 160.011, top: 300 } 9 | const iphone8 = {} 10 | const iphone8p = {} 11 | 12 | 13 | const config = iphone7 14 | export default config 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jump", 3 | "version": "1.0.0", 4 | "description": "Jump", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "babel-node index.js", 9 | "findNode": "babel-node findNode.js", 10 | "sharp": "babel-node sharp.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "lqs469", 14 | "license": "MIT", 15 | "dependencies": { 16 | "get-pixels": "^3.3.0", 17 | "ndarray-unpack": "^1.0.0", 18 | "node-fetch": "^3.2.10", 19 | "save-pixels": "^2.3.6", 20 | "sharp": "^0.18.4" 21 | }, 22 | "devDependencies": { 23 | "babel-cli": "^6.26.0", 24 | "babel-preset-es2015": "^6.24.1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:lqs469/jump.git" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LiQinshuo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 微信跳一跳全自动化“辅助”工具ios版 2 | 3 | 这段时间微信跳一跳这个小游戏挺火的,各种“辅助工具”也是层出不穷... 4 | 5 | 6 | 那我也来写一个好啦...现已实现完全自动化. ps: 这是外挂,好孩子不要用..... 7 | 8 | 9 | ![](./demo.jpg) 10 | ![](./jump1.gif) 11 | 12 | 13 | 14 | 15 | 16 | ## 需要环境 17 | (括号里是我使用的版本, 别的版本未经实验不保证成功, 机型: Iphone7) 18 | - Nodejs (v9.3.0) 19 | - NPM (v5.5.1) 20 | - xcode (v9.2) 21 | - WebDriverAgent 22 | 23 | 24 | ## 准备步骤 25 | 前两个就不说了,安装Node和NPM推荐使用[NVM](https://github.com/creationix/nvm) 26 | 27 | 28 | [WebDriverAgent](https://github.com/facebook/WebDriverAgent)是Facebook出的一套iOS的WebDriver工具, 简单来说可以实时把连接的iOS设备状态模拟到一个Web服务器上, 提供一系列调试接口. 更多信息可以在项目主页里看到, 安装过程为: 29 | 1. 克隆到本地 30 | ``` 31 | git clone https://github.com/facebook/WebDriverAgent.git 32 | ``` 33 | 2. 进入文件夹运行脚本 34 | ``` 35 | ./Scripts/bootstrap.sh 36 | ``` 37 | 脚本运行需要[`Carthage`](https://github.com/Carthage/Carthage)和`npm`, 这两个都是用来安装WDA的依赖包的, 看样子是依赖了node和iOS两种生态, 分别负责原生和Web两个模块, 安装`carthage`直接 `brew install carthage`. 38 | 39 | 40 | 3. 用Xcode打开文件夹里的 `WebDriverAgent.xcodeproj` 41 | 42 | 43 | 4. 如果是真机调试, 连接iOS设备后需要iOS开发者签名, 这一部分不做说明, 跟正常iOS开发一样, 需要注意的是, 要对项目内`WebDriverAgentLib`, `WebDriverRunner`, `integrationApp`都需填入开发者签名. 提供一个很好的中文参考文档[iOS 真机如何安装 WebDriverAgent](https://testerhome.com/topics/7220). 还有就是国行的iOS设备需要端口转发才能访问, 需要将手机的端口转发到Mac上. 使用工具`imobiledevice`即可 44 | ``` 45 | $ brew install libimobiledevice 46 | $ iproxy 8100 8100 47 | ``` 48 | 49 | 50 | 运行WDA的test: [Product] -> [test], 或者`CMD + U`. 之后iOS设备会安装一个WebDriverAgent App, 自动打开黑屏然后自动退出, 期间不用管它, 如果迟迟不退出, 有可能是失败了, 断开设备重连再运行test(还是失败可以删除App再试一试). 在App退出之后就可以正确运行js脚本了. 51 | 52 | 53 | 然后在项目根目录安装npm依赖(单独目录clone本项目, 和WDA结构上是独立分开的...), 运行: 54 | ``` 55 | npm i 56 | ``` 57 | 58 | 59 | ## 运行脚本 60 | WDA启动之后, 可以尝试打开`http://127.0.0.1:8100/status`查看设备状态, 正常情况会有JSON返回, 打开`http://127.0.0.1:8100/inspector`会是这样 61 | ![](./inspec.jpg) 62 | 63 | 64 | 其他API看[这里](https://github.com/facebook/WebDriverAgent/wiki/Queries), 其实这里的Wiki也不全, 我看过WDA源码之后找到这个[文件](https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBElementCommands.m#L60), 里面应该是完整的API代码, 比较语义化, 应该能看得懂(主要会用到`screenshot`和`touchAndHold`的API). 65 | 66 | 67 | 一切正常之后, 打开微信跳一跳, 开始游戏后在本项目根目录运行 68 | ``` 69 | npm start 70 | ``` 71 | 72 | 73 | --- 74 | 75 | ~~目前的精度还不能100%都完美落在中心点, 大概多次之后会有一点点误差, 后续会继续改进算法. (Ps: 欢迎和我谈论改进, 乐趣在开发过程, 所以我自己也就几百分就没挂着了, 不知道后面几万分会怎么样...)~~ 76 | 77 | 已经改进算法, 提升精度, 防作弊. 放心使用, 78 | 79 | 先写这么多, 详细的算法介绍接下来有时间就写, 欢迎提issue或直接找我. 80 | 81 | --- 82 | 83 | ### 关于不同设备的参数 84 | 85 | 配置文件 `/config.json` 86 | 87 | 我使用的iphone7, 也是配置文件中默认参数, 欢迎其他设备的同学提供对应的设备参数(PR, issue均可). 88 | 89 | - [X] iPhoneX 90 | - [X] iPhone7 91 | - [X] iPhoneSE // 由Lobster-King提供 92 | - [ ] iPhone7P 93 | - [ ] iPhone6P 94 | - [ ] iPhone8 95 | - [ ] iPhone8P 96 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import fetch from 'node-fetch' 3 | import getPixels from 'get-pixels' 4 | import savePixels from 'save-pixels' 5 | import sharp from 'sharp' 6 | import config from './config.js' 7 | 8 | const url = 'http://127.0.0.1:8100' 9 | var sid = '' 10 | var source = '' 11 | var jumpBtnId = '' 12 | var screenshot = '' 13 | var i = 0 14 | 15 | const get = (u, o) => fetch(u, o).then(res => res.json()) 16 | 17 | function getSId () { 18 | return new Promise(resolve => { 19 | get(url + '/status').then(data => { 20 | resolve(data.sessionId) 21 | }) 22 | }) 23 | } 24 | 25 | function getSource () { 26 | return new Promise(resolve => { 27 | get(url + '/source').then(data => { 28 | resolve(data) 29 | }) 30 | }) 31 | } 32 | 33 | function getJumpBtn () { 34 | return new Promise(resolve => { 35 | get(`${url}/session/${sid}/elements`, { 36 | method: 'POST', 37 | headers: { 38 | 'Content-Type': 'application/json' 39 | }, 40 | body: JSON.stringify({ 41 | using: 'class name', 42 | value: 'XCUIElementTypeStaticText' 43 | }) 44 | }).then(data => resolve(data.value[0].ELEMENT)) 45 | }) 46 | } 47 | 48 | function getScreenshot () { 49 | return new Promise(resolve => { 50 | get(`${url}/screenshot`) 51 | .then(data => resolve(data.value)) 52 | }) 53 | } 54 | 55 | function jump (s) { 56 | return new Promise(resolve => { 57 | get(`${url}/session/${sid}/wda/element/${jumpBtnId}/touchAndHold`, { 58 | method: 'POST', 59 | headers: { 60 | 'Content-Type': 'application/json' 61 | }, 62 | body: JSON.stringify({ 63 | duration: s 64 | }) 65 | }).then(data => { 66 | resolve(data) 67 | }) 68 | }) 69 | } 70 | 71 | function saveImg (screenshot, filename) { 72 | return new Promise(resolve => { 73 | screenshot += screenshot.replace('+', ' ') 74 | screenshot = new Buffer(screenshot, 'base64').toString('binary') 75 | fs.writeFile(filename, screenshot, 'binary', function (err) { 76 | if (err) throw err 77 | console.log(filename, 'saved.') 78 | resolve() 79 | }) 80 | }) 81 | } 82 | 83 | function getI (filename) { 84 | return new Promise(resolve => { 85 | getPixels(filename, function(err, p) { 86 | if (err) { 87 | console.log('Bad image path') 88 | return 89 | } 90 | let iX = 0 91 | let iY = 0 92 | let iN = 0 93 | for (let i = 100; i < p.shape[0] - 100; i++) { 94 | for (let j = 300; j < p.shape[1] - 300; j++) { 95 | const r = p.get(i, j, 0) 96 | const g = p.get(i, j, 1) 97 | const b = p.get(i, j, 2) 98 | 99 | // if (r < 40 && r > 33 && g < 40 && g > 33 && b < 40 && b > 33) { 100 | // if (r < 65 && r > 55 && g < 60 && g > 50 && b < 95 && b > 85) { 101 | if (inGradient('#413778', '#322364', r, g, b)) { 102 | iX += i 103 | iY += j 104 | iN++ 105 | } 106 | } 107 | } 108 | resolve({ x: iX / iN, y: iY / iN + 20 }) 109 | }) 110 | }) 111 | } 112 | 113 | const inGradient = (startHex, endHex, r, g, b) => { 114 | function hex2RGB (hex) { 115 | var rgb = [] 116 | for(var i = 1; i < 7; i += 2){ 117 | rgb.push(parseInt('0x' + hex.slice(i, i + 2))) 118 | } 119 | return rgb 120 | } 121 | 122 | const sColor = hex2RGB(startHex) 123 | const eColor = hex2RGB(endHex) 124 | const inR = Math.min(eColor[0], sColor[0]) <= r && Math.max(eColor[0], sColor[0]) >= r 125 | const inG = Math.min(eColor[1], sColor[1]) <= g && Math.max(eColor[1], sColor[1]) >= g 126 | const inB = Math.min(eColor[2], sColor[2]) <= b && Math.max(eColor[2], sColor[2]) >= b 127 | return (inR && inG && inB) 128 | } 129 | 130 | function getT (filename, fx, fy) { 131 | return new Promise(resolve => { 132 | getPixels(filename, function(err, p) { 133 | if(err) { 134 | console.log('Bad image path') 135 | return 136 | } 137 | 138 | const isBG = (r, g, b) => { 139 | return ( 140 | inGradient('#E0E0E0', '#C0C0C0', r, g, b) || 141 | inGradient('#E0E0E0', '#FFC9E0', r, g, b) || 142 | inGradient('#FFD4AB', '#D2946B', r, g, b) || 143 | inGradient('#FFFFCA', '#E0EB7B', r, g, b) || 144 | inGradient('#E0D5FF', '#BFB4F6', r, g, b) || 145 | inGradient('#C0C0C0', '#A0A0A0', r, g, b) 146 | ) 147 | } 148 | 149 | function getFristCube () { 150 | for (let j = config.top; j < fy - 50; j++) { 151 | for (let i = 10; i < p.shape[0] - 10; i++) { 152 | const lr = p.get(i, j, 0) 153 | const lg = p.get(i, j, 1) 154 | const lb = p.get(i, j, 2) 155 | 156 | if (!isBG(lr, lg, lb) && (Math.abs(i - fx) > 50)) { 157 | console.log('Frist point', i, j, lr, lg, lb) 158 | if (i < 11) { 159 | return { i: 0, j: 0 } 160 | } 161 | 162 | for (let k = p.shape[0] - 10; k > i; k--) { 163 | const rr = p.get(k, j, 0) 164 | const rg = p.get(k, j, 1) 165 | const rb = p.get(k, j, 2) 166 | 167 | if (!isBG(rr, rg, rb) && (Math.abs(k - fx) > 50)) { 168 | const middle = Math.floor((i + k) / 2) 169 | 170 | // find Y axis conter 171 | const topD = j + 20 172 | const tr = p.get(middle, topD, 0) 173 | const tg = p.get(middle, topD, 1) 174 | const tb = p.get(middle, topD, 2) 175 | let center = topD 176 | 177 | for (let l = j; l < fy - 50; l++) { 178 | const br = p.get(middle, l, 0) 179 | const bg = p.get(middle, l, 1) 180 | const bb = p.get(middle, l, 2) 181 | 182 | if (tr === br && tg === bg && tb === bb) { 183 | center = l 184 | } 185 | } 186 | center = Math.round((topD + center) / 2) 187 | console.log('[ center ]', middle, center) 188 | return { i: middle, j: center } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | return 195 | } 196 | 197 | try { 198 | const { i, j } = getFristCube() 199 | resolve({ x: i, y: j }) 200 | } catch (e) { 201 | resolve({ x: 0, y: 0 }) 202 | } 203 | }) 204 | }) 205 | } 206 | 207 | function hold (f, t) { 208 | return new Promise(resolve => { 209 | const x = Math.abs(t.x - f.x) 210 | const y = Math.abs(f.y - t.y) 211 | const d = Math.sqrt(x * x + y * y) 212 | const s = Math.sqrt((d + config.b) / config.k) 213 | console.log(f, t) 214 | console.log(`(${x}, ${y})`, `\n[distance]: ${d}`, ` [time]:${s}`) 215 | 216 | resolve(s) 217 | }) 218 | } 219 | 220 | function drawPoint (filename, from, target, screenshotIndex) { 221 | return new Promise(resolve => { 222 | getPixels(filename, function(err, p) { 223 | if(err) { 224 | console.log('Bad image path') 225 | return 226 | } 227 | 228 | from = { 229 | x: Math.round(from.x), 230 | y: Math.round(from.y) 231 | } 232 | 233 | const points = [ 234 | { index: 0, rgb: 255 }, 235 | { index: 1, rgb: 0 }, 236 | { index: 1, rgb: 0 } 237 | ] 238 | 239 | const zoom = (x, y) => (rgb, value) => { 240 | for (let i = -1; i < 2; i++) { 241 | for (let j = -1; j < 2; j++) { 242 | p.set(x + i, y + j, rgb, value) 243 | } 244 | } 245 | } 246 | 247 | points.forEach((point) => { 248 | zoom(from.x, from.y)(point.index, point.rgb) 249 | zoom(target.x, target.y)(point.index, point.rgb) 250 | }) 251 | 252 | const writableFile = fs.createWriteStream(`lastScreen${screenshotIndex}.png`) 253 | savePixels(p, 'png').pipe(writableFile) 254 | console.log(`[lastScreen${screenshotIndex}.png saved]`) 255 | resolve() 256 | }) 257 | }) 258 | } 259 | 260 | function restart () { 261 | return new Promise(resolve => { 262 | get(`${url}/session/${sid}/wda/touchAndHold`, { 263 | method: 'POST', 264 | headers: { 265 | 'Content-Type': 'application/json' 266 | }, 267 | body: JSON.stringify({ 268 | duration: 0.00001, 269 | x: 400, 270 | y: 1050 271 | }) 272 | }).then(() => resolve()) 273 | }) 274 | } 275 | 276 | function sharpHandle (screenshot) { 277 | return new Promise(resolve => { 278 | screenshot = new Buffer(screenshot, 'base64') 279 | 280 | sharp(screenshot) 281 | .jpeg({ 282 | quality: 1 283 | }) 284 | .toFile('screenshot.jpg', () => resolve()) 285 | }) 286 | } 287 | 288 | async function main () { 289 | sid = await getSId() 290 | source = await getSource() 291 | jumpBtnId = await getJumpBtn() 292 | 293 | const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) 294 | 295 | async function rockIt (i) { 296 | screenshot = await getScreenshot() 297 | // await saveImg(screenshot, 'screenshot.png') 298 | await sharpHandle(screenshot) 299 | const from = await getI('screenshot.jpg') 300 | const target = await getT('screenshot.jpg', from.x, from.y) 301 | 302 | const s = await hold(from, target) 303 | 304 | if (s && target.x > 10) { 305 | await drawPoint('screenshot.jpg', from, target, i) 306 | await jump(s) 307 | } else { 308 | console.log('[restart !]') 309 | await restart() 310 | } 311 | 312 | const randomWait = 3000 // Math.random() * 1 + 5 313 | await sleep(randomWait) 314 | rockIt(++i) 315 | } 316 | 317 | rockIt(i) 318 | } 319 | 320 | main() 321 | --------------------------------------------------------------------------------