You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to the server-authoritative smooth-lighting foundation in atrinik/atrinik#169. The historical 3D-engine request mentioned colored
torch light, but it was much broader and is not an actionable Classic issue.
Replacement work such as atrinik/server#66
explicitly leaves colored light out of scope; it neither implements nor blocks this
Classic feature.
Outcome
Let Classic light-emitting archetypes and map instances declare an RGB tint,
defaulting to neutral white. The server should resolve one authoritative colored
light field across cells, sub-layers, horizontal map boundaries, and linked depths;
the client should smoothly interpolate and apply that field to the complete scene.
Multiple lights must combine additively and deterministically. For example, equal
red and blue contributions should form a magenta overlap, while neutral ambient
light contributes equally to every channel. A later source must never replace an
earlier source's color.
This is feasible, but it is a coordinated server, protocol/libatrinik, client, and content@1.x change rather than a client-only tint.
Current behavior and feasibility
The existing pipeline already has the expensive structural pieces:
The missing information is color: all source contributions have already been
collapsed to one scalar before the client sees them. Sending visible source objects
and rebuilding their fields client-side is not acceptable. It would duplicate the
server's 3D obstruction model, lose hidden/off-screen contributions, and risk
disclosing source positions behind fog or structural boundaries.
Boundary
Current contract
Required contract
Authored object
glow_radius intensity/range only
Dedicated validated light tint, default white
Server map
Scalar ambient + scalar source accumulation
Existing scalar gameplay authority plus server-resolved RGB presentation field
MAP2
One scalar byte per signaled sub-layer
Existing scalar samples plus sparse, bounded RGB state under a new exact protocol version
Client renderer
Scalar interpolation and grayscale multiplication
Per-channel interpolation, modulation, blur, extrapolation, and cache identity
Authoring and server authority
Add a dedicated property such as light_color RRGGBB, with exactly six
hexadecimal digits and ffffff as the absent/default value. glow_radius
remains the source strength/range contract.
Do not overload the existing glow property. It is a separately transmitted,
animated sprite-outline effect and is also valid on objects that are not world
light sources.
Carry the new property through object initialization, clone/copy/compare,
load/save, archetype and map-instance overrides, Python-plugin exposure, and
dynamic light lifecycles. Applied lights must propagate the selected light's
color to the player/monster emitter; a same-radius color change must invalidate
the field even when glow_radius itself does not change.
Preserve the existing scalar map_get_darkness() result as the authority for
object visibility, special vision, and other gameplay decisions. Color is visual
presentation and must not make hidden gameplay state newly visible.
Retain enough source identity, or an exact equivalent grouping, to resolve
differently colored emitters on the same cell. The current MapSpace.light_source
counter combines co-located radii before selecting one mask, so attaching one
color to that counter would be order-dependent and incorrect.
Accumulate attenuated contributions in bounded wide/fixed-point RGB channels,
then normalize/tone-map and clamp once per final sample. Define the color space,
rounding, saturation, and authored-tint normalization so results do not depend on
source insertion order. Neutral white must reproduce the current scalar result in
all three channels.
Treat ambient, floor, world, special-vision, and tli illumination as neutral
white for the first implementation. Preserve negative/darkness sources as
achromatic subtraction; reject or ignore non-white color on a negative source
unless separately specified and tested.
Preserve the existing horizontal/vertical linked-map reach, floor boundaries,
opaque-cell behavior, add/remove symmetry, load/unload rebuilds, and bounded depth
from server/doc/MAP_RENDERING.md.
The authoritative content@1.x schema and its Gridarta projection need the same
validated property. Add representative maintenance-scoped examples/fixtures (for
example a warm torch, green sconce, and colored forcefields) and link the
coordinated content change; do not import replacement content into Classic.
Wire contract and client rendering
Advance the generated Classic gameplay protocol version at implementation time
(currently 1074). Classic already requires exact client/server version equality,
so mismatched peers should continue to reject cleanly; do not retain an indefinite
dual MAP layout.
Preserve the existing scalar MAP2_MASK_LIGHT_LEVEL{,_MORE} payloads for
gameplay authority and neutral scenes. Prefer a sparse color extension using a
new MAP2_FLAG_EXT_LIGHT_RGB bit in the existing tile-tail ext_flags
byte,
followed by a seven-bit sub-layer bitmap and ascending RGB888 triples. Define
whether that bitmap is complete color state or a delta, including an unambiguous
colored-to-neutral reset to (level, level, level). A complete-state bitmap may
be zero specifically to reset a previously colored tile. Ordinary neutral samples
should derive RGB from the scalar and incur no color-extension bytes.
Specify the extension's order relative to the existing animation tail
payload.
Cache scalar and RGB state independently so color-only updates are emitted, and
so simultaneous scalar/color changes, same-luminance hue changes, and neutral
resets cannot leave stale client state. If another bounded representation is
chosen, document its field order, widths, default/reset form, and malformed-input
behavior and prove equivalent output and bandwidth.
Update the server delta cache/producer, shared libatrinik MAP preflight, client
transactional length checks/decoder, client per-depth MapCell cache, packet
fixtures, and offline player-view snapshots together. Truncation or invalid data
must fail before any map state mutates.
Average unknown streamed-edge samples, bilinearly interpolate/extrapolate map
quads, and blur structure samples independently per channel. Every channel must
participate in lighting revisions and lit-sprite cache signatures.
For ground, use an RGB modulation lightmap (SDL3 SDL_BLENDMODE_MOD has the
required dstRGB = srcRGB * dstRGB behavior) or an exactly equivalent bounded
software path. Preserve exact-black color keys, alpha, RLE, and transparent
positive-depth scratch surfaces.
Multiply wall, roof, living, item, effect, and other elevated sprite pixels by
their corresponding RGB illumination, not one scalar. Explicitly cover the
current positive-depth ground/discrete fallback so colored light does not affect
only the base floor and walls.
Keep fog/grayscale/infravision precedence, cutaways, roof-only cells, structural
projection, smooth-lighting disable/reset behavior, and the discrete lighting
option coherent. The discrete path may use a documented scalar projection of RGB,
but existing neutral-white scenes must not regress.
Performance and limits
The sparse extension adds one bitmap byte plus three bytes for each explicitly
colored sub-layer only when its color state must be sent; ordinary neutral-light
updates retain their current size. A dense colored tile can still add up to 22
bytes including the extension flag and bitmap. Preserve delta-only emission,
measure initial/full, scrolling, and dynamic-light updates, and prove that the 65,534-byte game-envelope payload limit
cannot be exceeded by a valid multi-depth scene.
Packed RGB/presence samples will likely double the current two-byte per-pixel light
and structure buffers, and Classic can cache up to 13 relative-depth contexts.
Retain inactive-depth cleanup, consider lazy structure allocation, keep the bounded
lit-sprite LRU, and measure frame time, peak memory, cache hit rate, packet bytes,
incremental source updates, and full linked-map recalculation at representative and
large viewports. The existing lighting profiler should make regressions visible.
Acceptance criteria
With no color property, or with light_color ffffff, current smooth and
discrete neutral-light scenes remain pixel-equivalent and all gameplay
darkness/visibility decisions remain unchanged.
Isolated red, green, and blue sources tint ground, walls, roofs, living
objects, items, effects, and linked positive-depth ground consistently.
Equal red + blue produces a stable magenta overlap and equal red + green a
stable yellow overlap. Same-cell and different-cell combinations are
order-independent, additive, and bounded rather than last-writer-wins.
Adding, removing, moving, applying, extinguishing, burning out, or changing
the color of a source updates the field once; reversing the action restores
the exact previous scalar and RGB fields without drift, underflow, or stale
cache entries.
Recalculation, map reload/unload, opaque blockers, floors in both vertical
directions, horizontal map edges, and linked depths preserve the obstruction
and propagation behavior established by feat: add multi-level smooth lighting atrinik#169.
Neutral ambient/special vision and achromatic darkness sources combine with
colored sources according to one documented, deterministic saturation and
rounding rule.
Fog, unseen cells, hidden sources, cutaways, infravision, and roof-only
structural samples disclose no additional gameplay state.
Color-only and scalar-plus-color deltas, including same-luminance hue changes
and colored-to-neutral resets, update exactly the intended sub-layers. MAP
preflight and the client reject truncated RGB samples, impossible bitmaps,
unknown extension bits, extra bytes, invalid levels, and protocol-version
mismatches without partial state; reconnect and map-cache clearing cannot
retain stale color.
Valid dense colored scenes remain within the game-envelope payload limit, or
are split/bounded by a documented deterministic mechanism without dropping
authoritative lighting state.
Add a pixel-exact offline player-view fixture containing neutral ambient plus
overlapping red/blue lights across ordinary ground, a wall, roof-only cell,
ordinary object, fog boundary, and depths 0/+1/+2. Keep focused smooth and
discrete expected hashes.
Record before/after packet bytes, server incremental/rebuild time, client
lighting frame time, active-depth memory, and lit-sprite cache behavior for an
ordinary room and a dense multi-level emissive scene. Any material regression
is optimized or explicitly approved with a bound.
Protocol generation/tests, libatrinik, server, and client tests pass through
one Classic worktree/profile, including the client coverage/player-view suite,
server lighting/lifecycle tests, python3 tools/verify_import_history.py, and git diff --check; the coordinated content@1.x schema/editor validation also
passes.
Out of scope
Changing the current light-mask/radial falloff shape.
HDR, bloom, animated flicker, dynamic shadows, or a GPU renderer migration.
Client-authoritative source propagation or sending source positions/objects as a
substitute for the resolved field.
Colored negative/darkness sources beyond the neutral compatibility behavior.
Implementing the corresponding fresh MIT replacement-stack feature.
Classic implementation remains GPL-2.0-or-later. Product semantics and measurements
may inform replacement work, but source or fixtures cross that boundary only under
the repository's exact provenance rules.
Follow-up to the server-authoritative smooth-lighting foundation in
atrinik/atrinik#169. The historical
3D-engine request mentioned colored
torch light, but it was much broader and is not an actionable Classic issue.
Replacement work such as atrinik/server#66
explicitly leaves colored light out of scope; it neither implements nor blocks this
Classic feature.
Outcome
Let Classic light-emitting archetypes and map instances declare an RGB tint,
defaulting to neutral white. The server should resolve one authoritative colored
light field across cells, sub-layers, horizontal map boundaries, and linked depths;
the client should smoothly interpolate and apply that field to the complete scene.
Multiple lights must combine additively and deterministically. For example, equal
red and blue contributions should form a magenta overlap, while neutral ambient
light contributes equally to every channel. A later source must never replace an
earlier source's color.
This is feasible, but it is a coordinated server, protocol/libatrinik, client, and
content@1.xchange rather than a client-only tint.Current behavior and feasibility
The existing pipeline already has the expensive structural pieces:
MapSpacestores ambient and source illumination as scalar values.light_mask_adjust()additively applies source masks after linked-map andobstruction checks.
draw_client_map2()normalizes and delta-serializes oneuint8_tlight levelper sub-layer.
and
MapCellcaches one scalar per sub-layer.composites ground through a black alpha map,
and multiplies every sprite channel by the same scalar.
The missing information is color: all source contributions have already been
collapsed to one scalar before the client sees them. Sending visible source objects
and rebuilding their fields client-side is not acceptable. It would duplicate the
server's 3D obstruction model, lose hidden/off-screen contributions, and risk
disclosing source positions behind fog or structural boundaries.
glow_radiusintensity/range onlyAuthoring and server authority
light_color RRGGBB, with exactly sixhexadecimal digits and
ffffffas the absent/default value.glow_radiusremains the source strength/range contract.
glowproperty. It is a separately transmitted,animated sprite-outline effect and is also valid on objects that are not world
light sources.
load/save, archetype and map-instance overrides, Python-plugin exposure, and
dynamic light lifecycles. Applied lights must propagate the selected light's
color to the player/monster emitter; a same-radius color change must invalidate
the field even when
glow_radiusitself does not change.map_get_darkness()result as the authority forobject visibility, special vision, and other gameplay decisions. Color is visual
presentation and must not make hidden gameplay state newly visible.
differently colored emitters on the same cell. The current
MapSpace.light_sourcecounter combines co-located radii before selecting one mask, so attaching one
color to that counter would be order-dependent and incorrect.
then normalize/tone-map and clamp once per final sample. Define the color space,
rounding, saturation, and authored-tint normalization so results do not depend on
source insertion order. Neutral white must reproduce the current scalar result in
all three channels.
tliillumination as neutralwhite for the first implementation. Preserve negative/darkness sources as
achromatic subtraction; reject or ignore non-white color on a negative source
unless separately specified and tested.
opaque-cell behavior, add/remove symmetry, load/unload rebuilds, and bounded depth
from
server/doc/MAP_RENDERING.md.The authoritative
content@1.xschema and its Gridarta projection need the samevalidated property. Add representative maintenance-scoped examples/fixtures (for
example a warm torch, green sconce, and colored forcefields) and link the
coordinated content change; do not import replacement content into Classic.
Wire contract and client rendering
(currently 1074). Classic already requires exact client/server version equality,
so mismatched peers should continue to reject cleanly; do not retain an indefinite
dual MAP layout.
MAP2_MASK_LIGHT_LEVEL{,_MORE}payloads forgameplay authority and neutral scenes. Prefer a sparse color extension using a
new
MAP2_FLAG_EXT_LIGHT_RGBbit in the existing tile-tailext_flagsbyte,
followed by a seven-bit sub-layer bitmap and ascending RGB888 triples. Define
whether that bitmap is complete color state or a delta, including an unambiguous
colored-to-neutral reset to
(level, level, level). A complete-state bitmap maybe zero specifically to reset a previously colored tile. Ordinary neutral samples
should derive RGB from the scalar and incur no color-extension bytes.
payload.
Cache scalar and RGB state independently so color-only updates are emitted, and
so simultaneous scalar/color changes, same-luminance hue changes, and neutral
resets cannot leave stale client state. If another bounded representation is
chosen, document its field order, widths, default/reset form, and malformed-input
behavior and prove equivalent output and bandwidth.
transactional length checks/decoder, client per-depth
MapCellcache, packetfixtures, and offline player-view snapshots together. Truncation or invalid data
must fail before any map state mutates.
quads, and blur structure samples independently per channel. Every channel must
participate in lighting revisions and lit-sprite cache signatures.
SDL_BLENDMODE_MODhas therequired
dstRGB = srcRGB * dstRGBbehavior) or an exactly equivalent boundedsoftware path. Preserve exact-black color keys, alpha, RLE, and transparent
positive-depth scratch surfaces.
their corresponding RGB illumination, not one scalar. Explicitly cover the
current positive-depth ground/discrete fallback so colored light does not affect
only the base floor and walls.
projection, smooth-lighting disable/reset behavior, and the discrete lighting
option coherent. The discrete path may use a documented scalar projection of RGB,
but existing neutral-white scenes must not regress.
Performance and limits
The sparse extension adds one bitmap byte plus three bytes for each explicitly
colored sub-layer only when its color state must be sent; ordinary neutral-light
updates retain their current size. A dense colored tile can still add up to 22
bytes including the extension flag and bitmap. Preserve delta-only emission,
measure initial/full, scrolling, and dynamic-light updates, and prove that the
65,534-byte game-envelope payload limit
cannot be exceeded by a valid multi-depth scene.
Packed RGB/presence samples will likely double the current two-byte per-pixel light
and structure buffers, and Classic can cache up to 13 relative-depth contexts.
Retain inactive-depth cleanup, consider lazy structure allocation, keep the bounded
lit-sprite LRU, and measure frame time, peak memory, cache hit rate, packet bytes,
incremental source updates, and full linked-map recalculation at representative and
large viewports. The existing lighting profiler should make regressions visible.
Acceptance criteria
light_color ffffff, current smooth anddiscrete neutral-light scenes remain pixel-equivalent and all gameplay
darkness/visibility decisions remain unchanged.
objects, items, effects, and linked positive-depth ground consistently.
stable yellow overlap. Same-cell and different-cell combinations are
order-independent, additive, and bounded rather than last-writer-wins.
the color of a source updates the field once; reversing the action restores
the exact previous scalar and RGB fields without drift, underflow, or stale
cache entries.
directions, horizontal map edges, and linked depths preserve the obstruction
and propagation behavior established by feat: add multi-level smooth lighting atrinik#169.
colored sources according to one documented, deterministic saturation and
rounding rule.
structural samples disclose no additional gameplay state.
and colored-to-neutral resets, update exactly the intended sub-layers. MAP
preflight and the client reject truncated RGB samples, impossible bitmaps,
unknown extension bits, extra bytes, invalid levels, and protocol-version
mismatches without partial state; reconnect and map-cache clearing cannot
retain stale color.
are split/bounded by a documented deterministic mechanism without dropping
authoritative lighting state.
overlapping red/blue lights across ordinary ground, a wall, roof-only cell,
ordinary object, fog boundary, and depths 0/+1/+2. Keep focused smooth and
discrete expected hashes.
lighting frame time, active-depth memory, and lit-sprite cache behavior for an
ordinary room and a dense multi-level emissive scene. Any material regression
is optimized or explicitly approved with a bound.
one Classic worktree/profile, including the client coverage/player-view suite,
server lighting/lifecycle tests,
python3 tools/verify_import_history.py, andgit diff --check; the coordinatedcontent@1.xschema/editor validation alsopasses.
Out of scope
substitute for the resolved field.
Classic implementation remains GPL-2.0-or-later. Product semantics and measurements
may inform replacement work, but source or fixtures cross that boundary only under
the repository's exact provenance rules.