#include #include #include #include "resource.h" static const char *INI_RELATIVE = "\\HolosHanafuda_Data\\Plugins\\x86_64\\directip.ini"; static const char *TENOKE_RELATIVE = "\\HolosHanafuda_Data\\Plugins\\x86_64\\tenoke.ini"; static const char *GAME_EXE = "\\HolosHanafuda.exe"; static const char *SECTION = "Network"; static const char *KEYS[] = { "Role", "IP", "Port", "MaxMembers", "Platform", "WinRate", "IsCrossPlatform", "AppVersion", "OnlineRule" }; static const char *DEFAULTS[] = { "Client", "127.0.0.1", "7777", "4", "Steam", "10", "true", "1.2.0", "Koikoi" }; static const char *COMMENTS[] = { "Client or Host", "Target IP address", "UDP port", "Maximum lobby members", "Lobby platform identifier", "Win rate (for matchmaking)", "Cross-platform flag (true/false)", "App version (for matchmaking)", "Online rule (for matchmaking)" }; static char g_gameDir[MAX_PATH]; static void GetIniPath(char *buf, size_t size) { _snprintf(buf, size, "%s%s", g_gameDir, INI_RELATIVE); } static void GetTenokePath(char *buf, size_t size) { _snprintf(buf, size, "%s%s", g_gameDir, TENOKE_RELATIVE); } static void GetExePath(char *buf, size_t size) { _snprintf(buf, size, "%s%s", g_gameDir, GAME_EXE); } static char *ReadFileContent(const char *path, DWORD *outSize) { *outSize = 0; HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return NULL; DWORD size = GetFileSize(hFile, NULL); if (size == 0 || size > 65536) { CloseHandle(hFile); return NULL; } char *content = (char *)malloc(size + 2); DWORD read; if (!ReadFile(hFile, content, size, &read, NULL) || read != size) { free(content); CloseHandle(hFile); return NULL; } content[size] = '\0'; content[size + 1] = '\0'; CloseHandle(hFile); *outSize = size; return content; } static int WriteFileContent(const char *path, const char *data, DWORD len) { HANDLE hFile = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return 0; DWORD written; int ok = WriteFile(hFile, data, len, &written, NULL) && written == len; CloseHandle(hFile); return ok; } static const char *ReadTenokeUser(const char *path) { static char value[128]; value[0] = '\0'; DWORD size; char *content = ReadFileContent(path, &size); if (!content) return value; // Check for UTF-16LE BOM char *p = content; if (size >= 2 && (BYTE)p[0] == 0xFF && (BYTE)p[1] == 0xFE) { // Convert UTF-16LE to ASCII: skip BOM, take low bytes char *ascii = (char *)malloc(size / 2 + 1); int j = 0; for (DWORD i = 2; i + 1 < size; i += 2) { if (p[i] == '\r') continue; ascii[j++] = p[i]; } ascii[j] = '\0'; free(content); content = ascii; size = j; } // Find "user =" and extract quoted value char *userPos = strstr(content, "user"); while (userPos) { char *after = userPos + 4; while (*after == ' ' || *after == '\t') after++; if (*after == '=') { after++; while (*after == ' ' || *after == '\t') after++; if (*after == '"') { after++; int i = 0; while (*after && *after != '"' && i < (int)sizeof(value) - 1) value[i++] = *after++; value[i] = '\0'; break; } } userPos = strstr(after, "user"); } free(content); return value; } static void WriteTenokeUser(const char *path, const char *newUser) { DWORD size; char *content = ReadFileContent(path, &size); if (!content) return; int isUtf16 = (size >= 2 && (BYTE)content[0] == 0xFF && (BYTE)content[1] == 0xFE); char *work = content; DWORD workSize = size; if (isUtf16) { char *ascii = (char *)malloc(size / 2 + 1); int j = 0; for (DWORD i = 2; i + 1 < size; i += 2) ascii[j++] = content[i]; ascii[j] = '\0'; work = ascii; workSize = j; } char *valueStart = NULL; char *valueEnd = NULL; char *userPos = strstr(work, "user"); while (userPos) { char *after = userPos + 4; while (*after == ' ' || *after == '\t') after++; if (*after == '=') { after++; while (*after == ' ' || *after == '\t') after++; if (*after == '"') { valueStart = after + 1; valueEnd = valueStart; while (*valueEnd && *valueEnd != '"') valueEnd++; break; } } userPos = strstr(after, "user"); } char asciiBuf[32768]; int pos = 0; if (valueStart) { int beforeQuote = (int)(valueStart - work); memcpy(asciiBuf, work, beforeQuote); pos = beforeQuote; int newLen = (int)strlen(newUser); memcpy(asciiBuf + pos, newUser, newLen); pos += newLen; int afterQuote = (int)(work + workSize - valueEnd); memcpy(asciiBuf + pos, valueEnd, afterQuote); pos += afterQuote; } else { pos = _snprintf(asciiBuf, sizeof(asciiBuf), "%s\r\nuser = \"%s\"\r\n", work, newUser); } if (isUtf16) { char utf16Buf[65536]; int upos = 0; utf16Buf[upos++] = (char)0xFF; utf16Buf[upos++] = (char)0xFE; for (int i = 0; i < pos; i++) { utf16Buf[upos++] = asciiBuf[i]; utf16Buf[upos++] = '\0'; } WriteFileContent(path, utf16Buf, upos); free(work); } else { WriteFileContent(path, asciiBuf, pos); } free(content); } static void LoadConfig(HWND dlg) { char ini[MAX_PATH]; GetIniPath(ini, sizeof(ini)); char buf[256]; GetPrivateProfileStringA(SECTION, KEYS[0], DEFAULTS[0], buf, sizeof(buf), ini); if (SendDlgItemMessageA(dlg, IDC_ROLE, CB_SELECTSTRING, -1, (LPARAM)buf) == CB_ERR) SendDlgItemMessageA(dlg, IDC_ROLE, CB_SETCURSEL, 0, 0); GetPrivateProfileStringA(SECTION, KEYS[1], DEFAULTS[1], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_IP, buf); GetPrivateProfileStringA(SECTION, KEYS[2], DEFAULTS[2], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_PORT, buf); GetPrivateProfileStringA(SECTION, KEYS[3], DEFAULTS[3], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_MAX_MEMBERS, buf); GetPrivateProfileStringA(SECTION, KEYS[4], DEFAULTS[4], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_PLATFORM, buf); GetPrivateProfileStringA(SECTION, KEYS[5], DEFAULTS[5], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_WIN_RATE, buf); GetPrivateProfileStringA(SECTION, KEYS[6], DEFAULTS[6], buf, sizeof(buf), ini); if (SendDlgItemMessageA(dlg, IDC_CROSS_PLATFORM, CB_SELECTSTRING, -1, (LPARAM)buf) == CB_ERR) SendDlgItemMessageA(dlg, IDC_CROSS_PLATFORM, CB_SETCURSEL, 0, 0); GetPrivateProfileStringA(SECTION, KEYS[7], DEFAULTS[7], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_APP_VERSION, buf); GetPrivateProfileStringA(SECTION, KEYS[8], DEFAULTS[8], buf, sizeof(buf), ini); SetDlgItemTextA(dlg, IDC_ONLINE_RULE, buf); char tenoke[MAX_PATH]; GetTenokePath(tenoke, sizeof(tenoke)); if (GetFileAttributesA(tenoke) != INVALID_FILE_ATTRIBUTES) { ShowWindow(GetDlgItem(dlg, IDC_TENOKE_USER), SW_SHOW); ShowWindow(GetDlgItem(dlg, IDC_TENOKE_LABEL), SW_SHOW); const char *user = ReadTenokeUser(tenoke); SetDlgItemTextA(dlg, IDC_TENOKE_USER, user); } else { ShowWindow(GetDlgItem(dlg, IDC_TENOKE_USER), SW_HIDE); ShowWindow(GetDlgItem(dlg, IDC_TENOKE_LABEL), SW_HIDE); } } static void EnsurePathExists(const char *path) { char dir[MAX_PATH]; strncpy(dir, path, MAX_PATH); for (char *p = dir; *p; p++) { if (*p == '\\' || *p == '/') { char sep = *p; *p = '\0'; CreateDirectoryA(dir, NULL); *p = sep; } } } static void SaveConfig(HWND dlg) { char ini[MAX_PATH]; GetIniPath(ini, sizeof(ini)); char buf[256]; EnsurePathExists(ini); for (int i = 0; i < 9; i++) { GetDlgItemTextA(dlg, IDC_ROLE + i, buf, sizeof(buf)); if (buf[0]) WritePrivateProfileStringA(SECTION, KEYS[i], buf, ini); } char tenoke[MAX_PATH]; GetTenokePath(tenoke, sizeof(tenoke)); HWND hTenoke = GetDlgItem(dlg, IDC_TENOKE_USER); if (hTenoke && IsWindowVisible(hTenoke)) { GetDlgItemTextA(dlg, IDC_TENOKE_USER, buf, sizeof(buf)); if (buf[0]) WriteTenokeUser(tenoke, buf); } MessageBoxA(dlg, "Configuration saved.", "EOSHoloLauncher", MB_OK | MB_ICONINFORMATION); } static void ResetConfig(HWND dlg) { char ini[MAX_PATH]; GetIniPath(ini, sizeof(ini)); EnsurePathExists(ini); for (int i = 0; i < 9; i++) WritePrivateProfileStringA(SECTION, KEYS[i], NULL, ini); HANDLE hFile = CreateFileA(ini, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { DWORD read; char old[4096] = {0}; DWORD oldLen = GetFileSize(hFile, NULL); if (oldLen > 0 && oldLen < sizeof(old) - 1) ReadFile(hFile, old, oldLen, &read, NULL); char tmp[8192]; int pos = _snprintf(tmp, sizeof(tmp), "; To override default values, uncomment and edit the lines below:\n" "\n" "[Network]\n"); for (int i = 0; i < 9; i++) pos += _snprintf(tmp + pos, sizeof(tmp) - pos, "; %s=%s ; %s\n", KEYS[i], DEFAULTS[i], COMMENTS[i]); pos += _snprintf(tmp + pos, sizeof(tmp) - pos, "\n"); char *line = old; bool inNetwork = false; while (line && *line && (size_t)(line - old) < oldLen) { char *nl = strchr(line, '\n'); size_t len = nl ? (size_t)(nl - line) : strlen(line); char saved = line[len]; line[len] = '\0'; char *trim = line; while (*trim == ' ' || *trim == '\t' || *trim == '\r') trim++; if (trim[0] == '[') inNetwork = (_stricmp(trim, "[Network]") == 0); if (!inNetwork) { memcpy(tmp + pos, line, len); pos += (int)len; tmp[pos++] = '\r'; tmp[pos++] = '\n'; } if (nl) { line[len] = saved; line = nl + 1; } else { break; } } SetFilePointer(hFile, 0, NULL, FILE_BEGIN); SetEndOfFile(hFile); DWORD written; WriteFile(hFile, tmp, pos, &written, NULL); CloseHandle(hFile); } LoadConfig(dlg); MessageBoxA(dlg, "Settings reset to defaults.\nExample lines kept for reference.", "EOSHoloLauncher", MB_OK | MB_ICONINFORMATION); } static INT_PTR CALLBACK AboutDlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_INITDIALOG) { HICON hIcon = (HICON)LoadImageA(GetModuleHandleA(NULL), MAKEINTRESOURCEA(IDI_MAIN), IMAGE_ICON, 64, 64, LR_DEFAULTCOLOR); if (hIcon) { SendDlgItemMessageA(dlg, IDC_ABOUT_ICON, STM_SETICON, (WPARAM)hIcon, 0); SetPropA(dlg, "ABOUT_ICON", hIcon); } return TRUE; } if (msg == WM_CTLCOLORSTATIC) { HWND hCtrl = (HWND)lParam; if (hCtrl == GetDlgItem(dlg, IDC_ABOUT_LINK)) { HDC dc = (HDC)wParam; SetTextColor(dc, RGB(0, 102, 204)); SetBkMode(dc, TRANSPARENT); return (INT_PTR)GetStockObject(HOLLOW_BRUSH); } return FALSE; } if (msg == WM_SETCURSOR) { if ((HWND)wParam == GetDlgItem(dlg, IDC_ABOUT_LINK)) { SetCursor(LoadCursorA(NULL, IDC_HAND)); return TRUE; } return FALSE; } if (msg == WM_COMMAND) { if (LOWORD(wParam) == IDC_ABOUT_LINK && HIWORD(wParam) == STN_CLICKED) { ShellExecuteA(NULL, "open", "https://codeberg.org/Nixietab/EOSSDK-Holos", NULL, NULL, SW_SHOWNORMAL); return TRUE; } if (LOWORD(wParam) == IDOK) { EndDialog(dlg, IDOK); return TRUE; } } if (msg == WM_CLOSE) { EndDialog(dlg, IDCANCEL); return TRUE; } if (msg == WM_NCDESTROY) { HICON hIcon = (HICON)RemovePropA(dlg, "ABOUT_ICON"); if (hIcon) DestroyIcon(hIcon); return TRUE; } return FALSE; } static void LaunchGame(HWND dlg) { char exe[MAX_PATH]; GetExePath(exe, sizeof(exe)); if (GetFileAttributesA(exe) == INVALID_FILE_ATTRIBUTES) { char msg[512]; _snprintf(msg, sizeof(msg), "Game executable not found:\n%s\n\n" "Make sure the launcher is in the game's root directory.", exe); MessageBoxA(dlg, msg, "Error", MB_OK | MB_ICONERROR); return; } SaveConfig(dlg); HINSTANCE r = ShellExecuteA(dlg, "open", exe, NULL, g_gameDir, SW_SHOWNORMAL); if ((INT_PTR)r <= 32) { char msg[256]; _snprintf(msg, sizeof(msg), "Failed to launch game. Error code: %d.", (INT_PTR)r); MessageBoxA(dlg, msg, "Error", MB_OK | MB_ICONERROR); } else { EndDialog(dlg, IDOK); } } static INT_PTR CALLBACK DlgProc(HWND dlg, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_INITDIALOG: { HICON hIcon = LoadIconA(GetModuleHandleA(NULL), MAKEINTRESOURCEA(IDI_MAIN)); SendMessageA(dlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon); SendMessageA(dlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); } SendDlgItemMessageA(dlg, IDC_ROLE, CB_ADDSTRING, 0, (LPARAM)"Client"); SendDlgItemMessageA(dlg, IDC_ROLE, CB_ADDSTRING, 0, (LPARAM)"Host"); SendDlgItemMessageA(dlg, IDC_CROSS_PLATFORM, CB_ADDSTRING, 0, (LPARAM)"true"); SendDlgItemMessageA(dlg, IDC_CROSS_PLATFORM, CB_ADDSTRING, 0, (LPARAM)"false"); LoadConfig(dlg); { int sel = (int)SendDlgItemMessageA(dlg, IDC_ROLE, CB_GETCURSEL, 0, 0); EnableWindow(GetDlgItem(dlg, IDC_IP), sel == 0); } return TRUE; case WM_COMMAND: if (HIWORD(wParam) == CBN_SELCHANGE && LOWORD(wParam) == IDC_ROLE) { int sel = (int)SendDlgItemMessageA(dlg, IDC_ROLE, CB_GETCURSEL, 0, 0); EnableWindow(GetDlgItem(dlg, IDC_IP), sel == 0); return TRUE; } switch (LOWORD(wParam)) { case IDC_SAVE: SaveConfig(dlg); return TRUE; case IDC_RESET: case IDM_RESET: ResetConfig(dlg); return TRUE; case IDC_LAUNCH: LaunchGame(dlg); return TRUE; case IDM_ABOUT: DialogBoxParamA(GetModuleHandleA(NULL), MAKEINTRESOURCEA(IDD_ABOUT), dlg, AboutDlgProc, 0); return TRUE; case IDM_EXIT: EndDialog(dlg, IDCANCEL); return TRUE; case IDCANCEL: EndDialog(dlg, IDCANCEL); return TRUE; } return FALSE; case WM_CLOSE: EndDialog(dlg, IDCANCEL); return TRUE; } return FALSE; } int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int) { GetModuleFileNameA(NULL, g_gameDir, MAX_PATH); char *s = strrchr(g_gameDir, '\\'); if (!s) s = strrchr(g_gameDir, '/'); if (s) *s = '\0'; else g_gameDir[0] = '\0'; DialogBoxParamA(hInst, MAKEINTRESOURCEA(IDD_MAIN), NULL, DlgProc, 0); return 0; }