# New map entity review — 5 October 2026

This reviews the downloaded BSPs and the existing importer/runtime before the new map implementation. It separates authored map facts, public Source reference behavior, measured installed CSS behavior, and proposed browser rules. It does not claim that the public SDK is the shipped CSS game code. A limited native probe was subsequently authorized and completed in the isolated project directory; no installed game files were changed, and the server was shut down. See [native teleport results](NATIVE-CSS-TELEPORT-PROBE.md).

## Reviewed revisions

| Map | SHA-1 of decompressed BSP | Teleports | Trigger multiples | Push triggers |
| --- | --- | ---: | ---: | ---: |
| surf_demise | `15b16c315649d25420f25517e47454497f795b0c` | 17 | 5 | 0 |
| surf_kitsune | `41ff082a485d5b4a306589205187bc730ca4b2ac` | 53 | 22 | 2 |
| surf_aircontrol_ksf | `3c3b754ffb0f02b3d4a1506c8ffebdca5d18e902` | 46 | 14 | 4 |
| surf_lux | `54745509ca87f2616311a0b29e4774e80b566067` | 33 | 5 | 0 |

The source manifest is `scripts/fixtures/new-maps-sources.json`; the source files are `node_modules/.map-imports/<map>.bsp`. Entity facts below come directly from lump 0, retaining duplicate key/value pairs, and model bounds from lump 14 through `scripts/bsp_common.py`. Bounds quoted below are translated world bounds; actual trigger contact must use each model's convex brush union.

All 149 `trigger_teleport` entities have spawnflags 1. None specifies a landmark, a nonzero teleport angle, or an output that changes velocity/base velocity. All target names resolve to at least one authored entity using case-insensitive comparison. Teleport classification cannot be inferred from classname or target name alone.

## Required main-route teleports

### Kitsune: nine stages

The normal sequence is Red, Orange, Yellow, Green, Teal, Blue, Purple, Pink, White. Source target names use mixed capitalization, so resolve them without case sensitivity. These stages have no authored named timer start/checkpoint/end triggers. Timer boxes and the choice to start at Red rather than the lobby therefore require a separately documented server/browser rule.

| Stage destination | Authored feet origin | Yaw | Progression into the next stage | Failure returns to this stage |
| --- | --- | ---: | --- | --- |
| 1 Red | `(-15360, -15088, 825)` | 90 | `*1`, Hammer 1001 → Orange | `*3` |
| 2 Orange | `(-13312, -15216, -455)` | 90 | `*2`, Hammer 1612 → Yellow | `*4` |
| 3 Yellow | `(-11264, -15216, -1735)` | 90 | `*6`, Hammer 2487 → Green | `*5` |
| 4 Green | `(-8192, -15216, -2935)` | 90 | `*8`, Hammer 7224 → Teal | `*7` |
| 5 Teal | `(-5120, -15216, -5399)` | 90 | `*14`, Hammer 9566 → Blue | `*13` |
| 6 Blue | `(-2048, -15216, -7895)` | 90 | `*15`, Hammer 10394 → Purple | `*48`–`*51` |
| 7 Purple | `(512, -14928, -12119)` | 90 | `*16`, Hammer 12259 → Pink | `*17` |
| 8 Pink | `(8192, -368, 6560)` | 270 | `*19`, Hammer 13925 → White | `*18` |
| 9 White | `(-15104, 15072, 10793)` | 270 | `*36`, Hammer 34698 → lobby `start` | `*21`–`*35`, `*37`–`*43` |

All destination pitches are zero. The final `*36` portal is the end of the ninth stage, even though its target is named `start`. Its world bounds are `(-16064,10072,-11936)` to `(-16032,10280,-11744)`. A finish must be recorded before the destination enters the lobby. Lobby entry portal `*10` leads to Red; `*9` leads to the separate lobby `spawn` point. Lobby `start` is `(0,-1456,200)` and lobby `spawn` is `(0,-1964,395.063)`.

The secret path is distinct: `*60`, `*66`–`*71` are gated by `filter_activator_name`, with the targetname sequence established by `trigger_multiple` outputs. Treating those filtered portals as unconditional creates false shortcuts through ordinary stages. `*81` is initially disabled and only enabled temporarily by a secret-room button; it must not be enabled by default. Secret-room `*84` returns to lobby `start`. The main route does not require these secret mechanics. If only the main route is supported, list the excluded entities and reason in import metadata, and invalidate a timed run on explicit bonus/secret travel.

Kitsune's repeated `OnEndTouch` outputs to `counter_3` have no matching entity in this BSP. They do not justify incrementing browser stage state. Stage progress should follow the reviewed main portal sequence, never arbitrary targetname outputs.

### Lux: an essential midcourse transfer

Lux is linear. It contains 31 return portals to `spawn` and two progression portals:

| Source | Hammer ID | Destination | Destination origin | Destination yaw |
| --- | --- | --- | --- | ---: |
| `*38` | 142959 | `s2_left` | `(6784,-15168,-1856)` | 90 |
| `*39` | 142970 | `s2_right` | `(7168,-15168,-1856)` | 90 |

The paired entrances sit around `(-192,15520,-8384)` and `(192,15520,-8384)`. They preserve the continuous main route; neither is a fall or a new map start. Authored timer names are `start_zone` (`*6`), `checkpoint_1` (`*7`), `checkpoint_2` (`*8`), `checkpoint_3` (`*9`), and `end_zone` (`*16`). Main destination `spawn` is `(0,1984,1664)`, yaw 270. This revision contains TF2 `info_player_teamspawn` and `func_respawnroom` entities; they are not CSS player starts. Use the reviewed teleport destination for the browser spawn and describe that decision.

### Demise

Nine teleports return to `map_start`; eight belong to the bonus and target `bonus_start`. Main `map_start` is `(-11520,-3968,15120)`, yaw 0. `startzone` is model `*23`; `endzone` is `*75`. Both are `trigger_multiple` entities with spawnflags 0, as are `bonus_start` and `bonus_end`: extracting them as server timer geometry is distinct from claiming that unmodified native CSS would activate them for players. The BSP has no named main checkpoint trigger entities; the manifest's two checkpoints need independent server zone evidence or explicitly local zones.

The `bonus_start` name is shared by an earlier `trigger_multiple` and a later `info_teleport_destination`. Both origins are `(10496,-10496,-2176)` and both effectively have zero angles, so this revision yields the same transform either way. A generalized importer must preserve entity order and duplicate-name diagnostics rather than silently assume every teleport target is an `info_teleport_destination`.

The authored `logic_auto` explicitly issues `sv_maxvelocity 5000`. Trigger `*52` repeats `gravity 1` on start/end touch; it does not introduce a different gravity profile.

### Aircontrol KSF

The main course has `start_trigger` (`*37`), `end_trigger` (`*35`), and `cp1_trigger` through `cp5_trigger` (`*50`–`*54`). Main destination `start` is `(-1856,-9504,14720)`, yaw 90. These timer volumes use filter `mainmap`, which matches player targetname `main`.

An overlapping bonus uses `startbonus_trigger` (`*38`), `endbonus_trigger` (`*39`), filter `bonusf`, and `ModifySpeed 2`. Main start outputs restore speed modifier 1. Portal `*40` assigns the activator targetname `bonus`; `*41` assigns `main`; both return to `start`. Additional spawn triggers assign `main`. Exporting both overlapping start volumes without filter/state support would incorrectly double normal main-course movement time. Either implement those states faithfully, or expose the main route explicitly and omit the bonus switch/effect from timed main play.

Portal `*55` leads to a cosmetic secret room; `*56`–`*58` return to `start` while changing model index. Portals `*59`, `*64`–`*66` belong to the separate `coursestart` route, with four 400-unit continuous push triggers (`*60`–`*63`). Do not automatically classify these as main-course resets. No main-course midroute teleport is required by this revision's entity graph.

The [Aircontrol command witness](AIRCONTROL-CANONICAL-WITNESS.md) now completes the main course from the normal browser spawn. Its movement comparison independently matches all 2,534 native next-frame samples through the KSF finish bookmark within 0.002 units of position and velocity. The authored BSP `end_trigger` is reached at recorded frame 2,547, **13 ticks later** than KSF's finish bookmark at 2,534. The private KSF finish-zone bounds are unavailable; this evidence does not establish exact KSF timer parity.

## CSS evidence and the reference boundary

The installed CSS `bin/base.fgd`, lines 6305–6315, describes destination position and destination angles, landmark-relative offsets with preserved angles, and flag 32 preserving angles without a landmark. This is Valve's installed authoring metadata, not an execution test. Its adjacent `trigger_teleport_relative` description must not be substituted for `trigger_teleport`; none of these maps uses that separate classname.

The public Source reference resolves a target, clears ground, applies destination origin (adjusting by player minimum Z), and takes the target angles when no landmark or preserve-angle flag is present. A resolved landmark adds the player's world offset from that landmark and leaves angles unchanged. Its non-HL1 path passes a null velocity to `Teleport`, so ordinary world velocity is retained rather than zeroed or rotated. A missing destination returns without moving. This is an implementation reference, **not native CSS proof**. [Valve trigger implementation, pinned revision](https://github.com/ValveSoftware/source-sdk-2013/blob/b8cfb12c0e083a2ef5b2f9f9b50f3902fa034474/src/game/server/triggers.cpp#L2181)

On a new contact the shared reference calls `StartTouch` before `Touch`; existing contact calls `Touch` again. Thus ordinary teleports are not merely one-time OnStartTouch actions. [Valve shared contact handling](https://github.com/ValveSoftware/source-sdk-2013/blob/b8cfb12c0e083a2ef5b2f9f9b50f3902fa034474/src/game/shared/physics_main_shared.cpp#L843)

Outputs are queued, including zero-delay actions. Parsing prepends output actions, while equal-time queued events retain insertion order; file order is therefore not a safe synchronous execution rule. Events execute when their due time is reached by the queue service. Preserve duplicate outputs, delay and fire-count fields if implementing entity I/O. [Valve output and event queue implementation](https://github.com/ValveSoftware/source-sdk-2013/blob/b8cfb12c0e083a2ef5b2f9f9b50f3902fa034474/src/game/server/cbase.cpp#L284)

The earlier [native CSS probe](NATIVE-CSS-PROBE.md) establishes standing/crouched minimum Z of zero and hull heights 62/45 for installed build 11003710. The new [native teleport probe](NATIVE-CSS-TELEPORT-PROBE.md) confirms that Kitsune `*1` and Lux `*38`/`*39` preserve world velocity `(123,456,78)` and use exact authored destination origins in both hull states, with no crouch Z offset. Its connected VScript handlers sometimes execute several ticks later and can observe state from a subsequent teleport; those callbacks are unsuitable as contact-time telemetry. The one-tick RCON reads are the recorded post-contact samples. Bot aim drift limits exact eye-angle comparison. Neither probe establishes normal swept contact timing, overlap ordering, or all entity I/O timing.

KSF's own [command documentation](https://ksf.surf/commands) distinguishes restarting the map, returning to the current zone, selecting stages, and repeating stages. It does not publish the implementation of stage-failure timer preservation, zero-delay entity scheduling, or velocity handling. Stock CSS has no surf-run timer. Continuing total elapsed time after an authored Kitsune stage failure is the proposed browser rule unless separate current KSF evidence is obtained; describe it as such.

## Recommended minimal runtime contract

Keep timer geometry separate from movement-changing entity effects. A `trigger_teleport` is not a timer `reset` zone. The existing `GameSession.restart()` creates a fresh player, clears all commands and splits, resets tick/time, and therefore cannot be used for normal stage progression or a stage failure during a whole-map run.

An explicit import interface can be compact:

```ts
interface MapTeleport {
  id: string;
  sourceEntityIndex: number;
  sourceModelIndex: number;
  hammerId: string;
  min: Vec3;
  max: Vec3;
  hulls: Brush[];
  targetName: string;
  destination: Spawn;
  enabled: boolean;
  angleMode: 'destination' | 'preserve';
  velocityMode: 'preserve';
  landmarkOrigin?: Vec3;
  route: 'progress' | 'stageReturn' | 'mapReturn' | 'finish' | 'auxiliary';
  destinationStage?: number;
}
interface MapStage {
  number: number;
  name: string;
  spawn: Spawn;
  startZoneId: string;
}
```

`route` and `destinationStage` are reviewed browser semantics, not raw CSS entity properties. Preserve raw key/value pairs and omitted filter/disabled/bonus entities in metadata. For these four main routes the effect subset needs ordinary absolute teleports, target angles, and continuous pushes; no generic landmark rotation, velocity rotation or general I/O interpreter is required. Do not support a fabricated landmark-angle feature: the reviewed reference is a world-space translation only.

Recommended session rules:

- Use the existing swept convex trigger union to find candidate contacts, with a deterministic tie-breaker recorded as a browser policy until native overlap order is measured. Respect disabled/filter classification before choosing the first effective teleport.
- End the source path at the chosen transfer for timer processing. Never create a swept segment joining source to remote destination; that line can cross unrelated checkpoints or a finish. Discard source-path events after the teleport instead of processing them from the already-computed whole movement tick.
- Apply position/angles without `createPlayer` or `restart`; preserve duck state, buttons, global tick, command history, and appropriate velocity. Clear stale grounded/contact state. Rebase the renderer's previous position so interpolation cannot fly across the map. Refresh destination overlap state without a fictitious travel segment.
- Keep only one authoritative elapsed tick for each input command. The reviewed material does not justify accelerating again or adding an extra full gravity tick after a teleport. Whether native CSS spends residual movement time at the destination remains a native-probe question, so version and document the chosen policy.
- Mark Kitsune progression once and in order; falling on stage N returns to its authored spawn without discarding whole-map elapsed time or completed splits. Repeating a portal, touching a failure volume, or selecting a practice station cannot grant a missing stage. Explicit map restart still resets the run. Explicit practice travel remains unranked.
- Finish Kitsune when the ninth-stage end volume is reached, before the portal returns the player to the lobby. Lux's pair of midcourse portals preserves its linear timer and checkpoint sequence.

Replay/version identity must include the new effect semantics. A replay spanning a stage transfer must keep the same command timeline and reproduce from a fresh session. Changing teleport timing or velocity policy later requires a version change.

## Geometry and asset hazards

Kitsune lobby push `*11` has entity angles `(1,87,0)`, speed 1750, and `pushdir (0,90,0)`. Its broad bounds must be recomputed from transformed hull vertices or transformed bounds corners; merely translating lump-14 min/max lets the broad phase reject valid contacts. The importer now preserves the transformed bounds, with a focused fixture in `tests/new-map-import.test.ts`. Do not rotate the authored world push direction twice. The separate secret-room push `*83` is 2000 toward yaw 270.

Demise has 101 `prop_dynamic_override` instances with solid 6: 82 `game_tree2`, one `game_tree67`, two `game_tree12`, four each of `bones01`/`bones02`/`bones03`, and four `door1_hell`. Aircontrol has three solid-6 dynamic models, `models/custom/ed.mdl`, `edd.mdl`, and `eddy.mdl`. Their MDLs exist, but none of their ten distinct model paths has a PHY in the BSP or mounted installed CSS/HL2 packages. Record these missing collision resources explicitly. Do not invent collision from visual triangles or assume that every dynamic prop is decorative. Native missing-PHY collision behavior on route-relevant contacts remains unmeasured.

The installed native CSS server does accept Demise's 101 MDL-version-49 tree props (a different set from the 101 solid-6 props above: 82 `game_tree2`, 16 nonsolid `hallo_tree_2`, two `game_tree12`, one `game_tree67`). They instantiate as live `prop_dynamic` entities with real model indices and scaled bounds, rather than an error model. Do not omit their visuals on the assumption that CSS only accepts model version 48. The recorded inventory is `fixtures/native-css-demise-model-probe.txt`; it is server evidence of model loading, not a client-rendered visual test.

Demise `func_brush` models `*72` and `*74` have Solidity 1; rotating decoration `*73` has `solidbsp 0` and spawnflags 65. These are distinct from ordinary solid brush geometry. Kitsune's twelve `kitsune`-named `func_brush` entities have Solidity 2; do not exclude them merely because they share a decorative name.

## Focused native checks still needed

The limited placement tests are complete; see [the results and controls](NATIVE-CSS-TELEPORT-PROBE.md). For repetition, use Kitsune `*1` feet `(-15360,-11545,448)`, angles `(7,13,0)`, velocity `(123,456,78)`, and nearby free-space control `(-15360,-11650,448)`. **Do not use the portal AABB center** `(-15360,-11520,448)`: the player hull overlaps backing BSP solid `bsp-0-1122` by six units there, contaminating the measured velocity with stuck/collision behavior. The corrected test retains velocity for standing and crouched players.

Still unmeasured are Kitsune `*3` failure return, normal high-speed entry across a thin portal, and touching adjacent/overlapping volumes. For I/O ordering, add probe-only outputs to an existing entity rather than altering the downloaded BSP; record event timestamps and state before/after a queued zero-delay velocity change. Probe landmark behavior only if generic landmark support is actually included. Do not generalize the completed standing/crouched placement tests to full-route native parity.
