From 59a8833a432b0dcbba7065ae981c5422dbb5c479 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 29 Jan 2026 12:13:30 -0600 Subject: [PATCH 1/5] Can edit minimap tiles that are out of bounds, but they revert to deep sea when touched in-game --- .../PlanScriptDialog.xaml.cs | 125 +++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index c4c08a1..dd12ffe 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -244,7 +245,8 @@ private void UpdatePlan() TryPlanSimpleCopy(sourceSlot, "AUTOCMNDAT.BIN", mode == InclusionMode.Automatic); TryPlanSimpleCopy(sourceSlot, "AUTOSTGDAT.BIN", mode == InclusionMode.Automatic); - TryPlanSimpleCopy(sourceSlot, "CMNDAT.BIN", mode == InclusionMode.Automatic || mode == InclusionMode.JustCmndat, forceBackup: true); + //TryPlanSimpleCopy(sourceSlot, "CMNDAT.BIN", mode == InclusionMode.Automatic || mode == InclusionMode.JustCmndat, forceBackup: true); + TryPlanHackyCmndatEdit(sourceSlot); var sortedStages = sourceSlot.Stages .OrderBy(stage => stage == Project.GetSelectedSourceStage ? 0 : 1) @@ -314,6 +316,20 @@ private bool TryPlanSimpleCopy(SlotVM sourceSlot, string name, bool shouldCopy, return false; } + private bool TryPlanHackyCmndatEdit(SlotVM sourceSlot) + { + var sourceFile = new FileInfo(sourceSlot.GetFullPath("CMNDAT.BIN")); + if (sourceFile.Exists) + { + PlanItems.Add(new CmndatHackPlan + { + BackupDir = this.backupLocation, + }); + return true; + } + return false; + } + public async Task Execute() { if (!CanExecute) return; @@ -629,6 +645,113 @@ await Task.Run(() => } } + class CmndatHackPlan : PlanItemVM, IPlanItemVM + { + public bool WillBeBackedUp => BackupDir != null; + + public bool WillBeModified => true; + + public bool WillBeCopied => true; + + public string ShortName => "CMNDAT.BIN"; + + const int headerLength = 0x2A444; + + public async Task Execute(ExecutionContext context) + { + Status = "workinonit..."; + + var srcPath = context.FromSlot.GetFullPath("CMNDAT.BIN"); + + using var cmndatStream = new FileStream(srcPath, FileMode.Open, FileAccess.Read); + + var header = new byte[headerLength]; + await cmndatStream.ReadExactlyAsync(header, offset: 0, count: headerLength); + + using var zlib = new System.IO.Compression.ZLibStream(cmndatStream, System.IO.Compression.CompressionMode.Decompress); + + const int decompressedLength = 5627194; // hypothesis: The decompressed buffer will always have this length + using var decompressedStream = new MemoryStream(decompressedLength); + await zlib.CopyToAsync(decompressedStream); + await zlib.FlushAsync(); + await decompressedStream.FlushAsync(); + + if (decompressedStream.Length != decompressedLength && System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); // decompressedLength hypothesis invalidated? + } + + Memory data; + if (decompressedStream.TryGetBuffer(out var buffer)) + { + data = buffer.AsMemory(); + } + else + { + data = new Memory(decompressedStream.ToArray()); + } + + var mapData = data.Slice(2401803); // skip to start of first island's minimap data + + const int INTRO_SIZE = 0; + const int TILE_DATA_SIZE = 256 * 256 * 2; + const int OUTRO_SIZE = 4; + const int ISLAND_DATA_SIZE = INTRO_SIZE + TILE_DATA_SIZE + OUTRO_SIZE; + + const int islandId = 13; + const int start = INTRO_SIZE + ISLAND_DATA_SIZE * islandId; + var span = mapData.Slice(start, TILE_DATA_SIZE).Span; + + var tile = GetTileToCopy(span); + for (int z = 93; z < 95; z++) // 92 thru 96 is the actual range, but in case I'm off by 1... + { + for (int x = 93; x < 95; x++) + { + int offset = GetOffset(x, z); + span[offset] = tile.Item1; + span[offset + 1] = tile.Item2; + } + } + + Status = "Logic done, saving..."; + + await WriteCmndatFile(header, data, context.ToSlot.GetFullPath("CMNDAT.BIN")); + + Status = "Minimap modified!"; + } + + private (byte, byte) GetTileToCopy(ReadOnlySpan span) + { + // grab whatever tile is in the center of the minimap + const int x = 1024 / 8; + const int z = 1024 / 8; + int offset = GetOffset(x, z); + return (span[offset], span[offset + 1]); + } + + private static int GetOffset(int x, int z) => z * 256 * 2 + x * 2; + + private static async Task WriteCmndatFile(byte[] origHeader, Memory uncompressedContent, string dstPath) + { + using var cmndatStream = new FileStream(dstPath, FileMode.Create, FileAccess.Write); + await cmndatStream.WriteAsync(origHeader); + + using var compressedBody = new MemoryStream(); + using (var zlib = new ZLibStream(compressedBody, CompressionMode.Compress, leaveOpen: true)) + { + await zlib.WriteAsync(uncompressedContent); + await zlib.FlushAsync(); + compressedBody.Flush(); + } + + compressedBody.Seek(0, SeekOrigin.Begin); + await compressedBody.CopyToAsync(cmndatStream); + + await cmndatStream.FlushAsync(); + cmndatStream.Close(); + } + } + class CopyWithModificationsPlanItemVM : PlanItemVM, IPlanItemVM { public required bool DestIsSource { get; init; } From 4dd0d648752d6be7d841cf270b565d5089a35471 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 29 Jan 2026 12:23:54 -0600 Subject: [PATCH 2/5] use actual bounds of the expanded chunk --- src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index dd12ffe..7e3c698 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -703,9 +703,9 @@ public async Task Execute(ExecutionContext context) var span = mapData.Slice(start, TILE_DATA_SIZE).Span; var tile = GetTileToCopy(span); - for (int z = 93; z < 95; z++) // 92 thru 96 is the actual range, but in case I'm off by 1... + for (int z = 92; z < 96; z++) { - for (int x = 93; x < 95; x++) + for (int x = 92; x < 96; x++) { int offset = GetOffset(x, z); span[offset] = tile.Item1; From 7ff1c09e92b194a7dd0d46a603810bb299628dfe Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 29 Jan 2026 14:48:57 -0600 Subject: [PATCH 3/5] attempted swapping outro to match IoA outro --- src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs | 16 +++++++++++++--- src/Blocktavius.DQB2/Minimap.cs | 9 ++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index 7e3c698..133dc2c 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -700,12 +700,14 @@ public async Task Execute(ExecutionContext context) const int islandId = 13; const int start = INTRO_SIZE + ISLAND_DATA_SIZE * islandId; - var span = mapData.Slice(start, TILE_DATA_SIZE).Span; + var span = mapData.Slice(start, TILE_DATA_SIZE + OUTRO_SIZE).Span; var tile = GetTileToCopy(span); - for (int z = 92; z < 96; z++) + // The chunk with NW corner at 800,800 is in-bounds on the IoA. + // Tile offsets for this chunk are 100,100 -> 104,104. + for (int z = 100; z < 104; z++) { - for (int x = 92; x < 96; x++) + for (int x = 100; x < 104; x++) { int offset = GetOffset(x, z); span[offset] = tile.Item1; @@ -713,6 +715,14 @@ public async Task Execute(ExecutionContext context) } } + // What happens if we use "outro" bytes from the IoA? + var existingOutro = Convert.ToHexString(span.Slice(TILE_DATA_SIZE, OUTRO_SIZE)); + span[TILE_DATA_SIZE + 0] = 0x7E; + span[TILE_DATA_SIZE + 1] = 0x80; + span[TILE_DATA_SIZE + 2] = 0x01; + span[TILE_DATA_SIZE + 3] = 0x1E; + var replacedOutro = Convert.ToHexString(span.Slice(TILE_DATA_SIZE, OUTRO_SIZE)); + Status = "Logic done, saving..."; await WriteCmndatFile(header, data, context.ToSlot.GetFullPath("CMNDAT.BIN")); diff --git a/src/Blocktavius.DQB2/Minimap.cs b/src/Blocktavius.DQB2/Minimap.cs index f64e54a..3cc99e6 100644 --- a/src/Blocktavius.DQB2/Minimap.cs +++ b/src/Blocktavius.DQB2/Minimap.cs @@ -49,7 +49,7 @@ private Minimap(ReadOnlyMemory data) // It didn't really matter in the end, since no island would have tiles over there // // So I'll skip the top row also; better safe than sorry. - const bool skipTopRow = true; + const bool skipTopRow = false; const int INTRO_SIZE = 0; const int TILE_DATA_SIZE = 256 * 256 * 2; const int OUTRO_SIZE = 4; @@ -66,6 +66,13 @@ public I2DSampler ReadMapCropped(int islandId, IStage cropper, IRea { var sampler = ReadMap(islandId); + // TEMP OUTRO DEBUGGING CODE: + var start = INTRO_SIZE + ISLAND_DATA_SIZE * islandId; + var end = start + TILE_DATA_SIZE; + var outroBytes = data.Slice(end, OUTRO_SIZE).ToArray(); + var outroHex = Convert.ToHexString(outroBytes); + var debugString = $"Island {islandId}, outro {outroHex}"; + // Minimap tile grid is 256x256; chunk grid is 64x64. // This means there are 4x4 map tiles in each chunk. const int scale = 4; From d0c0b35c1f92c3789a34ff1a512aaaf8c1f36649 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 29 Jan 2026 14:54:00 -0600 Subject: [PATCH 4/5] revert some temp code --- src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs | 2 ++ src/Blocktavius.DQB2/Minimap.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs index 133dc2c..9cfb30a 100644 --- a/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs +++ b/src/Blocktavius.AppDQB2/PlanScriptDialog.xaml.cs @@ -715,6 +715,7 @@ public async Task Execute(ExecutionContext context) } } + /* // What happens if we use "outro" bytes from the IoA? var existingOutro = Convert.ToHexString(span.Slice(TILE_DATA_SIZE, OUTRO_SIZE)); span[TILE_DATA_SIZE + 0] = 0x7E; @@ -722,6 +723,7 @@ public async Task Execute(ExecutionContext context) span[TILE_DATA_SIZE + 2] = 0x01; span[TILE_DATA_SIZE + 3] = 0x1E; var replacedOutro = Convert.ToHexString(span.Slice(TILE_DATA_SIZE, OUTRO_SIZE)); + */ Status = "Logic done, saving..."; diff --git a/src/Blocktavius.DQB2/Minimap.cs b/src/Blocktavius.DQB2/Minimap.cs index 3cc99e6..34a4fa7 100644 --- a/src/Blocktavius.DQB2/Minimap.cs +++ b/src/Blocktavius.DQB2/Minimap.cs @@ -49,7 +49,7 @@ private Minimap(ReadOnlyMemory data) // It didn't really matter in the end, since no island would have tiles over there // // So I'll skip the top row also; better safe than sorry. - const bool skipTopRow = false; + const bool skipTopRow = true; const int INTRO_SIZE = 0; const int TILE_DATA_SIZE = 256 * 256 * 2; const int OUTRO_SIZE = 4; From 6bdf1783e14133e868c156fcc7da4df9943b3808 Mon Sep 17 00:00:00 2001 From: Ryan Kramer Date: Thu, 29 Jan 2026 15:52:52 -0600 Subject: [PATCH 5/5] outro looks relevant... --- src/Blocktavius.DQB2/Minimap.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Blocktavius.DQB2/Minimap.cs b/src/Blocktavius.DQB2/Minimap.cs index 34a4fa7..823ccf4 100644 --- a/src/Blocktavius.DQB2/Minimap.cs +++ b/src/Blocktavius.DQB2/Minimap.cs @@ -66,12 +66,18 @@ public I2DSampler ReadMapCropped(int islandId, IStage cropper, IRea { var sampler = ReadMap(islandId); - // TEMP OUTRO DEBUGGING CODE: + // TEMP INTRO/OUTRO DEBUGGING CODE: var start = INTRO_SIZE + ISLAND_DATA_SIZE * islandId; + string introHex = "[nope]"; + if (start > 0) + { + var introBytes = data.Slice(start - OUTRO_SIZE, OUTRO_SIZE).ToArray(); + introHex = Convert.ToHexString(introBytes); + } var end = start + TILE_DATA_SIZE; var outroBytes = data.Slice(end, OUTRO_SIZE).ToArray(); var outroHex = Convert.ToHexString(outroBytes); - var debugString = $"Island {islandId}, outro {outroHex}"; + var debugString = $"Island {islandId}, intro {introHex}, outro {outroHex}"; // Minimap tile grid is 256x256; chunk grid is 64x64. // This means there are 4x4 map tiles in each chunk.