// lity_editor.c - Lity3D Editor (Win32 Unicode + JSON i18n) #include #include #include #include #include #include #include #define IDC_TREE 1001 #define IDC_EDIT 1002 #define IDC_CONSOLE 1003 #define IDM_NEW 2001 #define IDM_OPEN 2002 #define IDM_SAVE 2003 #define IDM_RUN 2004 #define IDM_BUILD 2005 #define IDM_PACKAGE 2006 #define IDM_ABOUT 2007 #define IDM_LANG_BASE 3000 static HWND g_hwnd, g_tree, g_edit, g_console; static WCHAR g_current_file[MAX_PATH] = L""; static WCHAR g_engine_dir[MAX_PATH] = L""; static int g_lang_count = 0; static WCHAR g_lang_codes[128][32]; static WCHAR g_lang_names[128][128]; static WCHAR g_cur_lang[32] = L"en"; // Translation keys static WCHAR g_t[256][256]; // up to 256 keys static int g_t_count = 0; static WCHAR g_t_keys[256][64]; static WCHAR* T(const char* key) { for (int i = 0; i < g_t_count; i++) { char kbuf[64]; WideCharToMultiByte(CP_UTF8, 0, g_t_keys[i], -1, kbuf, sizeof(kbuf), NULL, NULL); if (strcmp(kbuf, key) == 0) return g_t[i]; } // fallback to English defaults static const WCHAR* defs[][2] = { {L"app_title",L"Lity3D Editor"},{L"file",L"File"},{L"build",L"Build"}, {L"scene",L"Scene"},{L"help",L"Help"},{L"lang",L"Language"}, {L"new",L"New"},{L"open",L"Open"},{L"save",L"Save"},{L"exit",L"Exit"}, {L"run",L"Run"},{L"compile",L"Build Engine"},{L"package",L"Package"}, {L"explorer",L"Explorer"},{L"output",L"Output"},{L"ready",L"Ready"}, {L"about",L"About Lity3D"}, }; for (int i = 0; i < 17; i++) { char kbuf2[64]; WideCharToMultiByte(CP_UTF8, 0, defs[i][0], -1, kbuf2, sizeof(kbuf2), NULL, NULL); if (strcmp(kbuf2, key) == 0) return defs[i][1]; } static WCHAR fb[128]; MultiByteToWideChar(CP_UTF8, 0, key, -1, fb, 128); return fb; } static void parse_json_value(const char* json, const char* key, WCHAR* out, int max) { char pattern[128]; sprintf(pattern, "\"%s\"", key); const char* p = strstr(json, pattern); if (!p) { out[0] = 0; return; } p = strchr(p + strlen(pattern), ':'); if (!p) { out[0] = 0; return; } p++; while (*p == ' ') p++; if (*p == '"') { p++; const char* end = strchr(p, '"'); if (!end) end = p + strlen(p); int len = (int)(end - p); char tmp[512] = {0}; if (len > 511) len = 511; memcpy(tmp, p, len); MultiByteToWideChar(CP_UTF8, 0, tmp, -1, out, max); } } static void load_lang(const WCHAR* code) { WCHAR path[MAX_PATH]; wsprintfW(path, L"%s\\languages\\%s.json", g_engine_dir, code); HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hf == INVALID_HANDLE_VALUE) { // try English fallback wsprintfW(path, L"%s\\languages\\en.json", g_engine_dir); hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hf == INVALID_HANDLE_VALUE) return; } DWORD sz = GetFileSize(hf, NULL); char* buf = malloc(sz + 1); DWORD rd; ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf); g_t_count = 0; const char* keys[] = {"app_title","file","build","scene","help","lang","new","open","save","exit", "run","compile","package","explorer","output","ready","running","saved","opened", "build_first","kill","packaged","about"}; for (int i = 0; i < 23; i++) { MultiByteToWideChar(CP_UTF8, 0, keys[i], -1, g_t_keys[g_t_count], 64); parse_json_value(buf, keys[i], g_t[g_t_count], 256); g_t_count++; } free(buf); wcscpy(g_cur_lang, code); } static void discover_languages(void) { g_lang_count = 0; WCHAR pattern[MAX_PATH]; wsprintfW(pattern, L"%s\\languages\\*.json", g_engine_dir); WIN32_FIND_DATAW fd; HANDLE h = FindFirstFileW(pattern, &fd); if (h == INVALID_HANDLE_VALUE) return; do { WCHAR name[MAX_PATH]; wcscpy(name, fd.cFileName); WCHAR* dot = wcsrchr(name, L'.'); if (dot) *dot = 0; wcscpy(g_lang_codes[g_lang_count], name); // Read language name from JSON WCHAR path[MAX_PATH]; wsprintfW(path, L"%s\\languages\\%s", g_engine_dir, fd.cFileName); HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hf != INVALID_HANDLE_VALUE) { DWORD sz = GetFileSize(hf, NULL); char* buf = malloc(sz + 1); DWORD rd; ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf); WCHAR title[256]; parse_json_value(buf, "app_title", title, 256); wsprintfW(g_lang_names[g_lang_count], L"%s (%s)", title, name); free(buf); } g_lang_count++; } while (FindNextFileW(h, &fd)); FindClose(h); } static void console_msg(const char* msg) { int len = GetWindowTextLengthA(g_console); SendMessageA(g_console, EM_SETSEL, len, len); SendMessageA(g_console, EM_REPLACESEL, FALSE, (LPARAM)msg); } static void populate_tree(void) { TreeView_DeleteAllItems(g_tree); TVINSERTSTRUCTW tvi = {0}; tvi.hParent = TVI_ROOT; tvi.hInsertAfter = TVI_LAST; tvi.item.mask = TVIF_TEXT; tvi.item.pszText = (WCHAR*)L"lity3d"; HTREEITEM root = TreeView_InsertItem(g_tree, &tvi); WIN32_FIND_DATAW fd; WCHAR pat[MAX_PATH]; wsprintfW(pat, L"%s\\*", g_engine_dir); HANDLE hFind = FindFirstFileW(pat, &fd); if (hFind == INVALID_HANDLE_VALUE) return; do { if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (!wcscmp(fd.cFileName,L".")||!wcscmp(fd.cFileName,L"..")) continue; if (!wcscmp(fd.cFileName,L".git")||!wcscmp(fd.cFileName,L"build")||!wcscmp(fd.cFileName,L"target")) continue; tvi.hParent = root; tvi.item.pszText = fd.cFileName; HTREEITEM d = TreeView_InsertItem(g_tree, &tvi); WCHAR sub[MAX_PATH]; wsprintfW(sub, L"%s\\%s\\*", g_engine_dir, fd.cFileName); WIN32_FIND_DATAW fd2; HANDLE h2 = FindFirstFileW(sub, &fd2); if (h2 != INVALID_HANDLE_VALUE) { do { if (fd2.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) continue; tvi.hParent = d; tvi.item.pszText = fd2.cFileName; TreeView_InsertItem(g_tree, &tvi); } while (FindNextFileW(h2, &fd2)); FindClose(h2); } } else { if (wcsstr(fd.cFileName,L".py")||wcsstr(fd.cFileName,L".c")||wcsstr(fd.cFileName,L".h")|| wcsstr(fd.cFileName,L".asm")||wcsstr(fd.cFileName,L".md")||wcsstr(fd.cFileName,L".txt")|| wcsstr(fd.cFileName,L".json")||wcsstr(fd.cFileName,L".toml")||wcsstr(fd.cFileName,L".bat")) { tvi.hParent = root; tvi.item.pszText = fd.cFileName; TreeView_InsertItem(g_tree, &tvi); } } } while (FindNextFileW(hFind, &fd)); FindClose(hFind); } static void open_file_at(WCHAR* path) { HANDLE hf = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hf == INVALID_HANDLE_VALUE) return; DWORD sz = GetFileSize(hf, NULL); char* buf = malloc(sz + 1); DWORD rd; ReadFile(hf, buf, sz, &rd, NULL); buf[sz] = '\0'; CloseHandle(hf); SetWindowTextA(g_edit, buf); free(buf); wcscpy(g_current_file, path); WCHAR title[MAX_PATH]; wsprintfW(title, L"%s - %s", T("app_title"), path); SetWindowTextW(g_hwnd, title); } static void open_file_dialog(void) { WCHAR file[MAX_PATH] = L""; OPENFILENAMEW of = {sizeof(of)}; of.lpstrFilter = L"All\0*.*\0Python\0*.py\0C\0*.c;*.h\0"; of.lpstrFile = file; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST; if (GetOpenFileNameW(&of)) open_file_at(file); } static void save_file(void) { if (!g_current_file[0]) { open_file_dialog(); return; } int len = GetWindowTextLengthA(g_edit); char* buf = malloc(len + 1); GetWindowTextA(g_edit, buf, len + 1); HANDLE hf = CreateFileW(g_current_file, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if (hf != INVALID_HANDLE_VALUE) { DWORD wr; WriteFile(hf, buf, len, &wr, NULL); CloseHandle(hf); } free(buf); console_msg("[OK] Saved.\n"); } static void run_game(void) { WCHAR p[MAX_PATH]; wsprintfW(p, L"%s\\build\\lity.exe", g_engine_dir); if (GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { console_msg("[!] Build engine first\n"); return; } ShellExecuteW(NULL, L"open", p, NULL, g_engine_dir, SW_SHOWNORMAL); console_msg("[>] Run game\n"); } static void build_engine(void) { WCHAR cmd[MAX_PATH*2]; wsprintfW(cmd, L"/c set PATH=C:\\mingw64\\bin;%%PATH%%&& cd /d %s\\build && cmake --build . -j4", g_engine_dir); ShellExecuteW(NULL, L"open", L"cmd.exe", cmd, NULL, SW_SHOWNORMAL); console_msg("[>] Build engine...\n"); } static void package_exe(void) { WCHAR src[MAX_PATH], dst[MAX_PATH] = L""; wsprintfW(src, L"%s\\build\\lity.exe", g_engine_dir); OPENFILENAMEW of = {sizeof(of)}; of.lpstrFilter = L"EXE\0*.exe\0"; of.lpstrFile = dst; of.nMaxFile = MAX_PATH; of.lpstrTitle = L"Package"; of.Flags = OFN_OVERWRITEPROMPT; if (GetSaveFileNameW(&of)) { CopyFileW(src, dst, FALSE); console_msg("[OK] Packaged.\n"); } } static void rebuild_lang_menu(void) { HMENU hBar = GetMenu(g_hwnd); HMENU hLang = CreatePopupMenu(); for (int i = 0; i < g_lang_count; i++) { UINT flags = (!wcscmp(g_lang_codes[i], g_cur_lang)) ? (MF_STRING|MF_CHECKED) : MF_STRING; AppendMenuW(hLang, flags, IDM_LANG_BASE + i, g_lang_names[i]); } // Replace old Language menu HMENU hOld = GetSubMenu(hBar, 3); // Language is 4th menu if (hOld) { RemoveMenu(hBar, 3, MF_BYPOSITION); DestroyMenu(hOld); } // Insert before Help InsertMenuW(hBar, 3, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hLang, T("lang")); DrawMenuBar(g_hwnd); } static LRESULT CALLBACK WP(HWND h, UINT m, WPARAM w, LPARAM l) { switch (m) { case WM_COMMAND: if (LOWORD(w) == IDM_NEW) { SetWindowTextA(g_edit, ""); g_current_file[0] = 0; } else if (LOWORD(w) == IDM_OPEN) open_file_dialog(); else if (LOWORD(w) == IDM_SAVE) save_file(); else if (LOWORD(w) == IDM_RUN) run_game(); else if (LOWORD(w) == IDM_BUILD) build_engine(); else if (LOWORD(w) == IDM_PACKAGE) package_exe(); else if (LOWORD(w) == IDM_ABOUT) MessageBoxW(h, L"Lity3D Engine v1.0\nC + MASM x64\nD3D11 + Python Scripting\n\nhttps://gitee.com/shuiyu1123/lity3d", T("about"), MB_OK|MB_ICONINFORMATION); else if (LOWORD(w) >= IDM_LANG_BASE && LOWORD(w) < IDM_LANG_BASE + g_lang_count) { int idx = LOWORD(w) - IDM_LANG_BASE; load_lang(g_lang_codes[idx]); rebuild_lang_menu(); SetWindowTextW(g_hwnd, T("app_title")); populate_tree(); } break; case WM_NOTIFY: { NMHDR* nm = (NMHDR*)l; if (nm->idFrom == IDC_TREE && nm->code == TVN_SELCHANGED) { TVITEMW ti = {0}; ti.mask = TVIF_TEXT|TVIF_HANDLE; ti.hItem = TreeView_GetSelection(g_tree); WCHAR name[MAX_PATH]; ti.pszText = name; ti.cchTextMax = MAX_PATH; TreeView_GetItem(g_tree, &ti); if (!ti.hItem) break; HTREEITEM par = TreeView_GetParent(g_tree, ti.hItem); WCHAR full[MAX_PATH]; if (par) { WCHAR parent[MAX_PATH]={0}; TVITEMW pti={0}; pti.mask=TVIF_TEXT; pti.hItem=par; pti.pszText=parent; pti.cchTextMax=MAX_PATH; TreeView_GetItem(g_tree, &pti); wsprintfW(full, L"%s\\%s\\%s", g_engine_dir, parent, name); } else wsprintfW(full, L"%s\\%s", g_engine_dir, name); DWORD attr = GetFileAttributesW(full); if (attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY)) open_file_at(full); } break; } case WM_SIZE: { int w2 = LOWORD(l), h2 = HIWORD(l); if (w2<100||h2<100) break; int tw=220, ch=160; MoveWindow(g_tree,0,0,tw,h2-ch,TRUE); MoveWindow(g_edit,tw+4,0,w2-tw-4,h2-ch,TRUE); MoveWindow(g_console,0,h2-ch,w2,ch-24,TRUE); break; } case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProcW(h, m, w, l); } int WINAPI wWinMain(HINSTANCE hI, HINSTANCE hP, LPWSTR nC, int nS) { (void)hP; (void)nC; (void)nS; GetModuleFileNameW(NULL, g_engine_dir, MAX_PATH); WCHAR* sl = wcsrchr(g_engine_dir, L'\\'); if (sl) *sl = 0; // Editor.exe is in lity3d/Editor/, engine root is one level up sl = wcsrchr(g_engine_dir, L'\\'); if (sl) *sl = 0; INITCOMMONCONTROLSEX icc = {sizeof(icc), ICC_TREEVIEW_CLASSES}; InitCommonControlsEx(&icc); WNDCLASSEXW wc = {sizeof(wc)}; wc.style=CS_HREDRAW|CS_VREDRAW; wc.lpfnWndProc=WP; wc.hInstance=hI; wc.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1); wc.lpszClassName=L"LityEditor"; wc.hCursor=LoadCursor(NULL,IDC_ARROW); wc.hIcon=LoadIcon(NULL,IDI_APPLICATION); RegisterClassExW(&wc); // Discover and load languages discover_languages(); // Auto-detect system language WCHAR sysLang[10]; int n = GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_SABBREVLANGNAME, sysLang, 10); if (n > 0) { for (int i = 0; i < g_lang_count; i++) { if (!_wcsicmp(g_lang_codes[i], sysLang)) { load_lang(g_lang_codes[i]); goto lang_done; } } } load_lang(L"en"); lang_done: g_hwnd = CreateWindowExW(0, L"LityEditor", T("app_title"), WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN, 100,100,1400,900, NULL, NULL, hI, NULL); g_tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"", WS_CHILD|WS_VISIBLE|TVS_HASLINES|TVS_HASBUTTONS|TVS_LINESATROOT, 0,0,220,600, g_hwnd, (HMENU)IDC_TREE, hI, NULL); g_edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_HSCROLL|ES_MULTILINE|ES_AUTOVSCROLL, 224,0,1176,600, g_hwnd, (HMENU)IDC_EDIT, hI, NULL); SendMessageW(g_edit, WM_SETFONT, (WPARAM)GetStockObject(DEFAULT_GUI_FONT), TRUE); g_console = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"", WS_CHILD|WS_VISIBLE|WS_VSCROLL|ES_MULTILINE|ES_READONLY, 0,600,1400,136, g_hwnd, (HMENU)IDC_CONSOLE, hI, NULL); HFONT hf = CreateFontW(14,0,0,0,FW_NORMAL,0,0,0,DEFAULT_CHARSET,0,0,0,FIXED_PITCH,L"Consolas"); SendMessageW(g_console, WM_SETFONT, (WPARAM)hf, TRUE); // Build menu HMENU hBar = CreateMenu(); HMENU hFile = CreatePopupMenu(); AppendMenuW(hFile,MF_STRING,IDM_NEW,T("new")); AppendMenuW(hFile,MF_STRING,IDM_OPEN,T("open")); AppendMenuW(hFile,MF_STRING,IDM_SAVE,T("save")); AppendMenuW(hFile,MF_SEPARATOR,0,NULL); AppendMenuW(hFile,MF_STRING,IDM_ABOUT,T("about")); AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hFile,T("file")); HMENU hBuild = CreatePopupMenu(); AppendMenuW(hBuild,MF_STRING,IDM_RUN,T("run")); AppendMenuW(hBuild,MF_SEPARATOR,0,NULL); AppendMenuW(hBuild,MF_STRING,IDM_BUILD,T("compile")); AppendMenuW(hBuild,MF_STRING,IDM_PACKAGE,T("package")); AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hBuild,T("build")); HMENU hLang = CreatePopupMenu(); for (int i=0;i