Hallo, dieser Artikel befasst sich mit legalen Möglichkeiten, mit einfachen Tools wie NodeJS, Electron und React einen Vorteil gegenüber dem Feind zu erlangen und dabei das Verbot zu umgehen. Ich wurde durch einen anderen Artikel zum Experimentieren inspiriert, in dem Roshans Respawn-Zeit und der Wunsch, einen Teil der Routine zu automatisieren, visualisiert wurden . Es ist erwähnenswert, dass wir jetzt Tools in Betracht ziehen, die das Spiel nicht auf unehrliche Weise modifizieren - alle APIs sind offen, die Daten werden auf ehrliche Weise empfangen, es treten keine Störungen im Spielprozess auf. Es werden mehrere Bilder und Code unter dem Schnitt sein.
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 . ?
Fazit: Es ist nicht schwierig, sich in Dota2 zu integrieren. Sie können direkt während des Spiels eine schnelle Analyse durchführen. Wenn Sie sich Esportspiele ansehen, können Sie eine große Menge an schönen Overlays für den Twitch-Stream erstellen. Sie können dieses Thema auch in Richtung einer retrospektiven Analyse entwickeln Wiederholungen, die höchstwahrscheinlich für Profis nützlich sein werden ...
Ich hoffe, es war interessant für Sie zu lesen, wie ich Cheats auf meinem Knie gesammelt habe (wirklich eine gute Frage - sind das Cheats oder nicht?). Und selbst in JS schreiben Sie bitte an das LAN, wenn Rechtschreib- oder Lexikfehler vorliegen. Vielen Dank für Ihre Aufmerksamkeit ...