# DAoC stream overlay

Live overlay widgets for Dark Age of Camelot, driven entirely by `chat.log`.
Realm points, combat, PvE and a killfeed, each as its own browser source for
OBS or Streamlabs.

Built for **Live (Broadsword official)**, patch **1.130a**. The grammar was
extracted from a 1.129 client, which 1.130a did not change -- it is a
server-side patch. A patch that does ship a new client is worth re-checking;
[docs/tools.md](docs/tools.md) says how.

**The overlay reads a text file.** The half that reads the log and serves the
widgets never touches the game at all -- nothing read from it, nothing loaded
into it, nothing sent to its servers. What it counts, it counts from `chat.log`,
the plain file the game writes when you type `/chatlog`.

The **flusher** is the part that keeps the log current. On its default setting,
**Real-time log**, it asks the game to save each line the moment it happens, and
reads a few things from the game while it does — your **character**, your
**zone**, your **realm rank** and your **lifetime realm points** — so the panel
can show who is being logged, keep the zone right, and get the rank, its bar and
`!rp` right without a web lookup. Asking the game to
save each line is a small, temporary change made in the running game's memory: it
touches no file the game owns and undoes itself when the client closes. (One
optional tickbox makes one more such change — keeping the game's sound playing
while it is not the focused window.) All of it happens only while the flusher is
running. Untick Real-time log and it presses your chat-log key on a timer
instead. It is all covered below.

The flusher is a separate program, `overlay.exe -flush`, started for you by the
overlay. It is separate on purpose: it is the one part that reaches the game and
the one part that asks Windows for administrator rights, while `overlay.exe`
itself only reads files and never needs them. It never runs unless you turn it on
— **Start** and **Stop** are on the control panel, and **Start it with the
overlay** starts it for you.

### What it does not do

- **It does not play the game.** Nothing here targets, casts, moves, loots,
  buffs, follows, or responds to anything in the world. It has no idea what is
  happening in the game except what the log says, after the fact.
- **It does not run more than one copy of the game**, assist, broadcast, or mirror input
  between windows.
- **The widget half reads files, not memory.** The part that serves the widgets
  reads `chat.log` (read, never written) and `user.dat`'s `[keyboard]` section
  only, to learn which key you chose — never `[quick]`, which holds character
  names, servers and server addresses. The flusher is the one part that reads and
  nudges the running game's memory (see above), and it changes no file the game
  owns. One file of your own can be written, and only if you ask for it by name:
  `overlay.exe -flush -add-stats-macro <character>` adds a macro to that
  character's own settings `.ini`, with that character logged out (character
  select is fine) and a `.bak` taken first. The **flusher** reads your character
  and zone from the game, and on its real-time setting asks the game to switch
  its own log to write immediately — that is the only part that reaches the game
  beyond a keystroke, and it is the flusher, never the overlay.
- **It sends exactly one keystroke, and only if you ask it to.** The flusher is
  a separate opt-in child process that presses the chat-log key *you* bound, on
  a timer, so the game saves its log promptly instead of a chunk at a time.
  Nothing else is ever sent. Run the overlay without it and no keys are pressed at all.
- **It sends nothing anywhere.** No telemetry, no accounts, no external service.
  The web server answers this machine only until you tell it to serve your LAN.
  Two outbound connections exist and both are off until you switch them on: to
  OBS or Streamlabs Desktop, on your own machine, to save a replay or change
  a scene, and to Twitch
  chat, to answer commands your viewers type. See
  [Acting on it: OBS or Streamlabs control](#acting-on-it-obs-or-streamlabs-control) and
  [Answering chat](#answering-chat).

The whole point is a stream overlay: it makes what already happened legible to
someone watching, and it has no capability to affect what happens next.

---

## Quick start

> **Never set up an OBS overlay before?** Read
> [`docs/START-HERE.txt`](docs/START-HERE.txt) instead of this. It is the same
> setup with nothing assumed: every click named, and what you should see after
> each step. It ships in the zip as `START HERE.txt`.

**1. Turn the log on, and turn it all the way on.**

In game, type `/chatlog` on its own — that is the form that writes `chat.log`,
which is the file the overlay looks for. (`/chatlog <name>` logs to a different
filename instead; if you use it, put that path in the **chat.log** box on the
control panel, or pass it with `-log`.) Then right-click
your chat tab → **Messages & Channels** → tick **all 16 Message Types**. An under-subscribed log is the most
common cause of "the numbers are wrong": whole categories of line simply never
get written, so the overlay cannot count what it never sees.

**2. Run the overlay.**

```
overlay.exe
```

It finds `chat.log` on its own and prints the URLs to use. Nothing to configure.

Windows interrupts the first run twice, and both are expected. **"Windows
protected your PC"** is SmartScreen, which says that about every program
without a code-signing certificate; this one does not have one, so click **More
info** and then **Run anyway**. **"Allow this app through the firewall?"** —
allow it on **Private** networks, or a second PC cannot reach the widgets.

Your antivirus may object as well. See
[If your antivirus objects](#if-your-antivirus-objects).

**3. Add the widgets in OBS.**

Add a **Browser Source** per widget, using the URLs it printed:

```
http://localhost:8752/w/all         everything at once
http://localhost:8752/w/rp          realm points, RP/hour, deaths
http://localhost:8752/w/combat      damage, DPS, hit rate, top opponents
http://localhost:8752/w/pve         XP/hour, coin, mob kills, level, zone
http://localhost:8752/w/killfeed    kills as they land, RP on the row, streak
http://localhost:8752/w/realm       frontier activity: losses, hot zone, captures, who is farming you
http://localhost:8752/w/cc          crowd control you landed, and who you held most
http://localhost:8752/w/alert       a moment, for a few seconds, then gone
```

If OBS runs on a **second PC**, tick *Let other machines on my network reach
the widgets* on the control panel and restart, then use the LAN address it
prints instead of `localhost` — see [Two PCs](#two-pcs-gaming-pc--streaming-pc).

Every widget is transparent, sized by the browser source, and updates itself
over SSE. No refreshing, no polling, no OBS scripts.

---

## The control panel

```
http://localhost:8752/control
```

Everything below has a flag, and a flag is a thing you have to know exists
before you can use it. The panel is the same settings with their values on
screen:

- **Browser sources** — pick a widget, untick the rows you do not want, set how
  many names a list shows, and copy the finished address into OBS. The widgets
  stay stateless: the settings live in the URL, which is what lets two sources
  of the same widget show different things.
- **How it looks** — six ready-made looks to start from, then text, label and
  number colours, the backdrop and how
  see-through it is, the font and the text size. One setting for the browser
  sources and the HUD together: a source picks it up within a few seconds and
  the HUD repaints on its next tick, so nothing has to be re-added in OBS. A
  source whose address names its own colours keeps those instead.
- **HUDs** — tick any of the panels to float it over the game as its own HUD,
  as many at once as you like; each has its own position, typed or nudged, and
  moves on the game as you place it. They cannot be dragged, because they pass
  clicks through to the game; this is how they get placed.
- **Flusher** — its interval and macro keys, a Start and Stop button, whether it
  starts with the overlay, and the last dozen lines it printed. It asks Windows
  for administrator rights when it starts; Windows grants those to a new
  process with a window of its own, so the pill then reads `running as
  administrator` and says that this page can no longer read what it prints.
  Stop still reaches it, and it still ends when the overlay does. If it dies
  for any other reason, the panel says why rather than going quiet.
- **Act on it in OBS or Streamlabs** — which program to drive, its address, its
  password or token, and the three rules. Off until switched on. See below.
- **Answer chat** — channel, the account it sends as, the token, a name for the
  bot if you want one, and a tickbox per command. Off until switched on. See
  below.
- **Lifetime realm points** — whether to ask the Camelot Herald for your total
  as of your last logout, which character, and which cluster. Off until
  switched on, and the only thing here that leaves your machine. See below.
- **Moments** — the rare things worth looking up for: a realm rank crossed, ten
  kills without dying, a best night beaten, a level, a keep taken. Put them on
  stream with the `alert` browser source, and tick the box here to write them to
  `chapters.txt` as VOD chapter markers as well.
- **Past sessions** — the last twenty evenings with tonight at the top: RP,
  RP/hour, kills, deaths, best streak, and how long each ran. Written to
  `sessions.json` a minute at a time while you play, so a crash or a closed
  window does not take the evening with it.
- **Log and server** — log path, port, whether the LAN may reach it, and a
  specific listen address if you want one. These are read once at startup, so
  they take effect at the next launch. Also **Save a copy of these settings**
  and **Load settings from a file**: the whole of `overlay.json`, for moving a
  setup to another PC or getting it back after a reinstall. The copy contains
  your OBS password or Streamlabs token, and your Twitch token, so keep it the
  way you keep those.

Anything changed here is written to `overlay.json` beside the exe. Delete that
file to go back to defaults; copy it to the streaming PC's folder to carry your
layout over.

**The panel is readable from the streaming PC and writable only from the gaming
PC.** Reading it over the LAN is the point — you check on the machine OBS runs
on — but nothing on the LAN gets to move your HUD, rewrite your settings, or
start a program that types into your game. The log path is hidden from LAN
readers for the same reason `/health` hides it: it names your Windows account.

## Acting on it: OBS or Streamlabs control

Everything else here describes the fight. This part acts on it: the overlay can
drive your streaming program itself, so a clip lands during the stream rather
than in a folder to edit afterwards.

Both programs are supported and you pick which one on the panel. They do not
speak the same language — OBS has obs-websocket, Streamlabs Desktop has its own
API — but the three rules, and everything you type, are the same either way.

It is off until you switch it on, and it is off by default in a fresh
`overlay.json`. Every rule is off on its own too, until you fill it in.

Three rules count something as it happens:

| Rule | What fires it | What it does |
|---|---|---|
| Multi-kill | *n* realm-point awards inside a window | saves the replay buffer |
| Death | you die | switches to a scene by name |
| Milestone | every *n* realm points this session | shows a source for a few seconds, then hides it again |

And every **moment** can fire one of those same three actions. These are the
moments the alert widget shows and the chapter file records — rarer than a
kill, which is the whole point of them:

| Moment | When |
|---|---|
| Realm rank up | you cross a realm rank, or a level inside one |
| RvR kill streak | 5 enemy players killed without dying, then 10, then every 10 |
| PvE kill streak | 5 mobs killed without dying, then 10, then every 10 |
| Best night yet | tonight passes the best evening on record |
| Level up | you level |
| Keep taken | a keep or tower is captured |
| Death | you die |

Each gets a row on the panel with a dropdown: **do nothing**, **save a replay**,
**switch to scene**, or **show a source**. So "save a clip every time I rank up"
or "cut to the celebration scene when I beat my best night" are two dropdowns
and a scene name.

The scene and source names come from a list the panel reads out of OBS or
Streamlabs Desktop, so you pick them rather than type them — **load scenes &
sources** fills the boxes, and the source list follows the scene you chose. It
reads over the same connection the actions use, so it reaches your program
wherever the address points, on this machine or another on the network. Until
something is connected the boxes are plain text, which is also how you fill them
in for a program that is not up yet.

Ranking up is the one milestone in this game that everybody watching
understands, and it is worth pointing something at.

**If you use OBS:** turn the server on first, under **Tools → WebSocket Server
Settings → Enable WebSocket server**. Note the port (4455 by default) and the
password if you set one. On the control panel, under **Act on it in OBS or
Streamlabs**, leave the program on **OBS** and paste the address
(`ws://127.0.0.1:4455`) and the password.

**If you use Streamlabs Desktop:** turn remote control on first, under
**Settings → Remote Control**. Click the QR code, then **Show details**, and copy
the API token. On the control panel set the program to **Streamlabs Desktop** —
the address fills itself in (`ws://127.0.0.1:59650/api/websocket`) — and paste
the token.

Either way, fill in the rules you want and save. It connects straight away —
there is nothing to restart — and the panel says whether it did.

Some detail worth knowing:

- **The multi-kill rule counts realm-point awards, not deathblows.** The log
  gives a mob kill and a player kill the same shape, so a rule keyed on
  deathblows would fire the replay buffer on three grey wolves. An RP award is
  the one line that only ever means a player died and you were paid for it.
- **The replay buffer is started for you.** Setting any rule that saves a replay
  — the multi-kill rule, or a moment pointed at **save a replay** — is asking
  for a clip afterwards, and neither program can produce one from a buffer that
  is not running — OBS refuses, Streamlabs takes the request and quietly writes
  nothing. So when the rule is set and the buffer is off, this starts it: once
  per connection, never again. Stop it yourself and it stays stopped; that is an
  answer, and this does not argue with it. The panel says what it did.

  It can still fail, and then it says so: OBS refuses if the replay buffer is
  switched off in **Settings → Output**, which is a different problem with a
  different fix.
- **Showing a source wants one you built and left switched off** — an image, a
  video, or your Streamlabs alert as a browser source. That is what the milestone
  rule does, and what a moment set to **show a source** does. It is shown,
  held, and hidden again, including if the overlay is shut down mid-hold. A
  celebration graphic stuck on screen is the one failure here with an audience.
- **Nothing fires twice in quick succession.** A cooldown, 20 seconds by
  default, is the floor between any two actions, so a good fight cannot turn
  into a scene strobing in front of viewers. A run of kills saves one clip, not
  one per kill after the third, and a single huge award fires one alert rather
  than a queue of them. It is one cooldown for all of it rather than one each: a
  death and a rank-up landing together is the case it exists for, and the second
  is dropped rather than queued.
- **A long silence resets the session**, the same four hours the widgets use.
  Yesterday's realm points do not count toward tonight's milestone.
- **The password or token stays on this machine.** It is stored in the clear in
  `overlay.json`, exactly as OBS stores its own, and it is blanked out of what
  the control panel sends to a reader on the LAN. The Streamlabs token is the
  one to keep closer: OBS is sent a hash of your password and never the password
  itself, but Streamlabs is sent its token as it is.
- **It talks to the program, never listens.** The connection subscribes to no
  events, so neither program's own chatter reaches this one. It does ask, every
  ten seconds, what the program is doing — streaming, recording, holding a
  replay — so the panel can say whether the rules can actually fire. "Connected"
  and "able to do anything" are different sentences, and only one of them was
  being shown.
- **Streamlabs only: Studio Mode changes what a scene switch means.** With Studio
  Mode on, Streamlabs puts the new scene in preview rather than on air and you
  transition it yourself. That is Streamlabs' own behaviour, left alone on
  purpose — forcing the transition would mean this program deciding to cut your
  live output, which is further than a scene change goes.

Both take a scene by the name you see, but Streamlabs switches internally by id,
so a Streamlabs scene change looks the name up first. That is one extra round
trip on loopback and nothing you can notice.

If the program is on a second PC, put its LAN address in instead of `127.0.0.1`
— the overlay is the one making the connection, so this works the other way
round from the browser sources. Only `ws://` is supported; neither program's
server does TLS.

## Answering chat

The widgets show the numbers to people already looking at them. This answers
the viewer who types instead, so you do not have to stop and read your own
overlay out loud.

Twitch chat is IRC, so this signs in to it directly. There is no third-party
bot service in the middle, nothing is hosted anywhere, and the connection is
made by your machine to Twitch and nowhere else.

| command | answers with |
|---|---|
| `!run` | RP this session, time played, RP/hour, kills, deaths, best streak |
| `!rp` | session RP, lifetime total, realm rank, and how far to the next one |
| `!kills` | kills, deaths, current and best streak, who you fought most |
| `!dps` | dps, damage dealt and taken, hit rate, healing |
| `!pve` | level, xp/hour, coin, zone |
| `!best` | tonight against the best evening on record, and how far short it is |

Every one is off until you tick it, so a later version adding a command does
not start answering in your chat because you upgraded.

Setting it up:

1. Get a **chat** OAuth token for the account the messages should come from —
   your own, or a second account you made for it. Any of the usual Twitch
   token generators will do; the scopes it needs are `chat:read` and
   `chat:edit`.
2. On the control panel, under **Answer chat**, fill in your channel, that
   account's name, and the token. Tick the commands you want. Save.
3. The status line says whether it signed in. It takes effect immediately —
   nothing to restart.

Give it a name under **Calls itself** and every answer starts with it, which is
worth doing if your chat already has another bot in it. Leave it empty and the
answers start with the numbers.

Some detail worth knowing:

- **Rank and the lifetime total come from the game; the Herald is a fallback.**
  With the real-time log running, your realm rank and your exact lifetime realm
  point total are both read straight from the client — so the realm-rank moment,
  the rank bar, and `!rp` all work with no web lookup. The chat log never carries
  the lifetime total (`/stats` counts the current session only, which is what its
  `this session` header means), so without the real-time log you switch on
  **Lifetime realm points** below and the Herald supplies it instead. The rank
  table runs 1L0
  to 15L0, which is the whole of it -- there is no rank above realm rank 15, so
  a character standing on 15L0 is told it is the top rather than being given a
  next rank to chase.
- **It reads and nothing else.** No command changes a scene, moves the HUD,
  writes a setting or reaches the game. There is no command that takes an
  argument, so there is nothing for a viewer to put one in.
- **Nothing anyone types is repeated back.** Every answer is assembled here out
  of numbers, and only the first word of a chat line is even looked at. A chat
  full of hostile strangers cannot make this say a sentence of their choosing.
- **Two cooldowns.** The same command answers at most once every 20 seconds by
  default, and no two answers come closer together than 3 seconds. Twitch times
  out a bot that talks too fast, and a chat that can make it repeat itself will.
- **One channel.** It joins yours and no other, and it ignores whispers.
- **The token stays on this machine**, like the OBS password, and is blanked out
  of what the control panel sends to a reader on the LAN. A chat token is a
  credential for an account rather than for a program on your PC, which is the
  more serious of the two to leak.

## Lifetime realm points

Your `chat.log` does not contain your lifetime realm point total. `/stats`
looks like it would and does not: its header reads `Statistics for <name> this
session`, and its `Total RP` is that session's. Nothing else in the log states
one either.

Broadsword publish it themselves. The Camelot Herald's public record says where
each character's total stood when they last logged off, and this reads it once:

1. Control panel, **Lifetime realm points**, tick the box. Save.
2. Leave **Character** empty and it uses whoever `/stats` last named, then
   writes the full name — surname included — back into the box for next time.
   Fill it in yourself if you would rather not wait, or if two characters on
   your cluster share a first name.
3. **Cluster** is `Ywain` for Live.

The total on screen is then that baseline plus everything counted from the log
tonight, so it moves on every realm point line rather than on every lookup, and
`!rp` can name your realm rank and how far the next one is.

This is the only part of this program that talks to a machine that is not
yours, which is why it is off until you switch it on. What it sends is one
character name and one cluster name to the game's own API — the same request
the Herald website makes when anybody looks that character up, at a public
record anybody can read. Nothing about your log, your machine or your session
goes with it, it asks once per session rather than on a timer, and it
identifies itself so Broadsword can see what it is.

## Two PCs (gaming PC + streaming PC)

This is the setup it is built for. `overlay.exe` runs on the **gaming PC**,
next to the game. OBS runs on the **streaming PC** and points its browser
sources at the gaming PC over the LAN.

**The overlay answers `localhost` only until you say otherwise**, so this is
the one setup that needs a decision. Tick *Let other machines on my network
reach the widgets* on the control panel, under Settings, and restart it: the
address is chosen when the server starts, so that tickbox is the one setting
here that does not apply the moment you save it.

It then prints its LAN address at startup — use that address on the streaming
PC, not `localhost`.

There is a `-lan` flag as well, and it does the same thing. Neither is per
launch: the tickbox is saved in `overlay.json`, and so is the flag the first
time you type it. This is one decision either way, made once.

Whatever you open, `localhost` keeps working on the gaming PC. That matters
more than it sounds: the control panel only accepts changes from this machine,
and a request arriving on the LAN address is not `localhost` even when it came
from the same PC.

One thing Windows will not do for you: a console program gets no
"allow through the firewall?" prompt. If the streaming PC cannot connect, add
the rule once, in an **elevated** PowerShell on the gaming PC:

```powershell
New-NetFirewallRule -DisplayName "DAoC overlay" -Direction Inbound `
  -Protocol TCP -LocalPort 8752 -Profile Private -Action Allow
```

`-bind 192.168.1.20` narrows it to one address instead of every interface, for
a machine with a VPN or a second network you would rather it stayed off.

---

## The delay, and the `flusher`

The game does not save `chat.log` on a clock. It holds your chat and saves it a
chunk at a time, so how far behind the overlay runs depends on how fast you are
generating chat:

| what you are doing | how far behind the overlay is |
|---|---|
| heavy combat | ~30 seconds |
| questing, small pulls | minutes |
| idle, or reading | no limit — measured at 21 minutes |

Nothing on the reading side can fix this: the lines are not saved yet.

The **flusher** fixes it. Its **Real-time log** setting, on by default, tells the
game to save each line the moment it happens, so the delay goes to zero for the
rest of the session. Untick it and the flusher instead presses your chat-log key
on a timer, which makes the game save what it is holding and caps the delay at
how often it presses.

The **flusher** does exactly that, and nothing else:

```
overlay.exe -flush
```

With Real-time log off, it reads your chat-log key out of `user.dat`, finds the
game window, and presses that key every 10 seconds — **whether or not DAoC is in
front**, so it does not steal focus mid-fight.

**You do not have to start it yourself.** The control panel has **Start** and
**Stop** beside the flusher settings, and a **Start it with the overlay** tickbox
that makes it one less thing to remember. Started that way it has no window of
its own — what it prints appears on the panel instead, so there is nothing new on
your taskbar. Started by hand it prints the same thing in its own window. One
case differs either way: when it asks Windows for administrator rights it runs as
a separate program, and the panel can no longer read what that window prints.
**Stop still stops it**, and it still closes when the overlay does — including
when the overlay is closed from Task Manager or crashes.

That is why it is a **separate program**. The overlay only ever reads files and
never needs administrator rights. This part reaches the game, and might. Keeping
them apart means the thing you leave running all stream is the harmless one, and
that the one asking for administrator rights is not also the one showing a web
page.

It used to be a separate *file* as well, `flusher.exe`, and is not any more:
`-flush` is a mode of `overlay.exe`. Nothing about what it does changed. What
changed is that the download is one program instead of two, so there is one
version to keep in step and one file for a scanner to have an opinion about.

### Real-time log: the default

The whole delay is really one **setting on the log**. The game saves its chat a
chunk at a time because that setting is off; it never tells the log to save each
line straight away. Real-time log turns that setting on at the source: it asks
the game to save each line the moment it happens. After that every line reaches
the file the instant it is written, for the rest of the session, with nothing
more to do — the delay above goes to zero. The overlay reads the file exactly as
before; the file is simply live now.

It is **on by default**. Untick **Real-time log** on the control panel and the
flusher goes back to pressing your chat-log key on a timer instead.

While it is at it, it reads two things from the game: which **character** you are
on and which **zone** you are in. That is how the panel shows the character being
logged and follows a character swap, and how the zone stays right even when you
have not moved — the log only names a zone when you cross a boundary, but the game
always knows it. It also switches logging on for you if it is off, with your own
chat-log key.

If you run two copies of DAoC out of the **same folder**, the folder cannot tell
them apart. The control panel's client list shows them by character — click the
one you are streaming, and it drives that one.

Two honest notes:

- It needs administrator rights, the same as the keystroke option and for the
  same reason: DAoC runs as administrator, and reaching a program running as
  administrator at all — even to read one thing from it — needs the same.
- This is the one part that reaches the game beyond pressing a key: it reads your
  character and zone, and changes that one log setting. The overlay's reading
  half still touches nothing. If you would rather it did not, untick the box.

### If your keys use Ctrl or Alt

Nothing to do here — just what happens under the hood. When the flusher presses a
key for you — your `/stats` key, or the chat-log key to switch logging on — and
that key uses a modifier like Ctrl or Alt, the modifier is pressed for real for a
fraction of a second, because there is no way to press only "into the game" for a
held modifier. The flusher lets go of any key you are physically holding first
and presses it again after, so your movement is not turned into some other
combination while it does. The one thing it cannot help: alt-tab out and type
during that fraction of a second, and the keystroke can land in the program you
switched to. A key with no modifier avoids all of it.

The widgets tell you whether the log is keeping up, so what is on screen is
honest — "live" when the flusher is running, and a warning when it is not and the
numbers may be minutes old.

---

## What gets counted

Everything below is read off a line the game writes. Nothing is estimated.

| metric | comes from |
|---|---|
| RP, bonus RP, BP, XP | `You earn 66 realm points.` and its siblings |
| Damage out, crits, extra hits | `You hit X for N damage!`, `You attack X with your sword...` |
| Pet damage | `Your walking rock attacks X and hits for N damage!` |
| Damage taken | `X hits your torso for N (-N) damage!`, `X hits you for N damage.` |
| Mitigated | the `(-N)` the game already subtracted for armour and resists |
| Avoided | swings at you that missed, or you blocked, parried or evaded |
| Healing done | `You heal X for N hit points.` — yourself, a pet, or someone else |
| Kills (RvR) | `You just killed X!` — the deathblow on an enemy player; this is what counts toward your streak |
| Mob kills (PvE) | `X dies!` — kept apart, so a night of grinding does not read as a night of RvR. Mobs stay off the RvR kill feed and the opponents board and go to the PvE kills board (`mob_top`) instead |
| Deaths | `You have died.` live, corrected upward by `/stats` |
| Streak, deathblows | `/stats` only — the server states them, no combat line does |

Player chat is excluded outright from every statistic, so someone typing
"300 damage" in guild chat is not damage.

### This fight, not tonight

The session totals only ever go up, which makes them the wrong number to react
to on stream: eleven thousand realm points is the same eleven thousand whether
the last fight was a rout or a wipe. So the combat rows also carry the fight
you are in -- `fight`, `fight_dps`, `fight_taken`, `fight_kills`, `fight_time`
-- and nothing else about them is different: same log, same lines, just the
ones since the last quiet moment.

A fight ends after **ninety seconds** with nothing in the log that is combat.
Combat here is wider than damage: a swing that missed, one that was blocked, a
heal landing between spikes. Ninety was measured rather than picked. In a
26-hour log donated by a player, the gaps between combat lines were either
under twenty seconds or over a hundred and twenty, with nothing whatever in
between -- so a boundary anywhere inside that band splits nothing that was
really one fight. It also matters which way the errors fall: merging two fights
understates one number, while splitting one zeroes the counter in the middle of
a fight somebody is watching. Where the data is quiet, the long answer is the
safe one. That log came out as 27 fights, the biggest of them 17,188 damage
dealt against 494 taken in 95 seconds.

Ninety seconds is the default, not the rule: **The fight counter** on the
control panel takes anything from `5s` to `30m` and applies it straight away,
no restart. Shorten it if your fights run into each other, lengthen it if a
heal-up is being counted as two. It is `fight_gap` in `overlay.json`. The
bounds are there because the ends are not preferences -- a couple of seconds is
shorter than the pause between two swings and would cut one fight into a dozen,
and an hour is longer than most evenings.

The two macro keys are `stats_key` and `loc_key` in the same file, under
`flusher`. Blank means that key is never pressed.

A finished fight stays on the widget rather than blanking back to zeroes -- the
moment you want to read what a fight came to is the moment it stopped -- and
says `(last)` beside the number until the next one starts.

`Ctrl+Alt+F`, or **End the fight** on the tray menu, breaks it by hand, for the
pull or the regroup that the log has no way to tell from a lull.

### The realm widget

`X was just killed in Caledonia by Y!` is a realm-wide broadcast, and the
client only prints it for **your own realm's losses**. That makes it a good
activity pulse and a bad scoreboard, and the widget is built accordingly:
losses in the last ten minutes, the zone most of them happened in, and which
enemy players are doing it. Guard and mob kills are counted in the totals but
kept off the leaderboard — guards kill constantly and would own it.

You will see your own name go past when you die. That is the same broadcast,
not a second death counter.

### The crowd control widget

The client announces crowd control landing on somebody else and names them.
It never announces one landing on you: there is no `You are stunned!` line
anywhere. Being held is only ever learned afterwards, from the line that says
it ended — and a stun that lasted until you died prints nothing at all.

So the widget shows two half-pictures rather than one whole one, and is worded
that way. **CC landed** is exact. **You broke out of** counts the recoveries,
which is not the number that hit you and never can be.

Only stuns and mesmerizes are announced this way, so roots and snares are not
in it.

## Deaths, deathblows, and the server's own totals

Your kill streak and deathblow count are never stated by any combat line, and
your death count is only stated the moment it happens — miss that line to a
buffer flush and it is gone.

They are all in the block the client prints when you type `/stats`. `flusher`
can type it for you on a schedule — but `/stats` is not a keyboard action, so
it cannot be read out of `user.dat` the way the chat-log toggle can. It has to
be a quickbar macro.

`flusher` can make it, with that character logged out (character select is fine):

```
overlay.exe -flush -add-stats-macro Prondr
```

That writes three lines to that character's settings file — the macro itself,
the quickbar slot pointing at it, and a quickbind putting that slot on a key —
after backing the file up to `.ini.bak`, and prints the key to use. It refuses while the client is running, because the
client holds its settings in memory and writes the whole file out when it
saves: an edit made underneath it is discarded at logout without a word. Run it
again later and it finds its own work rather than making a second macro.

To do it by hand instead, in game once:

```
/macro stats /stats
```

and drag it to a slot. Either way, `overlay.exe -flush -slots <character>` prints which
slots are taken and which keys the free ones answer to:

```
overlay.exe -flush -slots Prondr
```

A quickbar panel is ten pages of ten keys, and only the page on screen answers
to the number keys. `-add-stats-macro` skips pages **1, 2, 9 and 10** outright
-- those are the ones people actually keep things on -- and takes the highest
completely empty page of what is left, so page 8 on most characters. A key that
gets pressed on a timer should not sit next to a spell. Those four pages are
only used if every other page is occupied.

The slot also gets its own key, so it does not matter which page is on screen
when the timer fires. That key is **F15** by default. DirectInput has no
scancode above F15, so F16 to F24 cannot be bound at all however free they
look. No physical keyboard sends F13 to F15, so nothing you type can collide
with the slot -- though the game may already list one of them under Keyboard
Setup, and F15 is the one Live leaves free. Change it with `-macro-key`, which takes the same key names
`-stats-key` does (`-macro-key Ctrl+Alt+F15`), or `-macro-key none` to write no
binding and drive the slot by its page number instead. Then:

```
overlay.exe -flush -stats-key F15 -stats-every 5m
```

It presses it once at startup and on the interval after that. The flag is off
unless passed, because a quickbar key that is not the macro casts whatever else
is in that slot.

The control panel is the exception: its `/stats` box ships filled in with `\`.
Nothing is pressed until the flusher itself is switched on, and the flusher is
off until you start it -- but once it is running, that slot gets pressed on its
timer. Put the macro there, or empty the box.

The flusher checks before it starts. It reads every character's settings file,
finds the macro, the slot holding it and the key that reaches that slot, and
prints one line per key: `/stats on Backslash: ready for Lovila`. If no
character has it, that line becomes a warning naming the command that fixes it -- and it is a
warning rather than a refusal, because the key still works for whoever does
have the macro.

`-stats-every` is clamped to a 70-second minimum: the client holds `/stats` to
one run a minute and answers `You must wait N seconds before pulling stats
again.` inside it, where N is the time left rather than the length of the
cooldown. The default of five minutes is the useful setting -- the block is a
correction channel, not a live one.

### The zone the widgets show

The client prints `You have entered <zone>.` when you cross a boundary, so a
player who logged in and stood still has never printed one and the zone reads
blank or, worse, reads as whichever keep printed a `(Region)` line last.

With **Real-time log** on, the flusher reads the zone from the client directly
and hands it to the overlay -- always current, following you as you move, with
nothing to set up. That is one of the two values it reads (the other is your
character); see the real-time log section above.

## Moments

Everything else here answers "where are you now": RP so far, DPS this session,
kills tonight. None of that says "that just happened", which is the thing a
viewer reacts to.

A moment is raised for five things, and nothing else:

| moment | when |
|---|---|
| `rank` | a realm rank or a rank level crossed |
| `streak` | five kills without dying, then ten, then every ten |
| `best` | tonight passes the best evening on record |
| `level` | you level |
| `capture` | a keep or tower is taken |

The rule for adding a sixth would be the same as for these five: rare enough
that a viewer who looks up because the screen changed is not looking up at
nothing. A kill is not a moment; the tenth kill without dying is.

Put them on stream as a browser source:

```
http://localhost:8752/w/alert
http://localhost:8752/w/alert?hold=8&kinds=rank,best&sound=1
```

It is blank until something happens, shows one moment for six seconds, and goes
blank again. `?kinds=` narrows it, `?hold=` changes the six seconds, and
`?sound=1` plays a short blip when one lands. The control panel builds the
address for you under **Browser sources** with the `alert` widget picked.

Two things worth knowing:

- **Rank moments need the Herald lookup.** Your log never states a lifetime
  realm point total, so with the lookup off there is no rank to cross and this
  stays quiet rather than announcing a wrong one. The `rank_bar` row in the
  realm-point widget is silent for the same reason, and at 15L0 -- the top rank,
  with nothing above it -- the bar goes away rather than sitting full forever.
- **A reconnecting source does not replay.** OBS reloading the page mid-evening
  marks what has already happened as seen; only the next moment appears.

### Chapter markers for the VOD

Tick **Write chapter markers for the VOD** on the panel and every moment is
also written to `chapters.txt` beside `overlay.json`, in the format YouTube and
Twitch both read:

```
0:00 Stream start (overlay started 20:58:11 -- if your recording
began later, shift every line below by the difference)
1:42:10 Realm Rank 6 -- realm rank 6L0
2:03:55 10 in a row -- 10 kills, no deaths
```

The offsets are counted from when the overlay started, because this program
never asks your recorder anything. If the recording began at a different
moment, line one marker up with the video and shift the rest by the same
amount. The file is replaced at each launch: the list belongs to one recording.

## Past sessions and personal bests

The session resets after four hours of quiet, and until now nothing survived
that. Each evening is now also written to `sessions.json` beside
`overlay.json` -- a minute at a time while you play, rather than saved at the
end, because a crash, a closed console or a power cut all end a session without
telling anybody.

The control panel lists the last twenty under **Past sessions**, tonight at the
top: RP, RP/hour, kills, deaths, best streak, zone and how long each ran. The
best evening is picked out, and it is what the `best` moment fires on and what
`!best` answers with. Tonight is never its own record.

An evening with no RP, no kills and no XP is not kept -- the overlay left
running in a city is not a session.

## The icon by the clock

On Windows the overlay puts an icon in the notification area. Right-click it
for the control panel, the HUD, **End the fight**, **Start a new session**, the
black window, and Quit. Three shortcuts work while the game has focus, which is
the point of them:

| shortcut | what |
|---|---|
| `Ctrl+Alt+H` | show or hide the HUD |
| `Ctrl+Alt+N` | start a new session |
| `Ctrl+Alt+F` | end the fight |

All three are yours to change. Windows gives a combination to whichever program asks
for it first and will not say which program that was, so any default is one
somebody's PC has already taken -- the panel has a box for each under
**Shortcuts**.

Write them the way you would say them: one or more of `Ctrl`, `Alt`, `Shift`
and `Win`, then a letter, a digit or `F1`-`F24`, joined with `+`. A modifier is
required, because a global shortcut on a bare key would take that key from
every other program on the machine, the game's chat box included. Leave a box
empty to turn that shortcut off. Applying takes effect immediately -- no
restart -- and the pill beside each box says what happened to it:

| pill | what it means |
|---|---|
| working | registered; the key does what it says |
| in use by another program | Windows gave that combination to somebody else. Pick a different one |
| off | that box is empty |
| needs `-tray` | started with `-tray=false`, so nothing is registered |

`-tray=false` turns the icon and all three shortcuts off.

### Minimising the black window

Minimise the console and it goes to the clock rather than the taskbar: the
window is put away, the overlay keeps running, and the icon is how it comes
back. Double-click the icon, or right-click it and choose **Show the black
window**. The same menu item hides it without minimising first.

This needs the icon, so `-tray=false` leaves the window a normal window --
otherwise minimising would put it somewhere with no way back.

One case where it does not apply: Windows 11 can host consoles in **Windows
Terminal**, and a console hosted there is not a window this program owns, so it
cannot put it away. The overlay says so at startup by leaving the line about
minimising out. Setting **Windows Console Host** as the default terminal in
Settings → System → For developers gets the behaviour back.

## The same widgets on your own screen

The widgets are web pages, so the player can watch them too:

```
overlay.exe -window rp
```

That opens one widget in its own window -- no tabs, no address bar -- and pins
it above the game. It uses the browser already installed rather than embedding
one, which keeps the download a single file with no dependencies: Edge first,
then Chrome, both of which take the same `--app=` flag. If neither is there it
falls back to your default browser and says so, and you lose only the pinning.

For a real HUD -- over the play area rather than beside it -- use `-hud`:

```
overlay.exe -hud rp -hud-at 40,200
```

That is not a browser window. It is drawn by the overlay itself: a transparent
panel with no frame, no taskbar entry, and clicks that pass straight through to
the game. `-hud` takes any widget name -- the same names the web views use --
and opens one HUD; the control panel opens as many as you like at once, one per
panel, each in its own place.

It is a second renderer, not the web page in a window, but it draws the same
rows the web widget does -- a test holds the two in step -- with feeds and
leaderboards cut to five lines so a panel over a game stays small. The panel
grows and shrinks with what it is showing, so `killfeed` is a short list and
`all` is a tall one. The full widgets stay where they were too, in OBS and in
`-window`.

The browser route was tried first and cannot do this. Chrome and Edge draw
their own title bar inside the window, where the frame styles do not reach, and
composite their content past the transparency key, so the result was an opaque
box with a title bar on it. Only the click-through part worked.

`-hud-at` is in real screen pixels, not scaled ones: on a display at 150% the
HUD is drawn at 150% and `40,200` still means the pixel 40 across and 200 down.
A second monitor at a different scale is handled as the window moves.

A click-through window cannot be dragged, so `-hud-at x,y` is how it moves;
the default `40,200` is the top-left corner area. **Ctrl+C in the overlay's
console closes it**, and it closes with the overlay in every other case too.

A fullscreen client draws over any window, including a pinned one. Run DAoC in
**borderless windowed** if you want the overlay visible while you play; on a
second monitor it does not matter either way.

The window points at `127.0.0.1`, so it works whether or not the LAN is open --
that, and the firewall rule, only affect the address OBS uses from another PC.

---

## Command reference

### `overlay.exe`

| flag | default | meaning |
|---|---|---|
| `-log` | auto-detected | full path to `chat.log` |
| `-port` | `8752` | port for the web server |
| `-lan` | off | let other machines on your network reach the widgets; needed for OBS on a second PC |
| `-bind` | off | listen on one specific address as well as `localhost`, e.g. `192.168.1.20` |
| `-from-head` | off | read the whole existing log first, not just new lines |
| `-window` | off | also open one widget in a local always-on-top window, e.g. `-window rp` |
| `-hud` | off | draw a click-through HUD over the game: any widget name (`all`, `rp`, `combat`, `pve`, `killfeed`, `realm`, `cc`) |
| `-hud-at` | `40,200` | where to put the HUD, as `x,y` in screen pixels |
| `-demo` | off | invent an evening of play instead of reading a log — for placing sources in OBS and choosing colours without going out to fight for them |
| `-tray` | on | an icon by the clock: control panel, HUD, end the fight, new session, show or hide the black window, quit — with three global shortcuts, `Ctrl+Alt+H`, `Ctrl+Alt+N` and `Ctrl+Alt+F` until you change them in the panel, and minimise-to-clock for the console. Windows only; `-tray=false` for none of it |
| `-minimized` | off | start with the black window already tucked into the tray, instead of on screen. Double-click the tray icon to bring it back. Windows only |
| `-no-save` | off | use these flags for this run only, and leave `overlay.json` alone |
| `-version` | | print the version and exit |

Flags and the control panel are the same settings. A flag you type wins for
that run **and is written to `overlay.json`**, so the next double-click behaves
the way the last run did and the panel opens showing what is actually in use.
`-no-save` is the exception, for a run you do not want to keep: a different
port to test something, somebody else's log opened once, a HUD position tried
out before deciding.

`-from-head` on a log that spans years is safe: a gap longer than four hours is
treated as a logoff and starts a fresh session, so you get tonight's numbers
rather than a lifetime total.

### `overlay.exe -flush`

The flusher. `-flush` must come first: it takes the rest of the command line,
and everything in this table belongs to it rather than to the overlay.

| flag | default | meaning |
|---|---|---|
| `-interval` | `10s` | time between flushes |
| `-stats-key` | off | key holding a `/stats` macro, e.g. `F9`, `Ctrl+F9` |
| `-stats-every` | `5m` | how often to press it (minimum 70s) |
| `-client` | the running client | installation folder of the client to send to |
| `-user-dat` | auto-detected | full path to `user.dat` |
| `-daoc-dir` | auto-detected | directory holding `chat.log` |
| `-chat-log` | passed in by the overlay | the log file itself, for a log `/chatlog <name>` put somewhere else |
| `-slots` | | print a character's quickbar slots and exit, e.g. `-slots Prondr` |
| `-add-stats-macro` | | write a `/stats` macro to a free quickbar slot and exit (that character logged out) |
| `-macro-key` | `F15` | key `-add-stats-macro` binds the new slot to; `none` for no binding |
| `-unbuffer` | off | make chat.log a real-time stream via the client's own setvbuf, instead of keystrokes |
| `-sound-unfocus` | off | keep the game's sound playing while it is not the focused window (in-memory patch, reverts on client restart) |
| `-client-pid` | | target one client by process id, for same-folder multibox |
| `-no-elevate` | | do not ask for administrator rights; send keys with what this process already has |
| `-list` | | print every decoded keybind and exit |
| `-dry-run` | | report what it found, send nothing |
| `-once` | | flush a single time and exit |
| `-double-toggle` | on | close and reopen the log in one visit; `=false` holds it shut for 250ms and measures instead, see below |
| `-toggle-gap` | `50ms` | gap between the two presses of a double toggle |
| `-selftest` | | run internal checks and exit |
| `-version` | | print the version and exit |

### The time the log spends shut (`-double-toggle`)

A flush is a close and a reopen. The close is what writes the buffer to disk;
the reopen puts logging back on. The obvious way to do that is one press per
visit, waiting **250ms** in between so the file can be read and the press
identified — the log was open and that press closed it, or the log was off and
that press turned it on. That decision is what makes a toggle safe when you
cannot see the client's state.

But the wait is paid with the log **closed**, and a closed log does not write.
Chat the client emits inside that window is not delayed, it is never written at
all. At the default 10s interval that is **~2.5% of an evening** missing from
every total, and it gets worse in the direction people turn the setting: a 5s
interval doubles it.

`-double-toggle` sends both presses in one visit and measures afterwards, when
the log is already open again. On the messaged path `SendMessage` does not
return until the client's window proc has handled it, so the shut window
becomes the gap between two presses rather than a quarter of a second. Each
line prints what it measured: **50ms** against a live client, instead of 250.

The two presses cannot go out back to back. The client reads key state once a
**frame**, so a pair inside one frame is one toggle to it however cleanly it
was sent — measured, not assumed: with no gap this client toggled once and the
flusher desynced every other cycle. `-toggle-gap` is that gap, and the default
of 50ms is deliberately larger than the smallest that worked. It is sized to
clear a frame at 20fps, because the frames that matter are the ones in a keep
fight, not the ones in a quiet run to the frontier. Lowering it below the
client's frame time costs the whole flush, not part of it.

It is **on by default**, and there is nothing to decide. It depends on the
client treating the pair as two toggles; if that client treats them as one, the
log ends up shut for whole cycles — worse than the 250ms this avoids — so the
flusher watches for exactly that and **switches itself back** after the third
time, saying so on its own output. That takes about half a minute, once, at
the start of a run. Leaving it off to be safe would cost 2.5% of every evening
instead.

`-double-toggle=false` forces the old close-and-measure cycle if you want it.
`-toggle-gap` stays a command-line flag on purpose: tuning it means reading the
shut times off the flusher's output and knowing what your frame rate does under
load, which is a thing to do while debugging, not a setting.

### More than one client, or a client somewhere unusual

A DAoC installation is any folder holding `game.dll` and `camelot.exe`.
`C:\Program Files (x86)\Electronic Arts\Dark Age of Camelot` is only the
usual one, and several copies on one machine is normal.

Each installation carries a `paths.dat` naming the settings profile it uses,
and that profile is a folder under
`%APPDATA%\Electronic Arts\Dark Age of Camelot` holding that copy's own
`user.dat` and per-character `.ini` files. So keybinds are per installation,
not per machine — `flusher` reads `paths.dat` from the client it is sending
to rather than assuming `Main`.

With one client running it finds it. With several, it refuses to guess and
prints what to pass:

```
overlay.exe -flush -client "D:\Games\DAoC-second"
```

If the client is not running yet, the flusher waits for it rather than
stopping — starting the overlay before the game is the normal order, and it
picks the client up the moment its window appears, the same way it does when
the client is restarted mid-session.

Exit codes: `2` chat-log toggle not bound, `4` its modifier is disabled
client-wide, `6` no DAoC window found and `-once` was asked for, `1` anything
else.

### Widget query strings

| parameter | effect |
|---|---|
| `?bg=dark` | opaque backdrop, for previewing in a normal browser tab |
| `?max=8` | rows in the list widgets (default 5) |
| `?state=off` | hide the live/buffered status line |
| `?hide=deaths,crits` | drop these rows |
| `?show=rp,dps,kills` | keep only these rows, in this order |
| `?fg=%23F2F4F8` | text colour, `#RRGGBB` (`#` is `%23` in an address) |
| `?dim=%239AA4B2` | the labels beside the numbers |
| `?accent=%23FFD479` | the headline number, and the amounts in a feed |
| `?bgcolor=%230D1117` | the backdrop, when `?bg=` is on |
| `?opacity=59` | how solid that backdrop is, 0-100 |
| `?font=Verdana` | font family, as installed on this PC |
| `?size=18` | text size in pixels, 8-48 |
| `?hold=8` | `/w/alert` only: seconds a moment stays on screen (default 6) |
| `?kinds=rank,best` | `/w/alert` only: which moments this source shows |
| `?sound=1` | `/w/alert` only: a short blip when a moment lands |

The seven appearance parameters are only for a source that should not match
the rest. Left off, a widget takes the look set in **How it looks** on the
control panel, which is one place for all of them and the HUD as well; anything
the address does say wins over it, which is what lets two sources of the same
widget look different.

`?hide=` and `?show=` work on every widget, including `/w/all` — which is how
one page becomes your own dashboard rather than a dump of everything:

```
/w/all?show=rp,rp_hour,deaths,streak,dps
/w/combat?hide=mitigated,avoided
```

Row keys, all optional and all safe to name on any widget:

| group | keys |
|---|---|
| realm points | `rp` `rp_hour` `bp` `deaths` `streak` `deathblows` `rank` `rank_bar` `zone` `played` |
| combat | `damage_out` `dps` `kills` `damage_in` `pet` `crits` `healed` `healed_in` `hit_rate` `avoided` `mitigated` |
| this fight | `fight` `fight_dps` `fight_taken` `fight_kills` `fight_time` |
| PvE | `xp_hour` `xp` `mob_kills` `coin` `coin_hour` `best_streak` `level` `xp_pct` `champ` `zone` `zone_time` |
| realm activity | `realm_recent` `realm_hour` `realm_total` `hot_zone` `last_taken` `taken` |
| crowd control | `cc_landed` `cc_stuns` `cc_mezzes` `cc_recovered` |
| lists | `top` (opponents) `mob_top` (mobs killed) `feed` (killfeed) `realm_top` `realm_feed` `cc_top` `cc_feed` |

A key that names something the session has not seen yet simply shows nothing,
and a number the session cannot honestly state yet is a dash rather than a
zero: `dps` before anything has been fought, `hit_rate` before anything has
been swung.
`healed_in` is healing somebody else landed on you, which the client states by
name -- it is reported apart from `healed` because that one is your own output. A hit rate of 0% and no swings at all are the same number and very
different facts, and only one of them is about the player. `swings` is in the
JSON for readers that want to tell them apart themselves.

Three more numbers are in the JSON and on no widget. `absorbed_out` is damage
of yours a barrier on the target ate, `absorbed_in` is damage aimed at you that
a health buffer, an ablative or an endurance conversion ate, and both are kept
out of `damage_out` and `damage_in` on purpose: those two mean HP that moved,
and a viewer reads them against the health bar on screen. `heal_in` is healing
somebody else landed on you, which `healed_in` shows.

Per-hour figures (`rp_hour`, `xp_hour`, `coin_hour`) stay blank until five
minutes of play are behind them -- a rate extrapolated from thirty seconds is
arithmetic, not information.

`rp_hour` has a second way through: **five awards and five minutes, or twenty
minutes however few awards there are.** A rate is a lie while one kill still
dominates the total -- measured against a real log, the first award read 146
RP/hour against an evening that finished at 42, and it was still saying 75
twelve awards in. But a solo roamer can take an hour to find five fights, and
holding their rate all night would be the same failure the other way round.
Time tames a single award on its own: one 1,000 RP kill reads as 20,000/hour
three minutes in and 3,000/hour twenty minutes in, and 3,000/hour is what that
player is really doing. With nothing earned at all it stays blank rather than
reading `0`. The chat bot follows the same rule -- `!run` and `!rp` leave the
rate out altogether rather than answer `0/hr`.

`played` is what they divide by: the time the session has been running, or the
span of the log's own timestamps when an existing log is read back, whichever
is longer. Reading last night's log therefore shows last night's rates
immediately, not a five-minute wait.

### JSON

`http://<host>:8752/snapshot` returns the same object the widgets render, and
`/events` is the SSE stream of it. Build your own widget against either.

---

## Before you go live

Six checks, in order. `docs/START-HERE.txt` carries the same list with what
each one looks like when it is right.

1. **Start a fresh session.** Every figure counts from the start of the session,
   so a window left open since this morning puts this morning in tonight's
   numbers. `Ctrl+Alt+N` -- or whatever you set that shortcut to -- or **Start a
   new session** in the tray menu, or just relaunch.
2. **`http://localhost:8752/` says `Working`.** Anything else, that page names
   which of the four states it is in. Re-check the 16 Message Types after a
   patch day even when it looks healthy.
3. **The flusher pill is green and counting up.** Stopped, the numbers freeze
   for minutes and then jump, which reads as broken on camera.
4. **The OBS sources are live, not cached.** A browser source keeps the page it
   loaded last time: right-click → *Refresh cache of current page* after any
   settings change.
5. **The optional halves report themselves.** Read the panel's pills rather
   than trusting memory — a Twitch token expires quietly. `!rp` in your own
   chat is the proof for the bot; the Herald pill should name your character
   and a total; the OBS or Streamlabs pill should read `armed`, with the rule
   count.
6. **`/control` is not on screen.** It is the one page with secrets typed into
   it in plain text — the Twitch token, and the OBS password or Streamlabs
   token. Do not leave that
   tab open at all when you capture a whole monitor. Everything else here is
   safe on camera.

---

## Troubleshooting

| symptom | cause |
|---|---|
| Numbers frozen, state says `buffered` | Working as designed — run `flusher`. |
| Widget says `log stopped` | The log has not been written for six minutes: `/chatlog` was switched off, or the client is closed. Type `/chatlog` to turn it back on. |
| Widget says `nothing counted` | The log is growing and none of it counts — not all 16 Message Types are ticked. |
| Whole categories missing (no XP, no RP) | Not all 16 Message Types are ticked. |
| Streaming PC cannot connect | The overlay is `localhost`-only unless you ask otherwise — tick *Let other machines on my network reach the widgets* on the control panel and restart it. If it still cannot connect, the firewall rule is missing; see above. |
| `flusher` says several clients are running | Pass `-client` with the folder of the one you stream. |
| `flusher` reads the wrong keybinds | That copy's `paths.dat` points at a different settings profile. Pass `-client`, or `-user-dat` to be certain. |
| `flusher` says `NOT ACTIVE` | You bound e.g. Ctrl+L, but Ctrl is switched off client-wide. Options → Keyboard → enable it as a modifier. |
| `flusher` flushes 0 bytes six times | Fine if idle. If you were fighting, the key is not reaching the client. |
| `flusher` runs but nothing happens at all | DAoC is running as administrator and `flusher` is not — Windows drops the input with no error. `flusher` asks for administrator rights at startup for exactly this reason, so this means the prompt was declined, or `-no-elevate` is set. |
| A UAC prompt appears every time `flusher` starts | By design. Elevation is what lets it type into a DAoC that runs as administrator. If your client is not elevated you do not need it: pass `-no-elevate`. |
| Keystrokes misbehaving in other apps while `flusher` runs | Your chat-log key has a modifier, which is pressed desktop-wide for ~80ms per cycle. Rebinding it to a plain key removes the window entirely. |
| Everything counts except RP, or except XP | `/rp` and `/xp` each toggle that gain off. The log stays healthy and the client simply never awards any, so there is nothing to count. Type `/rp on` or `/xp on`. |
| `Zone —` on the RP/PvE widgets | The client only prints a zone line on a transition. It fills in when you zone. |
| Panel says `nothing is listening` | The server is off, or the program is closed. OBS → Tools → WebSocket Server Settings → Enable WebSocket server; Streamlabs → Settings → Remote Control. Check the port matches. |
| Panel says `wrong password` | OBS shows the current one under Tools → WebSocket Server Settings → Show Connect Info. |
| Panel says Streamlabs refused the token | Copy it again: Settings → Remote Control → QR code → Show details. |
| Panel says there is no scene of that name | The name has to match what the program shows, capitals included. Streamlabs also says this when the scene belongs to a scene collection that is not the one loaded. |
| Rules armed but nothing ever fires | Check the panel's count of what fired. A multi-kill needs that many kills inside the window; the replay buffer must already be running; a scene or source name must match exactly, capitals included. |
| Chat panel says the token has expired | Twitch chat tokens do expire. Generate a new one for the same account and paste it in. |
| `!rp` says the lifetime total is unknown | Your log never states one — `/stats` is per-session. Tick **Look my realm point total up on the Camelot Herald** on the panel. |
| The Herald lookup says it found more than one character | Two characters on that cluster share the first name your log gave. Put the full name, surname included, in the Character box. |
| The Herald lookup says it is waiting for the log to name your character | Run `/stats` in game once, or type the character name in yourself. |
| The bot signed in but says nothing | Check the commands are ticked, and that the prefix matches what people are typing. Answers are also on a cooldown; ask again in twenty seconds. |
| Windows will not let you delete `chat.log` | The overlay is reading it. The client can still write to it — only deleting and renaming are blocked — so close the overlay first if you want to clear the file out. |

---

## If your antivirus objects

It happens, and it is worth explaining rather than telling you to click through
it. On VirusTotal, `overlay.exe` v0.2.3 was flagged by two scanners out of
seventy, and Microsoft Defender quarantined the separate `flusher.exe`
outright.

From v0.2.5 there is no `flusher.exe` — it is `overlay.exe -flush`, the same
program. Say plainly what that costs: the code a scanner objects to is now
inside the file you have to run, so a detection takes the whole overlay with it
rather than the optional half. It was done anyway, because two unsigned
anonymous exes are two files to be guessed about instead of one, and because
the half that was actually being quarantined was the half nobody could run
without. If your scanner disagrees, the answer below is now "check the hash and
decide", and it is a real decision rather than a shrug.

Measured rather than assumed, since that is the only honest way to put it:
Defender on an ordinary Windows 11 machine and on a clean build runner both
call v0.2.5 clean, where it quarantined v0.2.4's separate flusher on sight.
What that did not buy is quiet — the first person to download v0.2.5 still had
to allow it through as an exemption. One file instead of two is a smaller ask,
not no ask.

The names to expect are `Trojan:Win32/Wacatac`, `VirTool/Obfuscator`, or
something else ending in `!ml`. That suffix is Microsoft's own mark for "a
model guessed", and the rest are the buckets scanners use for a file they
cannot identify. None of them names a piece of malware that anyone has found in
this; they describe a *shape*, and this program has that shape for reasons you
can check:

- **The flusher reaches the game.** On its default setting it changes one of the
  game's own log settings so the log saves in real time, and reads your character
  and zone; with that off it presses a key into the game window. Either is
  precisely what a scanner watches for, and it is also the whole job. See
  [The delay, and the flusher](#the-delay-and-the-flusher).
- **Nothing here is signed.** A code-signing certificate is what lets a scanner
  check a publisher instead of guessing about a file, and this project does not
  have one. That is the single biggest reason for these results.
- **Every Windows call is looked up by name while running**, because this is
  pure Go with no cgo. It means the file's import table lists almost nothing,
  which is a thing packers also do.
- **A new build is a new file with no history**, so a scanner is guessing about
  something it has never seen before, every release.

What you can do about it, in the order worth trying:

1. **Check the hash.** `SHA256SUMS` ships beside the download and covers the
   zip and the exe. `Get-FileHash .\overlay.exe -Algorithm SHA256` in
   PowerShell. This tells you the file arrived as it was built.
2. **Leave the flusher switched off.** It never starts unless you start it, so
   nothing sends a keystroke unless you ask. Everything except the update rate
   works without it. This is no longer a way to avoid downloading the flagged
   file — there is one file — but it is still the answer if what you object to
   is the behaviour rather than the binary.
3. **Restore it and exclude it, if you are satisfied.** Windows Security →
   Protection history → Allow, then add the folder under Exclusions. Do that
   because you checked, not because a README asked you to.
4. **Build it yourself.** See below. The build is reproducible: the same tag
   built on another machine produces byte-identical exes, so you can compare
   what you built against what you downloaded.

The build does what it can from this side. The exe carries a proper Windows
version resource, so Properties → Details shows a publisher, a product name and
a version rather than being blank, and an application manifest, so it states
that it runs as you rather than as an administrator. Every release is scanned
by Defender on a clean machine before it goes out. That scan matters more than it used to —
there is no longer a second exe for a detection to be confined to — so a
release that trips it does not ship.

## Building

Go, standard library only. No cgo, no dependencies.

```
go build -o overlay.exe .
```

A release build also stamps the Windows version resource that Go does not
emit, which is why a downloaded exe has a filled-in Properties → Details tab
and a locally built one does not. To match it:

```
cd tools/winres
go run . -out ../../resource_windows_amd64.syso -exe overlay.exe -version dev -name "DAoC Stream Overlay" -desc "Chat log overlay for Dark Age of Camelot"
```

Then build as above. The `.syso` file is build output, not source, and the
release stamps it with the tag instead of `dev`.

Tests, including a documentation drift check:

```
go test ./...
go run . -flush -selftest    # Windows only
cd tools/winres && go test ./...
```

Four of those tests talk to something real and skip unless it is switched on,
so a clone with none of it running still goes green:

```
DAOC_LOG="C:\path\to\chat.log" go test -run Unparsed -v   # mine a real log
OBS_LIVE=1 OBS_PASSWORD=... go test -run OBSLive -v
SLOBS_LIVE=1 SLOBS_TOKEN=... go test -run StreamlabsLive -v  # SLOBS_URL optional
```

Each takes its secret from the environment and nowhere else. Nothing here
reads another program's configuration file for a password, and no log, token or
password belongs in this repository.

## Privacy

- `chat.log` is read and never modified.
- `user.dat` is read for its `[keyboard]` section only. The `[quick]` section
  holds character names, servers and server IPs, and is never parsed.
- Nothing leaves your machine unless you switch it on. The overlay serves your
  LAN and, by default, talks to no external service at all. Two settings change
  that, and both are off until ticked: the chat bot signs in to Twitch, and the
  Herald lookup asks the game's own public API for one character's realm point
  total. Neither sends anything out of your log, and no third-party service is
  involved in either.
- Three files are written, all beside the exe: `overlay.json` (settings),
  `sessions.json` (a line per evening), and `chapters.txt` (only when chapter
  markers are ticked on). Nothing is written anywhere else, and nothing is sent
  anywhere.
- Donated player logs are never committed to this repo. Only extracted line
  shapes are.

Developer and experiment tooling: [`docs/tools.md`](docs/tools.md).
Design and evidence: [`docs/designs/daoc-stream-overlay.md`](docs/designs/daoc-stream-overlay.md).

## Licence

MIT. See `LICENSE`.

Every log pattern this parser matches was extracted from a real Live `chat.log`
(client 1.129) by the tools in `tools/`. No third-party code and no third-party
content: nothing to attribute, no licence to carry.
