Files
birth1mark--picoplay-ps4/source/vm.cpp
T
birth1mark 29a17766a1 Add in-app Lexaloffle BBS Store and cartridge downloader
- Added integrated 🌐 BBS Store tab to arcade BIOS interface (DefaultCart.h)
- Implemented C++ Lua API bindings: __bbs_listfeatured, __bbs_download, __bbs_isdownloaded, __refreshcarts
- Curated top-rated community games catalog with 1-click download & play
- Added custom BBS Cartridge ID downloader with on-screen character picker
- Added robust magic-byte verification (PNG/p8) on downloaded cartridges
- Updated README.md and ARCHITECTURE.md documentation
2026-10-22 19:12:10 +01:00

1553 lines
42 KiB
C++

#include <string>
#include <functional>
#include <chrono>
#include <math.h>
#include <setjmp.h>
#include <algorithm>
#include <string.h>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <ctime>
#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 <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <fix32.h>
//}
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 the filename is an absolute path, update the cart directory
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());
}
}
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<uint8_t> HexToBytes(std::string hex) {
std::vector<uint8_t> 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<uint8_t> 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<string> cartList){
std::sort(cartList.begin(), cartList.end());
_cartList = cartList;
}
vector<string> Vm::GetCartList(){
return _cartList;
}
vector<string> 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<string> 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<BBSCartItem> 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);
}