Hai, artikel ini akan membahas cara - cara legal untuk mendapatkan keuntungan dari musuh menggunakan alat sederhana seperti NodeJS, Electron dan React, sambil melewati larangan. Saya terinspirasi untuk bereksperimen dengan artikel lain Memvisualisasikan waktu respawn Roshan dan keinginan untuk mengotomatiskan bagian dari rutinitas. Perlu dicatat bahwa sekarang kami akan mempertimbangkan alat yang tidak memodifikasi game dengan cara yang tidak jujur - semua API terbuka, data diterima dengan cara yang jujur, tidak ada gangguan dalam proses game yang terjadi. Akan ada beberapa gambar dan beberapa kode di bawah potongan.
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 . ?
Kesimpulan: tidak sulit untuk berintegrasi dengan Dota2, Anda dapat melakukan analisis cepat langsung selama pertandingan, saat menonton game esports Anda dapat membuat overlay indah dalam jumlah besar untuk aliran Twitch, Anda juga dapat mengembangkan topik ini untuk analisis retrospektif dari tayangan ulang, yang kemungkinan besar akan berguna bagi para profesional ...
Saya harap menarik bagi Anda untuk membaca tentang bagaimana saya mengumpulkan cheat di lutut saya (benar-benar pertanyaan yang bagus - apakah ini cheat atau tidak?), Dan bahkan di JS, jika ada kesalahan ejaan atau leksikal, silakan tulis ke LAN, terima kasih atas perhatiannya ...