diff --git a/dialog.jai b/dialog.jai index 8ffd98b..0d323b7 100644 --- a/dialog.jai +++ b/dialog.jai @@ -533,7 +533,16 @@ download_dialog_proc :: (hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) - if state.thread_data.done { KillTimer(hwnd, 1); thread_is_done(*state.thread, -1); + // Capture error_msg (which may be temp-allocated inside download thread) BEFORE deinit frees the thread temp storage. + captured_success := state.thread_data.success; + captured_err := copy_string(state.thread_data.error_msg); thread_deinit(*state.thread); + // Overwrite with safe copies (heap allocated) so that the post-loop return sees valid data. + state.thread_data.success = captured_success; + state.thread_data.error_msg = captured_err; + // Free the (copied) input paths now that thread is done with them. + if state.thread_data.url.count > 0 { free(state.thread_data.url); state.thread_data.url = ""; } + if state.thread_data.dest_path.count > 0 { free(state.thread_data.dest_path); state.thread_data.dest_path = ""; } DestroyWindow(hwnd); } } @@ -581,8 +590,8 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str dialog_y := (screen_h - dialog_h) / 2; state: Download_Dialog_State; - state.thread_data.url = url; - state.thread_data.dest_path = dest_path; + state.thread_data.url = copy_string(url); + state.thread_data.dest_path = copy_string(dest_path); dialog_hwnd := CreateWindowExW( WS_EX_DLGMODALFRAME, @@ -596,7 +605,11 @@ show_download_progress_dialog :: (parent_hwnd: HWND, url: string, dest_path: str *state ); - if !dialog_hwnd return false, "Failed to create dialog window"; + if !dialog_hwnd { + free(state.thread_data.url); + free(state.thread_data.dest_path); + return false, "Failed to create dialog window"; + } if parent_hwnd { EnableWindow(parent_hwnd, FALSE); diff --git a/main.jai b/main.jai index 710b5a4..67b813e 100644 --- a/main.jai +++ b/main.jai @@ -111,6 +111,11 @@ App_State :: struct { log_reader_thread: Thread; log_reader_data: Log_Reader_Data; log_reader_started: bool; + + local_version: string; + latest_web_version: string; + version_checker_thread: Thread; + version_checker_data: Version_Checker_Data; } Log_Level :: enum { @@ -457,11 +462,6 @@ get_last_singbox_logs_string :: () -> string { } find_singbox_in_userspace :: () -> string { - target_direct := tprint("%\\sing-box.exe", global_userspace_dir); - if file_exists(target_direct) { - return target_direct; - } - search_path := tprint("%\\*", global_userspace_dir); find_data: WIN32_FIND_DATAW; hFind := FindFirstFileW(utf8_to_wide(search_path), *find_data); @@ -528,6 +528,311 @@ run_command_blocking :: (cmd: string, working_dir: string = "") -> success: bool return true, exit_code; } +get_command_output :: (cmd: string, working_dir: string = "") -> string { + hReadPipe, hWritePipe: HANDLE; + sa: SECURITY_ATTRIBUTES; + sa.nLength = size_of(SECURITY_ATTRIBUTES); + sa.bInheritHandle = 1; // TRUE + sa.lpSecurityDescriptor = null; + + if !CreatePipe(*hReadPipe, *hWritePipe, *sa, 0) return ""; + SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); + + startup_info: STARTUPINFOW; + startup_info.cb = size_of(STARTUPINFOW); + startup_info.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; + startup_info.wShowWindow = SW_HIDE; + startup_info.hStdOutput = hWritePipe; + startup_info.hStdError = hWritePipe; + + process_info: PROCESS_INFORMATION; + CREATE_NO_WINDOW :: 0x08000000; + + cmd_wide := utf8_to_wide(cmd); + working_dir_wide: *u16 = null; + if working_dir.count > 0 { + working_dir_wide = utf8_to_wide(working_dir); + } + + success := CreateProcessW( + null, + cmd_wide, + null, + null, + 1, // TRUE to inherit handles + CREATE_NO_WINDOW, + null, + working_dir_wide, + *startup_info, + *process_info + ); + + CloseHandle(hWritePipe); // Close write end so read returns 0 on EOF + + if !success { + CloseHandle(hReadPipe); + return ""; + } + + builder: String_Builder; + defer free_buffers(*builder); + + buffer: [1024] u8; + bytes_read: DWORD; + while true { + ok := ReadFile(hReadPipe, buffer.data, xx buffer.count, *bytes_read, null); + if !ok || bytes_read == 0 break; + append(*builder, buffer.data, xx bytes_read); + } + + WaitForSingleObject(process_info.hProcess, INFINITE); + CloseHandle(process_info.hProcess); + CloseHandle(process_info.hThread); + CloseHandle(hReadPipe); + + return builder_to_string(*builder); +} + +get_local_singbox_version :: () -> string { + exe_path := tprint("%\\sing-box.exe", global_userspace_dir); + if !file_exists(exe_path) return copy_string("Not Installed"); + + cmd := tprint("\"%\" version", exe_path); + output := get_command_output(cmd, global_userspace_dir); + defer free(output); + + if output.count == 0 return copy_string("Unknown"); + + lines := split(output, "\n"); + defer array_free(lines); + + if lines.count > 0 { + first_line := trim(lines[0]); + prefix := "sing-box version "; + if starts_with(first_line, prefix) { + ver := slice(first_line, prefix.count, first_line.count - prefix.count); + space_idx := find_index_from_left(ver, " "); + if space_idx != -1 { + ver = slice(ver, 0, space_idx); + } + return copy_string(ver); + } + return copy_string(first_line); + } + + return copy_string("Unknown"); +} + +fetch_latest_web_version :: () -> string { + url := "https://api.github.com/repos/SagerNet/sing-box/releases/latest"; + + hInternet := InternetOpenW( + utf8_to_wide("SingboxTray"), + INTERNET_OPEN_TYPE_PRECONFIG, + null, + null, + 0 + ); + if !hInternet return ""; + defer InternetCloseHandle(hInternet); + + headers := "User-Agent: SingboxTray\r\n"; + hUrl := InternetOpenUrlW( + hInternet, + utf8_to_wide(url), + utf8_to_wide(headers), + cast(u32) headers.count, + INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_SECURE, + 0 + ); + if !hUrl return ""; + defer InternetCloseHandle(hUrl); + + response_code: DWORD; + response_code_size: DWORD = size_of(type_of(response_code)); + if HttpQueryInfoW(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, *response_code, *response_code_size, null) { + if response_code != 200 return ""; + } else { + return ""; + } + + builder: String_Builder; + defer free_buffers(*builder); + + buffer: [4096] u8; + bytes_read: DWORD; + while true { + ok := InternetReadFile(hUrl, buffer.data, xx buffer.count, *bytes_read); + if !ok || bytes_read == 0 break; + append(*builder, buffer.data, xx bytes_read); + } + + json_data := builder_to_string(*builder); + defer free(json_data); + + tag := get_json_string_field(json_data, "\"tag_name\""); + if tag.count > 0 { + if tag[0] == #char "v" { + return copy_string(slice(tag, 1, tag.count - 1)); + } + return copy_string(tag); + } + + return ""; +} + +Version_Checker_Data :: struct { + app: *App_State; +} + +version_checker_thread_proc :: (thread: *Thread) -> s64 { + data := cast(*Version_Checker_Data) thread.data; + if !data return 1; + + latest := fetch_latest_web_version(); + if latest.count > 0 { + EnterCriticalSection(*global_singbox_log_mutex); + if data.app.latest_web_version.count > 0 { + free(data.app.latest_web_version); + } + data.app.latest_web_version = latest; + LeaveCriticalSection(*global_singbox_log_mutex); + } else { + EnterCriticalSection(*global_singbox_log_mutex); + if data.app.latest_web_version.count > 0 { + free(data.app.latest_web_version); + } + data.app.latest_web_version = copy_string("Unknown"); + LeaveCriticalSection(*global_singbox_log_mutex); + } + + return 0; +} + +install_or_update_singbox_core :: (app: *App_State, is_update: bool) -> bool { + clear_last_singbox_logs(); + + version_to_download := "1.13.14"; // fallback + EnterCriticalSection(*global_singbox_log_mutex); + if app.latest_web_version != "Checking..." && app.latest_web_version != "Unknown" && app.latest_web_version.count > 0 { + version_to_download = app.latest_web_version; + } + LeaveCriticalSection(*global_singbox_log_mutex); + + download_url := tprint("https://github.com/SagerNet/sing-box/releases/download/v%/sing-box-%-windows-amd64.zip", version_to_download, version_to_download); + temp_zip := tprint("%\\sing-box-temp.zip", global_userspace_dir); + DeleteFileW(utf8_to_wide(temp_zip)); // remove any stale/partial from previous failed attempt + exe_path := tprint("%\\sing-box.exe", global_userspace_dir); + + prompt_title := ifx is_update then "Update Sing-box" else "Sing-box Core Installation"; + success_msg := tprint("Sing-box core % successfully!", ifx is_update then "updated" else "installed"); + + // 1. Download while VPN is still active + success, err_msg := show_download_progress_dialog(app.hwnd, download_url, temp_zip); + if !success { + msg := tprint("Failed to download Sing-box core.\nError: %\n\nPlease ensure you have an active internet connection.", err_msg); + MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); + if err_msg.count > 0 { free(err_msg); } + return false; + } + if err_msg.count > 0 { free(err_msg); } // free the (copied) error string returned from dialog on success path too (usually "") + + // 2. Stop and forcefully terminate any running sing-box instances to unlock the files + was_running := app.singbox_running; + if was_running { + stop_singbox(app); + } + + // Kill any other running instances of sing-box.exe (e.g. from the main tray app or orphaned processes) + run_command_blocking("taskkill /f /im sing-box.exe"); + Sleep(500); // Give OS some time to release file locks + + // 3. Extract the zip file + tar_cmd := tprint("tar -xf \"%\" -C \"%\"", temp_zip, global_userspace_dir); + tar_ok, exit_code := run_command_blocking(tar_cmd); + if !tar_ok || exit_code != 0 { + // Fallback to powershell + ps_cmd := tprint("powershell.exe -NoProfile -NonInteractive -Command \"Expand-Archive -Path '%' -DestinationPath '%' -Force\"", temp_zip, global_userspace_dir); + ps_ok, ps_exit := run_command_blocking(ps_cmd); + if !ps_ok || ps_exit != 0 { + MessageBoxW(app.hwnd, utf8_to_wide("Failed to extract Sing-box core zip archive using tar or PowerShell."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); + DeleteFileW(utf8_to_wide(temp_zip)); + if was_running start_singbox(app); + return false; + } + } + + found_exe := find_singbox_in_userspace(); + if found_exe.count > 0 { + if found_exe != exe_path { + // Delete old files first to make sure MoveFileW does not fail + DeleteFileW(utf8_to_wide(exe_path)); + move_ok := MoveFileW(utf8_to_wide(found_exe), utf8_to_wide(exe_path)); + if !move_ok { + err := GetLastError(); + msg := tprint("Failed to replace sing-box.exe. Error code: %", err); + MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); + DeleteFileW(utf8_to_wide(temp_zip)); + if was_running start_singbox(app); + return false; + } + + // Also move libcronet.dll if present + dir_part := found_exe; + if ends_with(dir_part, "\\sing-box.exe") { + dir_part.count -= 13; + } + lib_path := tprint("%\\libcronet.dll", dir_part); + if file_exists(lib_path) { + dest_lib := tprint("%\\libcronet.dll", global_userspace_dir); + DeleteFileW(utf8_to_wide(dest_lib)); + MoveFileW(utf8_to_wide(lib_path), utf8_to_wide(dest_lib)); + } + + // Clean up the extracted subdirectory + rm_cmd := tprint("cmd.exe /c rmdir /s /q \"%\"", dir_part); + run_command_blocking(rm_cmd); + } + } else { + MessageBoxW(app.hwnd, utf8_to_wide("Could not locate sing-box.exe in the extracted files."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); + DeleteFileW(utf8_to_wide(temp_zip)); + if was_running start_singbox(app); + return false; + } + + DeleteFileW(utf8_to_wide(temp_zip)); + + // Update cached local version + new_ver := get_local_singbox_version(); + EnterCriticalSection(*global_singbox_log_mutex); + if app.local_version.count > 0 { + free(app.local_version); + } + app.local_version = new_ver; + LeaveCriticalSection(*global_singbox_log_mutex); + + MessageBoxW(app.hwnd, utf8_to_wide(success_msg), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION); + + // Always start/restart to ensure the connection is restored immediately + start_singbox(app); + + return true; +} + +modify_config_cache_file :: (json: string, cache_path: string) -> string { + replace_target :: "\"cache_file\": {"; + replace_idx := find_index_from_left(json, replace_target); + if replace_idx != -1 { + builder: String_Builder; + append(*builder, slice(json, 0, replace_idx + replace_target.count)); + append(*builder, tprint(" \"path\": \"%\",", cache_path)); + append(*builder, slice(json, replace_idx + replace_target.count, json.count - (replace_idx + replace_target.count))); + return builder_to_string(*builder); + } + + return copy_string(json); +} + start_singbox :: (app: *App_State) -> bool { if app.singbox_running return true; @@ -601,12 +906,21 @@ start_singbox :: (app: *App_State) -> bool { // Strip custom fields (_url, _mode, _port) to avoid sing-box decoding error clean_config := strip_json_metadata(modified); - write_entire_file(config_run_filename, clean_config); + // Redirect cache file in test mode to avoid database locking collisions + final_config: string; + if app.is_test_mode { + final_config = modify_config_cache_file(clean_config, "cache_test.db"); + free(clean_config); + } else { + final_config = clean_config; + } + + write_entire_file(config_run_filename, final_config); log_print("Generated clean % for sing-box (port %).\n", config_run_filename, app.port); free(config_data); free(modified); - free(clean_config); + free(final_config); } } @@ -614,60 +928,8 @@ start_singbox :: (app: *App_State) -> bool { if !file_exists(exe_path) { response := MessageBoxW(app.hwnd, utf8_to_wide("Sing-box core executable (sing-box.exe) was not found in your userspace folder.\n\nWould you like to download and install it now?"), utf8_to_wide("Sing-box Tray"), MB_YESNO | MB_ICONQUESTION); if response == IDYES { - clear_last_singbox_logs(); - - download_url := "https://github.com/SagerNet/sing-box/releases/download/v1.13.14/sing-box-1.13.14-windows-amd64.zip"; - temp_zip := tprint("%\\sing-box-temp.zip", global_userspace_dir); - - success, err_msg := show_download_progress_dialog(app.hwnd, download_url, temp_zip); - if !success { - msg := tprint("Failed to download Sing-box core.\nError: %\n\nPlease ensure you have an active internet connection.", err_msg); - MessageBoxW(app.hwnd, utf8_to_wide(msg), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); - return false; - } - - // Extract the zip file - tar_cmd := tprint("tar -xf \"%\" -C \"%\"", temp_zip, global_userspace_dir); - tar_ok, exit_code := run_command_blocking(tar_cmd); - if !tar_ok || exit_code != 0 { - // Fallback to powershell - ps_cmd := tprint("powershell.exe -NoProfile -NonInteractive -Command \"Expand-Archive -Path '%' -DestinationPath '%' -Force\"", temp_zip, global_userspace_dir); - ps_ok, ps_exit := run_command_blocking(ps_cmd); - if !ps_ok || ps_exit != 0 { - MessageBoxW(app.hwnd, utf8_to_wide("Failed to extract Sing-box core zip archive using tar or PowerShell."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); - DeleteFileW(utf8_to_wide(temp_zip)); - return false; - } - } - - found_exe := find_singbox_in_userspace(); - if found_exe.count > 0 { - if found_exe != exe_path { - MoveFileW(utf8_to_wide(found_exe), utf8_to_wide(exe_path)); - - // Also move libcronet.dll if present - dir_part := found_exe; - if ends_with(dir_part, "\\sing-box.exe") { - dir_part.count -= 13; - } - lib_path := tprint("%\\libcronet.dll", dir_part); - if file_exists(lib_path) { - dest_lib := tprint("%\\libcronet.dll", global_userspace_dir); - MoveFileW(utf8_to_wide(lib_path), utf8_to_wide(dest_lib)); - } - - // Clean up the extracted subdirectory - rm_cmd := tprint("cmd.exe /c rmdir /s /q \"%\"", dir_part); - run_command_blocking(rm_cmd); - } - } else { - MessageBoxW(app.hwnd, utf8_to_wide("Could not locate sing-box.exe in the extracted files."), utf8_to_wide("Sing-box Tray"), MB_ICONERROR); - DeleteFileW(utf8_to_wide(temp_zip)); - return false; - } - - DeleteFileW(utf8_to_wide(temp_zip)); - MessageBoxW(app.hwnd, utf8_to_wide("Sing-box core installed successfully!"), utf8_to_wide("Sing-box Tray"), MB_ICONINFORMATION); + installed := install_or_update_singbox_core(app, false); + return installed; // install_or_update_singbox_core handles starting sing-box } else { return false; } @@ -742,6 +1004,7 @@ stop_singbox :: (app: *App_State) { if !app.singbox_running return; TerminateProcess(app.singbox_process_handle, 0); + WaitForSingleObject(app.singbox_process_handle, 3000); CloseHandle(app.singbox_process_handle); CloseHandle(app.singbox_thread_handle); CloseHandle(app.singbox_job_handle); @@ -850,6 +1113,32 @@ trigger_immediate_update :: (app: *App_State) { } } +is_newer_version :: (current: string, latest: string) -> bool { + if current.count == 0 || latest.count == 0 return false; + if current == "Unknown" || latest == "Unknown" || latest == "Checking..." return false; + if current == "Not Installed" return false; + + c_parts := split(current, "."); + defer array_free(c_parts); + l_parts := split(latest, "."); + defer array_free(l_parts); + + min_parts := ifx c_parts.count < l_parts.count then c_parts.count else l_parts.count; + + for i: 0..min_parts-1 { + c_val, c_ok := to_integer(trim(c_parts[i])); + l_val, l_ok := to_integer(trim(l_parts[i])); + if c_ok && l_ok { + if l_val > c_val return true; + if c_val > l_val return false; + } + } + + if l_parts.count > c_parts.count return true; + + return false; +} + show_context_menu :: (app: *App_State) { hMenu := CreatePopupMenu(); if !hMenu return; @@ -872,6 +1161,33 @@ show_context_menu :: (app: *App_State) { } AppendMenuW(hConfigureMenu, autostart_flags, CMD_TOGGLE_AUTOSTART, utf8_to_wide("Start on Windows boot")); + AppendMenuW(hConfigureMenu, MF_SEPARATOR, 0, null); + + EnterCriticalSection(*global_singbox_log_mutex); + local_ver := copy_string(app.local_version); + latest_ver := copy_string(app.latest_web_version); + LeaveCriticalSection(*global_singbox_log_mutex); + defer free(local_ver); + defer free(latest_ver); + + local_ver_str := tprint("Current Core Version: %", local_ver); + AppendMenuW(hConfigureMenu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, utf8_to_wide(local_ver_str)); + + is_newer := is_newer_version(local_ver, latest_ver); + if is_newer { + latest_ver_str := tprint("✨ Update Available: %! 🚀", latest_ver); + AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide(latest_ver_str)); + + AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide("👉 Update Sing-box Core Now!")); + } else { + latest_ver_str := tprint("Latest Core Version: %", latest_ver); + AppendMenuW(hConfigureMenu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, utf8_to_wide(latest_ver_str)); + + AppendMenuW(hConfigureMenu, MF_STRING, CMD_UPDATE_CORE, utf8_to_wide("Update Sing-box Core")); + } + + AppendMenuW(hConfigureMenu, MF_SEPARATOR, 0, null); + hUpdateMenu := CreatePopupMenu(); current_interval := app.updater_data.update_interval_seconds; AppendMenuW(hUpdateMenu, MF_STRING | (ifx current_interval == 0 then MF_CHECKED else 0), CMD_UPDATE_NEVER, utf8_to_wide("Never")); @@ -1019,6 +1335,9 @@ show_context_menu :: (app: *App_State) { case CMD_CONFIG_DIR; { ShellExecuteW(null, utf8_to_wide("open"), utf8_to_wide(global_userspace_dir), null, null, 1); } + case CMD_UPDATE_CORE; { + install_or_update_singbox_core(app, true); + } case CMD_UPDATE_NEVER; { save_update_interval(app, 0); log_print("Auto-update interval set to: Never\n"); @@ -1265,6 +1584,15 @@ main :: () { init_userspace_paths(is_test); + if is_test { + config_test := global_config_path; + config_prod := sprint("%\\config.json", global_userspace_dir); + defer free(config_prod); + if !file_exists(config_test) && file_exists(config_prod) { + CopyFileW(utf8_to_wide(config_prod), utf8_to_wide(config_test), FALSE); + } + } + hInstance := GetModuleHandleW(null); class_name_str := ifx is_test then "SingboxTrayControllerClassTest" else "SingboxTrayControllerClass"; class_name := utf8_to_wide(class_name_str); @@ -1351,6 +1679,15 @@ main :: () { } } + app_state.local_version = get_local_singbox_version(); + app_state.latest_web_version = copy_string("Checking..."); + + app_state.version_checker_data.app = *app_state; + thread_init(*app_state.version_checker_thread, version_checker_thread_proc); + app_state.version_checker_thread.data = *app_state.version_checker_data; + thread_start(*app_state.version_checker_thread); + + nid: NOTIFYICONDATAW; nid.cbSize = size_of(NOTIFYICONDATAW); nid.hWnd = hwnd; @@ -1398,6 +1735,18 @@ main :: () { CloseHandle(app_state.updater_data.stop_event); } + if app_state.version_checker_thread.data { + thread_is_done(*app_state.version_checker_thread, -1); + thread_deinit(*app_state.version_checker_thread); + } + + if app_state.local_version.count > 0 { + free(app_state.local_version); + } + if app_state.latest_web_version.count > 0 { + free(app_state.latest_web_version); + } + stop_singbox(*app_state); if app_state.icon_running DestroyIcon(app_state.icon_running); diff --git a/updater.jai b/updater.jai index 608f404..aa1261a 100644 --- a/updater.jai +++ b/updater.jai @@ -15,6 +15,12 @@ Updater_Thread_Data :: struct { download_url :: (url: string) -> string, bool, string { log_info("Downloading config from URL: %\n", url); + if file_exists(url) { + content, ok := read_entire_file(url); + if ok return content, true, ""; + return "", false, tprint("Failed to read local file: %", url); + } + hInternet := InternetOpenW( utf8_to_wide("SingboxTrayUpdater"), INTERNET_OPEN_TYPE_PRECONFIG, @@ -32,11 +38,12 @@ download_url :: (url: string) -> string, bool, string { flags |= INTERNET_FLAG_SECURE; } + headers := "User-Agent: SingboxTray\r\n"; hUrl := InternetOpenUrlW( hInternet, utf8_to_wide(url), - null, - 0, + utf8_to_wide(headers), + cast(u32) headers.count, flags, 0 ); @@ -361,11 +368,12 @@ download_file :: (url: string, dest_path: string) -> bool, string { flags |= INTERNET_FLAG_SECURE; } + headers := "User-Agent: SingboxTray\r\n"; hUrl := InternetOpenUrlW( hInternet, utf8_to_wide(url), - null, - 0, + utf8_to_wide(headers), + cast(u32) headers.count, flags, 0 ); diff --git a/win32.jai b/win32.jai index cd75a4b..b1bef6e 100644 --- a/win32.jai +++ b/win32.jai @@ -165,6 +165,8 @@ CMD_UPDATE_DAILY :: 1015; CMD_UPDATE_3D :: 1016; CMD_UPDATE_WEEKLY :: 1017; CMD_CONFIG_DIR :: 1018; +CMD_UPDATE_CORE :: 1019; + // Boolean constants FALSE :: 0;