#include #include #include #include #include #include #include #include #include #include #include #include "vm.h" #include "graphics.h" #include "fontdata.h" #include "cart.h" #include "stringToDataHelpers.h" #include "picoluaapi.h" #include "logger.h" #include "Input.h" #include "p8GlobalLuaFunctions.h" #include "hostVmShared.h" #include "emojiconversion.h" #include "filehelpers.h" #include "NoLabel.h" //extern "C" { #include #include #include #include //} using namespace z8; static const char DefaultCartName[] = "__FAKE08-DEFAULT.p8"; static const char SettingsCartName[] = "__FAKE08-SETTINGS.p8"; bool _initializeLuaState(lua_State* luaState) { // initialize Lua interpreter // load Lua base libraries (print / math / etc) luaL_openlibs(luaState); lua_pushglobaltable(luaState); //system //must be registered before loading globals for pause menu to work lua_register(luaState, "__listcarts", listcarts); lua_register(luaState, "__listdirs", listdirs); lua_register(luaState, "__cd", cd); lua_register(luaState, "__pwd", pwd); lua_register(luaState, "__refreshcarts", refreshcarts); lua_register(luaState, "__bbs_listfeatured", bbs_listfeatured); lua_register(luaState, "__bbs_download", bbs_download); lua_register(luaState, "__bbs_isdownloaded", bbs_isdownloaded); lua_register(luaState, "__getbioserror", getbioserror); lua_register(luaState, "__loaddefaultcart", loaddefaultcart); lua_register(luaState, "__loadsettingscart", loadsettingscart); lua_register(luaState, "__togglepausemenu", togglepausemenu); lua_register(luaState, "__ispaused", ispaused); lua_register(luaState, "__resetcart", resetcart); lua_register(luaState, "__load", load); //settings lua_register(luaState, "__getsetting", getsetting); lua_register(luaState, "__setsetting", setsetting); lua_register(luaState, "__installpackins", installpackins); //label lua_register(luaState, "__loadlabel", loadlabel); lua_register(luaState, "__getlualine", getlualine); //register global functions first, they will get local aliases when //the rest of the api is registered //graphics lua_register(luaState, "cls", cls); lua_register(luaState, "pset", pset); lua_register(luaState, "pget", pget); lua_register(luaState, "color", color); lua_register(luaState, "line", line); lua_register(luaState, "tline", tline); lua_register(luaState, "circ", circ); lua_register(luaState, "circfill", circfill); lua_register(luaState, "oval", oval); lua_register(luaState, "ovalfill", ovalfill); lua_register(luaState, "rect", rect); lua_register(luaState, "rectfill", rectfill); lua_register(luaState, "rrect", rrect); lua_register(luaState, "rrectfill", rrectfill); lua_register(luaState, "print", print); lua_register(luaState, "cursor", cursor); lua_register(luaState, "spr", spr); lua_register(luaState, "sspr", sspr); lua_register(luaState, "fget", fget); lua_register(luaState, "fset", fset); lua_register(luaState, "sget", sget); lua_register(luaState, "sset", sset); lua_register(luaState, "camera", camera); lua_register(luaState, "clip", clip); lua_register(luaState, "pal", pal); lua_register(luaState, "palt", palt); lua_register(luaState, "mget", mget); lua_register(luaState, "mset", mset); lua_register(luaState, "map", gfx_map); lua_register(luaState, "mapdraw", gfx_map); //stubbed in graphics: lua_register(luaState, "fillp", fillp); //input lua_register(luaState, "btn", btn); lua_register(luaState, "btnp", btnp); lua_register(luaState, "time", time); lua_register(luaState, "t", time); //audio: lua_register(luaState, "music", music); lua_register(luaState, "sfx", sfx); //memory lua_register(luaState, "cstore", cstore); lua_register(luaState, "memcpy", api_memcpy); lua_register(luaState, "memset", api_memset); lua_register(luaState, "peek", peek); lua_register(luaState, "poke", poke); lua_register(luaState, "peek2", peek2); lua_register(luaState, "poke2", poke2); lua_register(luaState, "peek4", peek4); lua_register(luaState, "poke4", poke4); lua_register(luaState, "reload", reload); lua_register(luaState, "reset", reset); //cart data // lua_register(luaState, "cartdata", cartdata); lua_register(luaState, "__cartdata", cartdata); lua_register(luaState, "dget", dget); lua_register(luaState, "dset", dset); // lua_register(luaState, "printh", printh); lua_register(luaState, "stat", stat); lua_register(luaState, "_update_buttons", _update_buttons); lua_register(luaState, "run", run); lua_register(luaState, "extcmd", extcmd); lua_register(luaState, "_set_fps", setFps); //rng lua_register(luaState, "rnd", rnd); lua_register(luaState, "srand", srand); //load in global lua fuctions for pico 8- part of this is setting a local variable //with the same name as all the globals we just registered //auto convertedGlobalLuaFunctions = convert_emojis(p8GlobalLuaFunctions); auto convertedP8Bios = charset::utf8_to_pico8(p8Bios); int loadedBiosResult = luaL_dostring(luaState, convertedP8Bios.c_str()); if (loadedBiosResult != LUA_OK) { Logger_Write("ERROR loading pico 8 bios\n"); Logger_Write("Error: %s\n", lua_tostring(luaState, -1)); lua_pop(luaState, 1); return false; } // Push the eris.init_persist_all function on the top of the lua stack (or nil if it doesn't exist) // we call this function to establish the default global state of things not to save in the save state // needs to be called after globals are loaded but before the cart is run, or _init is called //TODO: move these calls to the glue code? // lua_getglobal(luaState, "eris"); // lua_getfield(luaState, -1, "init_persist_all"); // if (lua_pcall(luaState, 0, 0, 0)){ // Logger_Write("Error setting up lua persistence: %s\n", lua_tostring(luaState, -1)); // lua_pop(luaState, 1); // return false; // } // //pop the eris.init_persist_all fuction off the stack now that we're done with it // lua_pop(luaState, 1); return true; } int panic_hook(lua_State* l) { char const *message = lua_tostring(l, -1); printf("Lua panic: %s\n", message); Logger_Write("ERROR: Lua panic: %s\n", message); return 0; } void printLuaStack(lua_State* L) { int top = lua_gettop(L); std::string str = "From top to bottom, the lua stack is \n"; for (unsigned index = top; index > 0; index--) { int type = lua_type(L, index); switch (type) { // booleans case LUA_TBOOLEAN: str = str + (lua_toboolean(L, index) ? "true" : "false") + "\n"; break; // numbers case LUA_TNUMBER: str = str + std::to_string(lua_tonumber(L, index)) + "\n"; break; // strings case LUA_TSTRING: str = str + lua_tostring(L, index) + "\n"; break; // other default: str = str + lua_typename(L, type) + "\n"; break; } } str = str + "\n"; std::cout << str; } Vm::Vm( Host* host, PicoRam* memory, Graphics* graphics, Input* input, Audio* audio) : _loadedCart(nullptr), _luaState(nullptr), _cleanupDeps(false), //_targetFps(30), _picoFrameCount(0), _cartChangeQueued(false), _nextCartKey(""), _cartLoadError(""), _cartdataKeyCount(0), _currentCartdataKey("") { _host = host; if (memory == nullptr) { memory = new PicoRam(); _cleanupDeps = true; } _memory = memory; _pauseMenu = false; _clearInputOnResume = false; memset(_drawStateCopy, 0, sizeof(drawState_t)); if (graphics == nullptr) { graphics = new Graphics(get_font_data(), _memory); _cleanupDeps = true; } _graphics = graphics; if (input == nullptr){ input = new Input(memory); _cleanupDeps = true; } _input = input; if (audio == nullptr) { audio = new Audio(_memory); _cleanupDeps = true; } _audio = audio; //this can probably go away when I'm loading actual carts and just have to expose api to lua Logger_Write("Initializing global api\n"); initPicoApi(_memory, _graphics, _input, this, _audio); //initGlobalApi(_graphics); //reset memory (may have to be more selective about zeroing out to be accurate?) _memory->Reset(); //seed rng auto now = std::chrono::high_resolution_clock::now(); api_srand(fix32::frombits((int32_t)now.time_since_epoch().count())); //set graphics state _graphics->color(); _graphics->clip(); _graphics->pal(); //reset audio _audio->resetAudioState(); _luaState = luaL_newstate(); lua_atpanic(_luaState, panic_hook); lua_setpico8memory(_luaState, (uint8_t *)&_memory->data); _initializeLuaState(_luaState); } Vm::~Vm(){ CloseCart(); if (_luaState != nullptr) { Logger_Write("closing lua state\n"); lua_close(_luaState); _luaState = nullptr; Logger_Write("closed lua state\n"); } else { Logger_Write("lua state was null\n"); } if (_cleanupDeps){ if (_input != nullptr) { delete _input; } if (_graphics != nullptr) { delete _graphics; } if (_audio != nullptr) { delete _audio; } if (_memory != nullptr) { delete _memory; } } } PicoRam* Vm::getPicoRam(){ return _memory; } jmp_buf place; bool abortLua; bool Vm::loadCart(Cart* cart) { _picoFrameCount = 0; _cartdataKeyCount = 0; _currentCartdataKey = ""; //reset memory (may have to be more selective about zeroing out to be accurate?) _memory->Reset(); //seed rng auto now = std::chrono::high_resolution_clock::now(); api_srand(fix32::frombits((int32_t)now.time_since_epoch().count())); //set graphics state _graphics->color(); _graphics->clip(); _graphics->pal(); if (cart->LuaString == "") { if (_cartLoadError == "") { _cartLoadError = "No Lua to load. Aborting cart load"; } Logger_Write("%s\n", _cartLoadError.c_str()); // Clear cart change queue to prevent infinite loop _cartChangeQueued = false; return false; } //reset audio _audio->resetAudioState(); //copy data from cart rom to ram vm_reload(0, 0, sizeof(cart->CartRom), cart); _loadedCart = cart; _cartChangeQueued = false; abortLua = false; //customize bios per host's requirements if (cart->FullCartPath == DefaultCartName || cart->FullCartPath == SettingsCartName) { std::string customBiosLua = _host->customBiosLua(); if (customBiosLua.length() > 0) { int doStrRes = luaL_dostring(_luaState, customBiosLua.c_str()); if (doStrRes != LUA_OK){ //bad lua passed Logger_Write("Error: %s\n", lua_tostring(_luaState, -1)); lua_pop(_luaState, 1); } } } //move this to cart glue code? if (_cartBreadcrumb.length() > 0) { ExecuteLua("__addbreadcrumb(\"" + _cartBreadcrumb +"\", \"" + _prevCartKey +"\")", ""); } // Only clear error if we successfully loaded a non-default cart // (preserve error when loading default cart to display it) if (cart->FullCartPath != DefaultCartName) { _cartLoadError = ""; } return true; } bool Vm::LoadBiosCart(){ CloseCart(); Cart *cart = new Cart(DefaultCartName, ""); bool success = loadCart(cart); if (!success) { CloseCart(); } return success; } void Vm::LoadSettingsCart(){ CloseCart(); Cart *cart = new Cart(SettingsCartName, ""); bool success = loadCart(cart); if (!success) { CloseCart(); } } bool Vm::LoadCart(std::string filename, bool loadBiosOnFail){ // if (filename == "__FAKE08-DEFAULT.p8") { // LoadBiosCart(); // return; // } if (filename == "__FAKE08-SETTINGS.p8") { LoadSettingsCart(); return true; } Logger_Write("Loading cart %s\n", filename.c_str()); CloseCart(); #if !defined(__PS4__) && !defined(__ORBIS__) // If the filename is an absolute path on desktop/handhelds, update the cart directory. // On PS4, keep _cartDirectory pointed to writable /data/picoplay/carts so BBS downloads // never attempt to write into read-only /app0/carts or USB mounts. if (isAbsolutePath(filename)) { std::string dir = getDirectory(filename); if (dir.length() > 0) { Logger_Write("Setting cart directory to %s\n", dir.c_str()); _host->setCartDirectory(dir); // Also update the cart list for the default cart SetCartList(_host->listcarts()); } } #endif Logger_Write("Calling Cart Constructor\n"); auto cartDir = _host->getCartDirectory(); Cart *cart = new Cart(filename, cartDir); Logger_Write("Cart Constructor called\n"); _cartLoadError = cart->LoadError; // If cart has a load error (file not found, etc.), load default cart with error if (cart->LoadError.length() > 0) { Logger_Write("Cart load error: %s\n", cart->LoadError.c_str()); // Save the error message so default cart can display it _cartLoadError = cart->LoadError; delete cart; // Load default cart to display error LoadBiosCart(); return false; } bool success = loadCart(cart); Logger_Write("Loaded cart into vm\n"); if (!success) { delete cart; if (_cartLoadError.length() == 0) { _cartLoadError = "Cart file has no Lua code"; } LoadBiosCart(); } return success; } bool Vm::LoadCart(const unsigned char* cartData, size_t size, bool loadBiosOnFail){ Logger_Write("Loading cart from memory\n"); CloseCart(); Logger_Write("Calling Cart Constructor\n"); Cart *cart = new Cart(cartData, size); _cartLoadError = cart->LoadError; bool success = loadCart(cart); if (loadBiosOnFail && !success) { CloseCart(); //todo: show an error message on the bios? //LoadBiosCart(); } return success; } void Vm::togglePauseMenu(){ _input->SetState(0, 0); if (_memory->drawState.suppressPause) { _memory->drawState.suppressPause = 0; return; } _pauseMenu = !_pauseMenu; if (_pauseMenu){ //save old draw state //0x5f00-0x5f3f - 64 bytes memcpy(_drawStateCopy, &_memory->drawState, 64); _graphics->pal(); _graphics->fillp(0); _graphics->clip(); _graphics->camera(); _graphics->color(); // Pause audio _audio->setPaused(true); } else{ //restore old draw state memcpy(&_memory->drawState, _drawStateCopy, 64); // Clear input on next update so the button press that closed the menu // doesn't get passed to the cart _clearInputOnResume = true; // Resume audio _audio->setPaused(false); } } bool Vm::IsPaused(){ return _pauseMenu; } //https://stackoverflow.com/a/30606613 std::vector HexToBytes(std::string hex) { std::vector bytes; hex.erase(std::remove(hex.begin(), hex.end(), '\n'), hex.end()); hex.erase(std::remove(hex.begin(), hex.end(), '\r'), hex.end()); for (unsigned int i = 0; i < hex.length(); i += 2) { std::string byteString = hex.substr(i, 2); uint8_t byte = (uint8_t) strtol(byteString.c_str(), NULL, 16); bytes.push_back(byte); } return bytes; } std::string Vm::getSerializedCartData() { std::string outputstr = ""; char hex_string[9] = "00000000"; //ATTN: writing one byte at a time instead of one 32 bit int at a time //to ensure same behavior across platforms and cpu architectures for(int i = 0; i < 64; i++){ for(int b = 3; b >= 0; b--){ uint8_t byte = _memory->data[0x5e00 + (i*4) + b]; sprintf(hex_string, "%02x", byte); outputstr.append(hex_string); } if ((i + 1) % 8 == 0) { outputstr.append("\n"); } } return outputstr; } void Vm::deserializeCartDataToMemory(std::string cartDataStr) { //populate from string (assume correct length? TODO: validation) std::vector bytesVector = HexToBytes(cartDataStr); //ATTN: writing one byte at a time instead of one 32 bit int at a time //to ensure same behavior across platforms and cpu architectures for(size_t i = 0; i < 64; i++){ size_t idxStart = (i*4); size_t idxEnd = idxStart + 3; if (idxEnd < bytesVector.size()){ _memory->data[0x5e00 + idxStart + 0] = bytesVector[idxEnd]; _memory->data[0x5e00 + idxStart + 1] = bytesVector[idxEnd - 1]; _memory->data[0x5e00 + idxStart + 2] = bytesVector[idxEnd - 2]; _memory->data[0x5e00 + idxStart + 3] = bytesVector[idxEnd - 3]; } } } bool Vm::Step(){ _picoFrameCount++; bool ret = false; lua_getglobal(_luaState, "__z8_tick"); int status = lua_pcall(_luaState, 0, 1, 0); if (status != LUA_OK) { char const *message = lua_tostring(_luaState, -1); Logger_Write("error calling tick function: %s\n", message); _cartLoadError = "Error in main loop: " + std::string(message); lua_pop(_luaState, 1); // Return to menu on error QueueCartChange(DefaultCartName); return false; } else { int tickResult = (int)lua_tonumber(_luaState, -1); ret = tickResult >= 0; // If __z8_tick returned -1, it means there was an error (already handled in Lua) // but we should ensure error message is set if it's not already if (tickResult == -1 && _cartLoadError.empty()) { _cartLoadError = "Error in cart execution"; } } lua_pop(_luaState, 1); // Process any queued cart change (from reset, load, etc.) if (_cartChangeQueued) { Logger_Write("Processing cart change, _pauseMenu = %s\n", _pauseMenu ? "true" : "false"); _prevCartKey = CurrentCartFilename(); if (_nextCartSize > 0){ LoadCart(_nextCartData, _nextCartSize); } else { LoadCart(_nextCartKey); } // Only run if cart loaded successfully (has Lua code) if (_loadedCart && !_loadedCart->LuaString.empty()) { Logger_Write("Before vm_run, _pauseMenu = %s\n", _pauseMenu ? "true" : "false"); vm_run(); Logger_Write("After vm_run, _pauseMenu = %s\n", _pauseMenu ? "true" : "false"); } else { Logger_Write("Cart load failed, not running vm_run()\n"); // Clear cart change queue to prevent infinite retry loop _cartChangeQueued = false; } } return ret; } // void Vm::UpdateAndDraw() { // update_buttons(); // _picoFrameCount++; // if (_cartChangeQueued) { // _prevCartKey = CurrentCartFilename(); // if (_nextCartSize > 0){ // LoadCart(_nextCartData, _nextCartSize); // } // else { // LoadCart(_nextCartKey); // } // } // if (_pauseMenu){ // lua_getglobal(_luaState, "__f08_menu_update"); // lua_call(_luaState, 0, 0); // lua_pop(_luaState, 0); // lua_getglobal(_luaState, "__f08_menu_draw"); // lua_call(_luaState, 0, 0); // lua_pop(_luaState, 0); // } // else{ // // Push the _update function on the top of the lua stack // if (_targetFps == 60) { // lua_getglobal(_luaState, "_update60"); // if (!lua_isfunction(_luaState, -1)) { // lua_getglobal(_luaState, "_update"); // } // } else { // lua_getglobal(_luaState, "_update"); // if (!lua_isfunction(_luaState, -1)) { // lua_getglobal(_luaState, "_update60"); // } // } // if (lua_isfunction(_luaState, -1)) { // if (lua_pcall(_luaState, 0, 0, 0)){ // _cartLoadError = lua_tostring(_luaState, -1); // Logger_Write("Error: %s\n", lua_tostring(_luaState, -1)); // lua_pop(_luaState, 1); // QueueCartChange(BiosCartName); // return; // } // } // //pop the update fuction off the stack now that we're done with it // lua_pop(_luaState, 0); // lua_getglobal(_luaState, "_draw"); // if (lua_isfunction(_luaState, -1)) { // if (lua_pcall(_luaState, 0, 0, 0)){ // _cartLoadError = lua_tostring(_luaState, -1); // Logger_Write("Error: %s\n", lua_tostring(_luaState, -1)); // lua_pop(_luaState, 1); // QueueCartChange(BiosCartName); // return; // } // } // lua_pop(_luaState, 0); // if (_input->btnp(6)) { // togglePauseMenu(); // } // } // } uint8_t* Vm::GetPicoInteralFb(){ return _graphics->GetP8FrameBuffer(); } uint8_t* Vm::GetScreenPaletteMap(){ return _graphics->GetScreenPaletteMap(); } void Vm::FillAudioBuffer(void *audioBuffer, size_t offset, size_t size){ _audio->FillAudioBuffer(audioBuffer, offset, size); } void Vm::CloseCart() { if (_loadedCart){ Logger_Write("deleting cart\n"); delete _loadedCart; _loadedCart = nullptr; } // if (_luaState) { // Logger_Write("closing lua state\n"); // lua_close(_luaState); // _luaState = nullptr; // } Logger_Write("writing cart data\n"); if (_currentCartdataKey.length() > 0) { _host->saveCartData(_currentCartdataKey, getSerializedCartData()); } Logger_Write("resetting state\n"); _targetFps = 60; _picoFrameCount = 0; } void Vm::QueueCartChange(std::string filename){ _nextCartKey = filename; _nextCartData = nullptr; _nextCartSize = 0; _cartChangeQueued = true; _pauseMenu = false; _clearInputOnResume = true; _audio->setPaused(false); } void Vm::QueueCartChange(const unsigned char* cartData, size_t size){ _nextCartKey = ""; _nextCartData = cartData; _nextCartSize = size; _cartChangeQueued = true; _pauseMenu = false; _clearInputOnResume = true; _audio->setPaused(false); } int Vm::GetTargetFps() { return _targetFps; } std::string Vm::CurrentCartFilename(){ if (_loadedCart){ return _loadedCart->FullCartPath; } return ""; } int Vm::GetFrameCount() { return _picoFrameCount; } void Vm::SetCartList(vector cartList){ std::sort(cartList.begin(), cartList.end()); _cartList = cartList; } vector Vm::GetCartList(){ return _cartList; } vector Vm::GetDirList(){ return _host->listdirs(); } bool Vm::ChangeDirectory(string dir){ std::string currentDir = _host->getCartDirectory(); std::string newDir; if (dir == "..") { // Go up one directory size_t lastSlash = currentDir.find_last_of("/\\"); if (lastSlash != std::string::npos && lastSlash > 0) { newDir = currentDir.substr(0, lastSlash); } else { return false; // Already at root } } else if (dir.length() > 0 && (dir[0] == '/' || dir.find(':') != std::string::npos)) { // Absolute path newDir = dir; } else { // Relative path newDir = currentDir + "/" + dir; } // Verify the directory exists by trying to list its contents _host->setCartDirectory(newDir); vector testList = _host->listcarts(); // Update cart list even if empty (the directory might only contain subdirs) SetCartList(testList); Logger_Write("Changed directory to: %s\n", newDir.c_str()); return true; } string Vm::GetCurrentDirectory(){ return _host->getCartDirectory(); } string Vm::GetBiosError() { return _cartLoadError; } bool Vm::DownloadBBSCart(string lid, string filename) { if (!_host) return false; bool ok = BBSClient::DownloadCartByLid(lid, filename, _host->getCartDirectory()); if (ok) { SetCartList(_host->listcarts()); } return ok; } bool Vm::IsBBSCartDownloaded(string filename) { if (!_host) return false; return BBSClient::IsCartDownloaded(filename, _host->getCartDirectory()); } vector Vm::GetBBSFeaturedCarts() { if (!_host) return {}; return BBSClient::GetFeaturedCarts(_host->getCartDirectory()); } void Vm::RefreshCartList() { if (_host) { SetCartList(_host->listcarts()); } } void Vm::GameLoop() { Logger_Write("Start of GameLoop()\n"); while (_host->shouldRunMainLoop()) { //shouldn't need to set this every frame //_host->setTargetFps(_targetFps); //is this better at the end of the loop? _host->waitForTargetFps(); if (_host->shouldQuit()) break; // break in order to return to hbmenu //this should probably be handled just in the host class _host->changeStretch(); //update buttons needs to be callable from the cart, and also flip //it should update call the pico part of scanInput and set the values in memory //then we don't need to pass them in here //UpdateAndDraw(); Step(); uint8_t* picoFb = GetPicoInteralFb(); uint8_t* screenPaletteMap = GetScreenPaletteMap(); _host->drawFrame(picoFb, screenPaletteMap, _memory->drawState.drawMode); if (_host->shouldFillAudioBuff()) { FillAudioBuffer(_host->getAudioBufferPointer(), 0, _host->getAudioBufferSize()); _host->playFilledAudioBuffer(); } } } bool Vm::ExecuteLua(string luaString, string callbackFunction) { // Get the sandbox environment lua_getglobal(_luaState, "__cart_sandbox"); if (!lua_istable(_luaState, -1)) { fprintf(stderr, "__cart_sandbox is not a table\n"); lua_pop(_luaState, 1); return false; } // Load the Lua code with the sandbox as its environment if (luaL_loadstring(_luaState, luaString.c_str()) != LUA_OK) { fprintf(stderr, "Error loading Lua code: %s\n", lua_tostring(_luaState, -1)); lua_pop(_luaState, 2); // Remove the error message and sandbox table return false; } // Set the environment of the loaded function to the sandbox lua_pushvalue(_luaState, -2); // Duplicate the sandbox table lua_setupvalue(_luaState, -2, 1); // Set it as the first upvalue of the function (environment) // Execute the loaded Lua code in the sandbox environment if (lua_pcall(_luaState, 0, 0, 0) != LUA_OK) { fprintf(stderr, "Error executing Lua code: %s\n", lua_tostring(_luaState, -1)); lua_pop(_luaState, 2); // Remove the error message and sandbox table return false; } if (callbackFunction.length() > 0) { // Get the callback function from the sandbox lua_getfield(_luaState, -1, callbackFunction.c_str()); if (!lua_isfunction(_luaState, -1)) { fprintf(stderr, "Callback function '%s' not found in sandbox\n", callbackFunction.c_str()); lua_pop(_luaState, 2); // Remove the nil value and sandbox table return false; } // Call the function if (lua_pcall(_luaState, 0, 1, 0) != LUA_OK) { fprintf(stderr, "Error calling callback function: %s\n", lua_tostring(_luaState, -1)); lua_pop(_luaState, 2); // Remove the error message and sandbox table return false; } // Get the result bool result = lua_toboolean(_luaState, -1); lua_pop(_luaState, 2); // Remove the result and sandbox table return result; } // Clean up the sandbox table lua_pop(_luaState, 1); return true; } const int MemoryUpperBound = 0x10000; uint8_t Vm::vm_peek(int addr){ if (addr < 0 || addr > MemoryUpperBound){ return 0; } return _memory->data[addr]; } int16_t Vm::vm_peek2(int addr){ //zepto8 int16_t bits = 0; for (int i = 0; i < 2; ++i) { /* This code handles partial reads by adding zeroes */ if (addr + i < MemoryUpperBound) bits |= _memory->data[addr + i] << (8 * i); else if (addr + i >= MemoryUpperBound) bits |= _memory->data[addr + i - MemoryUpperBound] << (8 * i); } return bits; } //note: this should return a 32 bit fixed point number fix32 Vm::vm_peek4(int addr){ //zepto8 int32_t bits = 0; for (int i = 0; i < 4; ++i) { /* This code handles partial reads by adding zeroes */ if (addr + i < MemoryUpperBound) bits |= _memory->data[addr + i] << (8 * i); else if (addr + i >= MemoryUpperBound) bits |= _memory->data[addr + i - MemoryUpperBound] << (8 * i); } return fix32::frombits(bits); } void Vm::vm_poke(int addr, uint8_t value){ //todo: check how pico 8 handles out of bounds if (addr < 0 || addr > MemoryUpperBound){ return; } _memory->data[addr] = value; } void Vm::vm_poke2(int addr, int16_t value){ if (addr < 0 || addr > MemoryUpperBound - 1){ return; } _memory->data[addr] = (uint8_t)value; _memory->data[addr + 1] = (uint8_t)(value >> 8); } void Vm::vm_poke4(int addr, fix32 value){ if (addr < 0 || addr > MemoryUpperBound - 3){ return; } uint32_t ubits = (uint32_t)value.bits(); _memory->data[addr + 0] = (uint8_t)ubits; _memory->data[addr + 1] = (uint8_t)(ubits >> 8); _memory->data[addr + 2] = (uint8_t)(ubits >> 16); _memory->data[addr + 3] = (uint8_t)(ubits >> 24); } bool Vm::vm_cartdata(string key) { //match pico 8 errors if (key.length() == 0 || key.length() > 64) { QueueCartChange(DefaultCartName); _cartLoadError = "cart data id too long"; return false; } //todo: validate chars // If we have a current key, save its data before switching if (_currentCartdataKey.length() > 0) { _host->saveCartData(_currentCartdataKey, getSerializedCartData()); } // Check if this key is already in our list bool keyExists = false; for (int i = 0; i < _cartdataKeyCount; i++) { if (_cartdataKeys[i] == key) { keyExists = true; break; } } // If it's a new key, add it to our list (up to 4 keys max) if (!keyExists) { if (_cartdataKeyCount >= 4) { QueueCartChange(DefaultCartName); _cartLoadError = "too many cart data keys (max 4)"; return false; } _cartdataKeys[_cartdataKeyCount] = key; _cartdataKeyCount++; } // Only clear memory if we're switching to a different key if (_currentCartdataKey != key) { // Clear the cart data memory (0x5e00-0x5eff) memset(&_memory->data[0x5e00], 0, 0x100); } // Set this as the current active key _currentCartdataKey = key; // Try to load data from file for this key auto cartDataStr = _host->getCartDataFileContents(_currentCartdataKey); //todo: validate hex format if (cartDataStr.length() > 0) { deserializeCartDataToMemory(cartDataStr); return true; } else { return false; } } fix32 Vm::vm_dget(uint8_t n) { if (n < 64) { return vm_peek4(0x5e00 + 4 * n); } return 0; } void Vm::vm_dset(uint8_t n, fix32 value){ if (n < 64) { vm_poke4(0x5e00 + 4 * n, value); } } void Vm::vm_reload(int destaddr, int sourceaddr, int len, Cart* cart){ if (len <= 0) { return; } memcpy(&_memory->data[destaddr], &cart->CartRom.data[sourceaddr], len); } void Vm::vm_reload(int destaddr, int sourceaddr, int len, string filename){ if (len <= 0) { return; } if (destaddr < 0 || destaddr > (int)sizeof(PicoRam)) { //invalid dest address return; } if (sourceaddr < 0 || sourceaddr > (int)sizeof(CartRomData)) { //invalid source address return; } if (len < 0 || destaddr + len > (int)sizeof(PicoRam)) { //invalid length address return; } Cart* cart = _loadedCart; bool multicart = false; if (filename.length() > 0) { cart = new Cart(filename, _host->getCartDirectory()); if (cart->LoadError.length() > 0) { //error, can't load cart //todo: see what kind of error pico 8 throws, emulate delete cart; return; } multicart = true; } vm_reload(destaddr, sourceaddr, len, cart); if (multicart) { delete cart; } } void Vm::vm_memset(int destaddr, uint8_t val, int len){ if (len <= 0) { return; } if (destaddr < 0 || destaddr + len > (int)sizeof(PicoRam)) { return; } memset(&_memory->data[destaddr], val, len); } void Vm::vm_memcpy(int destaddr, int sourceaddr, int len){ if (len <= 0) { return; } if (sourceaddr < 0 || sourceaddr + len > (int)sizeof(PicoRam)) { return; } if (destaddr < 0 || destaddr + len > (int)sizeof(PicoRam)) { return; } memcpy(&_memory->data[destaddr], &_memory->data[sourceaddr], len); } void Vm::update_prng() { uint32_t* rngState = _memory->hwState.rngState; rngState[1] = rngState[0] + ((rngState[1] >> 16) | (rngState[1] << 16)); rngState[0] += rngState[1]; } fix32 Vm::api_rnd() { return api_rnd((fix32)1); } fix32 Vm::api_rnd(fix32 in_range) { update_prng(); uint32_t b = _memory->hwState.rngState[1]; uint32_t range = in_range.bits(); return fix32::frombits(range > 0 ? b % range : 0); } void Vm::api_srand(fix32 seed) { uint32_t* rngState = _memory->hwState.rngState; rngState[0] = seed ? seed.bits() : 0xdeadbeef; rngState[1] = rngState[0] ^ 0xbead29ba; for (int i = 0; i < 32; ++i) { update_prng(); } } void Vm::update_buttons() { //get button states from hardware auto inputState = _host->scanInput(); // If we just resumed from pause menu, clear input so the button press // that closed the menu doesn't get passed to the cart. // Keep clearing until the user releases all buttons. if (_clearInputOnResume) { // Check if action buttons (4=O, 5=X) are still held if ((inputState.KHeld & 0x30) == 0) { _clearInputOnResume = false; } _input->SetState(0, 0); _input->SetMouse(0, 0, 0); _input->SetKeyboard(false, ""); return; } _input->SetState(inputState.KDown, inputState.KHeld); if (_memory->drawState.devkitMode) { _input->SetMouse(inputState.mouseX, inputState.mouseY, inputState.mouseBtnState); _input->SetKeyboard(inputState.KBdown,inputState.KBkey); } else { _input->SetMouse(0, 0, 0); _input->SetKeyboard(false,""); } // Check for pause button (bit 6 / 0x40) if (_input->btnp(6)) { togglePauseMenu(); } } // void Vm::vm_flip() { // if (!_host->shouldRunMainLoop()){ // abortLua = true; // if (abortLua){ // longjmp(place, 1); // } // } // if (!_host->shouldQuit() && !_cartChangeQueued) { // update_buttons(); // _picoFrameCount++; // //todo: pause menu here, but for now just load bios // if (_input->btnp(6)) { // //QueueCartChange(BiosCartName); // togglePauseMenu(); // //shouldn't get here // return; // } // _host->changeStretch(); // _host->setTargetFps(_targetFps); // uint8_t* picoFb = GetPicoInteralFb(); // uint8_t* screenPaletteMap = GetScreenPaletteMap(); // if (_pauseMenu){ // //pause menu probably needs refactor out of lua. For now this is better than just quitting // lua_getglobal(_luaState, "__f08_menu_update"); // lua_call(_luaState, 0, 0); // lua_pop(_luaState, 0); // lua_getglobal(_luaState, "__f08_menu_draw"); // lua_call(_luaState, 0, 0); // lua_pop(_luaState, 0); // } // _host->drawFrame(picoFb, screenPaletteMap, _memory->drawState.drawMode); // //is this better at the end of the loop? // _host->waitForTargetFps(); // } // } void Vm::vm_run() { // if (_loadedCart) { // loadCart(_loadedCart); // } //should combine this with other memory reset code _memory->Reset(); //seed rng auto now = std::chrono::high_resolution_clock::now(); api_srand(fix32::frombits((int32_t)now.time_since_epoch().count())); //set graphics state _graphics->color(); _graphics->clip(); _graphics->pal(); _audio->resetAudioState(); // Clear any pending input so button presses from pause menu don't // get seen by the cart's _init() function. The flag stays set so the // first _update_buttons() call will also return cleared input. if (_clearInputOnResume) { _input->SetState(0, 0); } lua_getglobal(_luaState, "__z8_run_cart"); lua_pushstring(_luaState, _loadedCart->LuaString.c_str()); int status = lua_pcall(_luaState, 1, 0, 0); if (status != LUA_OK) { char const *message = lua_tostring(_luaState, -1); Logger_Write("error in vm_run: %s\n", message); _cartLoadError = "Error loading cart: " + std::string(message); lua_pop(_luaState, 1); // Return to menu on error QueueCartChange(DefaultCartName); } } void Vm::vm_extcmd(std::string cmd){ //screenshots, recording, and breadcrumb not supported if (cmd == "reset"){ QueueCartChange(CurrentCartFilename()); } else if (cmd == "pause") { togglePauseMenu(); } else if (cmd == "shutdown") { QueueCartChange(DefaultCartName); } } bool Vm::vm_load(std::string filename, std::string breadcrumb, std::string param){ Logger_Write("vm_load: loading %s\n", filename.c_str()); _cartBreadcrumb = breadcrumb; if (param.length() > 0) { _cartParam = param; } if (_currentCartdataKey.length() > 0) { _host->saveCartData(_currentCartdataKey, getSerializedCartData()); } _prevCartKey = CurrentCartFilename(); bool success = LoadCart(filename); // Only run if cart loaded successfully if (success) { vm_run(); return true; } else { return false; } } void Vm::vm_reset(){ memset(&_memory->data[0x5f00], 0, 0x7f); _memory->hwState.colorBitmask = 0xff; _memory->hwState.spriteSheetMemMapping = 0x00; _memory->hwState.screenDataMemMapping = 0x60; _memory->hwState.mapMemMapping = 0x20; _memory->hwState.widthOfTheMap = 128; _graphics->color(); _graphics->clip(); _graphics->pal(); } void Vm::setTargetFps(int targetFps){ //currently handled by lua loop? //_targetFps = targetFps; } int Vm::getFps(){ //TODO: return actual fps (as fix32?) return _targetFps; } int Vm::getTargetFps(){ return _targetFps; } int Vm::getYear(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_year + 1900; } int Vm::getMonth(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_mon + 1; } int Vm::getDay(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_mday; } int Vm::getHour(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_hour; } int Vm::getMinute(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_min; } int Vm::getSecond(){ std::time_t t = std::time(0); std::tm* now = std::localtime(&t); return now->tm_sec; } std::string Vm::getCartBreadcrumb() { return _cartBreadcrumb; } std::string Vm::getCartParam() { return _cartParam; } //settings int Vm::getSetting(std::string sname) { return _host->getSetting(sname); } void Vm::setSetting(std::string sname, int sval) { _host->setSetting(sname,sval); } void Vm::installPackins() { #if LOAD_PACK_INS _host->setSetting("packinloaded",0); _host->unpackCarts(); #endif } void Vm::loadLabel(std::string filename, bool mini, int minioffset) { auto cartDir = _host->getCartDirectory(); Cart *labelcart = new Cart(filename, cartDir); std::string labelstr = labelcart->LabelString; if(labelstr.length() == 0){ labelstr = NoLabelString; } if (mini) { copy_mini_label_to_sprite_memory(_memory->spriteSheetData, labelstr, minioffset); } else { copy_string_to_sprite_memory(_memory->spriteSheetData, labelstr); } delete labelcart; } std::string Vm::getLuaLine(string filename, int linenumber) { auto cartDir = _host->getCartDirectory(); Cart *luacart = new Cart(filename, cartDir); std::string luastr = luacart->LuaString; std::string line; std::istringstream luastream(luastr); while (linenumber-- >= 0){ std::getline(luastream,line); } delete luacart; return line; } size_t Vm::serializeLuaState(char* dest) { lua_getglobal(_luaState, "eris"); lua_getfield(_luaState, -1, "persist_all"); if (lua_pcall(_luaState, 0, 1, 0) != 0) { std::string e = lua_tostring(_luaState, -1); lua_pop(_luaState, 1); return 0; } size_t len; const char* result = lua_tolstring(_luaState, -1, &len); memcpy(dest, result, len); lua_pop(_luaState, 2); return len; } void Vm::deserializeLuaState(const char* src, size_t len) { lua_getglobal(_luaState, "eris"); lua_getfield(_luaState, -1, "restore_all"); lua_pushlstring(_luaState, src, len); if (lua_pcall(_luaState, 1, 0, 0) != 0) { std::string e = lua_tostring(_luaState, -1); lua_pop(_luaState, 1); return; } lua_pop(_luaState, 1); }