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.
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);
});
.
,
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 } }
}
}
- , , .
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 .
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% .
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, . , - , .
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 ...