Create your own Dota 2 overlay for casting Tournament— an Overview

Jérémy VIAL
5 min readSep 26, 2021

Despite some greate improvement since Dota 7.30.c, there is always a way to improve Dota2TV HUD. Since I watch Animajor in June 2021, I was curious to know how it is possible to link your overlay in real time with the game you are watching in the Dota2TV.

Dota2 Animajor Overlay
Custom display player info at WePlay Animajor
Dota2 Animajor Overlay
Custom display Aegis owner at WePlay Animajor

Lets see how we can do this

Dota2 Overlay

1- DOTA2 Game State integration

Valve let you the possibility to hear event from the game using game state integration the only thing you have to do is put a file at :

C:\Program Files (x86)\Steam\steamapps\common\dota 2 beta\game\dota\cfg\gamestate_integration\your_file.cfg using this format

"dota2-gsi Configuration"
{
"uri" "http://localhost:3000/"
"timeout" "5.0"
"buffer" "0.1"
"throttle" "0.1"
"heartbeat" "30.0"
"data"
{
"provider" "1"
"map" "1"
"player" "1"
"hero" "1"
"items" "1"
"abilities" "1"
}
}

uri: Game will be making POST requests to this uri.

timeout: Game expects an HTTP 2XX response code from its HTTP POST request, and game will not attempt submitting the next HTTP POST request while a previous request is still in flight. The game will consider the request as timed out if a response is not received within so many seconds, and will re-heartbeat next time with full state omitting any delta-computation. If the setting is not specified then default short timeout of 1.1 sec will be used.

buffer: Because multiple game events tend to occur one after another very quickly, it is recommended to specify a non-zero buffer. When buffering is enabled, the game will collect events for so many seconds to report a bigger delta. For localhost service integration this is less of an issue and can be tuned to match the needs of the service or set to 0.0 to disable buffering completely. If the setting is not specified then default buffer of 0.1 sec will be used.

throttle: For high-traffic endpoints this setting will make the game client not send another request for at least this many seconds after receiving previous HTTP 2XX response to avoid notifying the service when game state changes too frequently. If the setting is not specified then default throttle of 1.0 sec will be used.

heartbeat: Even if no game state change occurs, this setting instructs the game to send a request so many seconds after receiving previous HTTP 2XX response. The service can be configured to consider game as offline or disconnected if it didn’t get a notification for a significant period of time exceeding the heartbeat interval.

2- Listen to GSI

The second part is to retreve all those information. I choose to use a NodeJS server associate to an electon app to display a control window to the user/streamer. The tricks is to display one of your NodeJS page in the electron windows. This will result to run your NodeJS server and the electon app in the same time.

****       ELECTRON main.js (only for the logic)        ****"use strict";const { app } = require("electron");
const server = require("./app");
electron.app.disableHardwareAcceleration();const attachWindows = true;let window;function createWindow() {
window = new electron_1.BrowserWindow({
width: 500,
height: 760,
webPreferences: {
nodeIntegration: true,
}
});

window.loadURL("http://localhost:3000/home")
}
electron.app.on("ready", () => {
setTimeout(
createWindow,
process.platform === "linux" ? 1000 : 0
);
});

Without going into details, I retreve data and use events to send them continuously to my front page ( the overlay view)

****         NODEJS  app.js (only for the logic)           ****"use strict";
const express = require("express");
const fs = require("fs");
var requeteHeader = {
connection: "keep-alive",
"content-type": "text/event-stream",
"cache-control": "no-cache",
};
var app = express(),
server = require("http").createServer(app); // Set view engine to ejs
app.set("view engine", "ejs"); // Set static resource directory
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "\\public"));
app.get("/", (req, res) => {
res.render("index");
});
// ******** PLAYER ********
app.get("/players", function (req, res) {
res.status(200).set(requeteHeader);
setInterval(() => {
var data = { listPlayer };
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 10);
});
// ******** MAP ********
app.get("/map", function (req, res) {
res.status(200).set(requeteHeader);
setInterval(() => {
var data = { map };
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 100);
});
// ********* RECUPERATION DATA GSI *********app.post("/", function (req, res) {
let body = "";
req.on("data", (data) => {
body += data;
});
req.on("end", () => {
var parseBody = JSON.parse(body);
// ALL
// MY
// LOGIC
// AND DATA ANALYSIS
);
});
// start server
app.listen(3000, () => {
console.log("Listening at 3000");
});
module.exports = app;

3 — The front page (structure of your overlay)

I chose not to use any framwork here, just an .ejs page with a js script associate whose role is to calculate some input and retrieve event from the backend part.

*****   FRONT PAGE mainscript.js  (only for the logic)        ****"use strict";var source2 = new EventSource("/players");
var source3 = new EventSource("/map");
source3.onmessage = function (event) {
const input = JSON.parse(event.data);
//
// my logic
//
};********* FRONT index.ejs (only for the logic) *******<!DOCTYPE html>
<html lang="en">
<link href="public/index.css" rel="stylesheet" type="text/css"><head>
<title>MY DOTA OVERLAY</title>
</head>
<body id="bodybody" class="hypatia">
<!-- -------------- TOP BAR ----------------------- -->
<div id="top-barre" class="hypatia" style="display: flex; flex-wrap: nowrap; ">


<div id="winprob" class="winprobability"> </div>
<!-- -------------- TOP PLAYERS SLOT RADIANT ----------------------- -->

<div id="slot0" class="playerslottopbar0">
<div class="lvltopbar_radiant"><a id="lvlslot0"></a></div>
</div>
<div id="slot1" class="playerslottopbar1" style="margin-left: -0.04vw !important">
<div class="lvltopbar_radiant"><a id="lvlslot1"></a></div>
</div>

<div id="slot2" class="playerslottopbar2">
<div class="lvltopbar_radiant"><a id="lvlslot2"></a></div>
</div>

<div id="slot3" class="playerslottopbar3" >
<div class="lvltopbar_radiant"><a id="lvlslot3"></a></div>
</div>

<div id="slot4" class="playerslottopbar4" >
<div class="lvltopbar_radiant"><a id="lvlslot4"></a></div>
</div>

<!-- -------------- TOP PLAYERS SLOT DIRE ----------------------- -->

<div id="slot5" class="playerslottopbar5">
<div class="lvltopbar_dire"><a id="lvlslot5"></a></div>
</div>

<div id="slot6" class="playerslottopbar6">
<div class="lvltopbar_dire" style="margin-left: 0.1vw;"><a id="lvlslot6"></a></div>
</div>

<div id="slot7" class="playerslottopbar7">
<div class="lvltopbar_dire"><a id="lvlslot7"></a></div>
</div>

<div id="slot8" class="playerslottopbar8">
<div class="lvltopbar_dire"><a id="lvlslot8"></a></div>
</div>

<div id="slot9" class="playerslottopbar9">
<div class="lvltopbar_dire" ><a id="lvlslot9"></a></div>
</div>
</div><!-- -------------- AEGIS POPUP ----------------------- -->
<div id ="aegisOwner" class="aegis">

<div class="box">
<img class="imgAegis" src='public/aegis.png'>
<img id="playerimgagis" class="imgPlayer" style="visibility: hidden" src="public/portrait_png.png">
</div>
<img id='logoteamroshan' class="teamLogo" src="public/teams/_png.png">
<div class="box">

<img id="iconheroaegis" style="float: left;margin-left: -4.1vw;margin-top: 0.5vw; width: 1.40625vw;" src='public/hero_icon/npc_dota_hero_abaddon_png.png'>
<div id="aegisowner" class="boxLaneName">Name</div>

<div class="boxLane" id="app" >
</div>
</div>
</div>
<!-- -------------- BOTTOM ----------------------- --><!-- -------------- BOTTOM PLAYER info----------------------- --><script src="public/mainscript.js"></script></body></html>

4- Display in OBS

You can now display your server source as Browser an cast tournament with your very own overlay :)

Dota 2 Overlay

Live exemple on the French FroggedTV :

[The International 10] Invictus Gaming vs Team Secret — Game 1 — Demi Finale Upper Bracket : https://youtu.be/WuHdlBwHwY8?t=851

[The International 10] PSG.LGD vs Virtus.Pro — Game 1 — Demi Finale Upper Bracket : https://youtu.be/KqOavycWECs?t=805

This is a fast overview of the logic. I will do more detailed articles later on each of the parts

--

--