#include #include #include #include #include #include #include #include #include #include #include #include #include #include "lua.h" #include "lauxlib.h" #include "lualib.h" #include "audio.h" #include "font8x8.h" #include "pico_games.h" #undef ESP_LOGI #undef ESP_LOGW #undef ESP_LOGE #define ESP_LOGI(t,f,...) printf("[I] " f "\n",##__VA_ARGS__) #define ESP_LOGW(t,f,...) printf("[W] " f "\n",##__VA_ARGS__) #define ESP_LOGE(t,f,...) printf("[E] " f "\n",##__VA_ARGS__) #define TAG "LuaGame" #define GAME_W 240 #define GAME_H 240 #define TICK_MS 16 /* stubs for missing libc symbols */ void clearerr(FILE* f){(void)f;} clock_t clock(void){return 0;} char* getenv(const char* n){(void)n;return NULL;} struct tm* gmtime(const time_t* t){(void)t; static struct tm d={0};return &d;} char* setlocale(int c,const char* l){(void)c;(void)l;return NULL;} int setvbuf(FILE* s,char* b,int m,size_t z){(void)s;(void)b;(void)m;(void)z;return 0;} FILE* tmpfile(void){return NULL;} char* tmpnam(char* s){if(s)s[0]=0;return s;} int ungetc(int c,FILE* st){(void)st;return c;} typedef struct { AppHandle app; lv_obj_t* root; lv_obj_t* toolbar; lv_obj_t* game_area; lv_obj_t* canvas; lv_obj_t* label_layer; lv_obj_t* fps_label; uint16_t* fb; lv_timer_t* timer; TaskHandle_t lua_task; bool touch_pressed; bool prev_touch_pressed; int touch_x; int touch_y; lua_State* L; bool lua_ok; bool initialized; bool is_pico; uint64_t last_tick_us; bool should_exit; // fps uint32_t fps_frames; uint64_t fps_last_us; float fps; } AppCtx; static AppCtx* g_ctx=NULL; static uint16_t hex_to_rgb565(uint32_t hex){ uint8_t r=(hex>>16)&0xFF,g=(hex>>8)&0xFF,b=hex&0xFF; return ((r>>3)<<11)|((g>>2)<<5)|(b>>3); } static inline void set_px(AppCtx* c,int x,int y,uint16_t col){ if(!c||!c->fb)return; if(x<0||y<0||x>=GAME_W||y>=GAME_H)return; c->fb[y*GAME_W+x]=col; } static void fill_rect(AppCtx* c,int x,int y,int w,int h,uint16_t col,bool filled){ if(w<=0||h<=0)return; if(!filled){ for(int i=x;i GAME_W? GAME_W:x+w,y1=y+h>GAME_H?GAME_H:y+h; for(int yy=y0;yyfb+yy*GAME_W; for(int xx=x0;xx=y){set_px(c,cx+x,cy+y,col);set_px(c,cx+y,cy+x,col);set_px(c,cx-y,cy+x,col);set_px(c,cx-x,cy+y,col);set_px(c,cx-x,cy-y,col);set_px(c,cx-y,cy-x,col);set_px(c,cx+y,cy-x,col);set_px(c,cx+x,cy-y,col); y++; if(err<=0) err+=2*y+1; if(err>0){x--; err-=2*x+1;}} } } static void draw_line(AppCtx* c,int x0,int y0,int x1,int y1,uint16_t col){ int dx=abs(x1-x0),dy=-abs(y1-y0),sx=x0=dy){err+=dy;x0+=sx;} if(e2<=dx){err+=dx;y0+=sy;}} } static void clear_labels(AppCtx* ctx){ // Now text is drawn directly to framebuffer, no LVGL labels to clean (void)ctx; } static void invalidate_canvas(AppCtx* ctx){if(!ctx||!ctx->canvas)return; lv_obj_invalidate(ctx->canvas);} static void draw_char8(AppCtx* ctx, int x, int y, char ch, uint16_t col){ unsigned char uch = (unsigned char)ch; if(uch>=128) return; // font8x8_basic is 8x8, each byte is row, bit 0 is leftmost? Actually font8x8_basic stores rows as bits for(int row=0; row<8; row++){ uint8_t bits = font8x8_basic[(int)ch][row]; for(int colb=0; colb<8; colb++){ if(bits & (1<GAME_W){ cx=x; cy+=10; } } } /* Lua bindings */ static int l_sys_log(lua_State* L){const char* m=luaL_checkstring(L,1); printf("[LUA] %s\n",m); return 0;} static int l_sys_clear(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx)return 0; uint32_t hex=0; if(lua_gettop(L)>=1) hex=(uint32_t)(luaL_checknumber(L,1)); fill_rect(ctx,0,0,GAME_W,GAME_H,hex_to_rgb565(hex),true); clear_labels(ctx); return 0; } static int l_sys_print(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx)return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); const char* txt=luaL_checkstring(L,3); uint32_t hex=0xFFFFFF; if(lua_gettop(L)>=4) hex=(uint32_t)luaL_checknumber(L,4); uint16_t col = hex_to_rgb565(hex); draw_text8(ctx, x, y, txt, col); return 0; } static int l_sys_rect(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx)return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); int w=(int)luaL_checknumber(L,3); int h=(int)luaL_checknumber(L,4); uint32_t hex=(uint32_t)luaL_checknumber(L,5); bool f=false; if(lua_gettop(L)>=6) f=lua_toboolean(L,6); fill_rect(ctx,x,y,w,h,hex_to_rgb565(hex),f); return 0; } static int l_sys_circle(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx)return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); int r=(int)luaL_checknumber(L,3); uint32_t hex=(uint32_t)luaL_checknumber(L,4); bool f=false; if(lua_gettop(L)>=5) f=lua_toboolean(L,5); draw_circle(ctx,x,y,r,hex_to_rgb565(hex),f); return 0; } static int l_sys_line(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx)return 0; int x0=(int)luaL_checknumber(L,1); int y0=(int)luaL_checknumber(L,2); int x1=(int)luaL_checknumber(L,3); int y1=(int)luaL_checknumber(L,4); uint32_t hex=(uint32_t)luaL_checknumber(L,5); draw_line(ctx,x0,y0,x1,y1,hex_to_rgb565(hex)); return 0; } static int l_sys_touch(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx){lua_pushboolean(L,0); lua_pushinteger(L,0); lua_pushinteger(L,0); return 3;} lua_pushboolean(L,ctx->touch_pressed?1:0); lua_pushinteger(L,ctx->touch_x); lua_pushinteger(L,ctx->touch_y); return 3; } static int l_sys_tone(lua_State* L){ int note = (int)luaL_checknumber(L,1); int dur = 50; const char* wave = "sine"; float vol = 0.5f; if(lua_gettop(L)>=2) dur = (int)luaL_checknumber(L,2); if(lua_gettop(L)>=3) wave = luaL_checkstring(L,3); if(lua_gettop(L)>=4) vol = (float)luaL_checknumber(L,4); audio_play_tone(note, dur, wave, vol); return 0; } static int l_sys_sfx(lua_State* L){ const char* name = luaL_checkstring(L,1); printf("[LUA SFX] %s\n",name); audio_play_sfx(name); return 0; } /* ---------- Pico API compatibility ---------- */ static int l_renderer_clear(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; bool white = false; if(lua_gettop(L)>=1) white = lua_toboolean(L,1); uint16_t col = white ? 0xFFFF : 0x0000; fill_rect(ctx,0,0,GAME_W,GAME_H,col,true); clear_labels(ctx); return 0; } static int l_renderer_pixel(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); bool on = true; if(lua_gettop(L)>=3) on = lua_toboolean(L,3); uint16_t col = on ? 0xFFFF : 0x0000; set_px(ctx,x,y,col); return 0; } static int l_renderer_line(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x0=(int)luaL_checknumber(L,1); int y0=(int)luaL_checknumber(L,2); int x1=(int)luaL_checknumber(L,3); int y1=(int)luaL_checknumber(L,4); bool on = true; if(lua_gettop(L)>=5) on = lua_toboolean(L,5); // width ignored uint16_t col = on ? 0xFFFF : 0x0000; draw_line(ctx,x0,y0,x1,y1,col); return 0; } static int l_renderer_rect(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); int w=(int)luaL_checknumber(L,3); int h=(int)luaL_checknumber(L,4); bool on = true; if(lua_gettop(L)>=5) on = lua_toboolean(L,5); bool filled = false; if(lua_gettop(L)>=6) filled = lua_toboolean(L,6); uint16_t col = on ? 0xFFFF : 0x0000; fill_rect(ctx,x,y,w,h,col,filled); // if not filled and on=true, we already draw border via fill_rect false, but our fill_rect does border // To match pico: if filled false, draw outline with on color if(!filled){ // ensure border drawn (our fill_rect already draws border when filled=false) } return 0; } static int l_renderer_circle(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); int r=(int)luaL_checknumber(L,3); bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4); bool filled = false; if(lua_gettop(L)>=5) filled = lua_toboolean(L,5); uint16_t col = on ? 0xFFFF : 0x0000; draw_circle(ctx,x,y,r,col,filled); return 0; } // Simple triangle rasterizer: draw outline and optionally filled via scanline static void draw_triangle_filled(AppCtx* ctx, int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col){ // Sort vertices by y if(y0>y1){ int tx=x0; x0=x1; x1=tx; int ty=y0; y0=y1; y1=ty; } if(y1>y2){ int tx=x1; x1=x2; x2=tx; int ty=y1; y1=y2; y2=ty; } if(y0>y1){ int tx=x0; x0=x1; x1=tx; int ty=y0; y0=y1; y1=ty; } // Now y0 <= y1 <= y2 int total_height = y2 - y0; if(total_height==0) return; for(int i=0;i y1 - y0 || y1==y0; int segment_height = second_half ? y2 - y1 : y1 - y0; if(segment_height==0) continue; float alpha = (float)i / total_height; float beta = (float)(i - (second_half ? y1 - y0 : 0)) / segment_height; int ax = x0 + (int)((x2 - x0) * alpha); int bx; if(second_half) bx = x1 + (int)((x2 - x1) * beta); else bx = x0 + (int)((x1 - x0) * beta); if(ax>bx){ int t=ax; ax=bx; bx=t; } for(int j=ax;j<=bx;j++) set_px(ctx,j,y0+i,col); } } static int l_renderer_triangle(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x0=(int)luaL_checknumber(L,1); int y0=(int)luaL_checknumber(L,2); int x1=(int)luaL_checknumber(L,3); int y1=(int)luaL_checknumber(L,4); int x2=(int)luaL_checknumber(L,5); int y2=(int)luaL_checknumber(L,6); bool on = true; if(lua_gettop(L)>=7) on = lua_toboolean(L,7); bool filled = false; if(lua_gettop(L)>=8) filled = lua_toboolean(L,8); uint16_t col = on ? 0xFFFF : 0x0000; if(filled){ draw_triangle_filled(ctx,x0,y0,x1,y1,x2,y2,col); }else{ draw_line(ctx,x0,y0,x1,y1,col); draw_line(ctx,x1,y1,x2,y2,col); draw_line(ctx,x2,y2,x0,y0,col); } return 0; } static int l_renderer_text(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); const char* txt=luaL_checkstring(L,3); bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4); uint16_t col = on ? 0xFFFF : 0x0000; draw_text8(ctx,x,y,txt,col); return 0; } static int l_renderer_text_scaled(lua_State* L){ AppCtx* ctx=g_ctx; if(!ctx) return 0; int x=(int)luaL_checknumber(L,1); int y=(int)luaL_checknumber(L,2); const char* txt=luaL_checkstring(L,3); bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4); int scale = 1; if(lua_gettop(L)>=5) scale = (int)luaL_checknumber(L,5); if(scale<1) scale=1; if(scale>4) scale=4; uint16_t col = on ? 0xFFFF : 0x0000; // Simple scaled: draw each char scaled by duplicating pixels int cx=x; int cy=y; for(int i=0; txt[i]; i++){ char ch=txt[i]; if(ch=='\n'){ cx=x; cy+=8*scale+2; continue; } unsigned char uch=(unsigned char)ch; if(uch>=128) continue; for(int row=0; row<8; row++){ uint8_t bits = font8x8_basic[uch][row]; for(int colb=0; colb<8; colb++){ if(bits & (1<GAME_W){ cx=x; cy+=8*scale+2; } } return 0; } static int l_game_width(lua_State* L){ lua_pushinteger(L, GAME_W); return 1; } static int l_game_height(lua_State* L){ lua_pushinteger(L, GAME_H); return 1; } static int l_game_set_frame_updates(lua_State* L){ (void)L; return 0; } static int l_game_exit(lua_State* L){ (void)L; printf("[LuaGame] game.exit() called\n"); return 0; } static void register_sys(lua_State* L){ // sys table (Tactility original) lua_newtable(L); lua_pushcfunction(L,l_sys_log); lua_setfield(L,-2,"log"); lua_pushcfunction(L,l_sys_clear); lua_setfield(L,-2,"clear"); lua_pushcfunction(L,l_sys_print); lua_setfield(L,-2,"print"); lua_pushcfunction(L,l_sys_rect); lua_setfield(L,-2,"rect"); lua_pushcfunction(L,l_sys_circle); lua_setfield(L,-2,"circle"); lua_pushcfunction(L,l_sys_line); lua_setfield(L,-2,"line"); lua_pushcfunction(L,l_sys_touch); lua_setfield(L,-2,"touch"); lua_pushcfunction(L,l_sys_tone); lua_setfield(L,-2,"tone"); lua_pushcfunction(L,l_sys_sfx); lua_setfield(L,-2,"sfx"); lua_setglobal(L,"sys"); // renderer table (Pico) lua_newtable(L); lua_pushcfunction(L,l_renderer_clear); lua_setfield(L,-2,"clear"); lua_pushcfunction(L,l_renderer_pixel); lua_setfield(L,-2,"pixel"); lua_pushcfunction(L,l_renderer_line); lua_setfield(L,-2,"line"); lua_pushcfunction(L,l_renderer_rect); lua_setfield(L,-2,"rect"); lua_pushcfunction(L,l_renderer_circle); lua_setfield(L,-2,"circle"); lua_pushcfunction(L,l_renderer_triangle); lua_setfield(L,-2,"triangle"); lua_pushcfunction(L,l_renderer_text); lua_setfield(L,-2,"text"); lua_pushcfunction(L,l_renderer_text_scaled); lua_setfield(L,-2,"text_scaled"); lua_setglobal(L,"renderer"); // game table lua_newtable(L); lua_pushcfunction(L,l_game_width); lua_setfield(L,-2,"width"); lua_pushcfunction(L,l_game_height); lua_setfield(L,-2,"height"); lua_pushcfunction(L,l_game_set_frame_updates); lua_setfield(L,-2,"set_frame_updates"); lua_pushcfunction(L,l_game_exit); lua_setfield(L,-2,"exit"); // game.vars subtable lua_newtable(L); lua_setfield(L,-2,"vars"); lua_setglobal(L,"game"); // INPUT constants lua_newtable(L); lua_pushinteger(L,0); lua_setfield(L,-2,"NONE"); lua_pushinteger(L,1); lua_setfield(L,-2,"TOUCH_DOWN"); lua_pushinteger(L,2); lua_setfield(L,-2,"TOUCH_MOVE"); lua_pushinteger(L,3); lua_setfield(L,-2,"TOUCH_UP"); lua_pushinteger(L,4); lua_setfield(L,-2,"BUTTON_0"); lua_pushinteger(L,5); lua_setfield(L,-2,"BUTTON_1"); lua_pushinteger(L,6); lua_setfield(L,-2,"FRAME_TICK"); lua_pushinteger(L,7); lua_setfield(L,-2,"GESTURE"); lua_setglobal(L,"INPUT"); } /* Embedded breakout - faster ball: 3.5 instead of 2 */ static const char* embedded_breakout_lua_src = "local PADDLE_W=40 local PADDLE_H=8 local BALL_SIZE=6 local BRICK_ROWS=4 local BRICK_COLS=6 local BRICK_W=34 local BRICK_H=8 local BRICK_GAP=4\n" "local score=0 local lives=3 local game_state='serve' local px,py=100,210 local bx,by=120,150 local bdx,bdy=4.5,-4.5 local bricks={}\n" "function init() sys.log('Init Breakout fast v2!') reset_bricks() reset_ball() end\n" "function reset_bricks() bricks={} for r=1,BRICK_ROWS do bricks[r]={} for c=1,BRICK_COLS do bricks[r][c]=true end end end\n" "function reset_ball() bx=120 by=150 bdx=4.5 bdy=-4.5 game_state='serve' end\n" "function update(dt) local pressed,tx,ty=sys.touch() if pressed then px=tx-PADDLE_W/2 if px<0 then px=0 end if px>240-PADDLE_W then px=240-PADDLE_W end if game_state=='serve' then game_state='play' sys.sfx('confirm') elseif game_state=='gameover' or game_state=='win' then score=0 lives=3 reset_bricks() reset_ball() end end if game_state=='play' then bx=bx+bdx by=by+bdy if bx<=0 or bx>=240-BALL_SIZE then bdx=-bdx sys.tone(60,80,'sine',0.4) end if by<=0 then bdy=-bdy sys.tone(64,80,'sine',0.4) end if by>=240 then lives=lives-1 sys.sfx('hurt') if lives<=0 then game_state='gameover' sys.sfx('gameover') else reset_ball() end end if by+BALL_SIZE>=py and by<=py+PADDLE_H then if bx+BALL_SIZE>=px and bx<=px+PADDLE_W then bdy=-math.abs(bdy) local hp=(bx+BALL_SIZE/2)-(px+PADDLE_W/2) bdx=hp*0.25 sys.tone(72,100,'triangle',0.5) end end local hit=false for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then local bmin=(c-1)*(BRICK_W+BRICK_GAP)+BRICK_GAP local bymin=r*(BRICK_H+BRICK_GAP)+20 if bx+BALL_SIZE>=bmin and bx<=bmin+BRICK_W and by+BALL_SIZE>=bymin and by<=bymin+BRICK_H then bricks[r][c]=false bdy=-bdy score=score+10 sys.sfx('coin') hit=true break end end end if hit then break end end local has=false for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then has=true end end end if not has then game_state='win' sys.sfx('levelup') end end end\n" "function draw() sys.clear(0x1e1e2e) sys.print(4,4,'SCORE: '..score,0xf5e0dc) sys.print(160,4,'LIVES: '..lives,0xf5e0dc) sys.line(0,16,240,16,0x585b70) sys.rect(px,py,PADDLE_W,PADDLE_H,0x89b4fa,true) sys.circle(bx+BALL_SIZE/2,by+BALL_SIZE/2,BALL_SIZE/2,0xf9e2af,true) for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then local bmin=(c-1)*(BRICK_W+BRICK_GAP)+BRICK_GAP local bymin=r*(BRICK_H+BRICK_GAP)+20 local col=0xf38ba8 if r==2 then col=0xf9e2af elseif r==3 then col=0xa6e3a1 elseif r==4 then col=0x89b4fa end sys.rect(bmin,bymin,BRICK_W,BRICK_H,col,true) end end end if game_state=='serve' then sys.print(40,120,'TAP TO SERVE',0xa6e3a1) elseif game_state=='gameover' then sys.print(80,110,'GAME OVER',0xf38ba8) sys.print(45,130,'TAP AGAIN',0xa6e3a1) elseif game_state=='win' then sys.print(90,110,'YOU WIN!',0xa6e3a1) sys.print(45,130,'TAP AGAIN',0x89b4fa) end end\n" ; static void* alloc_psram(size_t sz){ void* p=heap_caps_malloc(sz,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT); if(!p) p=heap_caps_malloc(sz,MALLOC_CAP_8BIT); if(!p) p=malloc(sz); return p; } static void game_loop_task(void* arg); static char* load_file_to_mem(const char* path, size_t* out_len){ FILE* f=fopen(path,"rb"); if(!f) return NULL; fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); if(sz<=0 || sz>200*1024){ fclose(f); return NULL; } char* buf=(char*)alloc_psram(sz+1); if(!buf){ fclose(f); return NULL; } size_t r=fread(buf,1,sz,f); fclose(f); if(r!=(size_t)sz){ heap_caps_free(buf); return NULL; } buf[sz]=0; if(out_len) *out_len=sz; return buf; } static void lua_init_task(void* arg){ AppCtx* ctx=(AppCtx*)arg; printf("[LuaGame] init task start\n"); audio_init(); ctx->L=luaL_newstate(); if(!ctx->L){ printf("[LuaGame] lua newstate fail\n"); vTaskDelete(NULL); return;} luaL_openlibs(ctx->L); register_sys(ctx->L); const char* external_paths[]={ "/sdcard/lua/solitaire.lua", "/sdcard/lua/breakout.lua", "/sdcard/lua/game.lua", "/sdcard/lua/pong.lua", "/sdcard/lua/snake.lua", "/sdcard/lua/tetris.lua", NULL }; char* src_buf=NULL; size_t src_len=0; const char* loaded_from="embedded"; bool use_external = true; // enable SD loading for solitaire migration bool use_pico_test = false; const char* pico_test_src = NULL; if(use_pico_test){ pico_test_src = pong_lua_src; printf("[LuaGame] using Pico test game: Pong (%d bytes)\n", (int)strlen(pico_test_src)); src_len = strlen(pico_test_src); src_buf = (char*)alloc_psram(src_len+1); if(src_buf){ memcpy(src_buf, pico_test_src, src_len+1); loaded_from = "pico_pong"; } } if(!src_buf && use_external){ for(int i=0; external_paths[i]; i++){ src_buf=load_file_to_mem(external_paths[i], &src_len); if(src_buf){ loaded_from=external_paths[i]; printf("[LuaGame] loaded external %s %d bytes\n",loaded_from,(int)src_len); break; } } } // Detect Pico API by content before loading bool detected_pico = false; if(src_buf){ if(strstr(src_buf,"renderer.")!=NULL || strstr(src_buf,"INPUT.")!=NULL){ detected_pico = true; } } else { if(strstr(embedded_breakout_lua_src,"renderer.")!=NULL){ detected_pico = true; } } int st; if(src_buf){ st=luaL_loadbuffer(ctx->L,src_buf,src_len,loaded_from); heap_caps_free(src_buf); src_buf=NULL; }else{ printf("[LuaGame] loading embedded lua %d bytes\n",(int)strlen(embedded_breakout_lua_src)); st=luaL_loadstring(ctx->L,embedded_breakout_lua_src); loaded_from="embedded"; } if(st!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] load err (%s) %s\n",loaded_from,e); lua_pop(ctx->L,1); vTaskDelete(NULL); return;} if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] pcall err %s\n",e); lua_pop(ctx->L,1); vTaskDelete(NULL); return;} lua_getglobal(ctx->L,"init"); if(lua_isfunction(ctx->L,-1)){ if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] init err %s\n",e); lua_pop(ctx->L,1);} }else lua_pop(ctx->L,1); ctx->lua_ok=true; ctx->initialized=true; ctx->is_pico = detected_pico; ctx->last_tick_us=esp_timer_get_time(); ctx->fps_last_us=esp_timer_get_time(); ctx->fps_frames=0; printf("[LuaGame] init done OK from %s (pico=%d)\n",loaded_from, ctx->is_pico?1:0); printf("[LuaGame] entering game loop in same task\n"); // Enter game loop directly while(!ctx->should_exit){ if(ctx->lua_ok && ctx->initialized && ctx->L){ uint64_t now=esp_timer_get_time(); float dt= ctx->last_tick_us==0? (TICK_MS/1000.0f) : (now-ctx->last_tick_us)/1000000.0f; if(dt>0.1f) dt=0.1f; ctx->last_tick_us=now; lua_State* L=ctx->L; if(ctx->is_pico){ // Generate event based on touch int ev_type = 6; // FRAME_TICK default bool has_touch_event = false; if(!ctx->prev_touch_pressed && ctx->touch_pressed){ ev_type = 1; // TOUCH_DOWN has_touch_event = true; } else if(ctx->prev_touch_pressed && !ctx->touch_pressed){ ev_type = 3; // TOUCH_UP has_touch_event = true; } else if(ctx->touch_pressed){ ev_type = 2; // TOUCH_MOVE has_touch_event = true; } else { ev_type = 6; // FRAME_TICK } // Helper to push event table and call update // First call with touch event if any if(has_touch_event){ lua_getglobal(L,"update"); if(lua_isfunction(L,-1)){ lua_newtable(L); lua_pushinteger(L, ev_type); lua_setfield(L,-2,"type"); lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x"); lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y"); lua_pushboolean(L,1); lua_setfield(L,-2,"valid"); lua_pushinteger(L,0); lua_setfield(L,-2,"button_id"); if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} else { bool redraw = lua_toboolean(L,-1); lua_pop(L,1); // we will draw regardless, but could check redraw (void)redraw; } } else lua_pop(L,1); } // Always also call with FRAME_TICK if not already frame tick if(ev_type != 6){ lua_getglobal(L,"update"); if(lua_isfunction(L,-1)){ lua_newtable(L); lua_pushinteger(L, 6); lua_setfield(L,-2,"type"); lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x"); lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y"); lua_pushboolean(L,1); lua_setfield(L,-2,"valid"); if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update FRAME_TICK] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} else { lua_pop(L,1); } } else lua_pop(L,1); } else if(!has_touch_event){ // No touch, just frame tick (single call) lua_getglobal(L,"update"); if(lua_isfunction(L,-1)){ lua_newtable(L); lua_pushinteger(L, 6); lua_setfield(L,-2,"type"); lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x"); lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y"); lua_pushboolean(L,1); lua_setfield(L,-2,"valid"); if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} else { bool redraw = lua_toboolean(L,-1); lua_pop(L,1); (void)redraw; } } else lua_pop(L,1); } ctx->prev_touch_pressed = ctx->touch_pressed; } else { // Original sys API lua_getglobal(L,"update"); if(lua_isfunction(L,-1)){ lua_pushnumber(L,dt); if(lua_pcall(L,1,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} }else lua_pop(L,1); } lua_getglobal(L,"draw"); if(lua_isfunction(L,-1)){ if(lua_pcall(L,0,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR draw] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} }else lua_pop(L,1); if(tt_lvgl_lock(portMAX_DELAY)){ invalidate_canvas(ctx); ctx->fps_frames++; uint64_t elapsed = now - ctx->fps_last_us; if(elapsed >= 1000000){ ctx->fps = (float)ctx->fps_frames * 1000000.0f / (float)elapsed; printf("[LuaGame] FPS %.1f\n", ctx->fps); if(ctx->fps_label){ char buf[32]; snprintf(buf,sizeof(buf),"FPS %.0f",ctx->fps); lv_label_set_text(ctx->fps_label, buf); } ctx->fps_frames=0; ctx->fps_last_us=now; } tt_lvgl_unlock(); } } vTaskDelay(pdMS_TO_TICKS(TICK_MS)); } printf("[LuaGame] game loop exit\n"); vTaskDelete(NULL); } static void game_loop_task(void* arg){ AppCtx* ctx=(AppCtx*)arg; printf("[LuaGame] game loop task start\n"); while(!ctx->should_exit){ if(ctx->lua_ok && ctx->initialized && ctx->L){ uint64_t now=esp_timer_get_time(); float dt= ctx->last_tick_us==0? (TICK_MS/1000.0f) : (now-ctx->last_tick_us)/1000000.0f; if(dt>0.1f) dt=0.1f; // clamp ctx->last_tick_us=now; lua_State* L=ctx->L; lua_getglobal(L,"update"); if(lua_isfunction(L,-1)){ lua_pushnumber(L,dt); if(lua_pcall(L,1,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} }else lua_pop(L,1); lua_getglobal(L,"draw"); if(lua_isfunction(L,-1)){ if(lua_pcall(L,0,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR draw] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;} }else lua_pop(L,1); // Invalidate canvas with LVGL lock if(tt_lvgl_lock(portMAX_DELAY)){ invalidate_canvas(ctx); // fps tracking inside lock for label update ctx->fps_frames++; uint64_t elapsed = now - ctx->fps_last_us; if(elapsed >= 1000000){ ctx->fps = (float)ctx->fps_frames * 1000000.0f / (float)elapsed; printf("[LuaGame] FPS %.1f\n", ctx->fps); if(ctx->fps_label){ char buf[32]; snprintf(buf,sizeof(buf),"FPS %.0f",ctx->fps); lv_label_set_text(ctx->fps_label, buf); } ctx->fps_frames=0; ctx->fps_last_us=now; } tt_lvgl_unlock(); } } vTaskDelay(pdMS_TO_TICKS(TICK_MS)); } printf("[LuaGame] game loop task exit\n"); vTaskDelete(NULL); } static void touch_event_cb(lv_event_t* e){ AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e); if(!ctx)return; lv_event_code_t code=lv_event_get_code(e); lv_point_t pt; lv_indev_get_point(lv_indev_active(),&pt); if(ctx->canvas){ lv_area_t area; lv_obj_get_coords(ctx->canvas,&area); int lx=pt.x-area.x1; int ly=pt.y-area.y1; if(lx<0) lx=0; if(lx>=GAME_W) lx=GAME_W-1; if(ly<0) ly=0; if(ly>=GAME_H) ly=GAME_H-1; ctx->touch_x=lx; ctx->touch_y=ly; }else{ctx->touch_x=pt.x; ctx->touch_y=pt.y;} if(code==LV_EVENT_PRESSING||code==LV_EVENT_PRESSED||code==LV_EVENT_CLICKED) ctx->touch_pressed=true; else if(code==LV_EVENT_RELEASED||code==LV_EVENT_PRESS_LOST) ctx->touch_pressed=false; } static void* app_create_data(void){return calloc(1,sizeof(AppCtx));} static void app_destroy_data(void* d){ AppCtx* ctx=d; if(!ctx)return; ctx->should_exit=true; if(ctx->L) lua_close(ctx->L); if(ctx->fb) heap_caps_free(ctx->fb); free(ctx); } static void app_on_create(AppHandle a,void* d){AppCtx* c=d; if(c) c->app=a;} static void app_on_destroy(AppHandle a,void* d){(void)a;(void)d;} static void app_on_show(AppHandle app,void* data,lv_obj_t* parent){ AppCtx* ctx=data; if(!ctx)return; g_ctx=ctx; ctx->app=app; ctx->lua_ok=false; ctx->initialized=false; ctx->should_exit=false; ctx->fps=0; ctx->fps_frames=0; lv_obj_remove_flag(parent,LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(parent,0,0); lv_obj_set_style_bg_color(parent,lv_color_black(),0); lv_obj_set_flex_flow(parent,LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent,0,0); ctx->toolbar=tt_lvgl_toolbar_create_for_app(parent,app); ctx->root=parent; ctx->game_area=lv_obj_create(parent); lv_obj_set_width(ctx->game_area,LV_PCT(100)); lv_obj_set_flex_grow(ctx->game_area,1); lv_obj_set_style_bg_color(ctx->game_area,lv_color_black(),0); lv_obj_set_style_border_width(ctx->game_area,0,0); lv_obj_set_style_pad_all(ctx->game_area,0,0); lv_obj_set_flex_flow(ctx->game_area,LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(ctx->game_area,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER); lv_obj_remove_flag(ctx->game_area,LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(ctx->game_area,LV_OBJ_FLAG_CLICKABLE); size_t fb_bytes=GAME_W*GAME_H*sizeof(uint16_t); ctx->fb=alloc_psram(fb_bytes); if(!ctx->fb){printf("[LuaGame] FB alloc fail\n"); return;} memset(ctx->fb,0,fb_bytes); lv_obj_t* cont=lv_obj_create(ctx->game_area); lv_obj_set_size(cont,GAME_W,GAME_H); lv_obj_set_style_bg_color(cont,lv_color_black(),0); lv_obj_set_style_border_width(cont,0,0); lv_obj_set_style_pad_all(cont,0,0); lv_obj_remove_flag(cont,LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(cont,0,0); ctx->canvas=lv_canvas_create(cont); lv_canvas_set_buffer(ctx->canvas,ctx->fb,GAME_W,GAME_H,LV_COLOR_FORMAT_RGB565); lv_obj_set_style_bg_opa(ctx->canvas,LV_OPA_COVER,0); lv_obj_remove_flag(ctx->canvas,LV_OBJ_FLAG_SCROLLABLE); ctx->label_layer=lv_obj_create(cont); lv_obj_set_size(ctx->label_layer,GAME_W,GAME_H); lv_obj_set_pos(ctx->label_layer,0,0); lv_obj_set_style_bg_opa(ctx->label_layer,LV_OPA_TRANSP,0); lv_obj_set_style_border_width(ctx->label_layer,0,0); lv_obj_set_style_pad_all(ctx->label_layer,0,0); lv_obj_remove_flag(ctx->label_layer,LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(ctx->label_layer,LV_OBJ_FLAG_CLICKABLE); // FPS label outside canvas, top of game_area ctx->fps_label=lv_label_create(ctx->game_area); lv_label_set_text(ctx->fps_label,"FPS --"); lv_obj_set_style_text_color(ctx->fps_label, lv_color_white(),0); lv_obj_set_style_text_font(ctx->fps_label, lv_font_get_default(),0); lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESSED,ctx); lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESSING,ctx); lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_RELEASED,ctx); lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESS_LOST,ctx); lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_SHORT_CLICKED,ctx); lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESSED,ctx); lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESSING,ctx); lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_RELEASED,ctx); lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESS_LOST,ctx); printf("[LuaGame] spawning init task\n"); xTaskCreate(lua_init_task,"lua_init",16384,ctx,5,&ctx->lua_task); fill_rect(ctx,0,0,GAME_W,GAME_H,hex_to_rgb565(0x1e1e2e),true); if(tt_lvgl_lock(portMAX_DELAY)){ invalidate_canvas(ctx); tt_lvgl_unlock(); } printf("[LuaGame] on_show done\n"); } static void app_on_hide(AppHandle app,void* data){ (void)app; AppCtx* ctx=data; if(!ctx)return; ctx->should_exit=true; vTaskDelay(200/portTICK_PERIOD_MS); audio_deinit(); if(ctx->L){lua_close(ctx->L); ctx->L=NULL;} ctx->canvas=NULL; ctx->label_layer=NULL; ctx->fps_label=NULL; ctx->game_area=NULL; ctx->root=NULL; ctx->toolbar=NULL; g_ctx=NULL; printf("[LuaGame] on_hide\n"); } int main(int argc,char* argv[]){ (void)argc;(void)argv; tt_app_register((AppRegistration){ .createData=app_create_data, .destroyData=app_destroy_data, .onCreate=app_on_create, .onDestroy=app_on_destroy, .onShow=app_on_show, .onHide=app_on_hide }); return 0; }