74fe10f130
- Init task stack 16384->8192 to avoid allocation failure (ret=-1) - Replace GAME_W/H constants with fb_w/h dynamic for fullscreen 320x480 - Fix l_renderer_clear and app_on_show fill to use dynamic dimensions - Add PSRAM Lua allocator already, conditional audio skip for solitaire saves RAM - Exit gesture: top-right 40x40 tap calls tt_app_stop(), game.exit() also stops app - Screenshot now shows solitaire cards: 810B PNG, 3662 non-black pixels, ASCII preview shows piles with optimized LEFT=4 GAP=4 TOP=4 TAB=60 (was 37/10/30/98) - Tested solitaire loads external 24430 bytes, pico=1, init OK, no memory low warnings after fix
911 lines
39 KiB
C
911 lines
39 KiB
C
#include <tt_app.h>
|
|
#include <tt_lvgl.h>
|
|
#include <tt_lvgl_toolbar.h>
|
|
#include <lvgl.h>
|
|
#include <esp_heap_caps.h>
|
|
#include <esp_timer.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <time.h>
|
|
|
|
#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 GAME_W_DEFAULT 240
|
|
#define GAME_H_DEFAULT 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;}
|
|
|
|
/* Custom Lua allocator using PSRAM to reduce internal memory pressure */
|
|
static void* lua_psram_alloc(void *ud, void *ptr, size_t osize, size_t nsize){
|
|
(void)ud; (void)osize;
|
|
if(nsize==0){
|
|
if(ptr) heap_caps_free(ptr);
|
|
return NULL;
|
|
}
|
|
void* newptr = heap_caps_malloc(nsize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
|
if(!newptr){
|
|
newptr = heap_caps_malloc(nsize, MALLOC_CAP_8BIT);
|
|
}
|
|
if(!newptr) return NULL;
|
|
if(ptr){
|
|
size_t copy = osize < nsize ? osize : nsize;
|
|
memcpy(newptr, ptr, copy);
|
|
heap_caps_free(ptr);
|
|
}
|
|
return newptr;
|
|
}
|
|
|
|
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;
|
|
int fb_w;
|
|
int fb_h;
|
|
int game_w;
|
|
int game_h;
|
|
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;
|
|
bool wants_frame_updates;
|
|
uint64_t last_tick_us;
|
|
bool should_exit;
|
|
bool needs_redraw;
|
|
// 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;
|
|
int w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
|
|
int h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
|
|
if(x<0||y<0||x>=w||y>=h)return;
|
|
c->fb[y*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<x+w;i++){set_px(c,i,y,col);set_px(c,i,y+h-1,col);}
|
|
for(int j=y;j<y+h;j++){set_px(c,x,j,col);set_px(c,x+w-1,j,col);}
|
|
return;
|
|
}
|
|
int fb_w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
|
|
int fb_h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
|
|
int x0=x<0?0:x,y0=y<0?0:y,x1=x+w> fb_w? fb_w:x+w,y1=y+h>fb_h?fb_h:y+h;
|
|
for(int yy=y0;yy<y1;yy++){uint16_t* row=c->fb+yy*fb_w; for(int xx=x0;xx<x1;xx++) row[xx]=col;}
|
|
}
|
|
static void draw_circle(AppCtx* c,int cx,int cy,int r,uint16_t col,bool filled){
|
|
if(r<=0)return;
|
|
if(filled){
|
|
for(int dy=-r;dy<=r;dy++){int dxl=(int)sqrtf((float)(r*r-dy*dy)); for(int dx=-dxl;dx<=dxl;dx++) set_px(c,cx+dx,cy+dy,col);}
|
|
}else{
|
|
int x=r,y=0,err=0;
|
|
while(x>=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<x1?1:-1,sy=y0<y1?1:-1,err=dx+dy;
|
|
while(1){set_px(c,x0,y0,col); if(x0==x1&&y0==y1)break; int e2=2*err; if(e2>=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<<colb)){
|
|
set_px(ctx, x+colb, y+row, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static void draw_text8(AppCtx* ctx, int x, int y, const char* txt, uint16_t col){
|
|
if(!txt) return;
|
|
int cx=x;
|
|
int cy=y;
|
|
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
|
|
for(int i=0; txt[i]; i++){
|
|
char ch=txt[i];
|
|
if(ch=='\n'){ cx=x; cy+=10; continue; }
|
|
draw_char8(ctx, cx, cy, ch, col);
|
|
cx+=8;
|
|
// wrap if exceeds game width
|
|
if(cx+8>max_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));
|
|
int w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
|
|
int h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
|
|
fill_rect(ctx,0,0,w,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;
|
|
int w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
|
|
int h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
|
|
fill_rect(ctx,0,0,w,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<total_height;i++){
|
|
bool second_half = 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<<colb)){
|
|
// draw scale x scale block
|
|
for(int sy=0; sy<scale; sy++){
|
|
for(int sx=0; sx<scale; sx++){
|
|
set_px(ctx, cx+colb*scale+sx, cy+row*scale+sy, col);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cx+=8*scale;
|
|
{
|
|
int max_w = g_ctx && g_ctx->game_w ? g_ctx->game_w : GAME_W_DEFAULT;
|
|
if(cx+8*scale>max_w){ cx=x; cy+=8*scale+2; }
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
static int l_game_width(lua_State* L){
|
|
AppCtx* ctx=g_ctx;
|
|
int w = ctx && ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
|
|
lua_pushinteger(L, w); return 1;
|
|
}
|
|
static int l_game_height(lua_State* L){
|
|
AppCtx* ctx=g_ctx;
|
|
int h = ctx && ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
|
|
lua_pushinteger(L, h); return 1;
|
|
}
|
|
static int l_game_set_frame_updates(lua_State* L){
|
|
AppCtx* ctx=g_ctx;
|
|
bool en = false;
|
|
if(lua_gettop(L)>=1) en = lua_toboolean(L,1);
|
|
if(ctx) ctx->wants_frame_updates = en;
|
|
printf("[LuaGame] set_frame_updates %d\n", en?1:0);
|
|
return 0;
|
|
}
|
|
static int l_game_exit(lua_State* L){
|
|
(void)L;
|
|
AppCtx* ctx=g_ctx;
|
|
printf("[LuaGame] game.exit() called -> stopping app\n");
|
|
if(ctx) ctx->should_exit = true;
|
|
tt_app_stop();
|
|
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 (PSRAM alloc)\n");
|
|
// Use PSRAM allocator for Lua to reduce internal memory pressure
|
|
ctx->L=lua_newstate(lua_psram_alloc, NULL);
|
|
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;
|
|
bool needs_audio = false;
|
|
if(src_buf){
|
|
if(strstr(src_buf,"renderer.")!=NULL || strstr(src_buf,"INPUT.")!=NULL){
|
|
detected_pico = true;
|
|
}
|
|
if(strstr(src_buf,"sfx")!=NULL || strstr(src_buf,"tone")!=NULL){
|
|
needs_audio = true;
|
|
}
|
|
} else {
|
|
if(strstr(embedded_breakout_lua_src,"renderer.")!=NULL){
|
|
detected_pico = true;
|
|
}
|
|
if(strstr(embedded_breakout_lua_src,"sfx")!=NULL || strstr(embedded_breakout_lua_src,"tone")!=NULL){
|
|
needs_audio = true;
|
|
}
|
|
}
|
|
if(needs_audio){
|
|
printf("[LuaGame] audio needed, init SfxEngine\n");
|
|
audio_init();
|
|
} else {
|
|
printf("[LuaGame] audio not needed, skipping SfxEngine to save RAM\n");
|
|
}
|
|
|
|
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);
|
|
|
|
// Initial draw for on-demand games
|
|
lua_getglobal(ctx->L,"draw");
|
|
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("[LUA ERR initial draw] %s\n",e);
|
|
lua_pop(ctx->L,1);
|
|
}
|
|
} else lua_pop(ctx->L,1);
|
|
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
|
|
invalidate_canvas(ctx);
|
|
tt_lvgl_unlock();
|
|
}
|
|
|
|
printf("[LuaGame] entering game loop in same task\n");
|
|
|
|
// Enter game loop directly - on-demand for solitaire (no frame updates)
|
|
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;
|
|
bool should_draw = false;
|
|
|
|
if(ctx->is_pico){
|
|
bool has_touch_event = false;
|
|
int ev_type = 6;
|
|
if(!ctx->prev_touch_pressed && ctx->touch_pressed){
|
|
ev_type = 1; has_touch_event = true;
|
|
} else if(ctx->prev_touch_pressed && !ctx->touch_pressed){
|
|
ev_type = 3; has_touch_event = true;
|
|
} else if(ctx->touch_pressed){
|
|
ev_type = 2; has_touch_event = true;
|
|
}
|
|
|
|
if(ctx->wants_frame_updates){
|
|
// Continuous: handle touch event if any, then FRAME_TICK every frame
|
|
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 r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
|
|
} else lua_pop(L,1);
|
|
}
|
|
// FRAME_TICK
|
|
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] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
|
|
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
|
|
} else lua_pop(L,1);
|
|
} else {
|
|
// On-demand: only when touch event
|
|
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 r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
|
|
} else lua_pop(L,1);
|
|
}
|
|
}
|
|
ctx->prev_touch_pressed = ctx->touch_pressed;
|
|
} else {
|
|
// Original sys API - continuous
|
|
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);
|
|
should_draw = true;
|
|
}
|
|
|
|
if(should_draw){
|
|
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(pdMS_TO_TICKS(1000))){
|
|
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 (on-demand)\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();
|
|
} else {
|
|
printf("[LuaGame] lvgl lock timeout\n");
|
|
}
|
|
}
|
|
}
|
|
// For on-demand games, sleep longer to save CPU
|
|
if(ctx->is_pico && !ctx->wants_frame_updates){
|
|
vTaskDelay(pdMS_TO_TICKS(50));
|
|
} else {
|
|
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(pdMS_TO_TICKS(1000))){
|
|
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);
|
|
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
|
|
int max_h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
|
|
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>=max_w) lx=max_w-1;
|
|
if(ly<0) ly=0;
|
|
if(ly>=max_h) ly=max_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_PRESSED){
|
|
// Top-right corner 40x40 as exit gesture (since no toolbar)
|
|
if(ctx->touch_x > max_w - 40 && ctx->touch_y < 40){
|
|
printf("[LuaGame] exit gesture top-right tap -> stopping app\n");
|
|
tt_app_stop();
|
|
return;
|
|
}
|
|
}
|
|
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;
|
|
|
|
// Fullscreen: no toolbar, no statusbar (manifest HideStatusBar)
|
|
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=NULL;
|
|
ctx->root=parent;
|
|
|
|
// Get display resolution for dynamic framebuffer (320x480 on ES3C35P)
|
|
lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL);
|
|
lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL);
|
|
if(disp_w<=0) disp_w = 320;
|
|
if(disp_h<=0) disp_h = 480;
|
|
ctx->game_w = disp_w;
|
|
ctx->game_h = disp_h;
|
|
ctx->fb_w = disp_w;
|
|
ctx->fb_h = disp_h;
|
|
printf("[LuaGame] display %dx%d fullscreen, no toolbar\n", (int)disp_w, (int)disp_h);
|
|
|
|
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=(size_t)ctx->fb_w * ctx->fb_h * sizeof(uint16_t);
|
|
ctx->fb=alloc_psram(fb_bytes);
|
|
if(!ctx->fb){printf("[LuaGame] FB alloc fail %dx%d\n", ctx->fb_w, ctx->fb_h); return;}
|
|
memset(ctx->fb,0,fb_bytes);
|
|
lv_obj_t* cont=lv_obj_create(ctx->game_area);
|
|
lv_obj_set_size(cont,ctx->fb_w,ctx->fb_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,ctx->fb_w,ctx->fb_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);
|
|
// Label layer removed to save RAM - text now drawn directly to FB
|
|
ctx->label_layer=NULL;
|
|
// FPS label kept small but optional - remove to save RAM for solitaire
|
|
// For now, keep disabled to save internal memory
|
|
ctx->fps_label=NULL;
|
|
// If you want FPS, uncomment:
|
|
// 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_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");
|
|
BaseType_t ret = xTaskCreate(lua_init_task,"lua_init",8192,ctx,5,&ctx->lua_task);
|
|
printf("[LuaGame] init task create ret=%d\n", ret);
|
|
fill_rect(ctx,0,0,ctx->fb_w,ctx->fb_h,hex_to_rgb565(0x1e1e2e),true);
|
|
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
|
|
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;
|
|
}
|