vimp-engine-core 0.8.3

VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate.
Documentation
# Changelog

All notable changes to the `vimp-engine-core` **crate** are documented here.
The npm package `vimp-engine` has its own journal:
[../CHANGELOG.md](../CHANGELOG.md). The format is based on
[Keep a Changelog](https://keepachangelog.com/); this crate uses
[Semantic Versioning](https://semver.org/) (in `0.x`, a breaking change bumps
the minor version).

A bump here has to be repeated by hand in every game crate that depends on
it (e.g. `vimp-tanks/core/Cargo.toml` → `vimp-engine-core = "X.Y.Z"`), since
the dependency is by version, not by path.

## [Unreleased]

## [0.8.2] — 2026-08-21

Released without journal entries: the only change is internal — `write_hot`
now looks a row's width up in a reverse `id → width` index built once in
`ClientState::new`, instead of scanning the snapshot schema for every
predicted row of every frame (`8c379eb`).

## [0.8.1] — 2026-08-21

### Fixed

- The hot buffer's `HOT_HAS_PREDICTED` flag is raised when the game returned
  only its own predicted rows (`GameClientDef::render_rows`) without a
  predicted record for the local actor. The flag means "there are trailing
  records behind the groups", and both JS consumers gate the parse of the
  whole buffer on it, so such a buffer used to be dropped unparsed.

## [0.8.0] — 2026-08-21

### Added

- `ClientState::game()` — access to the game half of the core: a plugin's ABI
  wrapper (`ClientCore`) reaches its own subsystems through it, whose shape the
  engine never knows.

## [0.7.0] — 2026-08-21

### Added

- `GameClientDef` hooks for bodies a game predicts itself (map dynamics,
  remote actors in contact with the local one), all with empty defaults:
  `begin_reconcile(&DecodedSnapshot)` hands the game the authoritative frame
  before the input replay, `finish_reconcile()` runs right after
  `on_server_state` so the divergence is folded once the replay has carried
  those bodies too, and `render_rows() -> Vec<PredictedRow>` returns rows for
  the render tick.
- `PredictedRow` — a row (`key_id`, `id`, `fields`) with which a game
  overrides an interpolated one. `write_hot` appends the rows after the
  predicted tail in the same record shape and brings each row's width to the
  key's schema (extra fields dropped, missing ones zero-filled), so a stray
  row cannot shift the parse of the rest of the tail. Reading a record puts
  it into `game[key][id]`, so a trailing row wins over the interpolated one —
  the same mechanism the predicted tail of the local actor already used.

## [0.6.0] — 2026-08-20

### Added

- Optional row tail in the snapshot schema: `BlockSchema.optionalFrom` marks
  the index of the first field that is written only when the row carries it.
  Such a row starts with a flag byte (`1` — the tail follows, `0` — the row
  ends after the mandatory part), so a body that has nothing to report does
  not pay for the tail every frame. Decoding still yields a **full-width**
  row — a missing tail reads as zeros — so the interpolator, the hot buffer
  and the JS side stay fixed-width. `SnapshotConfig::validate()` rejects an
  `optionalFrom` that points outside a non-empty tail.
- `GameMap::dynamic_map_data` can append the velocity tail
  (`vx`, `vy`, `angvel`) of a moving body; a resting one (asleep, or below
  `REST_VELOCITY_EPSILON` in both linear and angular speed) ships only its
  transform. The client predicts map dynamics next to its own tank, and a
  velocity estimated by finite differences between 30 Hz frames is worst
  exactly at the moment of impact.

### Changed

- **Breaking.** `GameMap::dynamic_map_data(world)` takes a second argument,
  `with_velocities`. `EngineSim::build_snapshot_blocks` derives it from the
  map set's own schema entry (`optionalFrom` present), so the engine never
  imposes a row width on the game.
- **Breaking.** The frame format is v5 (see the npm package's changelog):
  the engine and the game crate have to ship as a matching pair.

## [0.5.0] — 2026-08-20

### Added

- `client::collision` and `client::rigid_body` — the contact primitives a
  client needs to predict the geometry the host resolves. `collision`
  brings `box_center_from_origin` (a map body's origin corner → the box
  centre), `obb_vs_obb` (SAT, returning the minimum translation vector,
  the depth and a blended contact point) and `collect_tile_contacts` (an
  OBB against the solid cells of the tile grid); it reads the map through
  the same `Box2` and the same grid triple as `client::raycast`, so a ray
  and a contact can never disagree about a wall. `rigid_body` brings the
  `Body`/`Surface`/`MassProperties` types, `integrate`, `separate_bodies`
  and `apply_contact_impulse` (a sequential-impulse solver with
  restitution and a Coulomb friction cone), `box_mass_properties` and
  `combine_surfaces` (Rapier's `CoefficientCombineRule::Average`). Its
  `MAP_SURFACE` is built from `map::DEFAULT_FRICTION` /
  `DEFAULT_RESTITUTION`, now public, so the replica and the host cannot
  drift apart on surface values. `client::raycast::Box2` is now `Copy`.

### Fixed

- Dynamic map objects no longer sink into geometry on impact. Rapier's
  default contact prediction distance is 0.002 units, a figure meant for a
  metre-scale world, while a body in a match covers orders of magnitude more
  per `1/120` step — so the contact was born only once the shapes already
  overlapped deeply. `GameMap::create_dynamic` now builds the body with
  `soft_ccd_prediction(width.min(height))`, the object's own thickness.
  Static walls are unchanged.

## [0.4.0] — 2026-08-19

### Added

- `GameSim::apply_aim(game_id, seq, x, y, flags)` and
  `GameClientDef::apply_aim(x, y, flags, local_now)` — the pointer input
  channel (mouse, finger, stylus), carrying a value the discrete
  `apply_input(action, key_name)` string cannot: a **world** point plus a bit
  mask (bit 0 «pressed», bit 1 «double tap»). Both are declared with a
  **default empty body**, so a game crate that ignores the pointer compiles
  unchanged; `export_game_core_abi!` and `export_client_core_abi!` expose
  them as `apply_aim(...)` on the respective WASM cores. The engine converts
  screen coordinates to world coordinates before the call, so the game half
  gets a point in the same space as `actor_position`.

## [0.3.0] — 2026-08-09

### Added

- `ClientState::resync()` — clears the network half only (interpolation
  buffer, outgoing frame queue), leaving prediction and the local identity
  intact. For a tab returning from a long pause: the clock offset is
  reseeded from the next frame instead of being chased by the EMA for dozens
  of frames, while entities on the canvas stay alive. Exposed as `resync()`
  by `export_client_core_abi!`.

### ⚠️ Breaking — `reset()` also clears `my_game_id`

`ClientState::reset()` (the `CLEAR` port) means "the world is gone", so it
now drops the local player's identity as well. Previously the identity
survived a clear, and the game half kept rendering a predicted entity for a
player the host no longer had — a ghost on an otherwise empty canvas after a
map change. The identity is restored from the first player block that
follows; a spectator has none, so no predicted entity is drawn.

### Migration

A game crate that reads `my_game_id()` right after a `CLEAR` now gets
`None`; wait for the first frame carrying a player block. A `GameClientDef`
that keeps its own copy of the local actor's identity should clear it in its
`reset()` too (this is what `TanksClient` does with `my_tank_meta`).

### ⚠️ Breaking — `GameClientDef::set_server_offset` removed

The method handed the interpolator's offset to the game half once per render
tick, documented as "a latency estimate for RTT compensation of visual
effects". That description was wrong and the API existed only to serve it:
the offset is `serverTime − localNow`, where `serverTime` is the host's
`Date.now()` and `localNow` is the client's `performance.now()` — a clock
difference on the order of 1e12 ms, not a network delay. A game that took it
for a latency and extrapolated a spawn position by `velocity × offset` threw
the entity out of the world (visible only while moving; standing still the
term is zero). Nothing else needed the hook: reconciliation already receives
the offset as an argument of `on_server_state`, and `ClientState::offset()`
(ABI `offset()`) still exposes it for diagnostics, now documented as a clock
difference.

### Migration

Delete `set_server_offset` from every `impl GameClientDef` — no replacement
call is needed. A game that used it to compensate a locally spawned entity's
position should spawn at the predicted position and let the authoritative
row correct it once on confirmation (rename it to the local id instead of
dropping it, so the entity is updated rather than recreated).

## [0.2.1] — 2026-08-05

### Fixed

- `DivergenceTracker` with `capacity: 0` reported one eviction more than it
  performed: the buffer always keeps one record, but the counter assumed an
  empty buffer, so a report claimed "N dropped" after N−1 evictions. The
  configured capacity is now clamped to at least 1 (`client/divergence.rs`).

## [0.2.0] — 2026-08-05

### Added

- `debug.rs` — a curated world dump behind `debug_json`: bodies, map, nav
  and RNG state in one JSON slice, so "the core has a body, the canvas is
  empty" is visible from a single file.
- `client/divergence.rs` — a prediction-drift tracker behind
  `take_divergence`: the predicted state is captured before reconciliation
  and compared with the frame's player block, matched by frame time rather
  than by input `seq`.
- `GameClientDef::predicted_state` — drift detection level 1. A game that
  implements it gets a component-wise comparison of its own predicted state;
  a game that does not falls back to level 0 (the overlay camera).
- Config section for the detector (`config.rs`) and the corresponding ABI
  entries (`abi.rs`), so a game implements nothing to get either feature.

## [0.1.0] — 2026-07-26

### Added

- First published release. The simulation framework extracted from the game
  core into a reusable rlib: `rapier2d` physics, map model, snapshot frame
  codec, interpolation/prediction/raycast primitives, navigation and spatial
  utilities, RNG, and the `macro_rules!` ABI generators. Deliberately without
  `wasm-bindgen` — the WASM ABI wrappers are built by each game's own crate.

[0.8.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.8.0
[0.7.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.7.0
[0.5.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.5.0
[0.4.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.4.0
[0.3.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.3.0
[0.2.1]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.2.1
[0.2.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.2.0
[0.1.0]: https://github.com/lgick/vimp/releases/tag/vimp-engine-core%400.1.0