Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b42c659
ARE ambient-animation detection + fpSEAM point effects; remove tile-s…
TheForgotten69 Jul 17, 2026
4609c20
fpSEAM: flatten point-effect loop into main; add ALIGN-mode point mar…
TheForgotten69 Jul 17, 2026
d1aab40
Point effects: night-adaptive intensity boost; immediate 1x latch for…
TheForgotten69 Jul 17, 2026
51d2709
Point effects v2: textured flame/smoke replacements + CGameStatic::Re…
TheForgotten69 Jul 18, 2026
1994d96
ci: add iee_effects_noise.rgba to the release-bundle allow-list
TheForgotten69 Jul 18, 2026
3beab79
Point effects: authored-size flames with palette colors; wispier smok…
TheForgotten69 Jul 18, 2026
d24a4a7
Point effects: authored BAM draw-box geometry; overlay art keeps its …
TheForgotten69 Jul 18, 2026
07eb724
Point effects: palette id moved to a plain slot-B float; elliptical d…
TheForgotten69 Jul 18, 2026
4fe81fb
Point effects: case-insensitive geometry/palette lookup; hearth glow …
TheForgotten69 Jul 18, 2026
7427b4a
Point effects: anchor is the flame base; remove heat-shimmer UV disto…
TheForgotten69 Jul 18, 2026
12b6d4b
Point effects: render from CGameObject::m_pos with the authored frame…
TheForgotten69 Jul 18, 2026
5ce679c
Point effects: engine-native flame geometry from CVidCell::m_pFrame
TheForgotten69 Jul 18, 2026
6707f86
Point effects: anchor flame at the object position, no positional offset
TheForgotten69 Jul 18, 2026
49975b2
Point effects: subtract the m_posZ elevation from the flame anchor
TheForgotten69 Jul 18, 2026
9dff696
Update build.yml
TheForgotten69 Aug 26, 2026
62b22e2
Support cross-compiling the Windows DLL with mingw-w64
TheForgotten69 Aug 26, 2026
a3423fb
Fix authored flame replacement anchors
TheForgotten69 Aug 27, 2026
3c2b6a6
Keep authored effects visible in alignment mode
TheForgotten69 Aug 27, 2026
73474b4
Constrain flame body to authored BAM envelope
TheForgotten69 Aug 27, 2026
ce063dd
Taper procedural flame tips beyond core envelope
TheForgotten69 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ jobs:
"override/M_IEEE.lua"
"override/fpSEAM.glsl"
"iee-textures/README.md"
"iee-textures/iee_effects_noise.rgba"
"iee-textures/iee_water_dudv.rgba"
"iee-textures/iee_water_foam.rgba"
"iee-textures/iee_water_foam.rgba"
"iee-textures/iee_water_normal.rgba"
) | Sort-Object
$actual = Get-ChildItem $root -File -Recurse | ForEach-Object {
Expand Down
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ Operational instructions for AI coding agents working in this repository.
and `8x`. Other values fail closed to the existing fallback path.
- `CResTileSet::h` is optional. Standard tilesets can have `header == null` while still exposing a valid 12-byte PVR entry table through `pData`. A null header does not imply missing deterministic metadata.
- Deterministic detection order for this build is:
`TIS header -> PVR entry table coordinate-grid GCD -> legacy heuristic fallback`
`TIS header -> PVR entry table coordinate-grid GCD -> fail closed to 1x`
(the legacy UV/texture-id heuristic produced false 4x detections on vanilla
areas and was removed — do not reintroduce it).
- `+0x1DC` is only the current linear-tiles tone flag for this build.

## Runtime Facts — Renderer / GL
Expand All @@ -59,6 +61,10 @@ Operational instructions for AI coding agents working in this repository.
- `src/iee/game/build_manifest.*` holds build-specific offsets, patterns, and callsites.
- `src/iee/game/tis_runtime.*` holds explicit runtime views.
- `src/iee/game/tile_upscale.*` holds scale selection logic.
- `src/iee/game/are_animations.*` + `src/iee/game/object_statics.*` classify
the active area's authored ARE ambient animations (fire/smoke/fountain/light
point sources) from the live CGameStatic objects; see
`docs/are-animation-detection.md`. Water bodies stay on the WED overlay path.
- `docs/` contains the architecture and reverse-engineering notes future agents should read first.
- `docs/threading-model.md` defines callback ownership, GL-thread rules, and ABI exception boundaries.
- `docs/superpowers/specs/2026-06-10-graphics-enhancement-roadmap-design.md` is the graphics roadmap: four feature pillars, validation gates (V1-V6), and the full evaluated/dropped/rejected idea ledger. Read it before proposing any rendering feature — most ideas have already been evaluated there.
Expand Down
19 changes: 15 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ add_library(iee_common STATIC
src/iee/core/config.cpp
src/iee/core/logger.cpp
src/iee/core/pattern_scanner.cpp
src/iee/game/are_animations.cpp
src/iee/game/area_texture.cpp
src/iee/game/build_manifest.cpp
src/iee/game/dds_texture.cpp
src/iee/game/game_addrs.cpp
src/iee/game/object_statics.cpp
src/iee/game/resref_runtime.cpp
src/iee/game/shader_override.cpp
src/iee/game/tile_liquid.cpp
Expand Down Expand Up @@ -189,17 +191,26 @@ if(IEE_BUILD_WINDOWS_DLL)

set(IEE_RELEASE_BUNDLE_DIR "${CMAKE_BINARY_DIR}/release-bundle")
set(IEE_SYMBOLS_DIR "${CMAKE_BINARY_DIR}/symbols")
add_custom_target(release_bundle
set(IEE_BUNDLE_COMMANDS
COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_RELEASE_BUNDLE_DIR}"
COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_SYMBOLS_DIR}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_RELEASE_BUNDLE_DIR}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_SYMBOLS_DIR}"
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_FILE:InfinityEngine-Enhancer>" "${IEE_RELEASE_BUNDLE_DIR}/InfinityEngine-Enhancer.dll"
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_PDB_FILE:InfinityEngine-Enhancer>" "${IEE_SYMBOLS_DIR}/InfinityEngine-Enhancer.pdb"
COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/tools/InfinityEngine-Enhancer.sample.ini" "${IEE_RELEASE_BUNDLE_DIR}/InfinityEngine-Enhancer.sample.ini"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/assets/override" "${IEE_RELEASE_BUNDLE_DIR}/override"
COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/tools/M_IEEE.lua" "${IEE_RELEASE_BUNDLE_DIR}/override/M_IEEE.lua"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/assets/game-textures" "${IEE_RELEASE_BUNDLE_DIR}/iee-textures"
)
if(MSVC)
# TARGET_PDB_FILE only resolves for linkers that emit a PDB. mingw-w64
# cross builds have no equivalent, so keep symbol export MSVC-only.
list(APPEND IEE_BUNDLE_COMMANDS
COMMAND "${CMAKE_COMMAND}" -E rm -rf "${IEE_SYMBOLS_DIR}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${IEE_SYMBOLS_DIR}"
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_PDB_FILE:InfinityEngine-Enhancer>" "${IEE_SYMBOLS_DIR}/InfinityEngine-Enhancer.pdb"
)
endif()
add_custom_target(release_bundle
${IEE_BUNDLE_COMMANDS}
DEPENDS InfinityEngine-Enhancer
VERBATIM
)
Expand Down
Binary file added assets/game-textures/iee_effects_noise.rgba
Binary file not shown.
209 changes: 209 additions & 0 deletions assets/override/fpSEAM.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ uniform highp vec2 uIeeZoom; // physical px per world px, per axis
uniform highp vec2 uIeeViewport; // physical px (w, h)
uniform highp vec2 uIeeWorldSizeInv; // 1 / world px
uniform highp vec3 uIeeWaterTint; // authored water color (avg of the area's water overlay tile)
uniform highp float uIeePointCount; // 0..32 classified ambient-animation points
// Two vec4 slots per point i: [2i] = (baseX, baseY, kind, heightPx) where the
// kind fraction is a palette id (.0 warm body / .1 blue body / .2 glow only);
// [2i+1] = (halfWidthPx, reserved...).
uniform highp vec4 uIeePoints[64];
uniform lowp sampler2D uIeeAreaMask; // unit 2: one liquid mode per 64px WED cell
uniform lowp sampler2D uIeeNormalMap; // unit 3: tiling water normal map
uniform lowp sampler2D uIeeDudvMap; // unit 4: tiling DuDv distortion map
uniform lowp sampler2D uIeeFoamMap; // unit 5: tiling foam mask
uniform lowp sampler2D uIeeNoiseMap; // unit 6: tiling FBM octaves (R/G smooth, B high-freq, A blobs)

varying highp vec2 vTc;
varying highp vec2 vRef;
Expand Down Expand Up @@ -266,9 +272,184 @@ void main()
vec4 base = art * vColor;
gl_FragColor = vec4(base.rgb, base.a);
}

// Point-placement markers: a filled dot plus a 40px ring at every fed
// effect point — orange = fire, grey = smoke, yellow = light. Absent
// rings in ALIGN mode mean the point feed (not the effect math) is
// broken.
if (uIeePointCount > 0.5 && vColor.a > 0.9)
{
float ring = 0.0;
vec3 ringColor = vec3(0.0);
for (int i = 0; i < 32; ++i)
{
if (float(i) >= uIeePointCount) { break; }
vec4 p = uIeePoints[2 * i];
float d = length(worldPos - p.xy);
float m = smoothstep(4.0, 1.5, abs(d - 40.0)) + smoothstep(6.0, 2.0, d);
if (m > ring)
{
ring = m;
if (p.z < 1.5) { ringColor = vec3(1.0, 0.45, 0.05); }
else if (p.z < 2.5) { ringColor = vec3(0.75, 0.75, 0.85); }
else { ringColor = vec3(1.0, 0.95, 0.20); }
}
}
if (ring > 0.02)
{
gl_FragColor = vec4(mix(gl_FragColor.rgb, ringColor, clamp(ring, 0.0, 1.0)),
gl_FragColor.a);
}
}
return;
}

// Ambient point effects. Base pass only; the WATER_ALPHA secondary pass
// and fades stay vanilla. Kept as flat locals (no struct/function) for
// maximum GLSL-frontend compatibility.
vec3 fxGlow = vec3(0.0);
vec3 fxFlame = vec3(0.0);
float fxFlameA = 0.0;
float fxHaze = 0.0;
if (uIeeEnabled > 0.5 && vColor.a > 0.9 && uIeePointCount > 0.5)
{
float fxT = uIeeTime;
for (int i = 0; i < 32; ++i)
{
if (float(i) >= uIeePointCount) { break; }
vec4 p = uIeePoints[2 * i];
vec4 pb = uIeePoints[2 * i + 1];
vec2 offs = worldPos - p.xy;
float kind = p.z;
float strength = clamp(p.w / 76.0, 0.05, 1.4);

if (kind < 1.5) // fire: textured flame body + cast light + shimmer
{
// Authored geometry from the BAM frame table: p.w = height,
// pb.x = half-width, position already at the flame's
// bottom-center; pb.y = palette (0 warm / 1 blue / 2 glow-only).
float fh = max(p.w, 5.0);
float fw = max(pb.x, 1.5);
float palette = pb.y;
float blue = (palette > 0.5 && palette < 1.5) ? 1.0 : 0.0;
bool bodyless = palette > 1.5;

// --- flame body (replaces the engine's BAM flame) ---
// Keep the luminous core inside the authored BAM envelope: p.xy is
// the proven bottom attachment point, so no body pixels belong below
// it. A low-opacity procedural tip may taper above the authored core;
// evaluate beyond fh so the animation never clips at a hard top edge.
if (!bodyless && offs.y <= 0.0 && offs.y > -fh * 1.3 && abs(offs.x) < fw * 2.6)
{
float v = clamp(-offs.y / fh, 0.0, 1.3); // 0 base -> 1 authored tip
float widthAt = fw * (1.05 - 0.60 * min(v, 1.0));
float xr = offs.x / max(widthAt, 1.0);
float radial = 1.0 - clamp(xr * xr, 0.0, 1.0);
float base = 1.0 - smoothstep(-2.0, 0.0, offs.y);
float tip = 1.0 - smoothstep(0.72, 1.24, v);
float column = radial * base * tip;
// Rising noise erodes the column: calm base, ragged tip.
// Sample scale follows the flame size so small flames keep
// visible structure.
float ns = max(fh, 14.0);
float seed = p.x * 0.61 + float(i) * 19.0;
float nA = texture2D(uIeeNoiseMap,
vec2((seed + offs.x) / (ns * 0.62),
(offs.y + fxT * ns * 1.35) / (ns * 0.9))).r;
float nB = texture2D(uIeeNoiseMap,
vec2((seed * 0.37 - offs.x) / (ns * 0.36),
(offs.y + fxT * ns * 2.1) / (ns * 0.52))).b;
float intensity = column * (1.15 - v * 0.5)
- (nA * 0.78 + nB * 0.60) * (0.36 + 0.95 * v);
intensity = clamp(intensity * 1.75, 0.0, 1.0);
if (intensity > 0.02)
{
// Ramp: deep -> mid -> bright -> hot core (kept small).
vec3 c0 = mix(vec3(0.42, 0.02, 0.0), vec3(0.01, 0.06, 0.42), blue);
vec3 c1 = mix(vec3(1.0, 0.28, 0.02), vec3(0.08, 0.38, 0.95), blue);
vec3 c2 = mix(vec3(1.0, 0.70, 0.16), vec3(0.45, 0.75, 1.0), blue);
vec3 c3 = mix(vec3(1.02, 0.95, 0.72), vec3(0.85, 0.95, 1.05), blue);
vec3 flame = mix(c0, c1, smoothstep(0.02, 0.38, intensity));
flame = mix(flame, c2, smoothstep(0.38, 0.74, intensity));
flame = mix(flame, c3, smoothstep(0.80, 0.97, intensity));
float a = smoothstep(0.03, 0.30, intensity);
if (a > fxFlameA)
{
fxFlameA = a;
fxFlame = flame;
}
}
}

// --- cast light on the surroundings ---
// Elliptical footprint (isometric floor projection) with
// noise dapple so the pool reads as firelight, not a disc.
vec2 g = offs;
// Glow-only overlays (hearths) anchor at the art's floor
// edge; center their light well up onto the coals.
g.y += fh * (bodyless ? 0.85 : 0.35);
g.y *= 1.9;
float radius = 40.0 + 80.0 * strength;
float d = length(g);
if (d < radius)
{
float fall = 1.0 - d / radius;
fall *= fall;
float fi = float(i) * 7.31;
float flick = 0.80 + 0.14 * sin(fxT * 9.7 + fi)
+ 0.06 * sin(fxT * 23.0 + fi * 1.7);
float dapple = 0.72 + 0.55 * texture2D(uIeeNoiseMap,
(worldPos + vec2(fxT * 5.0, -fxT * 3.0)) / 72.0).r;
vec3 glowColor = mix(vec3(1.0, 0.45, 0.15), vec3(0.30, 0.55, 1.0),
step(0.5, blue));
fxGlow += glowColor * (fall * (0.20 + 0.38 * strength) * flick * dapple);
}
}
else if (kind < 2.5) // smoke: textured plume replacing the BAM puffs
{
float height = max(p.w, 20.0);
float rise = -offs.y; // px above the source
if (rise > -10.0 && rise < height)
{
float prog = rise / height;
float seed = p.x * 0.37 + float(i) * 11.0;
// Slow lateral wander that grows with altitude.
float sway = (texture2D(uIeeNoiseMap,
vec2(seed / 64.0 + fxT * 0.045, prog * 1.7)).a - 0.5)
* (8.0 + 34.0 * prog);
float widthPx = max(pb.x, 6.0) + 26.0 * prog;
float lateral = (offs.x - sway) / widthPx;
float across = exp(-lateral * lateral * 1.9);
// Two scrolling octaves shape the billows.
float dA = texture2D(uIeeNoiseMap,
vec2((offs.x + seed) / 96.0, (offs.y + fxT * 42.0) / 96.0)).g;
float dB = texture2D(uIeeNoiseMap,
vec2((offs.x - seed) / 48.0, (offs.y + fxT * 66.0) / 48.0)).r;
float density = clamp(dA * 0.80 + dB * 0.55 - 0.38, 0.0, 1.0);
float along = smoothstep(-10.0, 12.0, rise) * (1.0 - prog);
fxHaze += across * along * density * 0.62;
}
}
else if (kind > 3.5 && kind < 4.5) // light: steady soft glow
{
float radius = max(p.w, 10.0);
float d = length(offs);
if (d < radius)
{
float fall = 1.0 - d / radius;
fall *= fall;
float breathe = 0.92 + 0.08 * sin(fxT * 2.1 + float(i) * 3.3);
fxGlow += vec3(1.0, 0.74, 0.40) * (fall * 0.40 * breathe);
}
}
}
fxHaze = clamp(fxHaze, 0.0, 0.62);
}

// No heat-shimmer UV distortion here: offsetting the atlas coordinate
// can cross a tile boundary and sample unrelated atlas content (a
// vertical smear-seam in the art). Shimmer belongs to a future
// world-space pass, not the tile pass.
vec4 texColor = seamSample(vTc);

// Inside flagged cells the engine draws the
Expand Down Expand Up @@ -368,6 +549,34 @@ void main()
texColor.a = max(texColor.a, waterMask);
}

// Firelight, candle glow, and smoke haze over the (possibly water-graded)
// background art, in linear light so the warm lift does not band.
if (fxHaze > 0.004 || fxFlameA > 0.004 || fxGlow.r + fxGlow.g + fxGlow.b > 0.004)
{
vec3 lin = ieeSrgbToLinear(texColor.rgb);
float sceneLuma = dot(lin, vec3(0.2126, 0.7152, 0.0722));
// True black means void/unexplored: no art exists there, so cast
// light must not paint it (the additive term is luma-gated; the
// multiplicative term is naturally zero on black).
float hasArt = smoothstep(0.0015, 0.012, sceneLuma);
// Night-adaptive cast light: dark scenes get real light, bright day
// scenes only a whisper.
float castLight = (0.04 + 0.30 * (1.0 - clamp(sceneLuma * 4.0, 0.0, 1.0))) * hasArt;
lin = lin * (vec3(1.0) + fxGlow * 1.2) + fxGlow * castLight;
// Smoke veil: moonlit smoke over dark roofs, shadowy over bright
// ground. Not art-gated — a per-texel luma gate mottles dark stone,
// and smoke drifting over the night sky reads naturally.
vec3 hazeTarget = vec3(0.40, 0.40, 0.45) * (0.40 + 0.55 * sceneLuma) + vec3(0.02);
lin = mix(lin, hazeTarget, fxHaze);
// Flame body composites over everything with its own self-glow;
// flames are visible against the void (a fire at a cave mouth), so it
// is deliberately not art-gated.
lin = mix(lin, fxFlame, fxFlameA);
lin += fxFlame * fxFlameA * 0.35;
texColor.rgb = ieeLinearToSrgb(lin);
texColor.a = max(texColor.a, fxFlameA);
}

texColor = texColor * vColor;

float grey = dot(texColor.rgb, vec3(0.299, 0.587, 0.114));
Expand Down
18 changes: 18 additions & 0 deletions cmake/toolchains/mingw-w64.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Cross-compile the Windows DLL target from Linux with mingw-w64.
# Usage:
# cmake -S . -B build-mingw -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
# -DIEE_BUILD_WINDOWS_DLL=ON -DBUILD_TESTING=OFF
# cmake --build build-mingw --target release_bundle

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)

set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
24 changes: 23 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,24 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one
- Encapsulates scale detection.
- TIS header metadata is authoritative.
- Headerless PVR tables are classified from same-page coordinate deltas, not raw atlas origins.
- UV / texture-id heuristics remain fallback only.
- No heuristic tier: unresolved metadata fails closed to standard 1x
delegation (the former UV / texture-id fallback produced false 4x
detections on vanilla areas and was removed).

`src/iee/game/are_animations.*`

- Host-safe ARE V1.0 animation-section parsing and conservative
fire/smoke/fountain/light classification of authored ambient animations.
- The classification table grows from runtime logs of unclassified resrefs,
not speculation. See [are-animation-detection.md](are-animation-detection.md).

`src/iee/game/object_statics.*`

- Host-safe decode of the `CGameObjectArray` globals out of the engine's
`GetShare` body (manifest pattern + RIP-operand decode) and the read-only
walk that collects the live `CGameStatic` records for an area.
- Sole source for the ARE-animation snapshot; any resolution ambiguity fails
closed and disables the scan for the session.

`src/iee/hooks.*`

Expand All @@ -72,6 +89,8 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one

- Active-area resolution (manifest-driven CInfGame offsets), view-transform reads, and
the post-LoadArea WED cache refresh plus render-thread area-texture queue.
- Also refreshes the ARE ambient-animation snapshot (`AppContext::areaAnimations`)
from the live object array at the same boundary, generation-checked.
- Refresh publication is generation-checked; an older load/render callback
cannot overwrite the newest immutable WED snapshot.
- GL objects are recreated when the current WGL context changes.
Expand Down Expand Up @@ -110,6 +129,9 @@ The supported runtime is intentionally narrow: one EEex-loaded Windows DLL, one
- Owns atomic uniform inputs, location resolution, and render-thread
uniform/texture binding. A state revision avoids repeating unchanged GL
queries, sampler writes, and enhancer texture binds.
- Also carries the area's classified ambient-animation point set
(`uIeePointCount`/`uIeePoints[32]`) with its own revision, feeding the
fpSEAM fire/smoke/light point effects.

`src/iee/shader_diagnostics.*`

Expand Down
Loading
Loading