Case study · 05 / 06 · 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
Used on our family server. Everyone installed the overlay and took updates through it

01What it is

The results card over the game after a boss kill: Defeated, Panthalus, level 70, 495k HP. Then 4:27, 495k damage, 1,850 raid DPS, 100% tracked. Two players ranked, each with three Pals nested under them and their share, and 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.

Tracked is the number I check first. 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. A player's own damage is kept apart from their Pals'. It is not a check against the game's own totals, which the game does not expose.

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. 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 entries only ever get added, never removed.

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 players in separate instanced towers meant one player's entire kill was recorded nowhere

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. The family were told the same day, along with what it had exposed and what it had not.

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, and 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.

a silent double count is worse than a crash

The installer is unsigned. A certificate is a few hundred dollars a year and the users are family. There is no hook watchdog either, because a re-register firing while the old hook is alive would double-count everyone's damage, and a silent double count is worse than a crash. Whoever controls the hub controls code on every player's PC, and the threat model stops at the token. The hub is on my PC, the players are my family, and the meter was never released to anyone else. The post shares the idea and the clip. The meter itself has never been published or open-sourced, so the installer has never left the family. In a public build that channel is the first thing to sign, and there is no public build.

  • 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 resolved to a name on the best fight, hook total equal to the HP curve to the point. One fight

07What I cut

  1. 01Open-world combat. Boss fights only. 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 does not tell you who is winning

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 project was about 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.