---
name: admin-game-integration
description: Change how the Admin panel reaches into the live game — the forced-logout flag (gcc.admin_reset), player PM + user_pm_abuse records, the chat mute flag, lastaccess, and game-side CSS the panel depends on. Use when an admin action must take effect in-game, or when a panel change needs a matching edit in a game file.
---

# Panel → game integration

The panel writes to the live game through a small number of **deliberate
contracts**. Each exists so the game and the panel stay coherent and so records
players already rely on don't change shape. Break one and the symptom usually
appears in the game, not the panel.

## The contracts

| Contract | Mechanism | Game side |
|---|---|---|
| Forced logout | Row in `gcc.admin_reset` | Reset block at top of `i_f_800.cfm` |
| Player notification | Row in `gcc.user_pm` via `playerPm()` | Normal in-game PM |
| Permanent record | Row in `gcc.user_pm_abuse` | Player's record; legacy tooling reads it |
| Chat silence | `server["gc_mute<uid>"] = 1` | `i_chatb.cfm` |
| Tag for deletion | Backdated `lastaccess` | Nightly cleanup job |
| Removed chat post | `<span class="removed">` in `chat.post` | `theme.css`, `i_chatt.cfm` |
| Staff chat post | `<span class="gc-chat-staff" data-admin="id">` in `chat.name` | `theme.css`; `gcChatStaffName()` / `gcChatStaffAdmin()` in `Functions.cfm` |
| Runtime refresh | Re-runs `s_loadvar.cfm` / `s_loadsystem.cfm` | Game globals reload |
| Announcement banner | `gcc.cluster_cache` id 6 + `GET i.cfm?yellowmsg=1` | `s_yellowmsg.cfm`, included by `s_loadbar.cfm` and by `i.cfm` |

## Announcement banner — `cluster_cache` id 6

Text in `list1`, expiry in `datetime`; the game shows it while both are set and
the stamp is in the future. The panel does not expose the expiry — it writes a
far-future stamp when there is text and `NULL` when there is not — so presence
of text is the only switch.

The part that is easy to get wrong is the **cache**. `s_loadbar.cfm` holds the
text in the server scope for 300 seconds, so a plain database write takes up to
five minutes to reach players — and a banner *taken down* keeps showing for that
long, which is the case where the delay actually matters. The game's own escape
hatch is `url.yellowmsg`, which forces a re-read whatever the cache's age.

Two traps found the hard way:

- **It has to be `i.cfm`, and `i.cfm` had to be taught to answer.**
  `s_loadbar.cfm` only runs via `i_f_800.cfm`, which is gated on `url.f` **and**
  `session.userid`. A session-less `i.cfm?yellowmsg=1` fell through to
  `i_p.cfm` and never touched the cache. Requesting `s_loadbar.cfm` directly
  answers `200` and still does nothing — it depends on state its includers set
  up. The refresh now lives in `s_yellowmsg.cfm`, included by `s_loadbar.cfm`
  for players and by `i.cfm` for the session-less panel call.
- **Do not `cfabort` out of `i.cfm`.** `Application.cfc`'s `onAbort` dumps
  *"request … ended with a abort!"* into the response unconditionally, so a
  short-circuit staples Lucee debug output onto a public URL. The refresh
  request just carries on and renders the ordinary public page.

## Forced logout — `gcc.admin_reset`

The most important one, because it is what stops a player continuing on a stale
session with pre-edit state.

**Panel side** (`Base.cfc`): `forceRelog()` calls `ensureResetTable()` — which
creates the table if absent, so a fresh install needs no manual step — then
inserts one row per player.

```
userid      INT PRIMARY KEY   one pending reset per player
newname     VARCHAR(40)       new login name when the edit was a rename
kind        VARCHAR(20)       '' | 'suspend' | 'blacklist'
reason      VARCHAR(255)      shown to the player on suspend/blacklist
created_at  DATETIME
```

**Game side** (top of `i_f_800.cfm`): on the player's next page load it selects
their row, clears the session and cached data, includes
`z_admin_reset_notice.cfm` to show the reason, then **deletes the row**. The
whole block is wrapped in `try/catch` and ignores failures — a missing table or
transient DB problem must never break the game.

It is a shared **DB flag**, not in-memory state, so it survives app restarts and
works across servers. `sql/admin_reset.sql` documents and can create the table
(targets `gcc`, declares `USE gcc;`).

### When to raise it

Any panel change to a player's account that the game caches: resources,
infrastructure, artifacts, fleet, colonies, projects, research, empire name,
plus suspend and blacklist. **If you add a new player editor, raise the flag** —
otherwise the player keeps playing against the old values until they happen to
re-log, and the edit looks like it silently failed.

If the hook can't fire, that is logged as `player.relogHookFailed` rather than
failing the action.

### The re-log is also a notification

It drops the player onto *"An administrator has updated your empire"*
(`p_relog.cfm`), so anything offering a "do not notify the player" option must
skip the re-log too — otherwise the player is told anyway, and louder.

`users.resourcesSave` is the one editor that does this today (`silent=1`). It is
safe **there specifically** because `i_f_800.cfm` re-reads credit/food/power from
the database into the session on every game page load, so a resource edit lands
live without a re-log. That is a property of those columns, not a general rule:
before adding a silent option elsewhere, confirm the game re-reads that data per
request rather than caching it. When in doubt, raise the flag.

Suppressing the notification never suppresses the record — the audit entry
(marked `silent`) and the `user_pm_abuse` row are still written. See
`docs/AUDIT-LOGGING.md`.

## Records and notifications

Moderation actions write **both**:

```cfc
playerPm(uid, "R:Chat Complain", "Chat Warning !" & chr(10) & …);   // gcc.user_pm
gamePmAbuse(uid, "Chat warning ref:" & abuseId);                    // gcc.user_pm_abuse
```

`user_pm_abuse` is the player's permanent record and is read by legacy tooling
as well as the panel, so:

- Keep `name2` values in the shapes already in use. The panel's Records tab
  filters chat entries out by **exact prefix** (`Chat warning ref:`,
  `Silenced ref:`, `Chat removed ref:`) — a new prefix that isn't added to those
  filters will leak into the wrong tab.
- `admin` is `left(currentAdminName(), 20)` — the column is narrow.
- Emails, where the legacy panel sent them, are still sent, gated by the
  `send_player_emails` panel setting.

## Editing a game file from panel work

Sometimes a panel feature needs a matching game-side change (the chat `.removed`
styling is the standing example). Rules:

1. **Find every surface.** Chat renders through the `chatt` frame for the classic
   UI *and* the modern side chat in `i_f_800.cfm`; a class defined in only one
   looks fine until someone opens the other. `.removed` is deliberately defined
   in `theme.css`, `i_chatt.cfm`, **and** `admin.css`.
2. **Respect what you were told not to touch.** `f_com_msgsector.cfm` is
   deliberately left inline and unmodified.
3. **Game CFML is tag-based** — different rules from the panel's script CFCs.
   Inside `<cfoutput>`, hex colours need `##` doubling while variable
   interpolation stays single `#`.
4. **Theme-scoped CSS**: follow the existing
   `body:is([data-theme="nebula"], [data-theme="daylight"]), body[data-theme="classic"]`
   double-selector shape or your rule won't apply in every theme.

## Storing markup in a game column

The panel stores `<span class="removed">…</span>` directly in `chat.post`. That
is safe **only** because a player can never produce it — the write path is
admin-only. Two consequences to carry forward:

- **Size the column for it.** The wrapper is ~30 characters; `chat.post` was
  widened to `varchar(255)` (`sql/chat_post_expand.sql`) so a full-length
  replacement still fits. Check the column before assuming.
- **Strip it when re-displaying in the panel.** The admin feed unwraps and
  renders the text as a **text node** — never route stored markup back through
  `innerHTML`.

Do not extend this pattern to anything a player can write.

## Verifying

Panel-side unit checks are not enough here — the whole point is the effect
in-game. At minimum:

1. Trigger the action through the panel/harness.
2. Confirm the game-side row exists (`admin_reset`, `user_pm`, `user_pm_abuse`)
   with the expected shape.
3. Confirm the game consumes it — load the affected game page and check the row
   is deleted / the notice shows / the styling applies.
4. Clean up **all** the tables the action touched; chat actions fan out into
   four plus the audit log.

See `admin-verify` for the harness and the cleanup discipline.
