From 762844abc24ff4a326c0b7f7c3e228ab88122f1b Mon Sep 17 00:00:00 2001 From: John Novak Date: Tue, 2 Apr 2024 21:19:56 +1000 Subject: [PATCH] Handle level IDs in links correctly when loading/saving --- extras/docs/fileformat.txt | 14 ++--- src/actions.nim | 18 ++---- src/common.nim | 4 ++ src/drawlevel.nim | 6 +- src/level.nim | 34 ++++++----- src/map.nim | 7 ++- src/persistence.nim | 112 ++++++++++++++++++++++--------------- 7 files changed, 111 insertions(+), 84 deletions(-) diff --git a/extras/docs/fileformat.txt b/extras/docs/fileformat.txt index ed06d7d4..b088b2af 100644 --- a/extras/docs/fileformat.txt +++ b/extras/docs/fileformat.txt @@ -185,11 +185,11 @@ Map links chunk numLinks records of: - UINT16 srcLevel (max=numLevels-1) + UINT16 srcLevelIndex (max=numLevels-1) UINT16 srcRow (max=numRows-1 of the given level) UINT16 srcColumn (max=numColumns-1 of the given level) - UINT16 destLevel (max=numLevel-1) + UINT16 destLevelIndex (max=numLevel-1) UINT16 destRow (max=numRows-1 of the given level) UINT16 destColumn (max=numColumns-1 of the given level) @@ -205,7 +205,7 @@ App state chunks (in 'stat' group chunk, optional) '/', '\', ':', '*', '?', '"', '<', '>', '|', '^', UINT8 zoomLevel (min=1, max=20) - UINT8 currLevel (max=numLevels-1) + UINT8 currLevelIndex (max=numLevels-1) UINT16 cursorRow (max=numRows-1 of the given level) UINT16 cursorColumn (max=numColumns-1 of the given level) UINT16 viewStartRow (max=numRows-1 of the given level) @@ -237,7 +237,7 @@ App state chunks (in 'stat' group chunk, optional) numLevels records of: - UINT32 levelId + UINT32 levelIndex UINT8 levelSection (0=closed, 1=open) if numRegions > 0 for the level (enableRegions can still be 0): @@ -254,9 +254,9 @@ Version 4 [Gridmonger v1.2.0 - 2024-04-22] - Remove redundant region coordinates from the 'regn' chunk. - - Convert 'disp' chunk into a group chunk with multiple subchunks and: - - Add 'optWraparound' to the 'opts' subchunk. - - Add 'notl' subchunk. + - Convert 'disp' chunk into a group chunk with multiple subchunks and + - add 'optWraparound' to the 'opts' subchunk. + - add 'notl' subchunk. Version 3 diff --git a/src/actions.nim b/src/actions.nim index 9fc8808c..ca336bb1 100644 --- a/src/actions.nim +++ b/src/actions.nim @@ -771,15 +771,13 @@ proc resizeLevel*(map; loc: Location, newRows, newCols: Natural, levelId = loc.levelId l = m.levels[levelId] + # Propagate ID as this is the same level, just resized var newLevel = newLevel(l.locationName, l.levelName, l.elevation, newRows, newCols, l.overrideCoordOpts, l.coordOpts, l.regionOpts, l.notes, - initRegions=false) - - # Propagate ID as this is the same level, just resized - newLevel.id = levelId + initRegions=false, overrideId=levelId.some) newLevel.copyCellsAndAnnotationsFrom(destRow, destCol, l, copyRect) @@ -846,12 +844,10 @@ proc cropLevel*(map; loc: Location, cropRect: Rect[Natural]; um): Location = newLevelRect, wraparound=false) let action = proc (m: var Map): UndoStateData = - let levelId = loc.levelId - var newLevel = m.newLevelFrom(levelId, cropRect) + let levelId = loc.levelId # Propagate ID as this is the same level, just cropped - newLevel.id = levelId - + let newLevel = m.newLevelFrom(levelId, cropRect, overrideId=levelId.some) m.setLevel(newLevel) # Adjust links @@ -907,18 +903,16 @@ proc nudgeLevel*(map; loc: Location, rowOffs, colOffs: int, # Do action let action = proc (m: var Map): UndoStateData = + # Propagate ID as this is the same level, just nudged var l = newLevel( sb.level.locationName, sb.level.levelName, sb.level.elevation, sb.level.rows, sb.level.cols, sb.level.overrideCoordOpts, sb.level.coordOpts, sb.level.regionOpts, sb.level.notes, - initRegions=false + initRegions=false, overrideId=levelId.some ) - # Propagate ID as this is the same level, just nudged - l.id = levelId - if wraparound: discard l.pasteWithWraparound(destRow=rowOffs, destCol=colOffs, srcLevel=sb.level, sb.selection, diff --git a/src/common.nim b/src/common.nim index 445e6b81..9cd53f72 100644 --- a/src/common.nim +++ b/src/common.nim @@ -107,7 +107,9 @@ type csLetter = 1 Level* = ref object + # Internal ID, never written to disk id*: Natural + locationName*: string levelName*: string elevation*: int @@ -139,7 +141,9 @@ type row*, col*: Natural Region* = object + # Internal ID, never written to disk id*: Natural + name*: string notes*: string diff --git a/src/drawlevel.nim b/src/drawlevel.nim index dd3f127d..69a7c74e 100644 --- a/src/drawlevel.nim +++ b/src/drawlevel.nim @@ -2751,13 +2751,15 @@ proc drawLevel*(map: Map, levelId: Natural; ctx) = dp.viewStartRow + dp.viewRows, dp.viewStartCol + dp.viewCols ), - border = ViewBufBorder + border = ViewBufBorder, + overrideId = 0.Natural.some ) else: newLevel( locationName = "", levelName = "", elevation = 0, rows = dp.viewRows + ViewBufBorder*2, - cols = dp.viewCols + ViewBufBorder*2 + cols = dp.viewCols + ViewBufBorder*2, + overrideId = 0.Natural.some ) assert dp.viewStartRow + dp.viewRows <= l.rows diff --git a/src/level.nim b/src/level.nim index 8bb67093..48510c9d 100644 --- a/src/level.nim +++ b/src/level.nim @@ -12,9 +12,7 @@ import selection using l: Level -const - # internal ID, never written to disk - MoveBufferLevelId* = Natural.high +const MoveBufferLevelId* = Natural.high # {{{ CellGrid @@ -309,9 +307,9 @@ proc eraseOrphanedWalls*(l; r,c: Natural) = # {{{ eraseCellWalls*() proc eraseCellWalls*(l; r,c: Natural) = l.setWall(r,c, dirN, wNone) - l.setWall(r,c, dirW, wNone) + l.setWall(r,c, dirW, wNone) l.setWall(r,c, dirS, wNone) - l.setWall(r,c, dirE, wNone) + l.setWall(r,c, dirE, wNone) # }}} # {{{ eraseCell*() @@ -527,20 +525,24 @@ const DefaultRegionOpts = RegionOptions( proc newLevel*(locationName, levelName: string, elevation: int, rows, cols: Natural, - overrideCoordOpts: bool = false, + overrideCoordOpts = false, coordOpts: CoordinateOptions = DefaultCoordOpts, regionOpts: RegionOptions = DefaultRegionOpts, notes: string = "", - initRegions: bool = true): Level = + initRegions = true, + overrideId: Option[Natural] = Natural.none): Level = var l = new Level - l.id = g_levelIdCounter - inc(g_levelIdCounter) + if overrideId.isSome: + l.id = overrideId.get + else: + l.id = g_levelIdCounter + inc(g_levelIdCounter) l.locationName = locationName - l.levelName = levelName - l.elevation = elevation + l.levelName = levelName + l.elevation = elevation l.overrideCoordOpts = overrideCoordOpts l.coordOpts = coordOpts @@ -565,8 +567,8 @@ proc calcNewLevelFromParams*( srcLevel: Level, srcRect: Rect[Natural], border: Natural = 0 ): tuple[copyRect: Rect[Natural], destRow, destCol: Natural] = - assert srcRect.r1 < srcLevel.rows - assert srcRect.c1 < srcLevel.cols + assert srcRect.r1 < srcLevel.rows + assert srcRect.c1 < srcLevel.cols assert srcRect.r2 <= srcLevel.rows assert srcRect.c2 <= srcLevel.cols @@ -601,7 +603,8 @@ proc calcNewLevelFromParams*( # NOTE: This method doesn't copy the regions. proc newLevelFrom*(srcLevel: Level, srcRect: Rect[Natural], - border: Natural = 0): Level = + border: Natural = 0, + overrideId: Option[Natural] = Natural.none): Level = let (copyRect, destRow, destCol) = calcNewLevelFromParams(srcLevel, srcRect, border) @@ -612,7 +615,8 @@ proc newLevelFrom*(srcLevel: Level, srcRect: Rect[Natural], cols = srcRect.cols + border*2, srcLevel.overrideCoordOpts, srcLevel.coordOpts, srcLevel.regionOpts, srcLevel.notes, - initRegions=false + initRegions = false, + overrideId = overrideId ) result.copyCellsAndAnnotationsFrom(destRow, destCol, srcLevel, copyRect) diff --git a/src/map.nim b/src/map.nim index 6f741566..96ecf3a6 100644 --- a/src/map.nim +++ b/src/map.nim @@ -397,12 +397,13 @@ proc calcRegionResizeOffsets*( # }}} -# {{{ newLevelFrom*() -proc newLevelFrom*(m; srcLevelId: Natural, srcRect: Rect[Natural]): Level = +proc newLevelFrom*(m; srcLevelId: Natural, srcRect: Rect[Natural], + overrideId: Option[Natural] = Natural.none): Level = + let src = m.levels[srcLevelId] alias(ro, src.regionOpts) - var dest = newLevelFrom(src, srcRect) + var dest = newLevelFrom(src, srcRect, overrideId=overrideId) # Copy regions let (copyRect, _, _) = calcNewLevelFromParams(src, srcRect) diff --git a/src/persistence.nim b/src/persistence.nim index d5000863..93ec3ef1 100644 --- a/src/persistence.nim +++ b/src/persistence.nim @@ -251,7 +251,7 @@ proc readLocation(rr): Location = # }}} # {{{ readAppState_v1_v2() -proc readAppState_v1_v2(rr; m: Map): AppState = +proc readAppState_v1_v2(rr; map: Map): AppState = debug(fmt"Reading app state...") pushDebugIndent() @@ -262,11 +262,11 @@ proc readAppState_v1_v2(rr; m: Map): AppState = checkValueRange(zoomLevel, "stat.zoomLevel", ZoomLevelLimits) # Cursor position - let maxLevelIndex = NumLevelsLimits.maxInt - 1 - let currLevelId = rr.read(uint16).int - checkValueRange(currLevelId, "stat.currLevelId", max=maxLevelIndex) + let maxLevelIndex = NumLevelsLimits.maxInt-1 + var currLevelIndex = rr.read(uint16).int + checkValueRange(currLevelIndex, "stat.currLevelIndex", max=maxLevelIndex) - let l = m.levels[currLevelId] + let l = map.levels[currLevelIndex] let cursorRow = rr.read(uint16) checkValueRange(cursorRow, "stat.cursorRow", max=l.rows.uint16-1) @@ -307,7 +307,7 @@ proc readAppState_v1_v2(rr; m: Map): AppState = themeName: themeName, zoomLevel: zoomLevel, - currLevelId: currLevelId, + currLevelId: currLevelIndex, cursorRow: cursorRow, cursorCol: cursorCol, viewStartRow: viewStartRow, @@ -334,7 +334,7 @@ proc readLinks_v1_v2(rr; levels: OrderedTable[Natural, Level]): Links = var numLinks = rr.read(uint16).int checkValueRange(numLinks, "links.numLinks", NumLinksLimits) - let maxLevelIndex = NumLevelsLimits.maxInt - 1 + let maxLevelIndex = NumLevelsLimits.maxInt-1 while numLinks > 0: pushDebugIndent() @@ -358,7 +358,7 @@ proc readLinks_v1_v2(rr; levels: OrderedTable[Natural, Level]): Links = # }}} # {{{ readLevelProperties_v1_v2() -proc readLevelProperties_v1_v2(rr): Level = +proc readLevelProperties_v1_v2(rr; levelId: Natural): Level = debug(fmt"Reading level properties...") pushDebugIndent() @@ -384,7 +384,8 @@ proc readLevelProperties_v1_v2(rr): Level = let notes = rr.readWStr checkStringLength(notes, "lvl.prop.notes", NotesLimits) - result = newLevel(locationName, levelName, elevation, numRows, numColumns) + result = newLevel(locationName, levelName, elevation, numRows, numColumns, + overrideId = levelId.some) result.overrideCoordOpts = overrideCoordOpts.bool result.notes = notes @@ -644,7 +645,7 @@ proc readLevelRegions_v1_v2(rr): tuple[regionOpts: RegionOptions, # }}} # {{{ readLevel_v1_v2() -proc readLevel_v1_v2(rr): Level = +proc readLevel_v1_v2(rr; levelId: Natural): Level = debug(fmt"Reading level...") pushDebugIndent() @@ -705,7 +706,7 @@ proc readLevel_v1_v2(rr): Level = if cellCursor.isNone: chunkNotFoundError(FourCC_GRMM_cell) rr.cursor = propCursor.get - var level = readLevelProperties_v1_v2(rr) + var level = readLevelProperties_v1_v2(rr, levelId) rr.cursor = coorCursor.get level.coordOpts = readCoordinateOptions_v1_v2(rr, groupChunkId.get) @@ -735,6 +736,7 @@ proc readLevelList_v1_v2(rr): OrderedTable[Natural, Level] = pushDebugIndent() var levels = initOrderedTable[Natural, Level]() + var levelId = 0 if rr.hasSubChunks: var ci = rr.enterGroup @@ -748,8 +750,12 @@ proc readLevelList_v1_v2(rr): OrderedTable[Natural, Level] = fmt"Map cannot contain more than {NumLevelsLimits.maxInt} levels" ) - let level = readLevel_v1_v2(rr) - levels[level.id] = level + # The level IDs must be set to their indices in the map file to + # ensure they are in sync with link (see writeLinks()). + let level = readLevel_v1_v2(rr, levelId) + levels[levelId] = level + inc(levelId) + rr.exitGroup else: invalidListChunkError(ci.formatTypeId, FourCC_GRMM_lvls) @@ -926,21 +932,21 @@ proc readMapFile*(path: string): tuple[map: Map, # Load chunks rr.cursor = mapCursor.get - let m = readMap_v1_v2(rr) + let map = readMap_v1_v2(rr) rr.cursor = levelListCursor.get - m.levels = readLevelList_v1_v2(rr) - m.sortLevels + map.levels = readLevelList_v1_v2(rr) + map.sortLevels rr.cursor = linksCursor.get - m.links = readLinks_v1_v2(rr, m.levels) + map.links = readLinks_v1_v2(rr, map.levels) if appStateCursor.isSome: rr.cursor = appStateCursor.get - let appState = readAppState_v1_v2(rr, m) - result = (m, appState.some) + let appState = readAppState_v1_v2(rr, map) + result = (map, appState.some) else: - result = (m, AppState.none) + result = (map, AppState.none) except MapReadError as e: raise e @@ -958,14 +964,17 @@ using rw: RiffWriter var g_runLengthEncoder: RunLengthEncoder # {{{ writeAppState() -proc writeAppState(rw; s: AppState) = +proc writeAppState(rw; map: Map, s: AppState) = rw.beginChunk(FourCC_GRMM_stat) rw.writeBStr(s.themeName) + let currLevelIndex = map.sortedLevelIds.find(s.currLevelId) + assert(currLevelIndex > -1) + # Cursor position rw.write(s.zoomLevel.uint8) - rw.write(s.currLevelId.uint16) + rw.write(currLevelIndex.uint16) rw.write(s.cursorRow.uint16) rw.write(s.cursorCol.uint16) rw.write(s.viewStartRow.uint16) @@ -986,22 +995,33 @@ proc writeAppState(rw; s: AppState) = # }}} # {{{ writeLinks() -proc writeLinks(rw; links: Links) = +proc writeLinks(rw; map: Map) = rw.beginChunk(FourCC_GRMM_lnks) - rw.write(links.len.uint16) + rw.write(map.links.len.uint16) + + # We map level IDs to their indices in sortedLevelIds when writing the + # links. Because we write the levels in the order they appear in + # sortedLevelIds, the indices will point to the correct levels. + # + # When loading the levels back, we assign the the first level ID 0, the + # second ID 1, etc. (their indices in the map file) to ensure the level IDs + # are in sync with the links. + + let levelIdToIndex = collect: + for idx, id in map.sortedLevelIds: {id: idx} - var sortedKeys = collect(newSeqOfCap(links.len)): - for k in links.sources: k + var sortedKeys = collect: + for k in map.links.sources: k sort(sortedKeys) proc writeLocation(loc: Location) = - rw.write(loc.levelId.uint16) + rw.write(levelIdToIndex[loc.levelId].uint16) rw.write(loc.row.uint16) rw.write(loc.col.uint16) for src in sortedKeys: - let dest = links.getBySrc(src).get + let dest = map.links.getBySrc(src).get writeLocation(src) writeLocation(dest) @@ -1152,51 +1172,53 @@ proc writeLevel(rw; l: Level) = # }}} # {{{ writeLevelList() -proc writeLevelList(rw; levels: OrderedTable[Natural, Level]) = +proc writeLevelList(rw; map: Map) = rw.beginListChunk(FourCC_GRMM_lvls) - for l in levels.values: - writeLevel(rw, l) + # We must write the levels in sortedLevelIds order to ensure the links + # are in sync with the level IDs (see writeLinks()). + for levelId in map.sortedLevelIds: + writeLevel(rw, map.levels[levelId]) rw.endChunk # }}} # {{{ writeMapProperties() -proc writeMapProperties(rw; m: Map) = +proc writeMapProperties(rw; map: Map) = rw.beginChunk(FourCC_GRMM_prop) rw.write(CurrentMapVersion.uint16) - rw.writeWStr(m.title) - rw.writeWStr(m.game) - rw.writeWStr(m.author) - rw.writeBStr(m.creationTime) - rw.writeWStr(m.notes) + rw.writeWStr(map.title) + rw.writeWStr(map.game) + rw.writeWStr(map.author) + rw.writeBStr(map.creationTime) + rw.writeWStr(map.notes) rw.endChunk # }}} # {{{ writeMap() -proc writeMap(rw; m: Map) = +proc writeMap(rw; map: Map) = rw.beginListChunk(FourCC_GRMM_map) - writeMapProperties(rw, m) - writeCoordinateOptions(rw, m.coordOpts) + writeMapProperties(rw, map) + writeCoordinateOptions(rw, map.coordOpts) rw.endChunk # }}} # {{{ writeMapFile*() -proc writeMapFile*(m: Map, appState: AppState, path: string) = +proc writeMapFile*(map: Map, appState: AppState, path: string) = initDebugIndent() var rw: RiffWriter try: rw = createRiffFile(path, FourCC_GRMM) - writeMap(rw, m) - writeLevelList(rw, m.levels) - writeLinks(rw, m.links) - writeAppState(rw, appState) + writeMap(rw, map) + writeLevelList(rw, map) + writeLinks(rw, map) + writeAppState(rw, map, appState) except MapReadError as e: raise e