DotA 2 routine automation or cheats?

Hi, this article will look at legal ways to gain an advantage over the enemy using simple tools like NodeJS, Electron and React, while bypassing the ban. I was inspired to experiment by another article Visualizing Roshan's respawn time and the desire to automate part of the routine. It is worth noting that now we will consider tools that do not modify the game in any dishonest way - all APIs are open, the data is received in an honest way, there is no interference in the game process. There will be several pictures and some code under the cut.





Example of use in demo game mode
Example of use in demo game mode

Github, , , , . , , .





, , , .





, - , , Twitch, .., - , .





Disclaimer: . . MOBA, . , , , .





:













  • (GPM)





  • OpenDota.com













:













  • /





  • ""













  • ....





Dota 2 GSI (Game State Integration), / ( ) . , - . NodeJS . GSI , , "Steam\steamapps\common\dota 2 beta\game\dota\cfg", , , :





"dota2-gsi Configuration"
{
    "uri"               "http://localhost:3001/"
    "timeout"           "5.0"
    "buffer"            "0.1"
    "throttle"          "0.1"
    "heartbeat"         "30.0"
    "data"
    {
        "buildings"     "1"
        "provider"      "1"
        "map"           "1"
        "player"        "1"
        "hero"          "1"
        "abilities"     "1"
        "items"         "1"
        "draft"         "1"
        "wearables"     "1"
    }
}
      
      



, GSI, HTTP localhost:3001, NodeJS :





var server = app.listen(3001, () => {
  console.log("Dota 2 GSI listening on port " + server.address().port);
});
      
      



, , NodeJS





.





,

Dota 2, GSI ,









  • ID









  • GPM





( )
{
    "ip": "::ffff:127.0.0.1",
    "gamestate": {
        "buildings": {
            "radiant": {
                "dota_goodguys_tower1_top": {
                    "health": 1800,
                    "max_health": 1800
                },
                "dota_goodguys_tower2_top": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower3_top": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower1_mid": {
                    "health": 1800,
                    "max_health": 1800
                },
                "dota_goodguys_tower2_mid": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower3_mid": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower1_bot": {
                    "health": 1800,
                    "max_health": 1800
                },
                "dota_goodguys_tower2_bot": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower3_bot": {
                    "health": 2500,
                    "max_health": 2500
                },
                "dota_goodguys_tower4_top": {
                    "health": 2600,
                    "max_health": 2600
                },
                "dota_goodguys_tower4_bot": {
                    "health": 2600,
                    "max_health": 2600
                },
                "good_rax_melee_top": { "health": 2200, "max_health": 2200 },
                "good_rax_range_top": { "health": 1300, "max_health": 1300 },
                "good_rax_melee_mid": { "health": 2200, "max_health": 2200 },
                "good_rax_range_mid": { "health": 1300, "max_health": 1300 },
                "good_rax_melee_bot": { "health": 2200, "max_health": 2200 },
                "good_rax_range_bot": { "health": 1300, "max_health": 1300 },
                "dota_goodguys_fort": { "health": 4500, "max_health": 4500 }
            }
        },
        "provider": {
            "name": "Dota 2",
            "appid": 570,
            "version": 47,
            "timestamp": 1613780229
        },
        "map": {
            "name": "dota",
            "matchid": "0",
            "game_time": 2,
            "clock_time": 1,
            "daytime": true,
            "nightstalker_night": false,
            "game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS",
            "paused": false,
            "win_team": "none",
            "customgamename": "C:\\Program Files (x86)\\Steam\\steamapps\\common\\dota 2 beta\\game\\dota_addons\\hero_demo",
            "ward_purchase_cooldown": 0
        },
        "player": {
            "steamid": "76561198282999022",
            "name": "D1rty F0x",
            "activity": "playing",
            "kills": 0,
            "deaths": 0,
            "assists": 0,
            "last_hits": 0,
            "denies": 0,
            "kill_streak": 0,
            "commands_issued": 0,
            "kill_list": {},
            "team_name": "radiant",
            "gold": 99999,
            "gold_reliable": 0,
            "gold_unreliable": 99999,
            "gold_from_hero_kills": 0,
            "gold_from_creep_kills": 0,
            "gold_from_income": 2,
            "gold_from_shared": 0,
            "gpm": 3913086,
            "xpm": 0
        },
        "hero": {
            "xpos": -6700,
            "ypos": -6700,
            "id": 6,
            "name": "npc_dota_hero_drow_ranger",
            "level": 1,
            "alive": true,
            "respawn_seconds": 0,
            "buyback_cost": 8540,
            "buyback_cooldown": 0,
            "health": 560,
            "max_health": 560,
            "health_percent": 100,
            "mana": 255,
            "max_mana": 255,
            "mana_percent": 100,
            "silenced": false,
            "stunned": false,
            "disarmed": false,
            "magicimmune": false,
            "hexed": false,
            "muted": false,
            "break": false,
            "smoked": false,
            "has_debuff": false,
            "talent_1": false,
            "talent_2": false,
            "talent_3": false,
            "talent_4": false,
            "talent_5": false,
            "talent_6": false,
            "talent_7": false,
            "talent_8": false
        },
        "abilities": {
            "ability0": {
                "name": "drow_ranger_frost_arrows",
                "level": 0,
                "can_cast": false,
                "passive": false,
                "ability_active": true,
                "cooldown": 0,
                "ultimate": false
            },
            "ability1": {
                "name": "drow_ranger_wave_of_silence",
                "level": 0,
                "can_cast": false,
                "passive": false,
                "ability_active": true,
                "cooldown": 0,
                "ultimate": false
            },
            "ability2": {
                "name": "drow_ranger_multishot",
                "level": 0,
                "can_cast": false,
                "passive": false,
                "ability_active": true,
                "cooldown": 0,
                "ultimate": false
            },
            "ability3": {
                "name": "drow_ranger_marksmanship",
                "level": 0,
                "can_cast": false,
                "passive": true,
                "ability_active": true,
                "cooldown": 0,
                "ultimate": true
            }
        },
        "items": {
            "slot0": { "name": "empty" },
            "slot1": { "name": "empty" },
            "slot2": { "name": "empty" },
            "slot3": { "name": "empty" },
            "slot4": { "name": "empty" },
            "slot5": { "name": "empty" },
            "slot6": { "name": "empty" },
            "slot7": { "name": "empty" },
            "slot8": { "name": "empty" },
            "stash0": { "name": "empty" },
            "stash1": { "name": "empty" },
            "stash2": { "name": "empty" },
            "stash3": { "name": "empty" },
            "stash4": { "name": "empty" },
            "stash5": { "name": "empty" }
        },
        "draft": {},
        "wearables": {
            "wearable0": 77,
            "wearable1": 76,
            "wearable2": 5841,
            "wearable3": 80,
            "wearable4": 78,
            "wearable5": 267,
            "wearable6": 79,
            "wearable7": 8632,
            "wearable8": 737,
            "wearable9": 14912
        },
        "previously": { "player": { "gpm": 5000054 } }
    }
}

      
      



, - . -, - GPM, , Id .





- , , .





UI, , Electron

UI Electron React. , Electron (). , - .





, :





const win = new BrowserWindow({
  width: 210,
  height: 200,
  //    
  frame: false,
  //    
  transparent: true,
  webPreferences: {
    //      React
    nodeIntegration: true,
  },
});
//      
win.setAlwaysOnTop(true, "screen-saver");
      
      



- , machine_convars.vcfg (Dota 2) "dota_mouse_window_lock", "0", ( ) .





UI React, dev (, ):





function loadWindow() {
  setTimeout(() => {
    win.loadURL("http://localhost:3000").catch(loadWindow);
  }, 3000);
}
loadWindow();
      
      



dev , 3 , setTimeout.





, overlay , UI .





Styled-Components, Typescript, Hooks, - stylish, fashionable, youthful
Styled-Components, Typescript, , - , ,

UI : TS, CRA (Styled / - ). , GSI Dota2 express , . GET . , . - , , ( , , ). :





import { State } from "../state/state";
import { useState } from "react";
import { useInterval } from "./useInterval";

const SERVER_URL = "http://localhost:3001/time";
const UPDATE_FREQUENTLY = Number(process.env.REACT_APP_SERVER_UPDATE_FREQUENTLY);

export function useServerState(): State {
  const [state, setState] = useState<State>({});

  useInterval(async () => {
    try {
      const data = await (await fetch(SERVER_URL)).json();
      setState(data);
    } catch {}
  }, UPDATE_FREQUENTLY);

  return state;
}
      
      



, , , ( 30 ) , (5, 10, 15, 20 ):





export function useBountyRunes(state: State) {
  const clockTime = clockTimeSelector(state);
  const [play] = useSound(bountiesMp3, { volume: 0.25 });
  const [lastIntervalPlay, setLastIntervalPlay] = useState<number>(-1);

  useEffect(() => {
    // ,     
    if (!clockTime || isNegative(clockTime)) {
      return;
    }

    //     4,5     
    if (isNeedToPlay(clockTime, lastIntervalPlay)) {
      //  
      play();
      //      
      setLastIntervalPlay(getInterval(clockTime + ALARM_BEFORE));
    }
  }, [clockTime, lastIntervalPlay, play]);
}
      
      



(setLastIntervalPlay) .





, , . - , :





useRoshanSpawn
export function useRoshanSpawn(state: State) {
  const currentGameTime = gameTimeSelector(state) || 0;
  const [play] = useSound(roshanRespawnMp3, { volume: 0.25 });
  const [roshanStopwatch, setRoshanStopwatch] = useState<RoshanStopwatch>({
    isActive: false,
    time: 0,
    isPlayedSound: false,
  });

  //   
  function handleDead() {
    setRoshanStopwatch({
      time: currentGameTime,
      isActive: true,
      isPlayedSound: false,
    });
  }

  //  
  function handleReset() {
    setRoshanStopwatch({
      time: 0,
      isActive: false,
      isPlayedSound: false,
    });
  }

  //   , /,       
  const isDead = roshanIsDead(roshanStopwatch, currentGameTime);
  const isDeadOrLive = schrodingerRoshan(roshanStopwatch, currentGameTime);
  const timeToSpawn = roshanTimeDeadSelector(roshanStopwatch, currentGameTime);

  useEffect(() => {
    //    30    
    if (needToPlaySound(roshanStopwatch, currentGameTime)) {
      play();
      setRoshanStopwatch({
        ...roshanStopwatch,
        isPlayedSound: true,
      });
    }
  }, [roshanStopwatch, currentGameTime, setRoshanStopwatch]);

  return { handleDead, handleReset, isDead, isDeadOrLive, timeToSpawn };
}
      
      



, - 9 12 . :





  • ( 9 )





  • ( 9 12 )





  • ( 12 )





:





  • -









  • ,





: 30 ( , , - , ). , , - , . - , .





, , OpenDota.com , , . 99%, , 1% .





Benchmarks for the hero Abaddon
Abaddon

useBenchmark:





export function useBenchmarks(state: State): State {
  const [localState, setLocalState] = useState<Benchmarks>();

  const updateBenchmarksForHero = useCallback(
    async function (id: number) {
      try {
        //    
        const response = await fetch(`${BENCHMARKS_URL}?hero_id=${id}`);
        const benchmarks = (await response.json()) as Benchmarks;

        //  
        setLocalState(benchmarks);
      } catch (error) {
        setLocalState({
          error,
        });
      }
    },
    [setLocalState]
  );

  useEffect(() => {
    const heroId = heroIdSelector(state);
    const benchmarksHeroId = localState?.hero_id;

    //  ,     
    if (heroId && heroId !== benchmarksHeroId && !localState?.error) {
      updateBenchmarksForHero(heroId);
    }
  }, [state, localState, updateBenchmarksForHero]);
  
  //    
  return { ...state, benchmarks: localState };
}
      
      



, , , , . : "server_log.txt" , ID , OpenDota Dotabuff. - Dota 2, . , - , .





Attention: BSoD .





import fs from "fs";

const DEFAULT_FILE =
  "C:\\Program Files (x86)\\Steam\\steamapps\\common\\dota 2 beta\\game\\dota\\server_log.txt";

//   ID  
const getDotaIdsFromLine = (line) => {
  let playersRegex = /\d:(\[U:\d:(\d+)])/g;
  let playersMatch;
  let dotaIds = [];
  while ((playersMatch = playersRegex.exec(line))) {
    dotaIds.push(playersMatch[2]);
  }
  return dotaIds;
};

const getState = () => {
  return new Promise((res) => {
    //      Lobby
    fs.readFile(DEFAULT_FILE, (err, data) => {
      const rowString = data.toString();
      const startIndex = rowString.lastIndexOf("Lobby");
      const finishIndex = rowString.indexOf("\n", startIndex);
      const lobbyString = rowString.slice(startIndex, finishIndex);

      res(getDotaIdsFromLine(lobbyString));
    });
  });
};

export const getSteamIDs = () => {
  return getState();
};

      
      



, , React localhost:3002. , . . "Ban this id", , , Dotabuff , .





Electron , :-)





:





  • DLL Injection Rust, , , .





  • ML OpenDota.com Valve ( - ML )





  • Dota 2 - , Protobuff . ?





Conclusion: it is not difficult to integrate with Dota2, you can do a quick analysis right during the game, when watching esports games you can make a huge amount of beautiful overlay for the Twitch stream, you can also develop this topic towards retrospective analysis from replays, which will most likely be useful for professionals ...





I hope it was interesting for you to read about how I collected cheats on my knee (really a good question - are these cheats or not?), And even in JS, if there are spelling or lexical errors, please write to the LAN, thanks for your attention ...








All Articles