//Dodgy implementation of Dezaemon 3D fileserver IO protocol.
//**To turn on the fileserver, enter the Build Tool main menu and put the cursor in the upper-right corner.
//  Enter the button sequence (C-Up, C-Down, C-Up, C-Down, C-Left, C-Right, C-Left, C-Right) to toggle the fileserver on & off.
//  A sound will chime and the game will halt until communication with the attached device is established.
//
//Hardware resides optionally at 080C0000 or 11000000.
//  That can be selected in-game by turning custom editor wallpaper on and off 0.o  (Hold all C Buttons when changing wallpaper.)
//  Note 080C0000 is the next SRAM "bank" (next LVX138 pin) and specific to this game.
//base+0	value f/ PC -> N64
//base+4	value f/ N64 -> PC
//base+8	status of PC; 1 when ready (value at base+0 has been set)
//
// An observant person would point out their microcontroller undoubtedly set and cleared status automatically on read/write to act as a proper semaphore.
// Like most UARTs, it probably set/cleared status bit based on Tx/Rx interrupts.  PC transmit triggers interrupt, N64 read (or write) clears it.
// Below code isn't very clever.  N64 only *reads* status, so there's no way for PC-side to know when it's read or the next value needs to be available without burning extra frames. (aka DESYNC!)
// The connection is slow enough as it is: one byte per frame max, downhill from there.
// Atypical for these kinds of things, the N64 is master, not slave.
//
//On the N64-side, this is what's going on.
//writing values:
//  while not @base+8 & 1:  pass
//  ret = @base+0		# read: value in base[3]
//  base+4 = (A0 << 16)	# write: value in upper 16 bits (base[5])
//
//reading values:
//  while not @base+8 & 1:  pass
//  ret = @base+0		# read: value in base[3]
//  base+4 = (ret << 16)	# write: value in upper 16 bits (base[5])
//
//init:
//  while (1):
//    if write(0x20) != 0x10:  continue
//    if write(0x10) == 0x20:  break
//
//If first byte == 0, assume it's an 0x5C packet followed by r/w data.
//There is no "close" command N64-side but you can turn off and reactivate the fileserver so yes, init can happen more than once per game.
//Packets:
//  0x0	4	command type
//    00000001	unused type, likely a directory change
//    00000002	PC sends {length} bytes from file {string} to N64
//    00000003	N64 sends {length} bytes to file {string}
//  0x4	4	length of data
//  0x8	4	{RESERVED, not set}
//  0xC	0x50	string; always a filename
//For types 2 & 3 this is immediately followed by length bytes of reads or writes.
//
//General:
//  L Button	saves a screencap named "DEZA%3.3d.BMP" to PC; only works if you have a framebuffer to capture!
//  Local saves are ignored.  Data will be saved and loaded f/ a 0x18000 byte file named "sram".
//
//When loading files (switching menus):
//  Hold R	read "tut.bin" (0x10000) f/ fileserver in addition to (most) other files to run a tutorial / demo file
//
//Enables tutorial script command 19, used to record controller input.  This is how the tutorials were recorded.
//Replays (i.e.: tutorials) will use "key%3.3d.bin" instead of the file on cartridge.
//  In standby:
//    + Left	quit
//    + Right	start recording; moves cursor back to origin at start
//    + Up	read "key%3.3d.bin" f/ fileserver (0x1000); demo replays from origin
//  While recording:
//    + Down	Stop recording and write "key%3.3d.bin" to fileserver.  File is automatically flushed when size exceeded.  After stopping you can move the cursor to fix whatever mess you made with the menu during the demo.
//
//In MDCODE.SYS: (Model Editor)
//  In root menu:
//    Hold R, press C-Up	removes any untextured face in the current model
//    Hold Z, press Start	write "test.de2" (0xC20)
//    Press Start		read "test.de2" (0xC20)
//  In paint submenu:
//    Hold R, press Start	read "bg.img" (0x8000), replacing eight image blocks
//    Hold Z, press Start	write "test.img" (0x1000)
//    Press Start		read "test.img" (0x1000), replacing one block (64x32)
//
//In OPCODE.SYS: (Opening)
//  R Button	replace	BG image (water effect) with a c16 32x32 image named "op.img"
//  C Buttons	rotate BG image
//  + Up/Down	scale BG image
//
//In BMCODE.SYS: (Build Menu)
//  Hold Start, press C-Left	write cur. game files to fileserver (GAM, CGR, CMD, CBL, ULB, MUS, GR, MD, BL)
//  Hold Start, press C-Right	read game files f/ fileserver (GAM, CGR, CMD, CBL, ULB, MUS, GR, MD, BL)
//  (also see button code to enable fileserver IO above)
//
//In MUCODE.SYS: (Music Editor)
//  Hold Start, press + Up	write cur. "BGM_**.BIN" (0x2C9A)
//  Hold Start, press + Down	read cur. "BGM_**.BIN" (0x2C9A)
//  Hold Start, press L		In music track editor, rolls loudness of instruments up one level, wrapping around, and copies cur. audio (802BA1C0) to audio buffer.
//
//In GSCODE.SYS: (Game Select)
//  Hold Start, press C-Up	read & execute {filename}.EXE f/ fileserver
//
//  {filename}.EXE works the same as the EXE files on disk: it's a "program extension".  It loads and runs a block of code--the same as each of the editor/menu pages--every frame.  No restrictions!
//  The hiccup here is that an entry for the file must already be added to the filetable.  The file length is read from the entry and the N64 will only read up to that many bytes from PC after posting the request.
//
//  If you aren't just running a hijack, this thing operates within editor mode (EDMCODE), not the game code (GACODE) for created works.  It executes from 802400B0 accepting two arguments: whatever 800C4C80[0] is, and a flag indicating if it's first run (True only right after load, so you can do init).  To stop executing it, set 800C4C87[0] to one of the other menus like 0xF, the game start menu.
//  "SAMPLE01.EXE" is the default name until a game is loaded but from the PC's perspective the filename isn't important.  You don't need a 64DD to run the fileserver version either, it's available via button code.
//
//This is a first foray into javascript.  Sorry for the mess ;*)
console.clear();
console.log("- Dezaemon 3D Fileserver IO -");

const autistic = false;

//Listen to both address ranges.  Player can switch them in-game.
const ADDR_SN = new AddressRange(0xB1000000, 0xB100000C-1);
const ADDR_DZ = new AddressRange(0xA80C0000, 0xA80C000C-1);

const _ADDR_SN_I = ADDR_SN.start + 0;
const _ADDR_DZ_I = ADDR_DZ.start + 0;
const _ADDR_SN_O = ADDR_SN.start + 4;
const _ADDR_DZ_O = ADDR_DZ.start + 4;
const _ADDR_SN_STAT = ADDR_SN.start + 8;
const _ADDR_DZ_STAT = ADDR_DZ.start + 8;

var DEZA_IO_BUF = new Buffer(12);
var DEZA_IO_CMD = new Buffer(92);
//Are values initialized to zero automatically?
DEZA_IO_BUF.writeUInt32BE(0, 0x00);
DEZA_IO_BUF.writeUInt32BE(0, 0x04);
DEZA_IO_BUF.writeUInt32BE(0, 0x08);

//Variables.
var DEZA_OP = -1;
var DEZA_OUT = 0;
var DEZA_IN = -1;
var DEZA_CNT = 0;
var DEZA_LEN = 0;
var DEZA_STR = [];
var DEZA_FD = false;
var DEZA_BRAKE = 1;
var abort = -1;

function DEZA_IO(direction, addr, length) {
	if (direction == OS_READ) {
		switch (addr) {
			case _ADDR_SN_STAT:
			case _ADDR_DZ_STAT://BP 8004DE78
				DEZA_IO_BUF.writeUInt8(DEZA_OUT, 3);
				DEZA_IO_BUF.writeUInt8(DEZA_BRAKE, 11);
				if (autistic) console.log("  Status Get:", DEZA_IO_BUF.readUInt32BE(0), DEZA_IO_BUF.readUInt16BE(4), DEZA_IO_BUF.readUInt32BE(8));
				if ((DEZA_OUT == 0x10) && (DEZA_OP == -1)) DEZA_OUT = 0x20;
				else if ((DEZA_OUT == 0x20) && (DEZA_OP == -1)) DEZA_OUT = 0x10;
				break;
			case _ADDR_SN_I:
			case _ADDR_DZ_I:
				DEZA_IO_BUF.writeUInt8(0, 11);
				if (autistic) console.log("  Status Set:", DEZA_IO_BUF.readUInt32BE(0), DEZA_IO_BUF.readUInt16BE(4), DEZA_IO_BUF.readUInt32BE(8));
				break;
		}
	}
	else {
		if (autistic) console.log("  Byte In:", DEZA_IO_BUF.readUInt32BE(0), DEZA_IO_BUF.readUInt16BE(4), DEZA_IO_BUF.readUInt32BE(8));
		DEZA_IN = -1;
		switch (addr) {
			case _ADDR_SN_O:
			case _ADDR_DZ_O:
				DEZA_IN = DEZA_IO_BUF.readUInt16BE(4);
				break;
		}
	}
	if ((direction != OS_READ) || (DEZA_BRAKE == 0)) {
		if (DEZA_IN != -1) {
			if (autistic) console.log("    Write State:", DEZA_IN, DEZA_OP);
			if (DEZA_OP == -1) {
				if ((DEZA_IN == 0x20) && (DEZA_OUT == 0)) {
					DEZA_OUT = 0x10;
					console.log("    Fileserver connected successful!");
				}
				else if (DEZA_IN == 0) {
					DEZA_OP = 0;
					DEZA_CNT = 0;
					console.log("  Receiving request...");
				}
			}
			if (DEZA_OP == 0) {//Read 92 byte command.
				DEZA_IO_CMD[DEZA_CNT] = DEZA_IN;
				DEZA_CNT += 1;
				if (DEZA_CNT == 92) {
					//Interpret command
					DEZA_OP = DEZA_IO_CMD[3];
					DEZA_LEN = DEZA_IO_CMD.readUInt32BE(4);
					DEZA_STR = DEZA_IO_CMD.toString('utf8', 12);
					DEZA_CNT = -1;
					switch (DEZA_OP) {
						case 1:
							console.log("  Change Directory: ", DEZA_STR);
							break;
						case 2:
							console.log("  Reading file: ", DEZA_STR, DEZA_LEN);
							break;
						case 3:
							console.log("  Writing file: ", DEZA_STR, DEZA_LEN);
							break;
					}
				}
			}
			if (DEZA_OP == 1) {//Change directory, not that it ever does this.
				console.log("  *** NOT A HANDLED COMMAND!  You tripped this?  How!?");
				DEZA_OP = -1;
			}
			if (DEZA_OP == 2) {//Open {file} for reading and send {length} bytes to N64.
				if (DEZA_CNT == -1) {
					DEZA_FD = false;
					DEZA_BRAKE = 0;
					if (fs.exists(DEZA_STR) == true) DEZA_FD = fs.open(DEZA_STR, 'rb');
					if (DEZA_FD != false) {
						DEZA_CNT += 1;
					} else if (abort == -1) {
						console.log("  ***Cannot open " + DEZA_STR);
						console.log('     To abort: pause the game, type "abort = true"\n     press Enter, and unpause!');
						abort = false;
					} else if (abort == true) {
						console.log("  ***Read canceled; dummying file.");
						abort = -1;
						DEZA_CNT += 1;
					}
				} else {
					//Place next byte in the buffer unless the file is borked.
					DEZA_BRAKE = 1;
					if (DEZA_FD != false) fs.read(DEZA_FD, DEZA_IO_CMD, 0, 1, DEZA_CNT);
					DEZA_OUT = DEZA_IO_CMD[0];
					DEZA_CNT += 1;
					if (DEZA_LEN > 0x10000) {
						if ((DEZA_CNT % (DEZA_LEN >> 3)) == 0) console.log("   ", DEZA_CNT, "of", DEZA_LEN, "bytes received.");
					}
					if (DEZA_CNT > DEZA_LEN) {
						DEZA_BRAKE = 0;
						if (DEZA_FD != false) fs.close(DEZA_FD);
						DEZA_BRAKE = 1;
						DEZA_FD = false;
						console.log("  ...done!");
						DEZA_OP = -1;
						DEZA_OUT = 0;
					}
				}
			}
			if (DEZA_OP == 3) {//Open {file} for writing, reading {length} bytes from N64.
				if (DEZA_CNT == -1) {
					DEZA_BRAKE = 0;
					DEZA_FD = fs.open(DEZA_STR, 'wb');
					if (DEZA_FD == false) console.log("  ***Cannot open " + DEZA_STR + ", dummying file.");
					DEZA_BRAKE = 1;
				} else {//Write next byte unless file is borked.
					DEZA_OUT = 0;
					DEZA_IO_CMD[0] = DEZA_IN;
					DEZA_BRAKE = 0;
					if (DEZA_FD != false) fs.write(DEZA_FD, DEZA_IO_CMD, 0, 1);
					DEZA_BRAKE = 1;
				}
				DEZA_CNT += 1;
				if (DEZA_LEN > 0x10000) {
					if ((DEZA_CNT % (DEZA_LEN >> 3)) == 0) console.log("   ", DEZA_CNT, "of", DEZA_LEN, "bytes written.");
				}
				if (DEZA_CNT == DEZA_LEN) {
					DEZA_BRAKE = 0;
					if (DEZA_FD != false) fs.close(DEZA_FD);
					DEZA_BRAKE = 1;
					DEZA_FD = false;
					console.log("  ...done!");
					DEZA_OP = -1;
					DEZA_OUT = 0;
				}
			}
			if (DEZA_OP > 3) {
				//Some kind of hanging command or error.
				console.log("  *Hanging chad:", DEZA_OP, DEZA_LEN, DEZA_STR, DEZA_IO_BUF.readUInt32BE(0), DEZA_IO_BUF.readUInt16BE(4), DEZA_IO_BUF.readUInt32BE(8));
				DEZA_OP = -1;
				DEZA_OUT = 0;
			}
		}
	}
}

var CartMapper = require("CartMapper.js");
CartMapper.init();
CartMapper.verbose(0);

CartMapper.add(ADDR_SN, DEZA_IO_BUF, DEZA_IO);
CartMapper.add(ADDR_DZ, DEZA_IO_BUF, DEZA_IO);
