Case study · 05 / 05 · 20266 min read
  • Lua
  • Node
  • Electron
  • Real-time

Palworld Raid Meter

How do you read a fight while you are still in it?

Where
Solo project, for our family server. Live damage telemetry for co-op boss fights.
Role
Built it. A Lua mod on the game server, a Node hub, an Electron overlay
The results card over the game after a boss kill: Defeated, the boss name, then time, damage, raid DPS and tracked share, and each player's damage with their Pals nested under them
Running on our server; everyone installs the overlay and takes updates through it

01What it is

The results card over the game after a boss kill: Defeated, Panthalus, level 70, 495k HP; 4:27, 495k damage, 1,850 raid DPS, 100% tracked; two players ranked, each with three Pals nested under them and their share; a line saying five attackers were excluded
The card that appears when the boss dies. Time, damage, raid DPS and the share the meter could account for, then each player with the Pals they brought nested under them. Player names blurred here.

A Details-style damage meter for our Palworld server. A Lua mod on the game server hooks the damage event and writes running totals per attacker. A Node hub on my PC reads that, works out who is fighting what, resolves the game's ids to real names, and nests each Pal under its owner. A small Electron app on each player's PC draws the meter over the game and shows the card when a boss dies.

It answers one question: who is doing the damage. A player who does little personally but fields a monster of a Pal is still carrying the fight, so the meter counts each player together with their Pals, and shows the split.

02The reading problem

Nobody can read a table mid-fight. So the overlay stays small while the boss is alive, and the full read-out waits for the kill, when everyone is standing still anyway. The card is the design: four numbers across the top, then the ranking, with the Pals indented so the eye reads player, then contribution, without a legend.

The one number I would not ship without is tracked. It is the share of the boss's HP the meter could attribute to someone, and a healthy fight sits between 95 and 102 percent. Below 90 the number is wrong and the card says so, instead of hiding it.

The hub's browser view: the engaged boss with its HP bar and a raid-DPS-over-time chart, then damage to boss, raid DPS and fight length as three tiles, then tabs for damage taken, deaths, roster and damage dealt, with two players ranked
The hub, in a browser. Every fight is kept, so a bad number can be audited after the fact: damage from the HP curve against damage from the hook, and every Pal against its owner.

03Identity, the hard part

The damage hook hands the hub an id, not a name. The first read of the game's last-attacker field looked like it returned nothing usable and was written off; it turned out the early attempt read a table as a scalar. Read properly, it resolves sixteen of eighteen combatants where the deep property probe managed two, over two hundred and thirty-three hits with zero conflicts, so it is now the primary mechanism and the probe is the live fallback. The byte order was pinned by test: the player id sits raw in the first field of the guid, and the three other orderings were ruled out rather than left unchosen. An all-zero guid is thirty-two valid hex characters and an early heuristic happily returned it as a name; that is a gotcha in the README now. Pals are named individually by guid, which killed the same-species ambiguity outright, and the identity cache on disk is never cleared, because identity only builds forward.

04What counts as a fight

A boss is either a big health pool or anything on a boss-arena stage, because those are different animals: a level 78 dungeon boss had 7,933 max HP in a session where ordinary field Pals reached 5,408. A fight ends when the boss resets after healing, when everyone is far away and idle for forty-five seconds (distance alone once split a live pull at thirty-seven percent health), or when nothing has hit it for three minutes. Detection lags the first hit by a couple of seconds and that used to cost coverage, a constant shortfall in time across pulls; a buffer of recent damage is replayed into the fight once it opens, and coverage went from seventy-nine percent to ninety-eight.

Two more things the first version got wrong. It tracked one fight globally, so two players in separate instanced towers meant one player's entire kill was recorded nowhere; now every fight is tracked and each overlay picks the one it appears in. And a raid that spawns four identical adds shares one damage pool, so all four tables came out byte-identical and coverage read up to five hundred percent; the pool is now split by each add's HP curve, and the fixture that proves the old logic fails is in the tests.

05The two-minute incident

The hub has two listeners. The one on the LAN skips the token for loopback requests, which is convenient on my own machine. The first version had one listener, and the public tunnel proxies from loopback, so every public request looked local and the token check was skipped entirely. The roster was readable by anyone with the URL for about two minutes before I caught it and pulled the tunnel. The fix is a second port bound to loopback only, where the token is always required, so 'arrived from outside' is a property of which socket accepted the connection and a proxy rewriting the source address cannot spoof it. Verified from outside afterwards: no token 401, wrong token 401, right token 200, reset 403. If a token leaks the exposure is read-only: who is fighting what, for how much. Nothing can reach the game server.

06Shipping to people who are playing

The overlay loads its UI from the hub, so a fix to the meter's HTML reaches everyone in seconds without anyone downloading anything. Hub logic ships by restarting the hub; overlays reconnect on their own. Only the Electron shell, the hotkeys and the tray, needs an installer, and for that the hub advertises the latest version and the overlay reads its own from its user agent, which was chosen specifically so builds shipped before the feature existed still get told. A toast for twenty seconds and a tray item that survives dismissal, because the two fail differently. The installer is unsigned; a certificate is a few hundred dollars a year and the users are family. That is a settled decision, and so is the absence of a hook watchdog: a re-register firing while the old hook is alive would double-count everyone's damage, which is worse than an obvious failure.

  • 102

    tests across seven files, including the fixture the old raid logic fails

  • 99%

    coverage on the last verified two-player tower kill

  • 8

    of eight combatants exact on the best fight, hook total equal to the HP curve to the point

07What I cut

  1. 01Open-world combat. Boss fights only, on purpose. The meter is for the fights we argue about
  2. 02Anyone who was not near the fight, or could not be identified. The card says how many it excluded rather than guessing them in
  3. 03A live per-hit feed. Totals on the overlay, the audit in the hub. Reading hits scroll by is a distraction dressed as information

08Where to see it

The clip from the post: a fight with the overlay in the corner, then the card. Silent.

The post is on r/Palworld (opens in a new tab). The users are my family and a few friends, and the point of the project was finding out what running a real app next to a live game feels like: updates that people take from a notification, a hub that has to be up before anyone sees anything, and a damage hook that dies silently and has to be watched.