こんにちは、この記事では、禁止を回避しながら、NodeJS、Electron、Reactなどのシンプルなツールを使用して敵に対して優位に立つための合法的な方法を見ていきます。Roshanのリスポーン時間を視覚化する別の記事と、ルーチンの一部を自動化したいという願望に触発されました。ここで、不正な方法でゲームを変更しないツールを検討することに注意してください。すべてのAPIが開いており、データが正直に受信され、ゲームプロセスに干渉することはありません。カットの下にいくつかの写真といくつかのコードがあります。
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 . ?
結論:Dota2と統合することは難しくありません。ゲーム中にすばやく分析を行うことができます。eスポーツゲームを見ると、Twitchストリームの美しいオーバーレイを大量に作成できます。また、このトピックを遡及的分析に向けて開発することもできます。リプレイ、これはおそらく専門家に役立つでしょう...
私がどのようにして膝にチートを集めたかについて読んでおもしろかったと思います(本当に良い質問です-これらのチートかどうか?)そしてJSでも、スペルや字句の誤りがある場合は、LANに書いてください、ご清聴ありがとうございました...