¿Automatización de rutina de DotA 2 o trampas?

Hola, este artículo analizará las formas legales de obtener una ventaja sobre el enemigo utilizando herramientas simples como NodeJS, Electron y React, sin pasar por la prohibición. Otro artículo me inspiró para experimentar Visualizando el tiempo de reaparición de Roshan y el deseo de automatizar parte de la rutina. Vale la pena señalar que ahora consideraremos herramientas que no modifican el juego de ninguna manera deshonesta: todas las API están abiertas, los datos se reciben de manera honesta, no se producen interferencias en el proceso del juego. Habrá varias imágenes y algún código debajo del corte.





Ejemplo de uso en modo de juego de demostración
Ejemplo de uso en modo de juego de demostración

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 .





Componentes de estilo, mecanografiado, ganchos, elegante, de moda, juvenil
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% .





Puntos de referencia para el héroe 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 . ?





Conclusión: no es difícil integrarse con Dota2, puedes hacer un análisis rápido justo durante el juego, cuando miras juegos de deportes electrónicos puedes hacer una gran cantidad de hermosas superposiciones para la transmisión de Twitch, también puedes desarrollar este tema para un análisis retrospectivo de repeticiones, que probablemente serán útiles para los profesionales ...





Espero que haya sido interesante para usted leer sobre cómo recopilé trampas en mi rodilla (realmente una buena pregunta, ¿son estas trampas o no?), E incluso en JS, si hay errores ortográficos o léxicos, escriba a la LAN, gracias por su atencion ...








All Articles