I don't know if anyone else has done a write up on this, but there is actually a pretty solid way to get around most of the data limits on the system, and it's kinda gross, and doesn't with the webplayer. The nice thing is that it gives you a legit file system, and basically limitless binary data!
Here is a proof of concept, I created two files (this one is test.pb)
CLS() PRINT("") PRINT("") RELOAD (0x6000, 0x4300, 128) LOAD ("TEST2") RELOAD (0x6100, 0x4300, 128) FUNCTION _DRAW() END FUNCTION _UPDATE() END
Test 2 contains nothing but garbage code that would crash the machine.
The rational behind this is that:
1) If LOAD trashes execution, it would crash when the new program runs
2) If LOAD aborts the current thread, it would simply return to the prompt with TEST2 loaded
3) If LOAD doesn't allow shared state, it would copy the same thing out of the code area of the cartridge
This test proves 3 things...
1) RUN performs LUA compilation, so LOADing garbage data doesn't harm execution state
2) The existing machine state is preserved after a LOAD call, while altering the loaded filename and data stored with in the cartridge
3) Storage space is essentially limitless.
Basic rundown of what happens with LOAD / RUN
LOAD: Sets the active filename for the cartridge, loads the 32k cartridge into the cartridge buffer, and loads the first 0x4300 bytes into memory (tiles, maps, sounds and patterns)
RUN: Disposes of existing Lua runtime state, discarding any global values, functions, etc, then parses and executes the code stored in the cartridge buffer.
This also means that whatever is in ram at 0x4300~0x7FFF is PRESERVED BETWEEN RUNS. The only downside here is that LOAD will print garbage to the screen, so write to this area after you LOAD, but before you RUN the LOADed cartridge.
So, if you want to get past the data storage limits, use a LOAD without running first.
If you want to get past the CODE limits, store persistent data at 0x4300~0x7FFF and then RUN.
Well, you could always poke few essential values to the memory at addresses untouched by the swap (less than 100 bytes should be enough to store things like player state, etc).
That's how detecting soft reset on the NES works by the way - during soft reset console keeps ram intact and game itself needs to tell it to initialize. So the dev can set some arbitrary byte to some value, then before game initializes it to that value (in my nes experiments I've always used 0xFE), check for it - if it was there before initialization it meant game was soft-reset.
If you do a load, write the state, and then do your run, you have a full 16k of state transfer (0x0000~0x7FFF). If you want to maintain tile/map/sfx/pattern memory, you get 0x4300~0x7FFF (approximately 15k)
The only sad thing there is that you can't expand the amount of code that is in the Lua memory space, and the handoff is kinda expensive (there is a noticable delay). It could be good for RPGs where you transition between different gameplay styles (think first person RPGs on the SNES where towns, overwords and dungeons looked significantly different)
If zep added any kind of eval() function, this would be completely resolved, but since I found this abuse vector I don't think I'll be able to talk him into it (although it would allow me to do single cart code compression... wink wink nudge nudge)
There is also another vector of code expansion: Self-modifying code.
There is nothing stopping you from using the current state of the cart to store mulitple code blocks, and using the lower section to store persistent data. Manually output LUA to the 0x4300+ section, preserve all your important state to the upper bounds of memory, RUN() and restore on repeat.
This would require a lot of fancy footwork though.
I did a bit of self modifying code in my initial tests at making game saves. It's super possible. I basically just left the first two lines of the code blank and then upon game save stored my data in those two lines but with a comment delimiter before them so it never junked up execution.
[Please log in to post a comment]