Skip to content

Commit 0bd333b

Browse files
Copilotbofh69
authored andcommitted
Fix more big face bugs with fog
1 parent d688e08 commit 0bd333b

2 files changed

Lines changed: 136 additions & 9 deletions

File tree

src/lib/mapdata.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,12 +1360,21 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
13601360
clear_space(): void {
13611361
const cell = this.requireWorkingCell();
13621362
const { px, py } = this.absoluteCoords();
1363+
const original = this.originalCell;
1364+
const originalHasData =
1365+
(original?.darkness ?? 0) !== 0 ||
1366+
(original?.labels.length ?? 0) > 0 ||
1367+
(original?.heads.some((head) => head.face !== 0) ?? false) ||
1368+
(original?.tails.some((tail) => tail.face !== 0) ?? false) ||
1369+
(original?.smooth.some((value) => value !== 0) ?? false);
13631370
notifyWatchedCell(px, py, "space cleared (transitioning to fog)");
13641371
if (mapdata_contains(px, py)) {
13651372
const idxL = ci(px, py) * L;
13661373
layerUpdatedAfterClear.fill(0, idxL, idxL + L);
13671374
}
1368-
if (this.originalState === MapCellState.Empty) {
1375+
// Some clear_space updates arrive with state=Empty while legacy tail/head
1376+
// data is still populated; only hard-clear when the cell is truly blank.
1377+
if (this.originalState === MapCellState.Empty && !originalHasData) {
13691378
for (let l = 0; l < L; l++) {
13701379
this.clearHeadLayer(cell.heads[l]!);
13711380
this.clearTailLayer(cell.tails[l]!);
@@ -1376,7 +1385,6 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
13761385
// Preserve remembered content when transitioning from Visible→Fog.
13771386
// In particular, keep big-face heads so objects remain visible in fog
13781387
// until a later explicit layer update replaces or clears them.
1379-
const original = this.originalCell;
13801388
if (original) {
13811389
for (let l = 0; l < L; l++) {
13821390
const prevHead = original.heads[l]!;
@@ -1401,7 +1409,10 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
14011409
) {
14021410
cell.needUpdate = true;
14031411
}
1404-
cell.state = MapCellState.Fog;
1412+
cell.state =
1413+
this.originalState === MapCellState.Empty
1414+
? MapCellState.Empty
1415+
: MapCellState.Fog;
14051416
this.clearSpaceCalled = true;
14061417
}
14071418

@@ -1599,6 +1610,25 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
15991610
const idx = ci(px, py);
16001611
const idxL = idx * L;
16011612
const original = this.originalCell;
1613+
if (
1614+
this.clearSpaceCalled &&
1615+
original &&
1616+
this.originalState !== MapCellState.Empty
1617+
) {
1618+
// A clear_space can arrive without per-layer updates; keep remembered
1619+
// tail coverage on untouched layers so fog transitions don't drop it.
1620+
for (let l = 0; l < L; l++) {
1621+
if (this.dirtyLayers[l]) {
1622+
continue;
1623+
}
1624+
if (
1625+
this.workingCell.tails[l]!.face === 0 &&
1626+
original.tails[l]!.face !== 0
1627+
) {
1628+
this.workingCell.tails[l] = { ...original.tails[l]! };
1629+
}
1630+
}
1631+
}
16021632
const originalHasBigFaceHead =
16031633
original?.heads.some(
16041634
(head) => head.face !== 0 && (head.sizeX > 1 || head.sizeY > 1),
@@ -1616,6 +1646,34 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
16161646
if (this.dirtyLayers[l] || layerUpdatedAfterClear[idxL + l]) {
16171647
continue;
16181648
}
1649+
const staleTail = original?.tails[l];
1650+
if (
1651+
staleTail &&
1652+
staleTail.face !== 0 &&
1653+
(staleTail.sizeX > 0 || staleTail.sizeY > 0)
1654+
) {
1655+
const staleHeadX = px + staleTail.sizeX;
1656+
const staleHeadY = py + staleTail.sizeY;
1657+
let danglingTail = !mapdata_contains(staleHeadX, staleHeadY);
1658+
if (!danglingTail) {
1659+
const staleHeadIdxL = ci(staleHeadX, staleHeadY) * L + l;
1660+
const staleHeadFace = headFace[staleHeadIdxL]!;
1661+
const staleHeadW = headSizeX[staleHeadIdxL]!;
1662+
const staleHeadH = headSizeY[staleHeadIdxL]!;
1663+
danglingTail =
1664+
staleHeadFace === 0 ||
1665+
staleHeadFace !== staleTail.face ||
1666+
(staleHeadW <= 1 && staleHeadH <= 1);
1667+
}
1668+
if (danglingTail && mapdata_contains(staleHeadX, staleHeadY)) {
1669+
// Clear stale fog-era big-face coverage that points to a missing
1670+
// or mismatched head so it no longer lingers in fog cells.
1671+
expandClearFaceFromLayer(staleHeadX, staleHeadY, l);
1672+
}
1673+
if (danglingTail) {
1674+
this.clearTailLayer(this.workingCell.tails[l]!);
1675+
}
1676+
}
16191677
this.clearHeadLayer(this.workingCell.heads[l]!);
16201678
}
16211679
}
@@ -1745,10 +1803,12 @@ class ReusableMapdataCellUpdate implements MapdataCellUpdate {
17451803
}
17461804
}
17471805

1748-
// Clear tails of the previous big face from neighboring cells.
1749-
// For Fog cells mapdata_clear_old already did this; for Visible cells
1750-
// we must do it here so stale neighbor tails don't linger.
1751-
expandClearFaceFromLayer(px, py, l);
1806+
// Clear tails of the previous big face from neighboring cells only
1807+
// when this update leaves the cell visible. For Visible->Fog
1808+
// transitions, keep remembered tail coverage in fog.
1809+
if (this.workingCell.state !== MapCellState.Fog) {
1810+
expandClearFaceFromLayer(px, py, l);
1811+
}
17521812

17531813
headFace[lOff] = head.face;
17541814
headSizeX[lOff] = head.sizeX;

tests/replay-mapdata/replay-mapdata.spec.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ let mapdata_cell: (
2929
mx: number,
3030
my: number,
3131
) => {
32-
heads: Array<{ face: number }>;
32+
heads: Array<{ face: number; sizeX: number; sizeY: number }>;
33+
tails: Array<{ face: number; sizeX: number; sizeY: number }>;
3334
state: number;
3435
};
36+
let mapdata_contains: (x: number, y: number) => boolean;
3537
let pl_mpos: () => { px: number; py: number };
3638
let getFaceTileSize: (face: number) => { w: number; h: number };
3739
let parseReplayLogFile: (text: string) => {
@@ -117,6 +119,32 @@ function parseRawRxLine(line: string): Uint8Array | null {
117119
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
118120
}
119121

122+
function assertNoDanglingTailsInView(): void {
123+
const player = pl_mpos();
124+
for (let dx = -12; dx <= 12; dx++) {
125+
for (let dy = -12; dy <= 12; dy++) {
126+
const ax = player.px + dx;
127+
const ay = player.py + dy;
128+
if (!mapdata_contains(ax, ay)) {
129+
continue;
130+
}
131+
const cell = mapdata_cell(ax, ay);
132+
for (let layer = 0; layer < cell.tails.length; layer++) {
133+
const tail = cell.tails[layer]!;
134+
if (tail.face === 0) {
135+
continue;
136+
}
137+
const headX = ax + tail.sizeX;
138+
const headY = ay + tail.sizeY;
139+
expect(mapdata_contains(headX, headY)).toBe(true);
140+
const head = mapdata_cell(headX, headY).heads[layer]!;
141+
expect(head.face).toBe(tail.face);
142+
expect(head.sizeX > 1 || head.sizeY > 1).toBe(true);
143+
}
144+
}
145+
}
146+
}
147+
120148
beforeAll(() => {
121149
installNodeTestGlobals();
122150
// Avoid browser-only globals failing during module evaluation.
@@ -136,7 +164,7 @@ beforeAll(async () => {
136164
({ dispatchPacket } = await import("../../src/lib/commands"));
137165
({ clientInit } = await import("../../src/lib/init"));
138166
({ getFaceTileSize } = await import("../../src/lib/image"));
139-
({ setGetMapImageSize, mapdata_cell, pl_mpos } =
167+
({ setGetMapImageSize, mapdata_cell, mapdata_contains, pl_mpos } =
140168
await import("../../src/lib/mapdata"));
141169
({ initCommands } = await import("../../src/lib/p_cmd"));
142170
({ parseReplayLogFile, resetReplaySandboxState, toPacketBuffer } =
@@ -207,4 +235,43 @@ describe("replay mapdata states", () => {
207235
expect(cell.heads[0]!.face).toBe(1005);
208236
expect(cell.heads[6]!.face).toBe(0);
209237
});
238+
239+
test("big-face head cell stays empty when only fog bookkeeping changes", () => {
240+
const logText = readFileSync(
241+
path.join(
242+
rootDir,
243+
"tests/replay-mapdata/logs/around-scorn-with-ui-log.log",
244+
),
245+
"utf8",
246+
);
247+
248+
resetReplaySandboxState();
249+
replayToMark(logText, "before arena disapears #1");
250+
251+
const headX = 280;
252+
const headY = 272;
253+
let cell = mapdata_cell(headX, headY);
254+
expect(cell.state).toBe(0);
255+
expect(cell.heads[0]!.face).toBe(0);
256+
expect(cell.heads[1]!.face).toBe(645);
257+
258+
resetReplaySandboxState();
259+
replayToMark(logText, "after arena disapears #1");
260+
261+
cell = mapdata_cell(headX, headY);
262+
expect(cell.state).toBe(0);
263+
expect(cell.heads[0]!.face).toBe(0);
264+
expect(cell.heads[1]!.face).toBe(645);
265+
});
266+
267+
test("zoo replay has no dangling in-view tail-to-head links", () => {
268+
const logText = readFileSync(
269+
path.join(rootDir, "tests/replay-mapdata/logs/zoo.log"),
270+
"utf8",
271+
);
272+
273+
resetReplaySandboxState();
274+
replayToMark(logText, "after retributor moves right #2");
275+
assertNoDanglingTailsInView();
276+
});
210277
});

0 commit comments

Comments
 (0)