349 lines
16 KiB
C
349 lines
16 KiB
C
// lity_editor.c - Lity3D Editor (Win32 Unicode + JSON i18n)
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include <commdlg.h>
|
|
#include <shellapi.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#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<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]);
|
|
}
|
|
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hLang,T("lang"));
|
|
HMENU hHelp=CreatePopupMenu(); AppendMenuW(hHelp,MF_STRING,IDM_ABOUT,T("about"));
|
|
AppendMenuW(hBar,MF_POPUP,(UINT_PTR)hHelp,T("help"));
|
|
SetMenu(g_hwnd,hBar);
|
|
populate_tree();
|
|
console_msg("Lity3D Editor ready.\n");
|
|
ShowWindow(g_hwnd,SW_SHOW);
|
|
MSG msg;
|
|
while(GetMessageW(&msg,NULL,0,0)){TranslateMessage(&msg);DispatchMessageW(&msg);}
|
|
return (int)msg.wParam;
|
|
}
|