Website: Inscription Tooltips

Inscribed items show extra lines in the game tooltip: Potential, slots, inscriptions and the bound owner. Item data on a website (character viewer, rankings, market, warehouse) only shows the 16-byte item, so these lines are missing.

This guide shows how to read the inscription data and print the same lines as the game client.

For what inscriptions are and how to configure them, see Inscription (overview).

What the tooltip shows#

The game client adds this block at the end of the item tooltip, after the normal option lines:

Potential 30 / 80                    <- yellow, bold
Might II                             <- violet, bold   (filled slot)
Damage +4%                           <- blue           (its description)
Inscription Slot (empty)             <- gray           (open slot)
Inscription Slot (corrupted)         <- dark red       (corrupted slot)
Bound to Hero                        <- red            (only if bound)
  • Potential 30 / 80 = Potential left / Potential total.
  • Closed slots print nothing.
  • An item with no inscription data prints nothing at all.

Where the data lives#

You need three things:

DataWhereWhat you get
Item serialThe 16-byte item, bytes 3-6Key into ItemExt
Inscription recordSQL table ItemExt, column DataPotential, slots, bound name
Inscription names and textsData/Plugins/Inscription/Pools.xmlName, description, magnitude per tier

Also read the Enabled attribute of Data/Plugins/Inscription/Inscription.xml. When it is 0, the game shows no inscription lines, so the website must not either.

Step 1 - Get the serial from the item#

Every 16-byte item has its serial in bytes 3-6, big-endian (most significant byte first). This is the same in every column that stores items: Character.Inventory, warehouse.Items, ExtWarehouse.Items, market rows.

ByteContent
0-2Item index, level, options (you already decode these)
3-6Serial, big-endian unsigned 32-bit
7-15More options, sockets

Skip:

  • Empty slots: byte 0 = 0xFF, byte 7 has bit 0x80 set, and the high 4 bits of byte 9 are all set (0xF0).
  • Serial 0: shop and system items without a serial can never carry inscriptions.
  • Stackable items (jewels, potions): never carry inscriptions.

Example: bytes 3-6 = 00 01 E2 40 → serial 0x0001E240 = 123456.

Step 2 - Load the ItemExt rows#

CREATE TABLE ItemExt (
    ItemSerial bigint         PRIMARY KEY,
    Data       varbinary(128) NOT NULL,
    Updated    datetime       NOT NULL
)

One row per item that has extra data. No row = nothing to show.

Collect the serials of all items on the page, then load them in one query:

SELECT ItemSerial, Data FROM ItemExt WHERE ItemSerial IN (@s0, @s1, @s2, ...)

Step 3 - Find the inscription record#

Data is a list of records, one per end-game system. Inscription is system 1. Other systems may be added later, so always walk the list; never assume the inscription record is first.

Each record:

OffsetSizeField
01SystemId (0 = end of list)
11Version
21Len = size of the body
3LenBody

Walk: start at offset 0. Stop when SystemId is 0 or the record would run past byte 128. Otherwise, if SystemId is 1 that is the record; if not, jump 3 + Len bytes to the next one.

Use the record only when Version = 1 and Len ≥ 21. Anything else: show nothing (same as the game).

Step 4 - Read the record body#

OffsetSizeFieldValues
01Potential0-100, total budget
11SpentPotential used so far
23Slot 1Id, Tier, State (one byte each)
53Slot 2Id, Tier, State
83Slot 3Id, Tier, State
1110BoundCharacter name, ASCII, zero-padded. All zero = not bound
212(server only)Reroll range. Never show it

Slot fields:

FieldMeaning
IdEntry Id in Pools.xml. 0 = none
Tier0 = inscribed, 1 = empowered once (II), 2 = twice (III) ... up to 7 (VIII)
State0 = closed, 1 = open (empty), 2 = filled, 3 = corrupted

Step 5 - Load the inscription texts from Pools.xml#

<Pool Name="Weapon" ItemMin="0" ItemMax="3071">
    <Entry Id="1" Name="Might" Desc="Damage +%d%%" Archetype="Amplify" OptionIndex="125"
           Magnitude="3,4,5" Cost="15,8,8" Weight="100" Catalyst="14,16"/>
</Pool>

For every <Entry> in every <Pool>, keep:

AttributeUse
IdKey (1-255, unique across all pools)
NameFirst 31 characters
DescFirst 95 characters. Can be empty
MagnitudeComma list, one number per tier (max 8)

Magnitude rule: a short list repeats its last number. Magnitude="3,4,5" means tiers 0-7 = 3, 4, 5, 5, 5, 5, 5, 5. An empty list = all 0.

Parse the file once and cache it. Reload it when the file changes.

Step 6 - Print the lines#

In this order:

  1. Potential line (yellow, bold): Potential {left} / {Potential} where left = Potential - Spent, never below 0.

  2. One block per slot, slot 1 to 3:

    StateLines
    0 closednothing
    1 openInscription Slot (empty) - gray
    3 corruptedInscription Slot (corrupted) - dark red
    2 filled, Id found in Pools.xmlName + tier suffix - violet, bold. Then the description - blue (skip if Desc is empty)
    2 filled, Id not in Pools.xmlUnknown Inscription - gray
  3. Bound line (red), only if the name is not empty: Bound to {name}.

Tier suffix: tier 0 = nothing, 1 = II, 2 = III, 3 = IV, 4 = V, 5 = VI, 6 = VII, 7+ = VIII. Example: Might II.

Description: take Desc, replace the first %d with that tier's magnitude, and replace every %% with %. Damage +%d%% at tier 1 → Damage +4%.

Colors used by the game client, for a dark tooltip background:

LineColor
Potential#FFCC1A (yellow)
Inscription name#B366FF (violet)
Description#80B3FF (blue)
Empty slot, unknown#666666 (gray)
Corrupted slotdark red, for example #A01818
Bound#FF331A (red)

Full code#

Uses mssql and fast-xml-parser (npm i fast-xml-parser).

// inscription.ts
import fs from "fs";
import sql from "mssql";
import { XMLParser } from "fast-xml-parser";

const DATA_DIR = "C:/MuServer/Data/Plugins/Inscription";   // your server's path
const SYS_INSCRIPTION = 1;
const TIER_TEXT = ["", " II", " III", " IV", " V", " VI", " VII", " VIII"];

// ---------- Step 1: serial ----------
export function itemSerial(item: Buffer): number | null {
  if (item.length < 16) return null;
  const empty = item[0] === 0xff && (item[7] & 0x80) !== 0 && (item[9] & 0xf0) === 0xf0;
  if (empty) return null;
  const serial = item.readUInt32BE(3);
  return serial === 0 ? null : serial;
}

// ---------- Step 2: ItemExt rows ----------
export async function loadItemExt(pool: sql.ConnectionPool, serials: number[]): Promise<Map<number, Buffer>> {
  const out = new Map<number, Buffer>();
  const unique = [...new Set(serials)];
  for (let i = 0; i < unique.length; i += 500) {
    const chunk = unique.slice(i, i + 500);
    const req = pool.request();
    chunk.forEach((s, n) => req.input(`s${n}`, sql.BigInt, s));
    const names = chunk.map((_, n) => `@s${n}`).join(",");
    const r = await req.query(`SELECT ItemSerial, Data FROM ItemExt WHERE ItemSerial IN (${names})`);
    for (const row of r.recordset) out.set(Number(row.ItemSerial), row.Data as Buffer);
  }
  return out;
}

// ---------- Step 3 + 4: record ----------
export interface InscSlot { id: number; tier: number; state: number }
export interface InscRecord { potential: number; spent: number; slots: InscSlot[]; bound: string }

export function readInscription(data: Buffer): InscRecord | null {
  let p = 0;
  while (p + 3 <= data.length && data[p] !== 0) {
    const len = data[p + 2];
    if (p + 3 + len > data.length) return null;              // broken tail
    if (data[p] === SYS_INSCRIPTION) {
      if (data[p + 1] !== 1 || len < 21) return null;          // unknown version
      const b = data.subarray(p + 3, p + 3 + len);
      const slots = [0, 1, 2].map((s) => ({ id: b[2 + s * 3], tier: b[3 + s * 3], state: b[4 + s * 3] }));
      const name = b.subarray(11, 21);
      const end = name.indexOf(0);
      return {
        potential: b[0],
        spent: b[1],
        slots,
        bound: name.subarray(0, end < 0 ? 10 : end).toString("latin1"),
      };
    }
    p += 3 + len;
  }
  return null;
}

// ---------- Step 5: Pools.xml ----------
interface InscEntry { name: string; desc: string; magnitude: number[] }
let cache: { mtime: number; enabled: boolean; entries: Map<number, InscEntry> } | null = null;

const asArray = <T>(v: T | T[] | undefined): T[] => (v === undefined ? [] : Array.isArray(v) ? v : [v]);

function loadConfig() {
  const poolsFile = `${DATA_DIR}/Pools.xml`;
  const mainFile = `${DATA_DIR}/Inscription.xml`;
  const mtime = Math.max(fs.statSync(poolsFile).mtimeMs, fs.statSync(mainFile).mtimeMs);
  if (cache && cache.mtime === mtime) return cache;

  const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "", parseAttributeValue: false });
  const main = parser.parse(fs.readFileSync(mainFile, "utf8"));
  const pools = parser.parse(fs.readFileSync(poolsFile, "utf8"));

  const entries = new Map<number, InscEntry>();
  for (const pool of asArray<any>(pools.Pools?.Pool)) {
    for (const e of asArray<any>(pool.Entry)) {
      const id = parseInt(e.Id, 10);
      if (!(id >= 1 && id <= 255) || entries.has(id)) continue;   // the server skips these too
      const mag = String(e.Magnitude ?? "").split(",").map((x) => parseInt(x.trim(), 10)).filter((x) => !isNaN(x)).slice(0, 8);
      if (mag.length === 0) mag.push(0);
      while (mag.length < 8) mag.push(mag[mag.length - 1]);      // short list repeats its last value
      entries.set(id, {
        name: String(e.Name ?? "").slice(0, 31),
        desc: String(e.Desc ?? "").slice(0, 95),
        magnitude: mag,
      });
    }
  }

  cache = { mtime, enabled: String(main.Inscription?.Enabled ?? "0") === "1", entries };
  return cache;
}

// ---------- Step 6: lines ----------
export type LineColor = "yellow" | "violet" | "blue" | "gray" | "darkred" | "red";
export interface TooltipLine { text: string; color: LineColor; bold: boolean }

function formatDesc(desc: string, value: number): string {
  let out = "";
  let replaced = false;
  for (let i = 0; i < desc.length; i++) {
    if (desc[i] === "%" && desc[i + 1] === "d" && !replaced) { out += String(value); replaced = true; i++; continue; }
    if (desc[i] === "%" && desc[i + 1] === "%") i++;
    out += desc[i];
  }
  return out;
}

export function inscriptionLines(data: Buffer | undefined): TooltipLine[] {
  if (!data) return [];
  const cfg = loadConfig();
  if (!cfg.enabled) return [];
  const rec = readInscription(data);
  if (!rec) return [];

  const lines: TooltipLine[] = [];
  const left = Math.max(0, rec.potential - rec.spent);
  lines.push({ text: `Potential ${left} / ${rec.potential}`, color: "yellow", bold: true });

  for (const slot of rec.slots) {
    if (slot.state === 1) lines.push({ text: "Inscription Slot (empty)", color: "gray", bold: false });
    else if (slot.state === 3) lines.push({ text: "Inscription Slot (corrupted)", color: "darkred", bold: false });
    else if (slot.state === 2) {
      const e = cfg.entries.get(slot.id);
      if (!e) { lines.push({ text: "Unknown Inscription", color: "gray", bold: false }); continue; }
      const tier = Math.min(slot.tier, 7);
      lines.push({ text: e.name + TIER_TEXT[tier], color: "violet", bold: true });
      if (e.desc) lines.push({ text: formatDesc(e.desc, e.magnitude[tier]), color: "blue", bold: false });
    }
  }

  if (rec.bound) lines.push({ text: `Bound to ${rec.bound}`, color: "red", bold: false });
  return lines;
}

Using it on a character page:

const inv: Buffer = character.Inventory;                  // varbinary(3792)
const items = [];
for (let slot = 0; slot * 16 + 16 <= inv.length; slot++) items.push(inv.subarray(slot * 16, slot * 16 + 16));

const ext = await loadItemExt(pool, items.map(itemSerial).filter((s): s is number => s !== null));

for (const item of items) {
  const serial = itemSerial(item);
  const extra = serial === null ? [] : inscriptionLines(ext.get(serial));
  // render your normal tooltip, then `extra` at the end (after a blank line)
}

Worked example#

A sword with serial 123456. Item bytes 3-6: 00 01 E2 40.

ItemExt.Data for serial 123456 (first 26 bytes, the rest is zeros):

01 01 17  50 19  01 01 02  00 00 01  00 00 00  00 00 00 00 00 00 00 00 00 00  32 64
BytesMeaning
01 01 17System 1 (Inscription), version 1, body 23 bytes
50Potential 80
19Spent 25
01 01 02Slot 1: entry 1, tier 1, filled
00 00 01Slot 2: open
00 00 00Slot 3: closed
00 × 10Not bound
32 64Server only - ignore

Entry 1 in the shipped Pools.xml: Name="Might" Desc="Damage +%d%%" Magnitude="3,4,5". Tier 1 → magnitude 4.

Result:

Potential 55 / 80
Might II
Damage +4%
Inscription Slot (empty)

Important Behavior#

  • Data is live. The game server writes ItemExt as soon as an inscription changes. The item columns (Inventory, ...) are saved on the normal save cycle, so for a few seconds a just-moved item can show in its old place. That is normal.
  • Rows follow the serial. Trade, warehouse, drop and pick-up keep the record. Selling to an NPC, using the item in a mix, or the item vanishing from the ground deletes the row.
  • No record yet. Eligible items get their Potential the first time a player gets them (pick-up, mix, shop, or the first login after the plugin was turned on). Until then the item has no row and shows nothing. The game shows nothing too.
  • Plugin off or unlicensed. With Enabled="0" the rows stay in the database but the game shows nothing. Your website checks Enabled for this. The website cannot see the license; if the paid module is not licensed, the server owner should set Enabled="0".
  • Entry removed from Pools.xml. Items keep the Id. The game shows Unknown Inscription; do the same. Never renumber entry Ids.
  • Same serial twice. Only stackable items can share a serial, and they never have records. One serial = one record.

Optional - catalyst lines on jewels#

The game also explains catalyst jewels. Hovering a jewel that is the Catalyst of an entry shows:

Inscription Catalyst                 <- yellow, bold
Weapon: Might                        <- violet  (pool name: entry name)
Damage +3%                           <- blue    (tier 0 magnitude)

One pair of lines per entry whose Catalyst="Group,Index[,Level]" matches the jewel (and its level, if given). The game only lists entries whose Class includes the viewer's class; on a website, either filter by the character being viewed or show all.

Common Issues#

Last updated October 1, 2026