Skip to content

Port 8bitworkshop crypto.c dungeon crawler sample#211

Open
Copilot wants to merge 23 commits intomainfrom
copilot/port-8bitworkshop-crypto-c
Open

Port 8bitworkshop crypto.c dungeon crawler sample#211
Copilot wants to merge 23 commits intomainfrom
copilot/port-8bitworkshop-crypto-c

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 15, 2026

Port of the crypto.c dungeon crawler from 8bitworkshop (~3000 lines of C) to a dotnes C# sample.

What's here

  • samples/crypto/ -- project file, CHR ROM, and ~2270-line C# port including all 140+ const data arrays (RLE backgrounds, metasprites, collision maps) and complete main game loop
  • All 7 local functions have been inlined (no closures)
  • PAD enum cleanup: uses (byte)PAD.UP etc. directly instead of redefined constants

Status: TRANSPILES AND RUNS

With transpiler fixes from PRs #293 and #294, plus sample-level workarounds, the ROM builds and renders in the emulator:

  • Title screen partially renders (tiles and text visible, layout needs debugging)
  • PPU is working (palette loaded, BG + sprites enabled, NMI active)
  • Remaining rendering issues are game logic bugs (likely VRAM/nametable addressing), not transpiler crashes

Sample-level workarounds needed

  1. 36 compound array assignments (arr[i]++, arr[i] += expr) rewritten as arr[i] = (byte)(arr[i] + expr) to avoid ldelema Byte
  2. Division in oam_spr args pre-computed to temp variables (1 occurrence)
  3. sfx_play calls commented out (6 calls, no 6502 subroutine implementation)

Transpiler fixes (separate PRs)

Fixes #105

Copilot AI and others added 3 commits March 15, 2026 02:47
Port the complete crypto.c (3092 lines) game to C# targeting the
dotnes NES transpiler. This is a 2-player cooperative dungeon crawler
where players carry items to target positions while fighting enemies.

Key porting decisions:
- All mutable state in static class G
- for loops converted to while loops
- switch on PAD values converted to if/else chains
- Signed char metasprite offsets converted to unsigned bytes (-8→0xF8, -16→0xF0)
- OAM attribute byte combinations precomputed (e.g. 1|OAM_FLIP_V→0x81)
- Palette bug fixed: 0x379 truncated to 0x79
- vram_write calls skipped (CHR comes from .s file)
- int_temp0 uses int type for 16-bit screen coordinate math
- Byte arithmetic uses explicit casts where needed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Complete port of the 3092-line C game to ~1885 lines of C#. Includes:
- All 7 game functions (isBlocked, took_damage, tick_enemies, draw_player, tick_players, init, reset)
- All data arrays (4 RLE backgrounds, 4 collision maps, 112 metasprites, spawn table, palettes)
- Static class G for shared mutable game state
- while loops (no for/foreach), PAD enum casts, OAM flip flag conversions
- Signed char to unsigned byte conversions in metasprite data (-8 -> 248, -16 -> 240)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Copilot AI changed the title [WIP] Port 8bitworkshop sample: crypto.c WIP: Port 8bitworkshop crypto.c sample Mar 15, 2026
Copilot AI requested a review from jonathanpeppers March 15, 2026 03:04
jonathanpeppers and others added 2 commits March 14, 2026 23:02
Use PAD.UP, PAD.DOWN, PAD.LEFT, etc. directly instead of redefining
const byte PAD_UP = (byte)PAD.UP etc. This follows the neslib convention
used by all other samples. PAD_DPAD composite constant is kept since
it's not provided by neslib.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend EmitLdsfldForArg to handle any static field (not just oam_off)
by looking up the field's allocated address via GetOrAllocateStaticField.

Also fix compound oam_spr argument scanning to track static field names
when a static field is part of a binary expression (e.g., G.temp0 + 8).
The emission code now checks isStaticField for compound args and loads
from the field address instead of trying Locals[-1].

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Owner

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Deep analysis and partial fixes for the crypto sample port.

What I fixed

1. PAD constant redefinitions → use PAD enum directly (45ca35b)

Replaced ~144 occurrences of PAD_UP, PAD_DOWN, etc. with (byte)PAD.UP, (byte)PAD.DOWN etc. Follows the neslib convention used by all other samples. The composite PAD_DPAD constant is kept since neslib doesn't provide it.

2. Transpiler: static fields in oam_spr arguments (4fe80a2)

Extended EmitLdsfldForArg to handle user-defined static fields by looking up their allocated address via GetOrAllocateStaticField. Previously only oam_off was supported. Also fixed compound expression scanning to track static field names when a field appears in a binary expression (e.g., G.temp0 + 8).

Root cause analysis: why transpilation fails

The PR description correctly identifies closures as the blocking issue. I did extensive analysis:

Closure issue: In C# top-level statements, all 7 functions are local functions. When they reference top-level byte[] arrays, the compiler creates closure structs. Capture analysis:

  • isBlocked → 4 collision arrays
  • draw_player → ~28 metasprite arrays
  • tick_enemies → ~48 sprite arrays + calls isBlocked
  • tick_players → palette, item_target_x/y + calls draw_player/isBlocked
  • init → 4 bg arrays
  • reset → spawn array
  • took_damage → NO captures ✓

I tested two refactoring approaches:

Approach A: Inline functions + keep static class G

✅ Eliminates closures
✅ Static fields work in regular code (ldsfld/stsfld)
❌ oam_spr compound arg issue (G.temp0 + 8 as arg, fixed but more edge cases remain)

Approach B: Inline functions + replace G.* with local variables

✅ Eliminates closures
✅ Locals work in oam_spr arguments
❌ Complex array index expressions not supported (arr[(x >> 3) + ((y >> 3) << 4)])

Both approaches hit different transpiler limitations beyond closures.

Transpiler features needed for this sample

  1. Complex array index expressions — patterns like arr[(x >> 3) + ((y >> 3) << 4)] fail with "Complex index expressions are not supported"
  2. Full static field support in oam_spr — compound expressions with static fields need more work
  3. int type support — the sample uses int g_int_temp0 for screen coordinate math (may already partially work)

Recommendation

This sample is an excellent stress test for the transpiler but needs several transpiler features before it can transpile. I'd suggest:

  1. Keep this PR as a draft — the C# code compiles correctly and serves as a target
  2. File individual issues for each transpiler limitation
  3. Tackle them one at a time, using simpler samples to test each feature
  4. Come back to this PR once the features are in place

The PAD fixes and transpiler improvements are independently valuable and don't regress any existing tests (all 475 pass).

jonathanpeppers and others added 2 commits March 17, 2026 19:03
…shop-crypto-c

# Conflicts:
#	src/dotnes.tasks/Utilities/IL2NESWriter.OamSprites.cs
…change

Resolve OamSprites.cs conflict by taking main's comprehensive fix (PR #239).
Update crypto sample: pad_trigger() now returns PAD enum, remove (byte)
casts in bitwise expressions against PAD values.

The sample still cannot transpile due to closure captures from local
functions (~120 byte[] arrays). Inlining the functions is needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
Copy link
Copy Markdown
Owner

Status Update — Merge conflicts resolved, blocking issues reassessed

Resolved

Blocking issue status

Issue Title Status
#225 Complex array index expressions Closed
#226 Closure structs from local functions Closed (helpful error msg only) ⚠️
#237 oam_spr compound expressions with static fields Closed ✅ (PR #239)

Remaining blocker: closures

The sample still has 7 local functions capturing ~120 byte[] arrays from the outer scope. The compiler generates a closure struct <>c__DisplayClass0_0 which the transpiler cannot handle.

Issue #226 was closed with a diagnostic message, not actual closure support. The fix is to inline the local functions into the main body. This is a large refactoring (~1880 line file, 7 functions to inline) but mechanically straightforward.

What's needed to unblock

  1. Inline the 7 local functions (isBlocked, draw_player, tick_enemies, tick_players, init, reset, took_damage) into the main loop
  2. Any remaining transpiler issues will surface after inlining

Inlined isBlocked (9 call sites), took_damage, draw_player,
tick_enemies, tick_players, init, and reset into the main body.
This eliminates the closure struct that blocked transpilation.

The transpiler now progresses past the closure check but hits a new
limitation: 'Array element access requires the array to be stored
in a local variable' in HandleLdelemU1ComplexIndex.

Also updated PAD usage for pad_trigger() return type change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
Copy link
Copy Markdown
Owner

Progress Update: Crypto Sample Transpilation

Investigated what's needed to get this sample transpiling. Here's what I found and fixed:

Fixes applied (sample-level, ready to commit to this PR)

  1. 14 compound array assignments (+=/-=) rewritten as explicit read-modify-write — avoids ldelema Byte which the transpiler doesn't support
  2. 22 array element ++/-- rewritten similarly
  3. Division in oam_spr args — pre-computed G.difficulty / 10 and % 10 to temp variables

Transpiler fix (separate PR)

  1. EmitBranchCompare overflow when comparing against 255 — filed as PR Fix byte comparison overflow in Ble/Bgt when value is 255 #293

Remaining blocker

  1. KeyNotFoundException: '-1' in EmitOamSprDecsp4 — the oam_spr backward scan doesn't handle Ldc_i4_m1 (-1 / 0xFF). This is a transpiler bug that needs a separate fix.

After fixing that, more issues may surface — this 2,270-line sample pushes every transpiler feature to its limits. Each fix reveals the next error.

@jonathanpeppers jonathanpeppers marked this pull request as ready for review March 20, 2026 03:25
Copilot AI review requested due to automatic review settings March 20, 2026 03:25
@jonathanpeppers jonathanpeppers changed the title WIP: Port 8bitworkshop crypto.c sample Port 8bitworkshop crypto.c dungeon crawler sample Mar 20, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new crypto sample project intended as an initial C# port of 8bitworkshop’s crypto.c dungeon crawler, to serve as a target workload for dotnes/transpiler development.

Changes:

  • Added samples/crypto/Program.cs: large top-level-statement game loop plus embedded const data tables (RLE backgrounds, collision maps, metasprites, etc.).
  • Added samples/crypto/crypto.csproj: new sample project referencing dotnes and dotnes.anese.
  • Added samples/crypto/chr_generic.s: CHR ROM data for the sample.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
samples/crypto/crypto.csproj New sample project definition (net10.0, dotnes package refs).
samples/crypto/chr_generic.s New CHR segment data for the sample.
samples/crypto/Program.cs Main sample implementation: data tables + initialization + menu + game loop + state container.

G.enemy_push_timer[G.i] = 0;
G.enemies_health[G.i] = G.spawn_hp;
G.enemies_type[G.i] = (byte)(G.i & 1);
return;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return; here exits the top-level Main and will terminate the ROM’s execution, bypassing the outer while (true) loops. If this is meant as an early-exit from the inlined tick-enemy logic, replace it with control flow that returns to the frame loop (e.g., continue the frame loop via a flag/label, or break out of the relevant loop(s)).

Suggested change
return;
break;

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +12
#pragma warning disable CS0649, CS8321, CS0219
using NES;
using static NES.NESLib;

const byte PLAYER_1 = 0;
const byte PLAYER_2 = 1;

// Use PAD enum values directly — neslib provides PAD.UP, PAD.DOWN, etc.
const byte PAD_DPAD = (byte)PAD.LEFT | (byte)PAD.RIGHT | (byte)PAD.UP | (byte)PAD.DOWN;

const byte OAM_FLIP_H = 0x40;
const byte OAM_FLIP_V = 0x80;
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions a transpiler fix (“support user-defined static fields in oam_spr arguments”) and broader PAD cleanup, but this PR only adds the new samples/crypto/* files. Either include the transpiler changes in this PR, or update the PR description to match what’s actually being changed.

Copilot uses AI. Check for mistakes.
jonathanpeppers and others added 3 commits March 20, 2026 16:41
When the backward scan produces a compound expression with no source
variable (compoundLocalIdx < 0, compoundStaticFieldName null), evaluate
the binary op at compile time instead of throwing. For example,
0 << N = 0, 0 | N = N, etc.

Also emit the sample-level workarounds for crypto:
- Rewrite compound array assignments to avoid ldelema Byte
- Pre-compute division in oam_spr args
- Comment out sfx_play (no 6502 subroutine)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 36 compound array assignments rewritten as explicit read-modify-write
- Division in oam_spr args pre-computed to temp variables
- sfx_play calls commented out (no built-in subroutine)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers and others added 3 commits March 21, 2026 10:48
The crypto game uses bank_bg(1) to put BG tiles at pattern table \.
The previous chr_generic.s only had 4KB of tiles at \, leaving \
empty (black screen). Now uses the jroatch.chr tileset from 8bitworkshop
(the tileset crypto.c was designed for) duplicated in both pattern table
halves so sprites and backgrounds both render correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace placeholder jroatch.chr with the actual game CHR data extracted
  from the purchased reference crypto.nes ROM (16KB, 2 banks)
- Set NESChrBanks=2 so BG tiles at pattern table 1 (\) render correctly
- Fix palette values to match reference ROM exactly

The title screen now renders with correct tile art. Remaining palette
color differences are from NES hardware palette mirroring in the NMI
handler (byte 0 gets overwritten by bytes 4/8/12 mirror writes).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the actual CHR pattern table data from the purchased reference
crypto.nes ROM's PPU state at runtime (MMC3 remaps CHR banks). The 8KB
CHR now has sprite tiles at pattern table 0 and background tiles at
pattern table 1, matching what the game expects with bank_bg(1).

Title screen now renders correctly: green maze, CRYPTO logo, menu.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Python-generated chr_generic.s had '\' instead of '' due to
string escaping. The assembler parsed the backslash as part of the hex
value, corrupting every tile. Fixed by stripping backslashes.

Also set NESChrBanks=2 to match the 16KB CHR layout (8KB data padded
to 16KB by the assembler).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers and others added 2 commits March 21, 2026 13:39
…shop-crypto-c

# Conflicts:
#	src/dotnes.tasks/Utilities/IL2NESWriter.OamSprites.cs
Added both CHR banks from the reference crypto.nes ROM as separate
chr_crypto_0.s and chr_crypto_1.s files. The second bank contains font
tiles that the reference ROM swaps in via MMC3 scanline IRQ mid-frame.

Reverted set_chr_mode() experiment -- without scanline IRQ support,
setting font pages globally breaks the maze tiles. The menu text
remains garbled until scanline IRQ is implemented (issue #306).

The game is playable: CRYPTO logo renders correctly, maze and sprites
work, press Start to begin gameplay.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers and others added 2 commits March 21, 2026 14:07
Copy famitone2.s and demosounds.s from the fami sample, add extern
declarations for sfx_play/famitone_update, initialize the sound
engine at startup, and uncomment all 6 sfx_play calls.

Sound effects now play for pickup, attack, damage, enemy kill,
item placement, and level completion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port 8bitworkshop sample: crypto.c

3 participants