Skip to content

Implement server-authoritative radial light falloff #66

Description

@zoeyrose

Important

This preserved product issue is now part of the fresh MIT replacement program. Final implementation owner: atrinik/server. Legacy C/SDL2, packet, global-state, and file-path details below are historical evidence only.

Replacement implementation contract

Preserve radial falloff behavior and all gameplay visibility decisions. Implement deterministic Go light-field calculation over structural surfaces; protocol sends bounded semantic light data and renderer owns the visual fixture/WGSL presentation.

New implementation and tests are independent MIT work unless an exact contribution by an approved MIT provenance grantor is admitted through the recorded file-level MIT grant. Do not copy or mechanically translate other GPL source/tests. Preserve every player-facing, accessibility, disclosure, and performance design decision below.

Required verification

  • Add deterministic Rust and/or Go tests at the owning boundary plus cross-language protocol fixtures for new fields.
  • Exercise malformed/stale/reordered inputs and lifecycle failure without partial state.
  • Use the shared renderer rather than a client/editor fork.
  • Validate through a wrapper-managed replacement scenario where the feature is interactive.
Preserved product/design specification and historical implementation notes

Outcome

Replace the visibly cross-shaped footprint of torches and other small light sources with a smooth, radial server-authoritative falloff while preserving linked-level propagation, obstruction by opaque cells/floors, overlapping sources, darkness sources, and the client's smooth/discrete lighting modes.

This is a focused visual follow-up to atrinik/atrinik#169. It should improve the existing lighting model rather than introduce a second client-only light system.

Why the current footprint forms a cross

The client is not missing the basic lighting field:

  • server/src/socket/request.c:draw_client_map2() sends the composite normalized light level for every visible cell and sub-layer through MAP2_MASK_LIGHT_LEVEL / MAP2_MASK_LIGHT_LEVEL_MORE.
  • client/src/gui/widgets/map.c:map_draw_lighting() projects those samples into the isometric view.
  • client/src/client/lighting.c bilinearly interpolates the field for ground pixels and samples it for walls/roofs.

The shape originates in server/src/server/light.c before the packet is built:

  • glow_radius is collapsed through light_mask[] into one of ten legacy intensity masks.
  • light_mask_width[] caps every source at a support radius of four cells.
  • light_masks[][] contains a few hard-coded illumination bands.
  • light_mask_value() rounds Euclidean distance upward to an integer ring, so all positions in that ring receive the same abrupt value.

A handheld torch uses strength 3 (last_sp 3 in arch/items/light/torch/torch.arc). That selects a support radius of only two cells: the center is 160 raw light, cardinal neighbors are 80, diagonals and cardinal distance-two cells are 40, and everything else is zero. On a square sample grid, this leaves the bright core and outer reach concentrated on the cardinal axes. Client interpolation smooths the transitions but cannot reconstruct a radial field that the server did not sample.

Wall sconces use strength 5 and have the same quantized behavior over only three cells.

Proposed design

1. Replace hand-authored masks with an algorithmic radial kernel

Keep MapSpace.light_source_value as the authoritative accumulated illumination, but replace lmask_x, lmask_y, light_mask_width, light_mask_size, light_masks, and ring-index lookup with a bounded source profile and a deterministic falloff function.

For each effective source strength, define independently:

  • center intensity;
  • physical support radius; and
  • falloff curve/profile.

Iterate the bounded (dx, dy, dz) volume, reject points outside the profile radius, and calculate contribution from Euclidean distance squared. Use fixed-point arithmetic or a distance-squared lookup generated from the profile so results are portable and do not require floating-point work in map updates. The curve must be monotonic and reach zero continuously at the boundary rather than stepping through the current powers-of-two rings.

Separating range from center intensity is important: merely changing the values in the existing two-tile torch mask cannot remove the cardinal-axis silhouette. Small sources need enough surrounding samples for the client interpolation to describe a round footprint, without making their center excessively bright.

The existing glow_radius name and values currently conflate range and intensity. Under the repository's greenfield policy, either redefine it as a real radius plus a separate intensity/profile field, updating current content and Python exposure together, or retain it as a strength/profile selector with clearly documented generated radius/intensity values. Do not leave the current misleading implicit mask mapping undocumented.

2. Preserve authoritative 3D visibility

Continue routing each candidate sample through the linked-map resolution and light_path_is_clear() logic added by atrinik/atrinik#169:

  • propagate across horizontal map boundaries and TILED_UP / TILED_DOWN levels;
  • stop at opaque intermediate cells;
  • respect floors as vertical boundaries;
  • allow an exposed opaque target face to receive light; and
  • apply negative/darkness sources with the same radial magnitude and obstruction rules.

The distance metric, including the physical cost of one linked vertical level relative to one horizontal tile, should be explicit and covered by tests rather than inherited accidentally from loop indexes.

3. Keep the current MAP contract for the first implementation

No protocol change is required to obtain a materially rounder result. The server already sends the final composite per-cell/per-surface light samples, which preserves hidden source positions, overlaps, occlusion, ambient light, and special vision in one authoritative value. The client should continue to interpolate those samples and must not try to rediscover light sources from visible sprites.

If visual fixtures show that a well-sampled continuous server kernel is still visibly faceted at normal zoom, evaluate a separate follow-up that sends a denser authoritative field (for example, bounded sub-cell/corner samples). Do not send only source position/radius and reconstruct lighting client-side: that would also require enough obstruction and surface data to reproduce the server result and would create two lighting models.

4. Bound update cost and make tuning observable

Profile both incremental adjust_light_source() updates and recalculate_light_sources() on representative dense-light maps before increasing support radii. Precompute one compact kernel per supported profile/vertical metric and reuse it for add/remove operations; keep the current exact additive removal behavior.

Add development diagnostics or a focused test helper that can dump a source's raw and normalized sample grid. This makes symmetry, monotonicity, cutoff, and content tuning reviewable without relying only on subjective screenshots.

Scope

  • Replace the legacy mask tables/ring lookup in server/src/server/light.c.
  • Define and document the source strength/radius/intensity contract.
  • Update affected torch, sconce, lamp, lava, spell-effect, and darkness-source content if the authored contract changes.
  • Retain light_level_from_raw() unless screenshot tuning demonstrates that the independent perceptual normalization also needs adjustment.
  • Extend the server lighting unit suite for radial geometry, overlap, removal, negative sources, blockers, floors, linked maps, and boundary crossing.
  • Add visual before/after evidence from the existing smooth-lighting client path.

Acceptance criteria

  • An isolated handheld torch and wall sconce produce a centered, monotonic radial pool with no visually dominant cardinal-axis cross in smooth-lighting mode.
  • Equal-distance unobstructed samples have equal contributions in every horizontal direction; the chosen vertical metric is equally explicit and symmetric.
  • Contribution never increases with distance and reaches zero at the declared support boundary without a bright terminal ring.
  • Multiple sources combine deterministically; adding and removing a source restores the exact previous field without drift or underflow.
  • Opaque cells, floors, linked horizontal maps, and depths +1 / +2 retain the obstruction and propagation behavior established by feat: add multi-level smooth lighting atrinik#169.
  • Negative light sources use the same spatial profile with inverted contribution.
  • Existing MAP framing and client parsing remain unchanged unless denser-sampling evidence is attached and the protocol change is scoped explicitly.
  • Dense emissive scenes do not show a material server tick/update regression; before/after source-update and full-recalculation measurements are recorded.
  • Discrete lighting remains coherent, while smooth lighting shows no new seams on long walls, roofs, map edges, or streamed cells.

Validation plan

  • Server unit tests: center/cardinal/diagonal values at equal and ordered distances, cutoff boundary, overlap/add-remove identity, negative sources, opaque blockers, floors in both directions, horizontal map edges, and multi-level propagation.
  • Native validation: ./build.sh atrinik-server, ./build.sh atrinik, focused server.light tests, then ./build.sh check when the runtime is prepared.
  • Visual matrix: handheld torch, wall sconce, overlapping torches, moving/toggled source, map edge, long wall, one-storey roof, three-storey stack (+1 and +2), and an outdoor facade supplied by an upper map. Capture smooth lighting on/off at the same coordinates.
  • Performance matrix: incremental add/remove/move and full recalculation on an ordinary dark room plus a dense lava/emissive map; report source count, affected cells, linked levels, and elapsed time.

Out of scope

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Effort

    None yet

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions