├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── examples └── docker-compose-autoupdate │ └── docker-compose.yml ├── gettoken.example.json ├── headless.js ├── headless ├── jquery_node.js ├── network.js └── webapi.js ├── index.user.js ├── package.json ├── uint64.js └── windows.bat /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | gettoken.json 4 | log.txt 5 | # IDE 6 | /.idea 7 | *.swp 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | gettoken.json 4 | log.txt 5 | # IDE 6 | /.idea 7 | *.swp 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json /app 6 | 7 | RUN yarn install 8 | 9 | COPY . /app 10 | 11 | CMD ["node", "headless.js"] 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Meepen 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salien Bot 2 | 3 | Salien Bot is a WIP bot for the Salien Minigame that came out for the Steam Summer Sale 2018. 4 | 5 | I picked this up because it reminded me of a challenge in programming I had before. I won't be manipulating any state, just injecting mouse clicks and other button presses. 6 | 7 | Also, a note: playing this game AT ALL will not net you better rewards! It's only the time you have been on the page. There's no need to waste computer resources. :) 8 | 9 | 10 | ## Userscript Mode 11 | 12 | Userscript Mode is a version of this bot that is ran as a script on top of your browser, while having a tab with the game open. It is generally easier to use. 13 | 14 | Here are the steps to use: 15 | 16 | - Download and install the TamperMonkey addon for your browser of choice - https://tampermonkey.net/ 17 | - Open this link: https://github.com/meepen/salien-bot/raw/master/index.user.js with TamperMonkey installed; it should open up a tab that prompts you to install the script 18 | - And finally, go to the Saliens game page! 19 | 20 | It should be running now! 21 | 22 | ### Tile Selection 23 | 24 | The tile select code, in this version, focuses on leveling up to level 13 on hard tiles; after you hit level 13 it will try and target the highest progress tile. 25 | 26 | 27 | ## Headless Mode 28 | 29 | Headless Mode is a version of this game that is ran without a UI with the bot controlling it, in your command prompt / terminal / shell / etc. 30 | 31 | Here are the steps to use: 32 | 33 | - Download and install nodejs - https://nodejs.org 34 | - Download https://github.com/meepen/salien-bot/archive/master.zip 35 | - Extract salien-bot-master.zip 36 | - Open command line in salien-bot-master 37 | - Write: 38 | ``` 39 | npm install 40 | notepad gettoken.json 41 | ``` 42 | - Log into Steam 43 | - Open https://steamcommunity.com/saliengame/gettoken in browser with Steam logged in 44 | - Copy the entire contents of the page 45 | - Paste into notepad and save the file as `gettoken.json`. 46 | - (IMPORTANT) Immediately copy something else to avoid accidentally giving this out to someone else! 47 | - Save and exit 48 | - Back to command line: 49 | ``` 50 | node headless 51 | ``` 52 | 53 | - (optional) See detailed bot options 54 | ``` 55 | node headless --help 56 | ``` 57 | 58 | It should be running now! 59 | 60 | ### Tile Selection 61 | 62 | This version, by default, will scour all available planets and get the highest EXP rewards from difficult tiles; if you don't want this, run the bot with: 63 | ``` 64 | node headless --care-for-planet 65 | ``` 66 | This will make the bot only use the last planet which you were on and allows you to select the planet for the bot to focus on by first logging onto the Steam website and selecting a planet. 67 | 68 | ### Run the bot inside Docker 69 | 70 | This bot can be ran inside a docker container in headless mode. 71 | 72 | 1. Get the gettoken.json file. 73 | 2. Run the docker image: 74 | 75 | ``` 76 | docker run -v /path/to/gettoken.json:/app/gettoken.json meepen/salien-bot:latest 77 | ``` 78 | 79 | You also can run an autoupdated stack with the [examples/docker-compose-autoupdate/docker-compose.yml](../master/examples/docker-compose-autoupdate/docker-compose.yml) configuration file for [docker-compose](https://docs.docker.com/compose/). -------------------------------------------------------------------------------- /examples/docker-compose-autoupdate/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | # Autoupdated stack 3 | services: 4 | bot: 5 | image: meepen/salien-bot:latest 6 | restart: always 7 | volumes: 8 | # The gettoken.json file must be in the same dir as docker-compose.yml file 9 | - ./gettoken.json:/app/gettoken.json 10 | labels: 11 | com.centurylinklabs.watchtower.enable: "true" 12 | updater: 13 | image: v2tec/watchtower 14 | restart: always 15 | volumes: 16 | - /var/run/docker.sock:/var/run/docker.sock 17 | command: --interval 300 --label-enable -------------------------------------------------------------------------------- /gettoken.example.json: -------------------------------------------------------------------------------- 1 | {"webapi_host":"https:\/\/community.steam-api.com\/","webapi_host_secure":"https:\/\/community.steam-api.com\/","token":"not real","steamid":"0","persona_name":"Meepen","success":1} -------------------------------------------------------------------------------- /headless.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const args = process.argv.slice(2); 4 | const network = require("./headless/network.js"); 5 | const fs = require("fs"); 6 | let uint64; 7 | try { 8 | uint64 = require("./uint64.js"); 9 | } 10 | catch (e) { 11 | console.log("Dependencies outdated - run `npm install` again"); 12 | process.exit(); 13 | } 14 | 15 | const help_page = ` 16 | Meeden's headless Salien bot 17 | https://github.com/meepen/salien-bot 18 | 19 | Usage: node headless.js [options] 20 | Options: 21 | -h, --help Display this information. 22 | -l, --log Writes log output to "log.txt". 23 | --lang LANG Enable changing the language of the steam API. 24 | See https://partner.steamgames.com/doc/store/localization#supported_languages. 25 | -t, --token TOKENFILE Allow specifying a custom token file. 26 | -tj, --token-json TOKEN_JSON Allow inlining gettoken.json contents as base64 string. 27 | -c, --care-for-planet Bot tries to stay on the same planet and does not change the 28 | planet even if difficulty would be better somewhere else. 29 | `; 30 | 31 | 32 | let CARE_ABOUT_PLANET = false; 33 | let DO_LOGS = false; 34 | 35 | const log_file = "./log.txt"; 36 | 37 | let token_file = "./gettoken.json"; 38 | let token_json_base64 = ""; 39 | let EXPERIMENTAL = false; 40 | 41 | // clear log 42 | 43 | global.log = function log(data) { 44 | if (!DO_LOGS) 45 | return; 46 | let offset = new Date().getTimezoneOffset() / -60; 47 | fs.appendFileSync(log_file, (new Date()).toLocaleString() + " GMT" + (offset >= 0 ? "+" + offset : offset) + " " + data.toString()); 48 | fs.appendFileSync(log_file, "\n"); 49 | } 50 | 51 | for (let i = 0; i < args.length; i++) { 52 | let arg = args[i]; 53 | if (arg == "--experimental" || arg == "-e") { 54 | EXPERIMENTAL = true; 55 | global.log("EXPERIMENTAL MODE ACTIVATED"); 56 | } 57 | else if (arg == "--log" || arg == "-l") { 58 | DO_LOGS = true; 59 | fs.writeFileSync(log_file, ""); 60 | global.log("Logging activated."); 61 | } 62 | else if (arg == "--lang" && args[i + 1]) { 63 | // https://partner.steamgames.com/doc/store/localization#supported_languages 64 | global.log(`language: ${args[++i]}`); 65 | network.ChangeLanguage(args[i]); 66 | } 67 | else if (arg == "--token" || arg == "-t") { 68 | global.log(`token file: ${args[++i]}`); 69 | token_file = args[i]; 70 | } 71 | else if (arg == "--token-json" || arg == "-tj") { 72 | global.log(`Inline token json used.`); 73 | token_json_base64 = args[++i]; 74 | } 75 | else if (arg == "--care-for-planet" || arg == "-c") { 76 | CARE_ABOUT_PLANET = true; 77 | global.log("Caring for previous planet."); 78 | } 79 | else if (arg == "--help" || arg == "-h") { 80 | console.log(help_page); 81 | process.exit(); 82 | } 83 | else 84 | throw new Error(`invalid command line argument ${arg}`); 85 | } 86 | 87 | const CServerInterface = network.CServerInterface; 88 | const k_NumMapTilesW = 12; 89 | 90 | const SCORE_TIME = 120; 91 | const WAIT_TIME = 110; 92 | 93 | 94 | // TODO: get these from json 95 | const difficulty_multipliers = [ 96 | 0, 1, 2, 4 97 | ]; 98 | 99 | const difficulty_names = [ 100 | "???", "easy", "medium", "hard" 101 | ]; 102 | 103 | const MaxScore = function MaxScore(difficulty) { 104 | return SCORE_TIME * 5 * difficulty_multipliers[difficulty]; 105 | } 106 | 107 | let gettoken; 108 | if (token_json_base64.length < 1) { 109 | gettoken = JSON.parse(fs.readFileSync(token_file, "utf8")); 110 | } else { 111 | gettoken = JSON.parse(Buffer.from(token_json_base64, 'base64').toString('ascii')); 112 | } 113 | 114 | const accountid = uint64(gettoken.steamid).toNumber(); 115 | 116 | const GetSelf = function GetSelf(players) { 117 | for (let i = 0; i < players.length; i++){ 118 | let ply = players[i]; 119 | if (ply.accountid == accountid) 120 | return ply; 121 | } 122 | } 123 | 124 | class Client { 125 | constructor(int) { 126 | this.int = int; 127 | this.gPlanets = {}; 128 | } 129 | 130 | Connect() { 131 | return new Promise(res => { 132 | this.GetPlayerInfo().then(() => { 133 | if (this.gPlayerInfo.active_zone_game || this.gPlayerInfo.active_boss_game) { 134 | this.LeaveGame().then(() => { 135 | this.Connect().then(res); 136 | }) 137 | } 138 | else { 139 | res(); 140 | } 141 | }) 142 | }); 143 | } 144 | 145 | GameInfo(offset) { 146 | this.endGameTime = Date.now() + offset; 147 | } 148 | 149 | FinishBossGame() { 150 | return new Promise(res => { 151 | global.log("boss active"); 152 | this.m_nConsecutiveFailures = 0; 153 | let per_tick = 5000; 154 | let per_heal = 120000; 155 | let next_heal = per_heal; 156 | let waiting = true; 157 | this.m_BossDamage = 0; 158 | this.m_BossInterval = setInterval(() => { 159 | let healed = false; 160 | if (!waiting && next_heal <= 0) { 161 | next_heal = per_heal; 162 | healed = true; 163 | } 164 | else { 165 | next_heal -= per_tick; 166 | } 167 | this.ReportBossDamage(waiting ? 0 : 1, healed).then(data => { 168 | if (!data) { 169 | global.log("FAILED"); 170 | return; // failed 171 | } 172 | if (data.game_over) { 173 | global.log("BOSS OVER"); 174 | clearInterval(this.m_BossInterval); 175 | res(); 176 | return; 177 | } 178 | waiting = data.waiting_for_players; 179 | if (waiting) { 180 | global.log("still waiting for players"); 181 | } 182 | this.bossStatus = data.boss_status; 183 | }).catch(() => { 184 | clearInterval(this.m_BossInterval); 185 | res(); 186 | }); 187 | }, per_tick); 188 | }); 189 | } 190 | 191 | LeaveGame() { 192 | return new Promise(res => { 193 | if (this.gPlayerInfo.active_zone_game) { 194 | if (this.gPlayerInfo.time_in_zone <= (SCORE_TIME + 20)) { 195 | // we can probably just finish our thing i guess 196 | this.GetPlanet(this.gPlayerInfo.active_planet).then(() => { 197 | let time_left = 1000 * (WAIT_TIME - this.gPlayerInfo.time_in_zone) 198 | this.GameInfo(time_left); 199 | setTimeout(() => { 200 | let planet = this.gPlanets[this.gPlayerInfo.active_planet]; 201 | let zone = planet.zones[this.gPlayerInfo.active_zone_position]; 202 | cl.ReportScore(MaxScore(zone.difficulty)).then(res); 203 | }, time_left); 204 | }); 205 | } 206 | else { 207 | this.int.LeaveGameInstance(this.gPlayerInfo.active_zone_game, res, () => { 208 | this.GetPlayerInfo().then(() => { 209 | if (this.gPlayerInfo.active_zone_game) { 210 | this.LeaveGame().then(res); 211 | } 212 | else { 213 | res(); 214 | } 215 | }) 216 | }) 217 | } 218 | } 219 | else if (this.gPlayerInfo.active_boss_game) { 220 | this.FinishBossGame().then(res); 221 | } 222 | else { 223 | res(); 224 | } 225 | }); 226 | } 227 | 228 | GetPlanets(active_only) { 229 | if (active_only === undefined) 230 | active_only = 1; 231 | return new Promise(res => { 232 | this.int.GetPlanets(active_only, data => { 233 | this.m_Planets = data.response.planets; 234 | res(this.m_Planets); 235 | }, () => { 236 | this.GetPlanets(active_only).then(res); 237 | }); 238 | }); 239 | } 240 | 241 | GetPlayerInfo() { 242 | return new Promise(res => { 243 | setTimeout(() => { 244 | this.int.GetPlayerInfo(d => { 245 | if (!d || !d.response) { 246 | this.GetPlayerInfo().then(res); 247 | return; 248 | } 249 | if (!this.gPlayerInfo) 250 | this.gPlayerInfoOriginal = d.response; 251 | this.gPlayerInfo = d.response; 252 | res(this.gPlayerInfo); 253 | }, () => { 254 | this.GetPlayerInfo().then(res); 255 | }); 256 | }, 100); 257 | }); 258 | } 259 | 260 | JoinPlanet(id) { 261 | return new Promise((res, rej) => { 262 | this.int.JoinPlanet(id, d => { 263 | this.gPlayerInfo.active_planet = id; 264 | res(); 265 | }, () => { 266 | this.GetPlanet(id).then(planet => { 267 | this.GetPlayerInfo().then(() => { 268 | if (planet.state.active && !this.gPlayerInfo.active_planet) { 269 | this.JoinPlanet(id).then(res).catch(rej); 270 | } 271 | else { 272 | rej(); 273 | } 274 | }); 275 | }) 276 | }); 277 | }); 278 | } 279 | 280 | GetPlanet(id) { 281 | return new Promise(res => { 282 | this.int.GetPlanet(id, d => { 283 | for (let i in d.response.planets) { 284 | let planet = d.response.planets[i]; 285 | this.gPlanets[planet.id] = planet; 286 | } 287 | res(this.gPlanets[id]); 288 | }, () => { 289 | this.GetPlanet(id).then(res); 290 | }); 291 | }); 292 | } 293 | 294 | JoinZone(zone) { 295 | return new Promise((res, rej) => { 296 | global.log(`joining zone ${zone.zone_position} with ${zone.difficulty} difficulty and ${zone.capture_progress} progress`); 297 | if (zone.boss_active) { 298 | this.int.JoinBossZone(zone.zone_position, results => { 299 | this.gPlayerInfo.active_zone_position = zone.zone_position; 300 | res(results.response); 301 | }, rej); 302 | } 303 | else { 304 | this.int.JoinZone(zone.zone_position, d => { 305 | this.gPlayerInfo.active_zone_position = zone.zone_position; 306 | res(d.response.zone_info); 307 | }, rej); 308 | } 309 | }); 310 | } 311 | 312 | ReportScore(score) { 313 | return new Promise(res => { 314 | this.int.ReportScore(score, d => { 315 | res(); 316 | }, () => { 317 | this.GetPlayerInfo().then(() => { 318 | if (this.gPlayerInfo.active_zone_game && this.gPlayerInfo.time_in_zone < SCORE_TIME + 20) { 319 | this.LeaveGame().then(res); 320 | } 321 | else { 322 | res(); 323 | } 324 | }) 325 | }) 326 | }) 327 | } 328 | 329 | LeavePlanet() { 330 | return new Promise(res => { 331 | this.int.LeaveGameInstance(this.gPlayerInfo.active_planet, () => { 332 | this.gPlayerInfo.active_planet = undefined; 333 | res(); 334 | }, () => { 335 | this.GetPlayerInfo().then(() => { 336 | if (this.gPlayerInfo.active_planet) { 337 | this.LeavePlanet().then(res); 338 | } 339 | else { 340 | res(); 341 | } 342 | }) 343 | }); 344 | }); 345 | } 346 | 347 | GetBestPlanet() { 348 | return new Promise(res => { 349 | if (CARE_ABOUT_PLANET && this.gPlayerInfo.active_planet) { 350 | this.GetPlanet(this.gPlayerInfo.active_planet).then(planet => { 351 | if (!planet.state.active) 352 | this.LeavePlanet().then(() => this.GetBestPlanet().then(res)); 353 | else 354 | res(this.gPlanets[this.gPlayerInfo.active_planet]); 355 | }); 356 | return; 357 | } 358 | this.GetPlanets().then(planets => { 359 | let i = 0; 360 | var GetPlanetIterator = (cb) => { 361 | let planet = planets[i++]; 362 | this.GetPlanet(planet.id).then(planets[i] ? () => GetPlanetIterator(cb) : cb); 363 | } 364 | GetPlanetIterator(() => { 365 | let best_planet = planets[0], best_difficulty = -1; 366 | for (let p of planets) { 367 | let planet = this.gPlanets[p.id]; 368 | let best_zone = GetBestZone(planet); 369 | 370 | if (best_zone && best_zone.boss_active) 371 | return res(planet, 3); // boss is always the best 372 | 373 | if (best_zone && best_zone.difficulty > best_difficulty) 374 | best_planet = planet, best_difficulty = best_zone.difficulty; 375 | } 376 | if (best_difficulty === -1) 377 | throw new Error("no difficulty?!"); 378 | if (!best_planet) { 379 | this.GetBestPlanet().then(res); 380 | return; 381 | } 382 | res(best_planet, best_difficulty); 383 | }); 384 | }, () => { 385 | this.GetBestPlanet().then(res); 386 | }) 387 | }); 388 | } 389 | 390 | ForcePlanet(id) { 391 | return new Promise((res, rej) => { 392 | if (this.gPlayerInfo.active_planet && this.gPlayerInfo.active_planet != id) { 393 | this.LeavePlanet().then(() => { 394 | this.ForcePlanet(id).then(res).catch(rej); 395 | }) 396 | return; 397 | } 398 | else if (this.gPlayerInfo.active_planet != id) { 399 | this.JoinPlanet(id).then(res).catch(rej); 400 | } 401 | else { 402 | res(); 403 | } 404 | }) 405 | } 406 | 407 | ReportBossDamage(damage, healed) { 408 | return new Promise((res, rej) => { 409 | // can we get away with 0 damage taken and no healing? 410 | this.int.ReportBossDamage(damage, 0, healed ? 1 : 0, results => { 411 | res(results.response); 412 | }, (_, eresult) => { 413 | if (++this.m_nConsecutiveFailures > 5) { 414 | rej(); 415 | } 416 | }); 417 | }); 418 | } 419 | 420 | FinishGame() { 421 | return new Promise(res => { 422 | this.GetPlayerInfo().then(() => { 423 | if (this.gPlayerInfo.active_boss_game || this.gPlayerInfo.active_zone_game) { 424 | this.LeaveGame().then(res); 425 | return; 426 | } 427 | this.GetBestPlanet().then(planet => { 428 | this.ForcePlanet(planet.id).then(() => { 429 | let zone = GetBestZone(planet); 430 | if (!zone) { 431 | this.LeavePlanet().then(() => { 432 | this.Connect().then(() => { 433 | this.FinishGame().then(res) 434 | }); 435 | }); 436 | return; 437 | } 438 | this.JoinZone(zone).then(zone_info => { 439 | if (zone.boss_active) { 440 | this.FinishBossGame().then(res); 441 | } 442 | else { 443 | if (!zone_info) { 444 | this.Connect().then(() => { 445 | this.FinishGame().then(res); 446 | }); 447 | return; 448 | } 449 | let time_left = 1000 * WAIT_TIME; 450 | this.GameInfo(time_left); 451 | setTimeout(() => { 452 | this.ReportScore(MaxScore(zone_info.difficulty)).then(res); 453 | }, time_left); 454 | } 455 | }).catch(() => { 456 | this.FinishGame().then(res); 457 | }); 458 | }).catch(() => { 459 | this.FinishGame().then(res); 460 | }); 461 | }); 462 | }); 463 | }); 464 | } 465 | } 466 | 467 | const cl = new Client(new CServerInterface(gettoken)); 468 | 469 | const GetBestZone = function GetBestZone(planet) { 470 | let bestZone; 471 | let highestDifficulty = -1; 472 | 473 | let maxProgress = 10000; 474 | 475 | for (let idx in planet.zones) { 476 | let zone = planet.zones[idx]; 477 | 478 | if (!zone.captured) { 479 | if (zone.boss_active) // boss 480 | return zone; 481 | 482 | if (zone.difficulty > highestDifficulty) { 483 | highestDifficulty = zone.difficulty; 484 | maxProgress = zone.capture_progress; 485 | bestZone = zone; 486 | } 487 | else if (zone.difficulty < highestDifficulty) 488 | continue; 489 | 490 | if (zone.capture_progress < maxProgress) { 491 | maxProgress = zone.capture_progress; 492 | bestZone = zone; 493 | } 494 | } 495 | } 496 | 497 | return bestZone; 498 | } 499 | 500 | var Finish = () => cl.FinishGame().then(Finish); 501 | 502 | let start_time = (Date.now() / 1000) | 0 503 | 504 | const FormatTimer = function FormatTimer(timeInSeconds) { 505 | if (parseInt(timeInSeconds) <= 0) { 506 | return ''; 507 | } 508 | 509 | const SECONDS_IN_MINUTE = 60; 510 | const MINUTES_IN_HOUR = 60; 511 | const HOURS_IN_DAY = 24; 512 | const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR; 513 | const SECONDS_IN_DAY = SECONDS_IN_HOUR * HOURS_IN_DAY; 514 | 515 | const days = Math.floor(timeInSeconds / SECONDS_IN_DAY); 516 | const hours = Math.floor(timeInSeconds / SECONDS_IN_HOUR % HOURS_IN_DAY); 517 | const minutes = Math.floor(timeInSeconds / SECONDS_IN_MINUTE % MINUTES_IN_HOUR); 518 | const seconds = timeInSeconds % SECONDS_IN_MINUTE; 519 | 520 | let formatted = ''; 521 | formatted += days ? `${days}d ` : ''; 522 | formatted += hours ? `${hours}h ` : ''; 523 | formatted += minutes ? `${minutes}m `: ''; 524 | formatted += seconds ? `${seconds}s ` : ''; 525 | 526 | return formatted; 527 | } 528 | 529 | // https://stackoverflow.com/a/41407246 530 | const difficulty_color_codes = [ 531 | "", "\x1b[1m\x1b[32m", "\x1b[1m\x1b[33m", "\x1b[1m\x1b[31m" 532 | ]; 533 | const reset_code = "\x1b[0m"; 534 | 535 | const PrintInfo = function PrintInfo() { 536 | // clear screen, taken from https://stackoverflow.com/a/14976765 537 | let info_lines = []; 538 | if (cl.gPlayerInfo) { 539 | let info = cl.gPlayerInfo; 540 | info_lines.push(["Playing as", gettoken.persona_name]); 541 | info_lines.push(["Running for", FormatTimer(((Date.now() / 1000) | 0) - start_time)]); 542 | info_lines.push(["Current level", `${info.level} (${info.score} / ${info.next_level_score})`]); 543 | info_lines.push(["Exp since start", info.score - cl.gPlayerInfoOriginal.score]); 544 | 545 | if (cl.gPlanets) { 546 | let current = cl.gPlanets[info.active_planet]; 547 | if (current) { 548 | info_lines.push(["Current planet", `${current.state.name} [${(current.state.capture_progress * 100).toFixed(3)}%] (id ${current.id})`]); 549 | if (cl.gPlayerInfo.active_zone_position) { 550 | let zoneIdx = parseInt(cl.gPlayerInfo.active_zone_position) || parseInt(cl.gPlayerInfo.active_boss_position); 551 | let zoneX = zoneIdx % k_NumMapTilesW, zoneY = (zoneIdx / k_NumMapTilesW) | 0; 552 | let zone = current.zones[zoneIdx]; 553 | 554 | if (zone) { 555 | let max_score = MaxScore(zone.difficulty); 556 | let exp_per_hour = 60 * 60 * max_score / (WAIT_TIME + 5); 557 | 558 | // keep in old position 559 | info_lines.splice(info_lines.length - 1, 0, ["Estimated exp/hr", exp_per_hour | 0]); 560 | 561 | info_lines.push(["Current zone", `(${zoneX}, ${zoneY}) ${zone.boss_active ? "BOSS " : ""}${difficulty_color_codes[zone.difficulty]}${difficulty_names[zone.difficulty]}${reset_code} [${(zone.capture_progress * 100).toFixed(3)}%] (id: ${zoneIdx})`]); 562 | 563 | if (!zone.boss_active) { 564 | let time_left = ((cl.endGameTime - Date.now()) / 1000) | 0; 565 | info_lines.push(["Round time left", FormatTimer(time_left)]); 566 | 567 | let offset = (new Date().getTimezoneOffset() / 60) * -1; 568 | let date = new Date(cl.endGameTime + offset); 569 | date.setSeconds(date.getSeconds() + (info.next_level_score - info.score - max_score) / exp_per_hour * 60 * 60); 570 | info_lines.push(["Next level up", date.toLocaleString()]); 571 | } 572 | else if (cl.bossStatus) { 573 | info_lines.push(["Boss HP", `${cl.bossStatus.boss_hp} / ${cl.bossStatus.boss_max_hp} [${(cl.bossStatus.boss_hp / cl.bossStatus.boss_max_hp * 100).toFixed(2)}%]`]); 574 | let self = GetSelf(cl.bossStatus.boss_players); 575 | if (self) { 576 | info_lines.push(["Boss EXP", self.xp_earned.toString()]); 577 | } 578 | else { 579 | info_lines.push(["No self", "error"]); 580 | } 581 | } 582 | } 583 | } 584 | } 585 | } 586 | } 587 | 588 | 589 | let max_length = 0; 590 | for (let i = 0; i < info_lines.length; i++) 591 | max_length = Math.max(max_length, info_lines[i][0].length); 592 | 593 | 594 | // https://stackoverflow.com/a/41407246 595 | 596 | for (let i = 0; i < info_lines.length; i++) 597 | info_lines[i] = "\x1b[33m" + info_lines[i].join(`${reset_code}: ${" ".repeat(max_length - info_lines[i][0].length)}`); 598 | 599 | console.log("\x1b[2J\x1b[0;0H" + info_lines.join("\n")); 600 | } 601 | 602 | setInterval(PrintInfo, 1000); 603 | 604 | cl.Connect().then(() => { 605 | Finish(); 606 | }); 607 | -------------------------------------------------------------------------------- /headless/jquery_node.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const request = require("request"); 4 | const qs = require("querystring"); 5 | 6 | let j = module.exports.jQuery = {} 7 | 8 | /* 9 | $J.ajax({ 10 | url: 'https://steamcommunity.com/saliengame/gettoken', 11 | dataType: "json" 12 | }).success(function(rgResult){ 13 | if( rgResult.success == 1) 14 | { 15 | instance.m_strSteamID = rgResult.steamid; 16 | instance.m_strWebAPIHost = rgResult.webapi_host; 17 | instance.m_WebAPI = new CWebAPI( rgResult.webapi_host, rgResult.webapi_host_secure, rgResult.token ); 18 | callback(rgResult); 19 | } 20 | }); 21 | */ 22 | 23 | class AjaxResponse { 24 | constructor() { 25 | this._fails = 0; 26 | } 27 | success(fn) { 28 | this.succ = fn; 29 | return this; 30 | } 31 | fail(fn) { 32 | this.nosucc = fn; 33 | return this; 34 | } 35 | } 36 | 37 | class AjaxResponseObject { 38 | constructor(resp) { 39 | this._ = resp; 40 | } 41 | getResponseHeader(name) { 42 | return this._.headers[name]; 43 | } 44 | } 45 | 46 | const jar = request.jar(); 47 | 48 | 49 | let internal_ajax = function internal_ajax(data, ajax_object) { 50 | let url = data.url; 51 | let form; 52 | if (data.method == "POST") 53 | form = data.data; 54 | else if (data.method == "GET" && data.data) 55 | url += "?" + qs.stringify(data.data); 56 | 57 | global.log(`url ${url} requested`); 58 | request({ 59 | url: url, 60 | headers: { 61 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36", 62 | "Referer": "https://steamcommunity.com/saliengame/play/", 63 | "Origin": "https://steamcommunity.com" 64 | }, 65 | method: data.method, 66 | jar: jar, 67 | form: form, 68 | timeout: 5000 69 | }, function response(err, resp, body) { 70 | if (!ajax_object.nosucc) 71 | global.log(new Error(`no ajax fail function ${url}`)); 72 | 73 | if (err || resp.statusCode == 500 || resp.statusCode == 503) { 74 | global.log(`failed url ${url}, count ${++ajax_object._fails}`); 75 | setTimeout(() => { 76 | if (ajax_object._fails > 1) 77 | ajax_object.nosucc(); 78 | else 79 | internal_ajax(data, ajax_object); 80 | }, 500); 81 | return; 82 | } 83 | 84 | let value = body; 85 | //if (data.dataType == "json") 86 | try { 87 | value = JSON.parse(value); 88 | } 89 | catch (e) { 90 | global.log(`failed url ${url}, count ${++ajax_object._fails}`); 91 | setTimeout(() => { 92 | if (ajax_object._fails > 1) 93 | ajax_object.nosucc(); 94 | else 95 | internal_ajax(data, ajax_object); 96 | }, 500); 97 | return; 98 | } 99 | 100 | if (ajax_object.succ) 101 | ajax_object.succ(value, null, new AjaxResponseObject(resp)); 102 | }) 103 | } 104 | 105 | j.ajax = function ajax(data) { 106 | jar.setCookie(request.cookie(`access_token=${j.token}`), data.url) 107 | let ajax_object = new AjaxResponse(); 108 | 109 | internal_ajax(data, ajax_object); 110 | 111 | return ajax_object; 112 | } -------------------------------------------------------------------------------- /headless/network.js: -------------------------------------------------------------------------------- 1 | //