Utiliser DOTA2 GSI et l’API Govee

Jouant fréquemment à DOTA2 et possédant une bande led de chez Govee, lorsque j’ai appris que ce dernier proposait une API gratuite à ses clients, je me suis mis à coder un script qui va mettre à jour la couleur de la loupiotte en fonction des évènements en pleine partie !

Pour cela, trois éléments :

  1. Avoir deno (ou node par exemple, mais c’était l’occasion pour moi de découvrir deno).
  2. Configurer le Game State Integration de DOTA2. J’invite à lire ce dépôt GIT pour savoir comment.
  3. Une bande led Govee et une clé d’API qui va avec. Ça tombe bien, ils expliquent eux-mêmes comment demander la clé.

Une fois ces éléments regroupés et la documentation de l’API Govee épluchée, le script proprement dit n’est pas très compliqué. Il peut se décomposer en trois étapes : le serveur HTTP, la logique d’intégration DOTA2 x Govee et les appels à l’API Govee.

Un serveur HTTP avec Deno

Configurer le Game State Integration de DOTA2 (GSI de son petit nom) va avoir pour conséquence que le jeu va envoyer des requêtes HTTP à intervale régulier vers l’adresse qui lui aura été spécifiée. Ces requêtes HTTP contiennent toute une collection de données sur une partie en cours. On a ainsi accès à la liste des héros, leurs caractéristiques, leur position sur la carte, etc. À noter qu’aucune information qui serait masquée pour le client du jeu n’est envoyée. Il n’est donc pas possible de connaître, par exemple, les objets possédés par un adversaire.

On le voit : il est nécessaire de démarrer un serveur HTTP : ce sera le point d’entrée de notre application.

Celui-ci est très basique puisqu’il n’y a pas d’interface utilisateur, pas de réponse HTTP à gérer. Mais paradoxalement, c’est la partie qui m’a posé le plus de difficultés. Tout simplement parce que je ne connaissais pas encore deno.

En deux mots : importer puis appeler la méthode serve en lui transmettant une fonction asynchrone de gestion des requêtes, ainsi qu’un objet contenant à minima le port.

await serve(requestHandler, { port });

La fonction de gestion des requêtes est elle aussi plutôt concise. Elle va d’abord contrôler que la requête nous envoie bien du json et si c’est le cas, on appellera une nouvelle fonction qui contiendra la vraie logique à mettre en place.

async function requestHandler(request: Request) {
  if (
    request.headers.has("content-type") &&
    request.headers.get("content-type")?.startsWith("application/json") &&
    request.body
  ) {
    const reqBody = await request.json();
    main(reqBody.hero);
    return new Response(reqBody, { status: 200 });
  }

  return new Response("", { status: 200 });
}

DOTA2 x Govee

Le serveur HTTP étant prêt et fonctionnel, chaque requête envoyée par le client DOTA2 sera reçue et son contenu transmis, on l’a vu ci-dessus, à une fonction qui va se charger d’agir en fonction des informations du jeu. Dans notre exemple, on ne transmet que l’objet hero du corps de la requête.

Comme on est en typescript, j’ai créé une interface décrivant cet objet. Je trouve que cela fluidifie le développement sous Visual Studio.

export interface jsonHero {
  xpos: number;
  ypos: number;
  id: number;
  name: string;
  level: number;
  xp: number;
  alive: boolean;
  respawn_seconds: number;
  buyback_cost: number;
  buyback_cooldown: number;
  health: number;
  max_health: number;
  health_percent: number;
  mana: number;
  max_mana: number;
  mana_percent: number;
  silenced: boolean;
  stunned: boolean;
  disarmed: boolean;
  magicimmune: boolean;
  hexed: boolean;
  muted: boolean;
  break: boolean;
  aghanims_scepter: boolean;
  aghanims_shard: boolean;
  smoked: boolean;
  has_debuff: boolean;
  talent_1: boolean;
  talent_2: boolean;
  talent_3: boolean;
  talent_4: boolean;
  talent_5: boolean;
  talent_6: boolean;
  talent_7: boolean;
  talent_8: boolean;
}

On utilisera ici les propriétés alive et health_percent.

L’objectif de l’application est de changer la couleur de la bande led Govee en fonction de la vie du héros : plus il a de vie, plus la bande sera verte. Proche de la mort, la bande tirera vers le rouge. En outre, la bande restera rouge tant que le héros sera mort, et redeviendra bien verte dès qu’il reviendra à la vie.

Étant donné que le jeu envoie plusieurs requêtes par seconde, il est nécessaire de conserver l’état des caractéristiques du héros entre deux requêtes : ceci permet de savoir si quelque chose a changé entre deux requêtes. En effet, si le héros n’a pas encore resuscité ou si sa vie n’a pas évolué, il n’y pas d’action à effectuer sur la bande led. On limite ainsi les appels à l’API (qui sont limité à une centaine par minute, sauf erreur).

Toujours dans cette optique de limiter les appels, pour observer les modifications de la vie du héros, on va mettre un place un système de seuil. Dans DOTA2, la vie remonte en permanence, à raison de quelques points par seconde. Si l’on voulait refléter ceci précisément au niveau de la bande led, il faudra plusieurs appels par seconde et on dépasserait trop vite la limite autorisée. On met donc en place une logique qui détermine que la couleur ne sera pas mise à jour tant que la vie n’a pas augmenté ou diminuer de plus de 4% par rapport au précédent état mémorisé.

Enfin, la couleur de la bande est calculée sur la base du pourcentage de la vie du héros. Par exemple, si celui-ci a toute sa vie, on aura une quantité de vert de 100%, soit 255. Et pas de rouge. Inversement, si le héros n’a plus que 5% de vie, il n’aura que 5% de vert et 95% de rouge (on ne fait pas fluctuer le bleu).

const govee = new Govee();

const RED: itfColor = { blue: 0, green: 0, red: 255 };
const GREEN: itfColor = { blue: 0, green: 255, red: 0 };

interface state {
  alive: boolean;
  health_percent: number;
}

function initState(hero: jsonHero): state {
  // invert values to pretend we don't know the real state
  // this allows to compare the first state to a fake previous state

  let reverted_health = hero.health_percent;
  if (reverted_health === 100) {
    reverted_health = 0;
  } else if (reverted_health === 0) {
    reverted_health = 100;
  }

  return { alive: !hero.alive, health_percent: reverted_health };
}

let previousState: state;

function main(hero: jsonHero): void {
  if (!hero) {
    return;
  }

  if (!previousState) {
    previousState = initState(hero);
  }

  if (!hero.alive) {
    if (previousState.alive !== hero.alive) {
      // hero just died: change color
      // and force health update
      govee.setColor(RED);
      previousState.health_percent = hero.health_percent;
    }
  } else {
    if (previousState.alive !== hero.alive) {
      // hero just respawned: change color
      // and force health update
      govee.setColor(GREEN);
      previousState.health_percent = hero.health_percent;
    } else {
      // in order to avoid to many Govee API calls (ie. health constantly regens)
      // add a threshold before triggering the color update

      const lowerBound = previousState.health_percent - 4;
      const upperBound = previousState.health_percent + 4;

      if (
        hero.health_percent > upperBound ||
        hero.health_percent < lowerBound
      ) {
        // hero health has been changed beyond the threshold: trigger color change
        // according to the health percent, then update the previous state
        const healthGreenColor = Math.floor((255 * hero.health_percent) / 100);
        const healthRedColor = 255 - healthGreenColor;
        govee.setColor({
          blue: 0,
          green: healthGreenColor,
          red: healthRedColor,
        });
        previousState.health_percent = hero.health_percent;
      }
    }
  }

  // always update this field
  previousState.alive = hero.alive;
}

API Govee

On peut le voir dans l’extrait de code ci-dessus : pour manipuler la bande led, la méthode setColor d’un objet govee est appelée, en lui transmettant un objet de couleur.

Cet objet govee est une instance d’une classe que j’ai écrite pour interface notre logique à l’API de Govee. Cette API est extrêmement basique.

Pour agir sur la bande led on effectue une requête HTTP en PUT, on fournissant la clé d’API, l’adresse MAC et le modèle de la bande led, ainsi que l’action à effectuer. Pour modifier la couleur, il suffit de transmettre la quantité de bleu, de vert et de rouge. Je vous mets le code source ci-dessous, ça se passe de commentaire.

export interface itfColor {
  red: number;
  green: number;
  blue: number;
}

export enum enumSwitch {
  on = "on",
  off = "off",
}

export class Govee {
  private mac = "votre adresse MAC";
  private model = "votre n° de modèle";
  private apiKey = "votre clé d'API";
  private controlURL = "https://developer-api.govee.com/v1/devices/control";

  private default_color: itfColor = {
    red: 229,
    green: 255,
    blue: 229,
  };
  private state: enumSwitch = enumSwitch.off;

  async switch(state: enumSwitch): Promise<boolean> {
    const URL = this.controlURL;

    const requestBody =
      '{"device": "' +
      this.mac +
      '", "model": "' +
      this.model +
      '", "cmd": {"name": "turn","value": "' +
      state +
      '"}}';

    const response = await fetch(URL, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
        "Govee-API-Key": this.apiKey,
      },
      body: requestBody,
    }).then((data) => {
      if (data.ok) {
        this.state = state;
      }
      console.log("switched");
      return data.ok;
    });

    return response;
  }

  async setColor(newColor: itfColor): Promise<boolean> {
    const URL = this.controlURL;

    const requestBody =
      '{"device": "' +
      this.mac +
      '", "model": "' +
      this.model +
      '", "cmd": {"name": "color","value": {"r": ' +
      newColor.red +
      ',"g": ' +
      newColor.green +
      ',"b": ' +
      newColor.blue +
      "}}}";

    const response = await fetch(URL, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
        "Govee-API-Key": this.apiKey,
      },
      body: requestBody,
    }).then((data) => {
      console.log(requestBody);
      console.log(data);

      console.log("color set");
      return data.ok;
    });

    return response;
  }
}

Touche finale

Et c’est tout !

Il n’y a plus qu’à exécuter le fichier deno, lancer une partie de DOTA2, et s’émerveiller devant cette belle couleur verte (en croisant les doigts pour qu’elle ne soit jamais trop rouge !).

J’envisage de peaufiner le script pour ajouter d’autres interactions, comme changer encore la couleur lorsque le héro est étourdi ou silencé, par exemple. Il est aussi possible de jouer sur les cooldowns des compétences ou objets. La seule limite, c’est l’imagination (et les cent appels maximum par minute imposés par Govee).

Ce qui est encore plus intéressant avec deno, c’est qu’il est possible de transformer notre application en fichier exécutable autonome. Ça permet de partager plus facilement notre réalisation, mais aussi de l’insérer dans une macro. On pourrait envisager de l’exécuter automatiquement au démarrage de DOTA2.
J’ai par ailleurs un script tout simple qui allume la bande led au démarrage de Windows, et ça fonctionne très bien.


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.