OXYGENHUB
◆ rrdc-perf enhanced-x16-emulator / tools / x16dbg / Commands.cs
23.0 KiB · C# · last changed chore(): initial commit 25 days ago source blame history raw
1
using System;
2
using System.Collections.Generic;
3
using System.Text;
4
using System.Text.Json.Nodes;
5
using System.Threading.Tasks;
6
7
namespace X16Debug;
8
9
/// <summary>
10
/// One-shot command implementations for CLI mode.
11
/// </summary>
12
public static class Commands
13
{
14
    public static async Task<int> RunOneShotAsync(X16DebugClient client, string cmd, string[] args)
15
    {
16
        try
17
        {
18
            switch (cmd.ToLower())
19
            {
20
                case "step":
21
                case "s":
22
                    return await DoStep(client);
23
                case "continue":
24
                case "c":
25
                    return await DoContinue(client);
26
                case "break":
27
                case "b":
28
                    return await DoBreak(client);
29
                case "status":
30
                case "t":
31
                    return await DoStatus(client);
32
                case "reset":
33
                    return await DoReset(client);
34
                case "regs":
35
                case "r":
36
                    return await DoRegs(client);
37
                case "setreg":
38
                case "p":
39
                    return await DoSetReg(client, args);
40
                case "mem":
41
                case "m":
42
                    return await DoMem(client, args);
43
                case "setmem":
44
                    return await DoSetMem(client, args);
45
                case "vram":
46
                case "v":
47
                    return await DoVram(client, args);
48
                case "setvram":
49
                    return await DoSetVram(client, args);
50
                case "joy":
51
                case "j":
52
                    return await DoJoy(client, args);
53
                case "bp":
54
                    return await DoBp(client, args);
55
                default:
56
                    Console.Error.WriteLine($"Unknown command: {cmd}");
57
                    return 1;
58
            }
59
        }
60
        catch (Exception ex)
61
        {
62
            Console.Error.WriteLine($"Error: {ex.Message}");
63
            return 1;
64
        }
65
    }
66
67
    static async Task<int> DoStep(X16DebugClient client)
68
    {
69
        var r = await client.StepInAsync();
70
        if (!DapSuccess(r))
71
            return 1;
72
        Console.WriteLine("OK (step issued, awaiting stopped notification)");
73
        return 0;
74
    }
75
76
    static async Task<int> DoContinue(X16DebugClient client)
77
    {
78
        var r = await client.ContinueAsync();
79
        PrintDapResult(r, "Running...");
80
        return DapSuccess(r) ? 0 : 1;
81
    }
82
83
    static async Task<int> DoBreak(X16DebugClient client)
84
    {
85
        var r = await client.PauseAsync();
86
        if (DapSuccess(r))
87
        {
88
            // After pause, fetch and display registers
89
            try
90
            {
91
                await PrintRegistersFromServer(client);
92
            }
93
            catch
94
            {
95
                Console.WriteLine("Paused (registers not yet available)");
96
            }
97
        }
98
        else
99
            PrintDapError(r);
100
        return DapSuccess(r) ? 0 : 1;
101
    }
102
103
    static async Task<int> DoStatus(X16DebugClient client)
104
    {
105
        var threads = await client.GetThreadsAsync();
106
        if (DapSuccess(threads))
107
        {
108
            var threadList = threads["body"]?["threads"]?.AsArray();
109
            if (threadList != null && threadList.Count > 0)
110
            {
111
                string name = threadList[0]?["name"]?.GetValue<string>() ?? "CPU";
112
                Console.WriteLine($"Thread: {name}");
113
            }
114
115
            // Try to get stack trace for current position
116
            try
117
            {
118
                var stack = await client.GetStackTraceAsync(1);
119
                var frames = stack["body"]?["stackFrames"]?.AsArray();
120
                if (frames != null && frames.Count > 0)
121
                {
122
                    string frameName = frames[0]?["name"]?.GetValue<string>() ?? "unknown";
123
                    Console.WriteLine($"Frame: {frameName}");
124
                }
125
                await PrintRegistersFromServer(client);
126
            }
127
            catch
128
            {
129
                Console.WriteLine("(CPU may be running, registers not available)");
130
            }
131
        }
132
        else
133
            PrintDapError(threads);
134
        return 0;
135
    }
136
137
    static async Task<int> DoReset(X16DebugClient client)
138
    {
139
        var r = await client.EvaluateAsync("reset");
140
        PrintDapResult(r);
141
        return 0;
142
    }
143
144
    static async Task<int> DoRegs(X16DebugClient client)
145
    {
146
        try
147
        {
148
            await PrintRegistersFromServer(client);
149
        }
150
        catch (Exception ex)
151
        {
152
            Console.Error.WriteLine($"Error: {ex.Message}");
153
            return 1;
154
        }
155
        return 0;
156
    }
157
158
    /// <summary>
159
    /// Map the short names a user types onto the names the Registers scope uses.
160
    /// </summary>
161
    static string ResolveRegisterName(string reg) => reg.ToLowerInvariant() switch
162
    {
163
        "a" => "A",
164
        "x" => "X",
165
        "y" => "Y",
166
        "sp" or "s" => "SP",
167
        "pc" => "PC",
168
        "p" or "status" or "flags" => "P (Status)",
169
        "d" or "dp" => "D",
170
        "dbr" => "DBR",
171
        "pbr" or "k" => "PBR",
172
        "rambank" or "ram" or "bka" => "RAM Bank",
173
        "rombank" or "rom" or "bko" => "ROM Bank",
174
        _ => reg.ToUpperInvariant(),
175
    };
176
177
    static async Task<int> DoSetReg(X16DebugClient client, string[] args)
178
    {
179
        if (args.Length < 2)
180
        {
181
            Console.Error.WriteLine("Usage: setreg <reg> <hex_value>   (e.g., setreg a FF)");
182
            return 1;
183
        }
184
185
        // setVariable, not evaluate: the evaluate expression "A=FF" is parsed as
186
        // a read, answers success and changes nothing.
187
        string name = ResolveRegisterName(args[0]);
188
        var r = await client.SetVariableAsync(name, $"${args[1].TrimStart('$')}");
189
        if (!DapSuccess(r)) { PrintDapError(r); return 1; }
190
191
        string? shown = r["body"]?["value"]?.GetValue<string>();
192
        Console.WriteLine($"OK: {name} = {shown ?? "$" + args[1]}");
193
        return 0;
194
    }
195
196
    /// <summary>
197
    /// Parse the address/length forms both mem and vram accept:
198
    /// "1030-1040" (inclusive range), "0800 10" (hex length), or "0800" (16 bytes).
199
    /// </summary>
200
    static bool TryParseRange(string[] args, out uint addr, out int length, out string error)
201
    {
202
        addr = 0; length = 0; error = "";
203
        try
204
        {
205
            if (args[0].Contains('-'))
206
            {
207
                var parts = args[0].Split('-');
208
                uint start = Convert.ToUInt32(parts[0], 16);
209
                uint end = Convert.ToUInt32(parts[1], 16);
210
                if (end < start) { error = "end address is before the start"; return false; }
211
                addr = start;
212
                length = (int)(end - start + 1);
213
            }
214
            else if (args.Length >= 2)
215
            {
216
                addr = Convert.ToUInt32(args[0], 16);
217
                length = (int)Convert.ToUInt32(args[1], 16);
218
            }
219
            else
220
            {
221
                addr = Convert.ToUInt32(args[0], 16);
222
                length = 16;
223
            }
224
        }
225
        catch (Exception ex)
226
        {
227
            error = ex.Message;
228
            return false;
229
        }
230
        if (length <= 0) { error = "length must be at least 1"; return false; }
231
        return true;
232
    }
233
234
    static async Task<int> DoMem(X16DebugClient client, string[] args)
235
    {
236
        if (args.Length < 1)
237
        {
238
            Console.Error.WriteLine("Usage: mem <addr>-<end>  or  mem <addr> <len>");
239
            return 1;
240
        }
241
        if (!TryParseRange(args, out uint addrVal, out int length, out string err))
242
        {
243
            Console.Error.WriteLine($"Error: {err}");
244
            return 1;
245
        }
246
247
        var r = await client.ReadMemoryAsync((int)addrVal, length);
248
        return PrintMemoryResponse(r, addrVal, 4);
249
    }
250
251
    static async Task<int> DoSetMem(X16DebugClient client, string[] args)
252
    {
253
        if (args.Length < 2)
254
        {
255
            Console.Error.WriteLine("Usage: setmem <addr> <hex_data>   (e.g., setmem 0400 DEADBEEF)");
256
            return 1;
257
        }
258
        return await WriteBytes(client, $"${args[0]}", args[1]);
259
    }
260
261
    // ─── VRAM ───────────────────────────────────────────────────────
262
    //
263
    // VERA's video RAM is a separate 17-bit space, reached with a "vram:"
264
    // memoryReference. The CPU cannot address it directly -- it goes through the
265
    // data port a byte at a time -- so these are not just mem with a different
266
    // number.
267
268
    static async Task<int> DoVram(X16DebugClient client, string[] args)
269
    {
270
        if (args.Length < 1)
271
        {
272
            Console.Error.WriteLine("Usage: vram <addr>-<end>  or  vram <addr> <len>");
273
            return 1;
274
        }
275
        if (!TryParseRange(args, out uint addrVal, out int length, out string err))
276
        {
277
            Console.Error.WriteLine($"Error: {err}");
278
            return 1;
279
        }
280
281
        var r = await client.ReadMemoryAsync($"vram:{addrVal:X}", length);
282
        return PrintMemoryResponse(r, addrVal, 5);
283
    }
284
285
    static async Task<int> DoSetVram(X16DebugClient client, string[] args)
286
    {
287
        if (args.Length < 2)
288
        {
289
            Console.Error.WriteLine("Usage: setvram <addr> <hex_data>   (e.g., setvram 1B000 DEADBEEF)");
290
            return 1;
291
        }
292
        uint addr;
293
        try { addr = Convert.ToUInt32(args[0], 16); }
294
        catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; }
295
296
        return await WriteBytes(client, $"vram:{addr:X}", args[1]);
297
    }
298
299
    /// <summary>
300
    /// Shared tail of setmem/setvram: decode the hex payload and write it, then
301
    /// read the same span back so the caller sees what actually landed rather
302
    /// than a bare OK.
303
    /// </summary>
304
    static async Task<int> WriteBytes(X16DebugClient client, string reference, string hex)
305
    {
306
        string clean = hex.Replace(" ", "").Replace("$", "");
307
        if (clean.Length == 0 || clean.Length % 2 != 0)
308
        {
309
            Console.Error.WriteLine("Error: hex data must be an even number of hex digits");
310
            return 1;
311
        }
312
313
        byte[] data;
314
        try { data = Convert.FromHexString(clean); }
315
        catch (Exception ex) { Console.Error.WriteLine($"Error: {ex.Message}"); return 1; }
316
317
        var w = await client.WriteMemoryAsync(reference, data);
318
        if (!DapSuccess(w)) { PrintDapError(w); return 1; }
319
320
        int written = w["body"]?["bytesWritten"]?.GetValue<int>() ?? data.Length;
321
        Console.WriteLine($"Wrote {written} byte{(written == 1 ? "" : "s")} to {reference}");
322
323
        var r = await client.ReadMemoryAsync(reference, data.Length);
324
        if (DapSuccess(r))
325
        {
326
            uint baseAddr = 0;
327
            string numeric = reference.StartsWith("vram:", StringComparison.OrdinalIgnoreCase)
328
                ? reference[5..] : reference;
329
            numeric = numeric.TrimStart('$');
330
            try { baseAddr = Convert.ToUInt32(numeric, 16); } catch { }
331
            PrintMemoryResponse(r, baseAddr, reference.StartsWith("vram:", StringComparison.OrdinalIgnoreCase) ? 5 : 4);
332
        }
333
        return 0;
334
    }
335
336
    // ─── Joystick ───────────────────────────────────────────────────
337
338
    static readonly string[] JoyButtonNames =
339
    {
340
        "up", "down", "left", "right", "a", "b", "x", "y", "start", "select", "l", "r"
341
    };
342
343
    static async Task<int> DoJoy(X16DebugClient client, string[] args)
344
    {
345
        if (args.Length < 1)
346
        {
347
            Console.Error.WriteLine("Usage: joy <port 1-4> [buttons...|release|connect]");
348
            Console.Error.WriteLine($"       buttons: {string.Join(", ", JoyButtonNames)}");
349
            Console.Error.WriteLine("       joy 1              query port 1 without changing it");
350
            Console.Error.WriteLine("       joy 1 a right      hold A and Right, release everything else");
351
            Console.Error.WriteLine("       joy 1 none         release every button, stay connected");
352
            Console.Error.WriteLine("       joy 1 release      hand the port back to a real gamepad");
353
            return 1;
354
        }
355
356
        // Ports are 1-4 here to match -joy1..-joy4 and BASIC's JOY(n); the DAP
357
        // request indexes them from 0.
358
        if (!int.TryParse(args[0], out int port) || port < 1 || port > 4)
359
        {
360
            Console.Error.WriteLine("Error: port must be 1, 2, 3 or 4");
361
            return 1;
362
        }
363
        int index = port - 1;
364
365
        JsonNode r;
366
        if (args.Length == 1)
367
        {
368
            r = await client.JoystickAsync(index);
369
        }
370
        else if (args[1].Equals("release", StringComparison.OrdinalIgnoreCase) ||
371
                 args[1].Equals("off", StringComparison.OrdinalIgnoreCase))
372
        {
373
            r = await client.JoystickAsync(index, enabled: false);
374
        }
375
        else if (args[1].Equals("connect", StringComparison.OrdinalIgnoreCase) ||
376
                 args[1].Equals("none", StringComparison.OrdinalIgnoreCase))
377
        {
378
            // A connected controller with nothing held. Distinct from an empty
379
            // port: the KERNAL can tell the difference, and a game that waits for
380
            // a controller needs to see one.
381
            r = await client.JoystickAsync(index, buttons: Array.Empty<string>());
382
        }
383
        else
384
        {
385
            var held = args[1..];
386
            foreach (var b in held)
387
            {
388
                if (Array.IndexOf(JoyButtonNames, b.ToLowerInvariant()) < 0)
389
                {
390
                    Console.Error.WriteLine($"Error: unknown button '{b}'");
391
                    Console.Error.WriteLine($"       known: {string.Join(", ", JoyButtonNames)}");
392
                    return 1;
393
                }
394
            }
395
            r = await client.JoystickAsync(index, buttons: held);
396
        }
397
398
        if (!DapSuccess(r)) { PrintDapError(r); return 1; }
399
        PrintJoystick(port, r);
400
        return 0;
401
    }
402
403
    static void PrintJoystick(int port, JsonNode r)
404
    {
405
        var b = r["body"];
406
        if (b == null) { Console.WriteLine("OK"); return; }
407
408
        int mask = b["mask"]?.GetValue<int>() ?? 0xFFFF;
409
        bool driven = b["enabled"]?.GetValue<bool>() ?? false;
410
        bool bound = b["controllerBound"]?.GetValue<bool>() ?? false;
411
412
        var pressed = new List<string>();
413
        var arr = b["buttons"]?.AsArray();
414
        if (arr != null)
415
            foreach (var n in arr)
416
                if (n != null) pressed.Add(n.GetValue<string>());
417
418
        string source = driven ? "virtual" : bound ? "gamepad" : "empty";
419
        string held = pressed.Count > 0 ? string.Join(" ", pressed) : "(nothing held)";
420
421
        Console.WriteLine($"  Joystick {port}: {held}");
422
        Console.WriteLine($"    mask=${mask:X4}  source={source}");
423
    }
424
425
    /// <summary>
426
    /// Decode a readMemory response and hex-dump it. width is the number of
427
    /// address digits: 4 for the CPU map, 5 for VRAM's 17-bit space.
428
    /// </summary>
429
    static int PrintMemoryResponse(JsonNode r, uint baseAddr, int width)
430
    {
431
        if (!DapSuccess(r)) { PrintDapError(r); return 1; }
432
433
        string? b64Data = r["body"]?["data"]?.GetValue<string>();
434
        if (b64Data == null)
435
        {
436
            Console.Error.WriteLine("Error: no data in response");
437
            return 1;
438
        }
439
        PrintMemoryDump(baseAddr, Convert.ToHexString(Convert.FromBase64String(b64Data)), width);
440
        return 0;
441
    }
442
443
    static async Task<int> DoBp(X16DebugClient client, string[] args)
444
    {
445
        if (args.Length < 1)
446
        {
447
            Console.Error.WriteLine("Usage: bp add|remove|list|clear [addr]");
448
            return 1;
449
        }
450
451
        switch (args[0].ToLower())
452
        {
453
            case "add":
454
            case "a":
455
                if (args.Length < 2) { Console.Error.WriteLine("Usage: bp add <addr>"); return 1; }
456
                var ra = await client.AddBreakpointAsync((int)Convert.ToUInt32(args[1], 16));
457
                PrintDapResult(ra);
458
                return DapSuccess(ra) ? 0 : 1;
459
460
            case "remove":
461
            case "r":
462
                if (args.Length < 2) { Console.Error.WriteLine("Usage: bp remove <addr>"); return 1; }
463
                var rr = await client.RemoveBreakpointAsync((int)Convert.ToUInt32(args[1], 16));
464
                PrintDapResult(rr);
465
                return DapSuccess(rr) ? 0 : 1;
466
467
            case "list":
468
            case "l":
469
                var rl = await client.ListBreakpointsAsync();
470
                if (DapSuccess(rl))
471
                {
472
                    string? result = rl["body"]?["result"]?.GetValue<string>();
473
                    if (string.IsNullOrWhiteSpace(result) || result == "none")
474
                        Console.WriteLine("No breakpoints set");
475
                    else
476
                        Console.WriteLine($"Breakpoints: {result}");
477
                }
478
                else
479
                    PrintDapError(rl);
480
                return 0;
481
482
            case "clear":
483
            case "x":
484
                var rc = await client.ClearBreakpointsAsync();
485
                PrintDapResult(rc);
486
                return 0;
487
488
            default:
489
                Console.Error.WriteLine($"Unknown bp action: {args[0]}");
490
                return 1;
491
        }
492
    }
493
494
    // ─── DAP helpers ────────────────────────────────────────────────
495
496
    static bool DapSuccess(JsonNode r) =>
497
        r["success"]?.GetValue<bool>() == true;
498
499
    static void PrintDapError(JsonNode r)
500
    {
501
        string? msg = r["message"]?.GetValue<string>();
502
        if (msg != null)
503
            Console.Error.WriteLine($"Error: {msg}");
504
        else
505
            Console.Error.WriteLine("Error: request failed");
506
    }
507
508
    static void PrintDapResult(JsonNode r, string? successMsg = null)
509
    {
510
        if (DapSuccess(r))
511
            Console.WriteLine(successMsg ?? "OK");
512
        else
513
            PrintDapError(r);
514
    }
515
516
    /// <summary>
517
    /// Fetch registers via DAP (stackTrace -> scopes -> variables) and print them.
518
    /// </summary>
519
    public static async Task PrintRegistersFromServer(X16DebugClient client)
520
    {
521
        var varsResponse = await client.GetRegistersAsync();
522
        var variables = varsResponse["body"]?["variables"]?.AsArray();
523
        if (variables == null)
524
        {
525
            Console.Error.WriteLine("No register data available");
526
            return;
527
        }
528
529
        // Build a dictionary from the DAP variables array
530
        var regs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
531
        foreach (var v in variables)
532
        {
533
            string? name = v?["name"]?.GetValue<string>();
534
            string? val = v?["value"]?.GetValue<string>();
535
            if (name != null && val != null)
536
                regs[name] = val;
537
        }
538
539
        PrintRegs(regs);
540
    }
541
542
    // ─── Display helpers ────────────────────────────────────────────
543
544
    /// <summary>
545
    /// Print registers from a DAP variables dictionary (name -> hex value).
546
    /// </summary>
547
    public static void PrintRegs(Dictionary<string, string> regs)
548
    {
549
        // DAP variable values are formatted as "$XX (decimal)" or "$XX [flags]"
550
        // Register names from DAP: PC, A, X, Y, SP, "P (Status)", "RAM Bank", "ROM Bank"
551
        string a = GetReg(regs, "A", "??");
552
        string x = GetReg(regs, "X", "??");
553
        string y = GetReg(regs, "Y", "??");
554
        string sp = GetReg(regs, "SP", "??");
555
        string pc = GetReg(regs, "PC", "????");
556
        string p = GetReg(regs, "P (Status)", GetReg(regs, "P", "??"));
557
        string bka = GetReg(regs, "RAM Bank", GetReg(regs, "BKA", "??"));
558
        string bko = GetReg(regs, "ROM Bank", GetReg(regs, "BKO", "??"));
559
560
        // Extract just the hex portion from DAP values like "$02 (2)" -> "02"
561
        string aHex = ExtractHex(a);
562
        string xHex = ExtractHex(x);
563
        string yHex = ExtractHex(y);
564
        string pHex = ExtractHex(p);
565
        string pcHex = ExtractHex(pc);
566
        string spHex = ExtractHex(sp);
567
        string bkaHex = ExtractHex(bka);
568
        string bkoHex = ExtractHex(bko);
569
570
        byte pByte = 0;
571
        try { pByte = Convert.ToByte(pHex, 16); } catch { }
572
        string flags = FormatFlags(pByte);
573
574
        try
575
        {
576
            Console.WriteLine($"  PC=${pcHex}  A=${aHex} ({Convert.ToByte(aHex, 16),3})  X=${xHex} ({Convert.ToByte(xHex, 16),3})  Y=${yHex} ({Convert.ToByte(yHex, 16),3})");
577
        }
578
        catch
579
        {
580
            Console.WriteLine($"  PC=${pcHex}  A=${aHex}  X=${xHex}  Y=${yHex}");
581
        }
582
        Console.WriteLine($"  SP=${spHex}  P=${pHex} [{flags}]  RAM Bank=${bkaHex}  ROM Bank=${bkoHex}");
583
    }
584
585
    static string GetReg(Dictionary<string, string> regs, string name, string fallback)
586
    {
587
        if (regs.TryGetValue(name, out var val))
588
            return val;
589
        return fallback;
590
    }
591
592
    /// <summary>
593
    /// Extract hex value from DAP format like "$02 (2)" or "$C1C6 (49606)" or "$1E [---B-I--]"
594
    /// Returns just the hex digits (e.g., "02", "C1C6", "1E")
595
    /// </summary>
596
    static string ExtractHex(string val)
597
    {
598
        if (string.IsNullOrEmpty(val) || val == "??" || val == "????") return val;
599
600
        // Strip leading $
601
        string s = val;
602
        if (s.StartsWith("$")) s = s[1..];
603
        if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) s = s[2..];
604
605
        // Take only hex digits before any space or other delimiter
606
        int end = 0;
607
        while (end < s.Length && Uri.IsHexDigit(s[end])) end++;
608
        return end > 0 ? s[..end] : val;
609
    }
610
611
    static string FormatFlags(byte p)
612
    {
613
        char n = (p & 0x80) != 0 ? 'N' : '-';
614
        char v = (p & 0x40) != 0 ? 'V' : '-';
615
        char b = (p & 0x10) != 0 ? 'B' : '-';
616
        char d = (p & 0x08) != 0 ? 'D' : '-';
617
        char i = (p & 0x04) != 0 ? 'I' : '-';
618
        char z = (p & 0x02) != 0 ? 'Z' : '-';
619
        char c = (p & 0x01) != 0 ? 'C' : '-';
620
        return $"{n}{v}-{b}{d}{i}{z}{c}";
621
    }
622
623
    public static void PrintMemoryDump(uint baseAddr, string hexData, int addrWidth = 4)
624
    {
625
        int numBytes = hexData.Length / 2;
626
        int offset = 0;
627
628
        while (offset < numBytes)
629
        {
630
            uint lineAddr = baseAddr + (uint)offset;
631
            int lineStart = offset;
632
            int lineEnd = Math.Min(offset + 16, numBytes);
633
634
            var hexPart = new StringBuilder();
635
            var asciiPart = new StringBuilder();
636
637
            for (int i = lineStart; i < lineEnd; i++)
638
            {
639
                string byteHex = hexData.Substring(i * 2, 2);
640
                hexPart.Append(byteHex);
641
                hexPart.Append(' ');
642
643
                byte b = Convert.ToByte(byteHex, 16);
644
                asciiPart.Append(b >= 0x20 && b < 0x7F ? (char)b : '.');
645
            }
646
647
            // Pad if less than 16 bytes
648
            for (int i = lineEnd - lineStart; i < 16; i++)
649
            {
650
                hexPart.Append("   ");
651
                asciiPart.Append(' ');
652
            }
653
654
            Console.WriteLine($"  {lineAddr.ToString($"X{addrWidth}")}: {hexPart} |{asciiPart}|");
655
            offset = lineEnd;
656
        }
657
    }
658
}