#include "vmio.h" #include "vmres.h" #include "vmgraph.h" #include "vmsys.h" #include "vmtimer.h" #include "vmchset.h" #include "vmmm.h" #include "vmstdlib.h" #include #include /* ========================================================================= */ /* CONFIGURATION & CONSTANTS */ /* ========================================================================= */ #define TIMER_INTERVAL_MS 100 /* 10 FPS engine tick */ #define UNLOCK_TIMEOUT_TICKS 30 /* 3000 ms / 100 ms = 30 ticks */ #define NPC_SPEED_PIXELS_PER_F 2 /* 2 px per tick / frame */ #define DEFAULT_GIF_PATH "default.gif" #define MOON_GIF_PATH "moon.gif" #define NPC_GIF_PATH "npc.gif" #define MAX_NPC_VARIANTS 18 /* 16 custom + default.gif + npc.gif */ #define MAX_CUSTOM_QUOTES 64 typedef enum { SKY_SHADE_NIGHT = 0, /* 21:00 - 05:00 */ SKY_SHADE_DAWN, /* 05:00 - 07:00 */ SKY_SHADE_MORNING, /* 07:00 - 11:00 */ SKY_SHADE_MIDDAY, /* 11:00 - 17:00 */ SKY_SHADE_SUNSET, /* 17:00 - 19:00 */ SKY_SHADE_TWILIGHT, /* 19:00 - 21:00 */ NUM_SKY_PRESETS } SkyShade; /* 6 Unified Sky Presets (RGB565) */ static const VMUINT16 SKY_PRESETS[NUM_SKY_PRESETS] = { VM_COLOR_888_TO_565(15, 20, 45), /* 0: Deep Midnight Navy */ VM_COLOR_888_TO_565(240, 112, 80), /* 1: Dawn Coral / Amber */ VM_COLOR_888_TO_565(120, 190, 240), /* 2: Crisp Morning Sky Blue */ VM_COLOR_888_TO_565(135, 206, 235), /* 3: Midday Bright Azure */ VM_COLOR_888_TO_565(245, 178, 71), /* 4: Sunset Golden Orange */ VM_COLOR_888_TO_565(22, 32, 130) /* 5: Twilight Indigo */ }; /* ========================================================================= */ /* EMBEDDED PRNG */ /* ========================================================================= */ static VMUINT32 g_rand_seed = 123456789; static void app_srand(void) { g_rand_seed = (VMUINT32)vm_get_tick_count(); if (g_rand_seed == 0) { g_rand_seed = 123456789; } } static int app_rand(void) { g_rand_seed = g_rand_seed * 1664525UL + 1013904223UL; return (int)((g_rand_seed >> 16) & 0x7FFF); } /* ========================================================================= */ /* STRUCTURES & TYPES */ /* ========================================================================= */ typedef struct { char filename[64]; VMUINT8* data; VMINT size; VMINT hcanvas; /* Canvas handle for true transparent blitting */ VMINT width; VMINT height; VMINT total_frames; VMINT current_frame; VMINT trans_color; /* Cached transparent color */ VMBOOL is_loaded; } AnimatedGif; typedef enum { SCHED_RULE_ALWAYS = 0, /* 0: Always */ SCHED_RULE_MONTH, /* 1: Month (v1=M) */ SCHED_RULE_MONTH_DAY, /* 2: Month & Day (v1=M, v2=D) */ SCHED_RULE_MONTH_YEAR, /* 3: Month & Year (v1=M, v3=Y) */ SCHED_RULE_TIME_RANGE, /* 4: Daily Minute Range (v1=StartMin, v2=EndMin) */ SCHED_RULE_WEEKDAY_TIME /* 5: Weekday Time Range (v1=Wkdy, v2=StartMin, v3=EndMin) */ } SchedRuleType; typedef struct { SchedRuleType type; VMINT v1; VMINT v2; VMINT v3; const char* message; } SchedRule; /* User-defined Schedule Table */ static const SchedRule SCHEDULE_TABLE[] = { { SCHED_RULE_MONTH_DAY, 8, 22, 0, "It is birthday of STACORP." }, { SCHED_RULE_MONTH, 1, 0, 0, "Happy New Year! I wish this year to be a time of grandiose achievements, bold decisions and amazing discoveries." }, { SCHED_RULE_MONTH, 1, 0, 0, "Let every your day be filled with inspiration and energy, which will help you make your most cherished dreams come true. In this wonderful moment, let all your desires become closer to fulfillment!" }, { SCHED_RULE_MONTH_YEAR, 1, 0, 2015, "Man, set the time settings properly." }, { SCHED_RULE_MONTH_YEAR, 1, 0, 2030, "This year is the final year of the phone's calendar." }, { SCHED_RULE_MONTH_YEAR, 12, 0, 2030, "Goodbye, MediaTek. We aren't forgetting you." }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Pressing # shows random quotes, including spoiling secret ones." }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Beware of MRE memory leaks when creating custom content." }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Pressing * changes the sky color; else it synchronizes with time" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Мы – то, что вкладываем в свою голову. - Бернард Шоу" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "We are whatever we put inside our head. - Bernard Show" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Press 0 to restore actual sky color and quotation." }, { SCHED_RULE_ALWAYS, 0, 0, 0, "Is http://stacorp.mw.lt still working?" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "To add up to 64 custom quotes, add UTF-8 encoded derpquot.dat to the same folder as this vxp, filled this way: 0,0,0,0,27,This text is 27 signs long.,1,1,0,0,21,Dot means list ended.." }, { SCHED_RULE_ALWAYS, 0, 0, 0, "0=Always. 1=M,0,0. 2=M,D,0. 3=M,0,Y. 4=Min,Min,0. 5=Wkdy,Min,Min (where 0 = Sun, 6 = Sat)" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "To add up to 16 custom bypassers, add unoptimized GIFs to the same folder as this vxp and create UTF-8 (without BOM) file named derpnpc.dat; there recall files so: 1.GIF?3.gif?2.gif" }, { SCHED_RULE_ALWAYS, 0, 0, 0, "This app is 100% neuro slop, made with Gemini Flash 3.8." } }; #define SCHEDULE_COUNT (sizeof(SCHEDULE_TABLE) / sizeof(SCHEDULE_TABLE[0])) typedef struct { char names[MAX_NPC_VARIANTS][64]; int count; } NpcList; typedef struct { SchedRule rules[MAX_CUSTOM_QUOTES]; char messages[MAX_CUSTOM_QUOTES][1024]; int count; } CustomQuotes; typedef enum { UNLOCK_IDLE_LEFT, UNLOCK_AWAITING_RIGHT } UnlockState; typedef struct { VMINT layer_hdl; VMINT timer_id; VMINT scr_w; VMINT scr_h; VMINT grass_h; UnlockState unlock_state; VMINT unlock_countdown_ticks; VMBOOL sky_manual_mode; VMINT sky_preset_index; AnimatedGif gif_default; AnimatedGif gif_moon; AnimatedGif gif_npc; VMINT npc_x; VMBOOL npc_active; VMINT npc_respawn_ticks; char active_message[1024]; VMBOOL force_quote_mode; NpcList npc_list; CustomQuotes custom_quotes; } AppState; static AppState g_app; /* ========================================================================= */ /* UTILITIES & HELPER FUNCTIONS */ /* ========================================================================= */ static VMBOOL utf8_to_ucs2(VMWCHAR* out, VMINT out_max_chars, const char* utf8_src) { VMINT res; if (!out || !utf8_src || out_max_chars <= 0) { return FALSE; } /* 1. Try UTF-8 conversion */ res = vm_chset_convert( VM_CHSET_UTF8, VM_CHSET_UCS2, (VMCHAR*)utf8_src, (VMCHAR*)out, out_max_chars * sizeof(VMWCHAR) ); if (res == VM_CHSET_CONVERT_SUCCESS) { return TRUE; } /* 2. Fallback: Windows-1251 (Russian ANSI) if UTF-8 fails */ res = vm_chset_convert( VM_CHSET_CYRILLIC_WIN, VM_CHSET_UCS2, (VMCHAR*)utf8_src, (VMCHAR*)out, out_max_chars * sizeof(VMWCHAR) ); if (res == VM_CHSET_CONVERT_SUCCESS) { return TRUE; } /* 3. Fallback: standard ASCII expansion */ return (vm_ascii_to_ucs2(out, out_max_chars * sizeof(VMWCHAR), (VMSTR)utf8_src) == 0); } static int calc_day_of_week(int y, int m, int d) { static const int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 }; if (m < 3) y -= 1; return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7; } static VMBOOL load_file_data(const char* filepath, VMUINT8** out_buf, VMINT* out_size) { /* 1. Resources inside .vxp */ *out_buf = vm_load_resource((char*)filepath, out_size); if (*out_buf != NULL && *out_size > 0) { return TRUE; } /* 2. Loose file on SD card / phone filesystem */ { VMWCHAR app_exec[128]; VMWCHAR app_dir[128]; VMWCHAR wfilename[64]; VMFILE handle; VMUINT bytes_read = 0; VMUINT file_size = 0; VMINT dlen = 0; if (vm_get_exec_filename(app_exec) != 0) { *out_buf = NULL; *out_size = 0; return FALSE; } vm_get_path(app_exec, app_dir); dlen = vm_wstrlen(app_dir); if (dlen > 0 && app_dir[dlen - 1] != (VMWCHAR)'\\' && app_dir[dlen - 1] != (VMWCHAR)'/') { VMWCHAR slash[2] = { (VMWCHAR)'\\', 0 }; vm_wstrcat(app_dir, slash); } utf8_to_ucs2(wfilename, 64, filepath); vm_wstrcat(app_dir, wfilename); handle = vm_file_open(app_dir, MODE_READ, 1); if (handle < 0) { *out_buf = NULL; *out_size = 0; return FALSE; } vm_file_getfilesize(handle, &file_size); if (file_size == 0) { vm_file_close(handle); *out_buf = NULL; *out_size = 0; return FALSE; } *out_buf = (VMUINT8*)vm_malloc(file_size); if (!*out_buf) { vm_file_close(handle); *out_size = 0; return FALSE; } vm_file_read(handle, *out_buf, file_size, &bytes_read); vm_file_close(handle); *out_size = (VMINT)bytes_read; return TRUE; } } static VMINT count_gif_frames(const VMUINT8* data, VMINT size) { VMINT frames = 0; VMINT pos; VMUINT8 packed; if (!data || size < 13) return 1; if (data[0] != 'G' || data[1] != 'I' || data[2] != 'F') return 1; pos = 13; packed = data[10]; if (packed & 0x80) { VMINT gct_size = 3 * (1 << ((packed & 0x07) + 1)); pos += gct_size; } while (pos < size) { VMUINT8 block_id = data[pos++]; if (block_id == 0x3B) { break; } else if (block_id == 0x2C) { frames++; if (pos + 9 > size) break; packed = data[pos + 8]; pos += 9; if (packed & 0x80) { VMINT lct_size = 3 * (1 << ((packed & 0x07) + 1)); pos += lct_size; } if (pos >= size) break; pos++; while (pos < size) { VMUINT8 sub_len = data[pos++]; if (sub_len == 0) break; pos += sub_len; } } else if (block_id == 0x21) { if (pos >= size) break; pos++; while (pos < size) { VMUINT8 sub_len = data[pos++]; if (sub_len == 0) break; pos += sub_len; } } else { break; } } return (frames > 0) ? frames : 1; } static void init_gif(AnimatedGif* agif, const char* name) { memset(agif, 0, sizeof(AnimatedGif)); strncpy(agif->filename, name, sizeof(agif->filename) - 1); agif->trans_color = -1; if (load_file_data(agif->filename, &agif->data, &agif->size)) { agif->hcanvas = vm_graphic_load_image(agif->data, agif->size); if (agif->hcanvas > 0) { agif->total_frames = vm_graphic_get_frame_number(agif->hcanvas); if (agif->total_frames <= 0) { agif->total_frames = count_gif_frames(agif->data, agif->size); } for (int f = 0; f < agif->total_frames; f++) { struct frame_prop* p = vm_graphic_get_img_property(agif->hcanvas, (VMUINT8)f); if (p && p->width > 0 && p->height > 0) { agif->width = p->width; agif->height = p->height; } if (p && p->trans_color != (VMUINT16)-1 && (VMINT)p->trans_color != -1) { agif->trans_color = (VMINT)p->trans_color; } } if (agif->width <= 0 || agif->height <= 0) { vm_graphic_imgprop gprop; if (vm_graphic_get_img_property_ex(agif->data, agif->size, &gprop) == VM_GDI_SUCCEED) { agif->width = gprop.width; agif->height = gprop.height; } } if (agif->trans_color == -1) { VMUINT8* cbuf = vm_graphic_get_canvas_buffer(agif->hcanvas); if (cbuf) { agif->trans_color = (VMINT)vm_graphic_get_pixel(cbuf, 0, 0); } } if (agif->trans_color != -1) { vm_graphic_canvas_set_trans_color(agif->hcanvas, agif->trans_color); } agif->current_frame = 0; agif->is_loaded = TRUE; return; } } agif->is_loaded = FALSE; } static void release_gif(AnimatedGif* agif) { if (agif->hcanvas > 0) { vm_graphic_release_canvas(agif->hcanvas); agif->hcanvas = 0; } if (agif->data) { vm_free(agif->data); agif->data = NULL; } agif->is_loaded = FALSE; } /* ========================================================================= */ /* TRUE TRANSPARENT BLITTER */ /* ========================================================================= */ static void draw_gif_transparent(AnimatedGif* agif, VMINT dst_x, VMINT dst_y) { if (!agif->is_loaded || agif->hcanvas <= 0) return; if (dst_x + agif->width <= 0 || dst_x >= g_app.scr_w || dst_y + agif->height <= 0 || dst_y >= g_app.scr_h) { return; } struct frame_prop* prop = vm_graphic_get_img_property(agif->hcanvas, (VMUINT8)agif->current_frame); if (prop != NULL && prop->trans_color != (VMUINT16)-1 && (VMINT)prop->trans_color != -1) { vm_graphic_canvas_set_trans_color(agif->hcanvas, (VMINT)prop->trans_color); } else if (agif->trans_color != -1) { vm_graphic_canvas_set_trans_color(agif->hcanvas, agif->trans_color); } VMBYTE* dst_buf = (VMBYTE*)vm_graphic_get_layer_buffer(g_app.layer_hdl); VMBYTE* src_buf = (VMBYTE*)vm_graphic_get_canvas_buffer(agif->hcanvas); if (!dst_buf || !src_buf) return; VMINT src_x = 0; VMINT src_y = 0; VMINT w = agif->width; VMINT h = agif->height; /* Screen Boundary Clipping */ if (dst_x < 0) { w += dst_x; src_x = -dst_x; dst_x = 0; } if (dst_y < 0) { h += dst_y; src_y = -dst_y; dst_y = 0; } if (dst_x + w > g_app.scr_w) { w = g_app.scr_w - dst_x; } if (dst_y + h > g_app.scr_h) { h = g_app.scr_h - dst_y; } /* 1-based frame number prevents dropping the 1st frame */ VMINT blt_frame = agif->current_frame + 1; if (w > 0 && h > 0) { vm_graphic_blt_ex( dst_buf, dst_x, dst_y, src_buf, src_x, src_y, w, h, blt_frame, 255 ); } } /* ========================================================================= */ /* DAT FILE PARSERS */ /* ========================================================================= */ static void load_npc_list(NpcList* list) { VMUINT8* data = NULL; VMINT size = 0; int i = 0; int start = 0; int len = 0; list->count = 0; if (!load_file_data("derpnpc.dat", &data, &size) || !data || size <= 0) { return; } /* Strip UTF-8 BOM if present */ if (size >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { i = 3; } while (i < size && list->count < MAX_NPC_VARIANTS - 1) { while (i < size && (data[i] == '?' || data[i] == ' ' || data[i] == '\r' || data[i] == '\n')) { i++; } if (i >= size) break; start = i; while (i < size && data[i] != '?' && data[i] != '\r' && data[i] != '\n') { i++; } len = i - start; while (len > 0 && (data[start + len - 1] == ' ' || data[start + len - 1] == '\t')) { len--; } if (len > 0 && len < 63) { memcpy(list->names[list->count], data + start, len); list->names[list->count][len] = '\0'; list->count++; } } vm_free(data); /* Also add default.gif and npc.gif to the pool for combined diversity */ if (list->count > 0 && list->count < MAX_NPC_VARIANTS) { strncpy(list->names[list->count++], NPC_GIF_PATH, 63); } } static void load_custom_quotes(CustomQuotes* cq) { VMUINT8* data = NULL; VMINT size = 0; int pos = 0; int type = 0, v1 = 0, v2 = 0, v3 = 0, len_field = 0; int msg_start = 0; int msg_len = 0; int chars_read = 0; cq->count = 0; if (!load_file_data("derpquot.dat", &data, &size) || !data || size <= 0) { return; } /* Strip UTF-8 BOM if present */ if (size >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { pos = 3; } while (pos < size && cq->count < MAX_CUSTOM_QUOTES) { while (pos < size && (data[pos] == ' ' || data[pos] == '\r' || data[pos] == '\n' || data[pos] == '\t')) { pos++; } if (pos >= size || data[pos] == '.') break; #define PARSE_INT(var) do { \ while (pos < size && (data[pos] == ' ' || data[pos] == '\t')) pos++; \ int sign = 1; \ if (pos < size && data[pos] == '-') { sign = -1; pos++; } \ int val = 0, has_digit = 0; \ while (pos < size && data[pos] >= '0' && data[pos] <= '9') { \ val = val * 10 + (data[pos] - '0'); \ has_digit = 1; \ pos++; \ } \ if (!has_digit) goto skip_entry; \ var = val * sign; \ } while (0) #define EXPECT_COMMA() do { \ while (pos < size && (data[pos] == ' ' || data[pos] == '\t')) pos++; \ if (pos >= size || data[pos] != ',') goto skip_entry; \ pos++; \ } while (0) PARSE_INT(type); EXPECT_COMMA(); PARSE_INT(v1); EXPECT_COMMA(); PARSE_INT(v2); EXPECT_COMMA(); PARSE_INT(v3); EXPECT_COMMA(); PARSE_INT(len_field); EXPECT_COMMA(); /* Strictly read len_field characters and all their UTF-8 continuation bytes */ msg_start = pos; chars_read = 0; while (pos < size && chars_read < len_field && data[pos] != '\r' && data[pos] != '\n') { pos++; chars_read++; /* Consume all continuation bytes belonging to this character */ while (pos < size && (data[pos] & 0xC0) == 0x80) { pos++; } } msg_len = pos - msg_start; if (msg_len > 0) { if (msg_len >= 1023) msg_len = 1023; memcpy(cq->messages[cq->count], data + msg_start, msg_len); cq->messages[cq->count][msg_len] = '\0'; /* Map rule types: auto-resolve between 4 (range) and 5 (weekday) */ if (type == 4 && v3 != 0) { type = (int)SCHED_RULE_WEEKDAY_TIME; } else if (type == 5 && v3 == 0) { type = (int)SCHED_RULE_TIME_RANGE; } else if (type < (int)SCHED_RULE_ALWAYS || type > (int)SCHED_RULE_WEEKDAY_TIME) { type = (int)SCHED_RULE_ALWAYS; } cq->rules[cq->count].type = (SchedRuleType)type; cq->rules[cq->count].v1 = v1; cq->rules[cq->count].v2 = v2; cq->rules[cq->count].v3 = v3; cq->rules[cq->count].message = cq->messages[cq->count]; cq->count++; } while (pos < size && (data[pos] == ' ' || data[pos] == '\t')) { pos++; } if (pos < size) { char delim = (char)data[pos++]; if (delim == '.') { break; } } continue; skip_entry: while (pos < size && data[pos] != ',' && data[pos] != '.') { pos++; } if (pos < size) { if (data[pos] == '.') break; pos++; } } #undef PARSE_INT #undef EXPECT_COMMA vm_free(data); } /* ========================================================================= */ /* NPC SPAWN ROUTINE */ /* ========================================================================= */ static void spawn_npc(void) { release_gif(&g_app.gif_npc); VMBOOL loaded = FALSE; if (g_app.npc_list.count > 0) { int start_idx = app_rand() % g_app.npc_list.count; int attempts = 0; while (attempts < g_app.npc_list.count) { int pick = (start_idx + attempts) % g_app.npc_list.count; init_gif(&g_app.gif_npc, g_app.npc_list.names[pick]); if (g_app.gif_npc.is_loaded) { loaded = TRUE; break; } attempts++; } } if (!loaded) { init_gif(&g_app.gif_npc, NPC_GIF_PATH); } VMINT nw = g_app.gif_npc.is_loaded ? g_app.gif_npc.width : 28; g_app.npc_x = -nw; g_app.npc_active = TRUE; } /* ========================================================================= */ /* SCHEDULE EVALUATION */ /* ========================================================================= */ static void evaluate_schedule(vm_time_t* t, char* out_msg, VMINT max_len) { int total_rules = SCHEDULE_COUNT + g_app.custom_quotes.count; int matches[SCHEDULE_COUNT + MAX_CUSTOM_QUOTES]; int match_count = 0; int curr_minute = t->hour * 60 + t->min; int wday = calc_day_of_week(t->year, t->mon, t->day); int i; for (i = 0; i < total_rules; i++) { const SchedRule* r = (i < (int)SCHEDULE_COUNT) ? &SCHEDULE_TABLE[i] : &g_app.custom_quotes.rules[i - SCHEDULE_COUNT]; VMBOOL matched = FALSE; switch (r->type) { case SCHED_RULE_MONTH: if (t->mon == r->v1) matched = TRUE; break; case SCHED_RULE_MONTH_DAY: if (t->mon == r->v1 && t->day == r->v2) matched = TRUE; break; case SCHED_RULE_MONTH_YEAR: if (t->mon == r->v1 && t->year == r->v3) matched = TRUE; break; case SCHED_RULE_TIME_RANGE: if (r->v1 <= r->v2) { if (curr_minute >= r->v1 && curr_minute <= r->v2) matched = TRUE; } else { if (curr_minute >= r->v1 || curr_minute <= r->v2) matched = TRUE; } break; case SCHED_RULE_WEEKDAY_TIME: if (wday == r->v1) { if (r->v2 <= r->v3) { if (curr_minute >= r->v2 && curr_minute <= r->v3) matched = TRUE; } else { if (curr_minute >= r->v2 || curr_minute <= r->v3) matched = TRUE; } } break; case SCHED_RULE_ALWAYS: matched = TRUE; break; } if (matched) { matches[match_count++] = i; } } if (match_count > 0) { int choice = matches[app_rand() % match_count]; const char* msg = (choice < (int)SCHEDULE_COUNT) ? SCHEDULE_TABLE[choice].message : g_app.custom_quotes.rules[choice - SCHEDULE_COUNT].message; strncpy(out_msg, msg, max_len - 1); out_msg[max_len - 1] = '\0'; } else { strncpy(out_msg, "There aren't any quotes; consider recompiling app and editing SCHEDULE_TABLE, or put derpquot.dat to the same folder as vxp, formatted this way: 0,0,0,0,27,This text is 27 signs long.,1,1,0,0,21,Dot means list ended..", max_len - 1); out_msg[max_len - 1] = '\0'; } } /* ========================================================================= */ /* DRAWING ROUTINES */ /* ========================================================================= */ static void set_draw_color(VMUINT16 c565) { vm_graphic_color c; c.vm_color_565 = c565; c.vm_color_888 = VM_COLOR_565_TO_888(c565); vm_graphic_setcolor(&c); } static VMUINT16 get_sky_color(vm_time_t* t) { if (g_app.sky_manual_mode) { return SKY_PRESETS[g_app.sky_preset_index % NUM_SKY_PRESETS]; } if (t->hour >= 5 && t->hour < 7) { return SKY_PRESETS[SKY_SHADE_DAWN]; } else if (t->hour >= 7 && t->hour < 12) { return SKY_PRESETS[SKY_SHADE_MORNING]; } else if (t->hour >= 12 && t->hour < 18) { return SKY_PRESETS[SKY_SHADE_MIDDAY]; } else if (t->hour >= 18 && t->hour < 20) { return SKY_PRESETS[SKY_SHADE_SUNSET]; } else if (t->hour >= 20 && t->hour < 22) { return SKY_PRESETS[SKY_SHADE_TWILIGHT]; } else { return SKY_PRESETS[SKY_SHADE_NIGHT]; } } static void draw_scene(void) { vm_time_t t; VMWCHAR wstr[16]; VMWCHAR w_msg[512]; char astr[16]; VMINT sky_h = g_app.scr_h - g_app.grass_h; VMINT time_box_w = 0; VMINT time_bottom_y = 0; VMINT center_gif_top_y = 0; vm_get_time(&t); /* 1. SKY & GRASS */ set_draw_color(get_sky_color(&t)); vm_graphic_fill_rect_ex(g_app.layer_hdl, 0, 0, g_app.scr_w, sky_h); set_draw_color(VM_COLOR_888_TO_565(20, 90, 25)); vm_graphic_fill_rect_ex(g_app.layer_hdl, 0, sky_h, g_app.scr_w, g_app.grass_h); set_draw_color(VM_COLOR_888_TO_565(40, 140, 40)); vm_graphic_line_ex(g_app.layer_hdl, 0, sky_h, g_app.scr_w, sky_h); /* 2. DATE & TIME (Top-Right) */ vm_graphic_set_font(VM_SMALL_FONT); set_draw_color(VM_COLOR_WHITE); sprintf(astr, "%02d:%02d:%02d", t.hour, t.min, t.sec); utf8_to_ucs2(wstr, 128, astr); time_box_w = vm_graphic_get_string_width(wstr); VMINT str_h = vm_graphic_get_string_height(wstr); VMINT tx = g_app.scr_w - time_box_w - 8; VMINT ty = 6; vm_graphic_textout_to_layer(g_app.layer_hdl, tx, ty, wstr, vm_wstrlen(wstr)); sprintf(astr, "%04d-%02d-%02d", t.year, t.mon, t.day); utf8_to_ucs2(wstr, 128, astr); VMINT date_w = vm_graphic_get_string_width(wstr); if (date_w > time_box_w) { time_box_w = date_w; } vm_graphic_textout_to_layer(g_app.layer_hdl, g_app.scr_w - date_w - 8, ty + str_h + 2, wstr, vm_wstrlen(wstr)); time_bottom_y = ty + (str_h * 2) + 6; /* 3. MOON DISPLAY (4 AM / night / dark presets) */ VMBOOL is_night = (t.hour >= 20 || t.hour < 5); if (g_app.sky_manual_mode) { is_night = (g_app.sky_preset_index == SKY_SHADE_NIGHT || g_app.sky_preset_index == SKY_SHADE_TWILIGHT); } if (is_night) { VMINT moon_w = g_app.gif_moon.is_loaded ? g_app.gif_moon.width : 24; VMINT moon_h = g_app.gif_moon.is_loaded ? g_app.gif_moon.height : 24; if (6 + moon_w + 12 < (g_app.scr_w - time_box_w - 8)) { if (g_app.gif_moon.is_loaded) { draw_gif_transparent(&g_app.gif_moon, 6, 6); } else { set_draw_color(VM_COLOR_888_TO_565(250, 250, 210)); vm_graphic_fill_ellipse_ex(g_app.layer_hdl, 6, 6, moon_w, moon_h); set_draw_color(get_sky_color(&t)); vm_graphic_fill_ellipse_ex(g_app.layer_hdl, 12, 4, moon_w - 4, moon_h); } } } /* 4. CENTER SPRITE (default.gif - Uses _blt with True Transparency) */ VMINT def_w = g_app.gif_default.is_loaded ? g_app.gif_default.width : 40; VMINT def_h = g_app.gif_default.is_loaded ? g_app.gif_default.height : 48; VMINT def_x = (g_app.scr_w - def_w) / 2; VMINT def_y = sky_h - def_h; center_gif_top_y = def_y; if (g_app.gif_default.is_loaded) { draw_gif_transparent(&g_app.gif_default, def_x, def_y); } else { set_draw_color(VM_COLOR_888_TO_565(180, 180, 180)); vm_graphic_fill_rect_ex(g_app.layer_hdl, def_x, def_y, def_w, def_h); set_draw_color(VM_COLOR_888_TO_565(50, 50, 50)); utf8_to_ucs2(wstr, 128, "OBJ"); vm_graphic_textout_to_layer(g_app.layer_hdl, def_x + 8, def_y + 14, wstr, vm_wstrlen(wstr)); } /* 5. WALKING NPC (Rendered with True Transparency BEHIND Card) */ if (g_app.npc_active) { VMINT nw = g_app.gif_npc.is_loaded ? g_app.gif_npc.width : 28; VMINT nh = g_app.gif_npc.is_loaded ? g_app.gif_npc.height : 36; VMINT ny = sky_h - nh; if (g_app.gif_npc.is_loaded) { draw_gif_transparent(&g_app.gif_npc, g_app.npc_x, ny); } else { set_draw_color(VM_COLOR_888_TO_565(220, 60, 60)); vm_graphic_fill_ellipse_ex(g_app.layer_hdl, g_app.npc_x + 4, ny, nw - 8, nh / 2); set_draw_color(VM_COLOR_888_TO_565(40, 40, 200)); vm_graphic_fill_rect_ex(g_app.layer_hdl, g_app.npc_x, ny + nh / 2, nw, nh / 2); } } /* ------------------------------------------------------------- */ /* 6. MULTI-LINE MESSAGE CARD (Max 6 Lines, Max 48 Chars/Line) */ /* ------------------------------------------------------------- */ #define MAX_CHARS_PER_LINE 40 /* 48 VMWCHARs = 96 bytes */ if (!g_app.force_quote_mode && (t.sec % 60 == 0)) { evaluate_schedule(&t, g_app.active_message, sizeof(g_app.active_message)); } vm_graphic_set_font(VM_SMALL_FONT); utf8_to_ucs2(w_msg, 512, g_app.active_message); VMINT mh = vm_graphic_get_character_height(); if (mh <= 0) mh = 14; VMINT line_gap = 2; VMINT box_margin = 12; VMINT box_w = g_app.scr_w - (box_margin * 2); VMINT max_text_w = box_w - 16; VMINT box_x = box_margin; /* Calculate maximum lines that physically fit above default.gif */ VMINT avail_v_space = center_gif_top_y - time_bottom_y - 8; VMINT max_possible_lines = (avail_v_space - 12) / (mh + line_gap); if (max_possible_lines < 1) max_possible_lines = 1; if (max_possible_lines > 12) max_possible_lines = 12; /* Each line buffer holds exactly 48 characters + 1 null terminator */ VMWCHAR lines[12][MAX_CHARS_PER_LINE + 1]; VMINT line_count = 0; VMINT total_len = vm_wstrlen(w_msg); VMINT cur_pos = 0; while (cur_pos < total_len && line_count < max_possible_lines) { /* Skip leading spaces on the line */ while (cur_pos < total_len && w_msg[cur_pos] == (VMWCHAR)' ') { cur_pos++; } if (cur_pos >= total_len) break; /* If remainder fits within 48 chars AND within pixel width */ VMINT remaining_len = total_len - cur_pos; if (remaining_len <= MAX_CHARS_PER_LINE && vm_graphic_get_string_width(w_msg + cur_pos) <= max_text_w) { for (int k = 0; k < remaining_len; k++) { lines[line_count][k] = w_msg[cur_pos + k]; } lines[line_count][remaining_len] = 0; line_count++; break; } /* If this is the 6th/final allowed line, fit up to 45 chars and append "..." */ if (line_count == max_possible_lines - 1) { VMINT fit_len = 0; VMWCHAR temp[MAX_CHARS_PER_LINE + 1]; VMWCHAR dots[4]; utf8_to_ucs2(dots, 4, "..."); VMINT dots_w = vm_graphic_get_string_width(dots); /* Leave 3 slots for "..." -> max 45 characters */ while (cur_pos + fit_len < total_len && fit_len < (MAX_CHARS_PER_LINE - 3)) { temp[fit_len] = w_msg[cur_pos + fit_len]; temp[fit_len + 1] = 0; if (vm_graphic_get_string_width(temp) + dots_w > max_text_w) { break; } fit_len++; } for (int k = 0; k < fit_len; k++) { lines[line_count][k] = w_msg[cur_pos + k]; } lines[line_count][fit_len] = 0; vm_wstrcat(lines[line_count], dots); line_count++; break; } /* Forward search for maximum segment (Capped at 48 chars and pixel width) */ VMINT end_pos = cur_pos; VMWCHAR temp[MAX_CHARS_PER_LINE + 1]; while (end_pos < total_len && (end_pos - cur_pos) < MAX_CHARS_PER_LINE) { VMINT seg_len = end_pos - cur_pos + 1; for (int k = 0; k < seg_len; k++) { temp[k] = w_msg[cur_pos + k]; } temp[seg_len] = 0; if (vm_graphic_get_string_width(temp) > max_text_w) { break; } end_pos++; } /* Walk backward to find the nearest space to wrap cleanly at a word */ VMINT break_pos = end_pos; while (break_pos > cur_pos && w_msg[break_pos] != (VMWCHAR)' ') { break_pos--; } /* If no space exists in the 48-char block (one giant word), force hard break */ if (break_pos <= cur_pos) { break_pos = (end_pos > cur_pos) ? end_pos : cur_pos + 1; } VMINT copy_len = break_pos - cur_pos; if (copy_len > MAX_CHARS_PER_LINE) copy_len = MAX_CHARS_PER_LINE; for (int k = 0; k < copy_len; k++) { lines[line_count][k] = w_msg[cur_pos + k]; } lines[line_count][copy_len] = 0; line_count++; cur_pos = break_pos; } if (line_count < 1) line_count = 1; /* Dynamic Card Height based on actual wrapped line count */ VMINT text_block_h = (line_count * mh) + ((line_count - 1) * line_gap); VMINT box_h = text_block_h + 10; VMINT box_y = center_gif_top_y - box_h - 4; /* Prevent box from overlapping the Date/Time header */ if (box_y < time_bottom_y + 2) { box_y = time_bottom_y + 2; } /* Draw Card Background & Border */ set_draw_color(VM_COLOR_WHITE); vm_graphic_fill_roundrect_ex(g_app.layer_hdl, box_x, box_y, box_w, box_h, 6); set_draw_color(VM_COLOR_888_TO_565(180, 180, 190)); vm_graphic_roundrect_ex(g_app.layer_hdl, box_x, box_y, box_w, box_h, 6); /* Draw All Lines Centered Horizontally */ set_draw_color(VM_COLOR_BLACK); for (VMINT li = 0; li < line_count; li++) { VMINT lw = vm_graphic_get_string_width(lines[li]); VMINT lx = box_x + (box_w - lw) / 2; VMINT ly = box_y + 5 + li * (mh + line_gap); vm_graphic_textout_to_layer(g_app.layer_hdl, lx, ly, lines[li], vm_wstrlen(lines[li])); } #undef MAX_CHARS_PER_LINE /* 7. SOFTKEY BAR & UNLOCK PROMPTS */ vm_graphic_set_font(VM_MEDIUM_FONT); set_draw_color(VM_COLOR_WHITE); if (g_app.unlock_state == UNLOCK_IDLE_LEFT) { utf8_to_ucs2(wstr, 64, "Unlock"); VMINT uy = sky_h + (g_app.grass_h - vm_graphic_get_string_height(wstr)) / 2; vm_graphic_textout_to_layer(g_app.layer_hdl, 10, uy, wstr, vm_wstrlen(wstr)); } else if (g_app.unlock_state == UNLOCK_AWAITING_RIGHT) { utf8_to_ucs2(wstr, 64, "Confirm ->"); VMINT uw = vm_graphic_get_string_width(wstr); VMINT uy = sky_h + (g_app.grass_h - vm_graphic_get_string_height(wstr)) / 2; vm_graphic_textout_to_layer(g_app.layer_hdl, g_app.scr_w - uw - 10, uy, wstr, vm_wstrlen(wstr)); VMINT bar_w = ((g_app.scr_w / 2) * g_app.unlock_countdown_ticks) / UNLOCK_TIMEOUT_TICKS; set_draw_color(VM_COLOR_888_TO_565(240, 200, 50)); vm_graphic_fill_rect_ex(g_app.layer_hdl, g_app.scr_w - bar_w - 10, g_app.scr_h - 4, bar_w, 2); } vm_graphic_flush_layer(&g_app.layer_hdl, 1); } /* ========================================================================= */ /* ENGINE TICK (10 FPS / 100 ms) */ /* ========================================================================= */ static void engine_tick(VMINT tid) { if (g_app.gif_default.is_loaded && g_app.gif_default.total_frames > 1) { g_app.gif_default.current_frame = (g_app.gif_default.current_frame + 1) % g_app.gif_default.total_frames; } if (g_app.gif_moon.is_loaded && g_app.gif_moon.total_frames > 1) { g_app.gif_moon.current_frame = (g_app.gif_moon.current_frame + 1) % g_app.gif_moon.total_frames; } if (g_app.unlock_state == UNLOCK_AWAITING_RIGHT) { if (--g_app.unlock_countdown_ticks <= 0) { g_app.unlock_state = UNLOCK_IDLE_LEFT; } } if (g_app.npc_active) { g_app.npc_x += NPC_SPEED_PIXELS_PER_F; if (g_app.gif_npc.is_loaded && g_app.gif_npc.total_frames > 1) { g_app.gif_npc.current_frame = (g_app.gif_npc.current_frame + 1) % g_app.gif_npc.total_frames; } if (g_app.npc_x > g_app.scr_w + 10) { g_app.npc_active = FALSE; g_app.npc_respawn_ticks = 100 + (app_rand() % (3000 - 99)); } } else { if (--g_app.npc_respawn_ticks <= 0) { spawn_npc(); } } draw_scene(); } /* ========================================================================= */ /* INPUT HANDLING */ /* ========================================================================= */ static void handle_key_evt(VMINT event, VMINT keycode) { if (event != VM_KEY_EVENT_DOWN) { return; } switch (keycode) { case VM_KEY_LEFT_SOFTKEY: if (g_app.unlock_state == UNLOCK_IDLE_LEFT) { g_app.unlock_state = UNLOCK_AWAITING_RIGHT; g_app.unlock_countdown_ticks = UNLOCK_TIMEOUT_TICKS; draw_scene(); } break; case VM_KEY_RIGHT_SOFTKEY: if (g_app.unlock_state == UNLOCK_AWAITING_RIGHT) { vm_exit_app(); } break; case VM_KEY_STAR: if (!g_app.sky_manual_mode) { g_app.sky_manual_mode = TRUE; g_app.sky_preset_index = 0; } else { g_app.sky_preset_index++; if (g_app.sky_preset_index >= (int)NUM_SKY_PRESETS) { g_app.sky_manual_mode = FALSE; g_app.sky_preset_index = 0; } } draw_scene(); break; case VM_KEY_POUND: { int total_rules = SCHEDULE_COUNT + g_app.custom_quotes.count; int random_idx = app_rand() % total_rules; const char* msg = (random_idx < (int)SCHEDULE_COUNT) ? SCHEDULE_TABLE[random_idx].message : g_app.custom_quotes.rules[random_idx - SCHEDULE_COUNT].message; strncpy(g_app.active_message, msg, sizeof(g_app.active_message) - 1); g_app.active_message[sizeof(g_app.active_message) - 1] = '\0'; g_app.force_quote_mode = TRUE; draw_scene(); break; } case VM_KEY_NUM0: { vm_time_t t; g_app.sky_manual_mode = FALSE; g_app.sky_preset_index = 0; g_app.force_quote_mode = FALSE; vm_get_time(&t); evaluate_schedule(&t, g_app.active_message, sizeof(g_app.active_message)); draw_scene(); break; } default: if (g_app.unlock_state == UNLOCK_AWAITING_RIGHT) { g_app.unlock_state = UNLOCK_IDLE_LEFT; draw_scene(); } break; } } /* ========================================================================= */ /* SYSTEM LIFECYCLE */ /* ========================================================================= */ static void handle_sysevt(VMINT message, VMINT param) { switch (message) { case VM_MSG_CREATE: { app_srand(); g_app.scr_w = vm_graphic_get_screen_width(); g_app.scr_h = vm_graphic_get_screen_height(); g_app.grass_h = 36; g_app.layer_hdl = vm_graphic_create_layer(0, 0, g_app.scr_w, g_app.scr_h, -1); vm_graphic_active_layer(g_app.layer_hdl); load_npc_list(&g_app.npc_list); load_custom_quotes(&g_app.custom_quotes); init_gif(&g_app.gif_default, DEFAULT_GIF_PATH); init_gif(&g_app.gif_moon, MOON_GIF_PATH); spawn_npc(); g_app.unlock_state = UNLOCK_IDLE_LEFT; g_app.sky_manual_mode = FALSE; g_app.force_quote_mode = FALSE; vm_time_t t; vm_get_time(&t); evaluate_schedule(&t, g_app.active_message, sizeof(g_app.active_message)); vm_reg_keyboard_callback(handle_key_evt); g_app.timer_id = vm_create_timer(TIMER_INTERVAL_MS, engine_tick); break; } case VM_MSG_PAINT: draw_scene(); break; case VM_MSG_HIDE: case VM_MSG_INACTIVE: if (g_app.timer_id > 0) { vm_delete_timer(g_app.timer_id); g_app.timer_id = 0; } break; case VM_MSG_ACTIVE: if (g_app.timer_id == 0) { g_app.timer_id = vm_create_timer(TIMER_INTERVAL_MS, engine_tick); } draw_scene(); break; case VM_MSG_QUIT: if (g_app.timer_id > 0) { vm_delete_timer(g_app.timer_id); } release_gif(&g_app.gif_default); release_gif(&g_app.gif_moon); release_gif(&g_app.gif_npc); if (g_app.layer_hdl >= 0) { vm_graphic_delete_layer(g_app.layer_hdl); } break; } } void vm_main(void) { memset(&g_app, 0, sizeof(AppState)); vm_reg_sysevt_callback(handle_sysevt); }