feat(api): map persistence layer, fallback reader and revision tracking (#3) - #71
feat(api): map persistence layer, fallback reader and revision tracking (#3)#71angelTomo9 wants to merge 20 commits into
Conversation
…isconnection on mobile background/lock (Bitcoindefi#17)
…pect saveData, and sharpen HUD resolution (Bitcoindefi#20)
…r with round-trip tests (Bitcoindefi#23)
…wser with search, filters and recents (Bitcoindefi#29)
…deration lifecycle (Bitcoindefi#25)
…scape guard and PWA manifest (Bitcoindefi#21)
…ql and document compose usage (Bitcoindefi#22)
…al pairing and topology audit (Bitcoindefi#10)
… states and multi-tile structures (Bitcoindefi#9)
…and collision guards (Bitcoindefi#8)
… tools and collision mutation (Bitcoindefi#7)
…lassification for password recovery (Bitcoindefi#1)
…999 and palette extension (Bitcoindefi#6)
…sions grid checks and SHA-256 deduplication (Bitcoindefi#5)
…nce reader and game_data_revisions engine (Bitcoindefi#3)
|
Este PR tiene el mismo error de tipos que los otros 13 que traen |
| for (let x = 0; x < width; x++) { | ||
| tiles[x] = []; | ||
| for (let y = 0; y < height; y++) { | ||
| if (offset + 10 <= mapBuffer.length) { | ||
| const flags = mapBuffer.readUInt8(offset); | ||
| const blocked = (flags & 0x01) !== 0; | ||
| offset += 1; | ||
|
|
||
| const layer1 = mapBuffer.readUInt16LE(offset); | ||
| offset += 2; | ||
|
|
||
| const layer2 = (flags & 0x02) ? mapBuffer.readUInt16LE(offset) : 0; | ||
| if (flags & 0x02) offset += 2; | ||
|
|
||
| const layer3 = (flags & 0x04) ? mapBuffer.readUInt16LE(offset) : 0; |
There was a problem hiding this comment.
⚠️ Bug: Map import drops trailing tiles, breaking roundtrip
Tiles are encoded with variable length (3–11 bytes: flags+layer1 always, other layers only when their flag bit is set), but the decode loop only processes a tile when offset + 10 <= mapBuffer.length. A minimal 3-byte tile at the end of the buffer therefore fails the guard and is silently replaced with a default tile, losing data. Since exportClassicMap trims the buffer with subarray(0, offset), the last tile(s) are always shorter than 10 bytes, so translatedTiles never reaches 10000 and the last cells never roundtrip correctly. Guard each field read against the actual bytes it needs instead of a blanket +10.
Require only the minimal 3 bytes to treat a tile as present and bounds-check each optional layer.:
if (offset + 3 <= mapBuffer.length) {
const flags = mapBuffer.readUInt8(offset); offset += 1;
const blocked = (flags & 0x01) !== 0;
const layer1 = mapBuffer.readUInt16LE(offset); offset += 2;
const readOpt = (bit: number) => {
if ((flags & bit) && offset + 2 <= mapBuffer.length) {
const v = mapBuffer.readUInt16LE(offset); offset += 2; return v;
}
return 0;
};
const layer2 = readOpt(0x02);
const layer3 = readOpt(0x04);
const layer4 = readOpt(0x08);
const trigger = readOpt(0x10);
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| buffer.writeUInt8(flags, offset); | ||
| offset += 1; | ||
|
|
||
| buffer.writeUInt16LE(tile.layer1 || 1, offset); |
There was a problem hiding this comment.
💡 Bug: exportClassicMap rewrites layer1 value 0 as 1
buffer.writeUInt16LE(tile.layer1 || 1, offset) converts any tile whose layer1 is 0 into 1 on export, silently mutating map data during a roundtrip. Use an explicit nullish check (tile.layer1 ?? 1) or write the raw value so a legitimate 0 layer is preserved.
Fix:
buffer.writeUInt16LE(tile.layer1 ?? 1, offset);
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| export function findUnreachableMaps( | ||
| allMaps: MapSpecials[], | ||
| entryMapId: number = 1 | ||
| ): { unreachableMapIds: number[]; totalMaps: number } { | ||
| const inboundCount = new Map<number, number>(); | ||
| for (const m of allMaps) { | ||
| inboundCount.set(m.id, 0); | ||
| } | ||
|
|
||
| for (const m of allMaps) { | ||
| for (const exit of Object.values(m.exits)) { | ||
| if (inboundCount.has(exit.map)) { | ||
| inboundCount.set(exit.map, (inboundCount.get(exit.map) || 0) + 1); | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Quality: findUnreachableMaps counts inbound edges, not reachability
Despite its name and the "topology audit" intent, this function only flags maps with zero inbound exits. A map whose only inbound edge originates from an otherwise-unreachable map is reported as reachable, so genuinely disconnected clusters are missed. If true reachability is required, perform a BFS/DFS from entryMapId over the exit graph and report any map not visited.
Traverse the exit graph from the entry map so only truly unreachable maps are reported.:
const graph = new Map<number, number[]>();
for (const m of allMaps) graph.set(m.id, Object.values(m.exits).map(e => e.map));
const seen = new Set<number>([entryMapId]);
const stack = [entryMapId];
while (stack.length) {
for (const next of graph.get(stack.pop()!) ?? []) {
if (!seen.has(next)) { seen.add(next); stack.push(next); }
}
}
const unreachable = allMaps.map(m => m.id).filter(id => !seen.has(id));
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| useEffect(() => { | ||
| try { | ||
| const savedFavs = localStorage.getItem("openao_editor_favorites"); | ||
| if (savedFavs) setFavorites(JSON.parse(savedFavs)); | ||
| const savedRecents = localStorage.getItem("openao_editor_recents"); | ||
| if (savedRecents) setRecentItems(JSON.parse(savedRecents)); | ||
| } catch (e) {} | ||
| }, []); |
There was a problem hiding this comment.
💡 Edge Case: localStorage favorites/recents parsed without validation
JSON.parse results from localStorage are assigned directly to favorites (expected number[]) and recentItems (expected AssetItem[]) with no shape validation. Corrupted or tampered storage (e.g. a JSON object or string) will set state to a non-array, causing later .includes/.filter/.map calls to throw and break the palette. Guard with Array.isArray before setting state.
Fix:
const savedFavs = localStorage.getItem("openao_editor_favorites");
if (savedFavs) {
const parsed = JSON.parse(savedFavs);
if (Array.isArray(parsed)) setFavorites(parsed);
}
const savedRecents = localStorage.getItem("openao_editor_recents");
if (savedRecents) {
const parsed = JSON.parse(savedRecents);
if (Array.isArray(parsed)) setRecentItems(parsed);
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
Important
Your trial ends in 5 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
🎯 Resolves #3: Map Persistence & Revision Control Engine (Etapa 0)
📋 Overview
Implements the foundational persistence layer for the in-game map editor (Modo Construcción - Etapa 0), establishing an idempotent map importer for the 294 source maps, fallback precedence (DB-first with zero-downtime filesystem fallback), and atomic revision tracking in
game_data_revisionswith cryptographic SHA-256 checksums.✨ Features Implemented
Map Persistence Engine (
api/src/services/map-persistence-layer.ts):mapas_source/disk files for unedited maps.game_data_revisionsmodel, generating SHA-256 state checksums on each mutation.Integration Test Suite (
api/src/tests/map-persistence-layer.test.ts):🧪 Acceptance Criteria Checklist
game_data_revisions