Zum Inhalt springen
KeystoneLoot.ioMidnight · Season 2

Dokumentation

Für Entwickler

Wie der Import-String aufgebaut ist, damit ein AddOn oder ein Tool ihn lesen und schreiben kann.

Aufbau

Ein Import-String besteht aus einem Präfix und einem Payload:

KeystoneLoot:v3,<base64( zlib( JSON ) )>

Der Payload entsteht in drei Schritten: das JSON wird serialisiert, mit zlib (RFC 1950, also deflate mit Header) komprimiert und anschließend Base64-kodiert, und zwar Standard-Base64 mit + und /, nicht die URL-sichere Variante. Zum Lesen dieselben Schritte rückwärts.

Payload

Das JSON ist ein Objekt, dessen Schlüssel Blizzards numerische Spezialisierungs-IDs sind. Ein String kann mehrere Spezialisierungen enthalten.

{
  "62": [
    { "tier": 3, "itemId": 271874, "gems": [240967], "enchant": 7991 },
    { "tier": 3, "itemId": 239648, "bonusIds": [13751, 13836, 9627] },
    { "tier": 5, "itemId": 251232 },
    { "tier": 3, "itemId": 271564 }
  ]
}

Felder

FeldTypPflichtBeschreibung
itemIdintegerjaDie Item-ID.
tierintegerneinPriorität, 1 bis 5. Default ist 2.
bonusIdsinteger[]neinBonus-IDs des Items.
gemsinteger[]neinItem-IDs der eingesetzten Edelsteine.
enchantintegerneinID der Verzauberung.

Tier-Werte

Die Prioritätsstufen, die tier annehmen kann. So heißen sie auch im AddOn:

WertSymbolBedeutung
1Wäre schön
2Muss haben (Default)
3Best in Slot
4Transmog
5Katalysator

Vier Dinge, die eine Tabelle nicht sagen kann:

  • Diese Seite schreibt Rang 3 und Rang 5. 3 ist, was du tragen sollst; 5 ist ein Item, das in den Katalysator gehört, und zu jedem solchen Eintrag steht das Ergebnis als eigener Rang-3-Eintrag daneben.
  • bonusIds trägt hier, was das Item beschreibt: die Kette bei gefertigten Teilen, das Mythisch+-Label bei Drops aus einem Schlüsselstein. Das Itemlevel steht nicht darin, weil dein Exemplar den Rang trägt, auf dem du es gelootet hast, und nicht den, auf den die Liste zielt.
  • enchant ist eine SpellItemEnchantmentID, nicht die Item-ID der Verzauberung. Das ist der Wert, den Wowheads ench-Parameter und das AddOn brauchen.

Leere Arrays und null werden beim Schreiben weggelassen, statt sie mitzuschicken.

Beispiele

Drei Implementierungen, je nachdem wo dein Code läuft. Alle drei erzeugen denselben String.

JavaScript

Beide Richtungen im Browser, ohne Dependencies. CompressionStream kann jeder aktuelle Browser, deshalb reicht zlib ohne Library:

// A KeystoneLoot:v3 string, built in the browser.
async function encodeV3(table) {
  const bytes = new TextEncoder().encode(JSON.stringify(table));
  // zlib, RFC 1950 (header 0x78 0x9c) — not "deflate-raw".
  const stream = new CompressionStream("deflate");
  const writer = stream.writable.getWriter();
  writer.write(bytes);
  writer.close();
  const packed = new Uint8Array(
    await new Response(stream.readable).arrayBuffer()
  );
  // Standard base64, with + and /, not base64url.
  let binary = "";
  for (const byte of packed) binary += String.fromCharCode(byte);
  return "KeystoneLoot:v3," + btoa(binary);
}

const string = await encodeV3({
  62: [{ tier: 3, itemId: 271874, gems: [240967], enchant: 7991 }],
});

Und zurück:

// And back again.
async function decodeV3(string) {
  const payload = string.slice(string.indexOf(",") + 1);
  const bytes = Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
  const stream = new DecompressionStream("deflate");
  const writer = stream.writable.getWriter();
  writer.write(bytes);
  writer.close();
  return JSON.parse(await new Response(stream.readable).text());
}

TypeScript

Dasselbe mit Typen, samt Struktur des Payloads. Wieder die Browser-Variante, also async:

export type Tier = 1 | 2 | 3 | 4 | 5;

export interface Entry {
  itemId: number;
  tier?: Tier;
  bonusIds?: number[];
  gems?: number[];
  enchant?: number;
}

/** Keyed by Blizzard's numeric specialisation id. */
export type ImportTable = Record<number, Entry[]>;

const PREFIX = "KeystoneLoot:v3,";

export async function encodeV3(table: ImportTable): Promise<string> {
  const bytes = new TextEncoder().encode(JSON.stringify(table));
  const stream = new CompressionStream("deflate");
  const writer = stream.writable.getWriter();
  writer.write(bytes);
  writer.close();
  const packed = new Uint8Array(
    await new Response(stream.readable).arrayBuffer()
  );
  let binary = "";
  for (const byte of packed) binary += String.fromCharCode(byte);
  return PREFIX + btoa(binary);
}

export async function decodeV3(text: string): Promise<ImportTable> {
  const payload = text.slice(text.indexOf(",") + 1);
  const bytes = Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
  const stream = new DecompressionStream("deflate");
  const writer = stream.writable.getWriter();
  writer.write(bytes);
  writer.close();
  return JSON.parse(await new Response(stream.readable).text()) as ImportTable;
}

Node.js

Serverseitig als ES-Modul. node:zlib macht dasselbe wie CompressionStream, Buffer das Base64, und der String ist Byte für Byte derselbe wie im Browser:

import { deflateSync, inflateSync } from "node:zlib";

const PREFIX = "KeystoneLoot:v3,";

export function encodeV3(table) {
  const json = Buffer.from(JSON.stringify(table), "utf8");
  return PREFIX + deflateSync(json).toString("base64");
}

export function decodeV3(text) {
  const payload = text.slice(text.indexOf(",") + 1);
  const json = inflateSync(Buffer.from(payload, "base64"));
  return JSON.parse(json.toString("utf8"));
}

API

Wer den String nicht selbst bauen will, holt ihn fertig ab:

GET/api/import/:class/:spec

Antwort ist JSON, hier für /api/import/deathknight/unholy:

{
  "class": "deathknight",
  "spec": "unholy",
  "classId": 6,
  "specId": 252,
  "view": "overall",
  "build": null,
  "updated": "2026-08-12T19:31:27Z",
  "string": "KeystoneLoot:v3,eJydkDFuwzAMRe..."
}

Ohne Parameter bekommst du die Overall-Liste. Zwei optionale gibt es:

  • view: overall, mythicplus oder raid
  • build: der Heldentalent-Build, wenn ein Spec mehrere Listen hat.
$ curl "https://keystoneloot.io/api/import/deathknight/unholy?view=raid"
$ curl "https://keystoneloot.io/api/import/priest/discipline?build=Oracle"

updated ist der Timestamp des letzten Updates. Wer ihn speichert, kann beim nächsten Mal vergleichen und den Import überspringen, wenn sich nichts geändert hat.

Für die Shell gibt es ?format=plain. Dann kommt nur der String selbst, mit einem Zeilenumbruch am Ende. Die Felder von oben stehen dort in den Headern: x-keystoneloot-class-id, -spec-id, -view, -build und -updated.

$ curl "https://keystoneloot.io/api/import/mage/fire?format=plain"
KeystoneLoot:v3,eJydkTtuwzAMhu/CmYNIiqSo...

Fehler kommen in derselben Form wie Treffer, damit du für eine Route nicht zwei Parser brauchst. Die gültigen Werte stehen als Array dabei:

{
  "error": "Unknown build: Quatsch",
  "available": ["Oracle", "Voidweaver"],
  "docs": "https://keystoneloot.io/en/developers#api"
}

Damit niemand 40 URLs raten muss, listet ein Index alle Specs auf, mit den verfügbaren Views und Builds:

GET/api/import

Die Listen ändern sich höchstens einmal pro Woche. Bitte cache die Antworten, die Header sagen dir wie lange.

Ältere Formate

v1 und v2 sind unkomprimierter Klartext und werden nicht mehr unterstützt. Bitte ausschließlich v3 erzeugen.

Ausprobieren

Der Import-String-Editor zerlegt einen String im Browser und zeigt, was darin steht. Praktisch zum Gegenprüfen einer eigenen Implementierung.