Port 8bitworkshop crypto.c dungeon crawler sample#211
Conversation
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>
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>
jonathanpeppers
left a comment
There was a problem hiding this comment.
🤖 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 arraysdraw_player→ ~28 metasprite arraystick_enemies→ ~48 sprite arrays + calls isBlockedtick_players→ palette, item_target_x/y + calls draw_player/isBlockedinit→ 4 bg arraysreset→ spawn arraytook_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
- Complex array index expressions — patterns like
arr[(x >> 3) + ((y >> 3) << 4)]fail with "Complex index expressions are not supported" - Full static field support in oam_spr — compound expressions with static fields need more work
inttype support — the sample usesint g_int_temp0for 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:
- Keep this PR as a draft — the C# code compiles correctly and serves as a target
- File individual issues for each transpiler limitation
- Tackle them one at a time, using simpler samples to test each feature
- 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).
…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>
Status Update — Merge conflicts resolved, blocking issues reassessedResolved
Blocking issue status
Remaining blocker: closuresThe sample still has 7 local functions capturing ~120 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
|
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>
Progress Update: Crypto Sample TranspilationInvestigated 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)
Transpiler fix (separate PR)
Remaining blocker
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. |
There was a problem hiding this comment.
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 referencingdotnesanddotnes.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; |
There was a problem hiding this comment.
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)).
| return; | |
| break; |
| #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; |
There was a problem hiding this comment.
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.
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>
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>
…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>
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>
Port of the
crypto.cdungeon 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(byte)PAD.UPetc. directly instead of redefined constantsStatus: TRANSPILES AND RUNS
With transpiler fixes from PRs #293 and #294, plus sample-level workarounds, the ROM builds and renders in the emulator:
Sample-level workarounds needed
arr[i]++,arr[i] += expr) rewritten asarr[i] = (byte)(arr[i] + expr)to avoidldelema ByteTranspiler fixes (separate PRs)
EmitBranchCompareoverflow when comparing against 255 (merged)Fixes #105