diff --git a/Launcher/.gitignore b/Launcher/.gitignore new file mode 100644 index 0000000..7017898 --- /dev/null +++ b/Launcher/.gitignore @@ -0,0 +1,3 @@ +*.exe +*.o +icon.ico diff --git a/Launcher/build.sh b/Launcher/build.sh new file mode 100755 index 0000000..6ba4aa6 --- /dev/null +++ b/Launcher/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +python3 gen_icon.py +x86_64-w64-mingw32-windres resource.rc -O coff -o resource.o +x86_64-w64-mingw32-g++ -mwindows -O2 -s -o EOSHoloLauncher.exe main.cpp resource.o -static +echo "Built: EOSHoloLauncher.exe" diff --git a/Launcher/gen_icon.py b/Launcher/gen_icon.py new file mode 100644 index 0000000..a43bc2f --- /dev/null +++ b/Launcher/gen_icon.py @@ -0,0 +1,31 @@ +import struct, os +from io import BytesIO +from PIL import Image + +SRC = os.path.join(os.path.dirname(__file__), "holos.png") +DST = os.path.join(os.path.dirname(__file__), "icon.ico") +SIZES = [16, 24, 32, 48, 64, 128, 256] + +src = Image.open(SRC).convert("RGBA") +entries = [] + +for size in SIZES: + img = src.resize((size, size), Image.LANCZOS) + buf = BytesIO() + img.save(buf, format='PNG') + data = buf.getvalue() + w = size if size < 256 else 0 + h = size if size < 256 else 0 + entries.append((w, h, 32, len(data), data)) + +with open(DST, 'wb') as f: + f.write(struct.pack(' +#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; +} diff --git a/Launcher/resource.h b/Launcher/resource.h new file mode 100644 index 0000000..78d8b2d --- /dev/null +++ b/Launcher/resource.h @@ -0,0 +1,23 @@ +#define IDD_MAIN 101 +#define IDI_MAIN 102 +#define IDD_ABOUT 103 +#define IDR_MENU 104 +#define IDM_RESET 40000 +#define IDM_ABOUT 40001 +#define IDM_EXIT 40002 +#define IDC_ROLE 1001 +#define IDC_IP 1002 +#define IDC_PORT 1003 +#define IDC_MAX_MEMBERS 1004 +#define IDC_PLATFORM 1005 +#define IDC_WIN_RATE 1006 +#define IDC_CROSS_PLATFORM 1007 +#define IDC_APP_VERSION 1008 +#define IDC_ONLINE_RULE 1009 +#define IDC_SAVE 1010 +#define IDC_RESET 1011 +#define IDC_LAUNCH 1012 +#define IDC_TENOKE_USER 1013 +#define IDC_TENOKE_LABEL 1014 +#define IDC_ABOUT_ICON 1015 +#define IDC_ABOUT_LINK 1016 diff --git a/Launcher/resource.rc b/Launcher/resource.rc new file mode 100644 index 0000000..646f796 --- /dev/null +++ b/Launcher/resource.rc @@ -0,0 +1,78 @@ +#include +#include "resource.h" + +IDI_MAIN ICON "icon.ico" + +IDR_MENU MENU +BEGIN + POPUP "File" + BEGIN + MENUITEM "Reset Config", IDM_RESET + MENUITEM SEPARATOR + MENUITEM "About", IDM_ABOUT + MENUITEM "Exit", IDM_EXIT + END +END + +IDD_MAIN DIALOGEX 0, 0, 340, 186 +STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "EOSHoloLauncher" +MENU IDR_MENU +FONT 9, "Segoe UI", 400, 0, 0x1 +BEGIN + LTEXT "Role:", -1, 8, 8, 25, 10 + COMBOBOX IDC_ROLE, 35, 6, 65, 100, CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + + LTEXT "IP:", -1, 108, 8, 16, 10 + EDITTEXT IDC_IP, 124, 6, 85, 13, ES_AUTOHSCROLL + + LTEXT "Port:", -1, 217, 8, 22, 10 + EDITTEXT IDC_PORT, 239, 6, 45, 13, ES_AUTOHSCROLL | ES_NUMBER + + LTEXT "User:", IDC_TENOKE_LABEL, 8, 26, 25, 10 + EDITTEXT IDC_TENOKE_USER, 35, 24, 249, 13, ES_AUTOHSCROLL + + GROUPBOX "Advanced Settings", -1, 5, 46, 330, 80 + + LTEXT "Max Members:", -1, 12, 60, 58, 10 + EDITTEXT IDC_MAX_MEMBERS, 72, 58, 40, 13, ES_AUTOHSCROLL | ES_NUMBER + + LTEXT "Platform:", -1, 160, 60, 38, 10 + EDITTEXT IDC_PLATFORM, 200, 58, 125, 13, ES_AUTOHSCROLL + + LTEXT "Win Rate:", -1, 12, 78, 42, 10 + EDITTEXT IDC_WIN_RATE, 72, 76, 40, 13, ES_AUTOHSCROLL | ES_NUMBER + + LTEXT "Cross-Platform:", -1, 160, 78, 62, 10 + COMBOBOX IDC_CROSS_PLATFORM, 225, 76, 100, 80, CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + + LTEXT "App Version:", -1, 12, 96, 52, 10 + EDITTEXT IDC_APP_VERSION, 72, 94, 40, 13, ES_AUTOHSCROLL + + LTEXT "Online Rule:", -1, 160, 96, 52, 10 + EDITTEXT IDC_ONLINE_RULE, 225, 94, 100, 13, ES_AUTOHSCROLL + + LTEXT "Note: Online Rule, App Version, and Win Rate are for matchmaking purposes.\nLeave at defaults unless you know what you are doing.", + -1, 8, 136, 324, 20 + + DEFPUSHBUTTON "Launch", IDC_LAUNCH, 25, 164, 90, 16 + PUSHBUTTON "Save Config", IDC_SAVE, 125, 164, 90, 16 +END + +IDD_ABOUT DIALOGEX 0, 0, 300, 130 +STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "About EOSHoloLauncher" +FONT 9, "Segoe UI", 400, 0, 0x1 +BEGIN + ICON IDI_MAIN, IDC_ABOUT_ICON, 10, 10, 64, 64 + LTEXT "EOSHoloLauncher v1.0", -1, 82, 12, 200, 12 + LTEXT "A graphical configuration tool for the EOSSDK-Holos\n" + "proxy project. Provides a simple interface to configure\n" + "direct IP multiplayer settings for Holos Hanafuda.\n" + "\n" + "Licensed under the GNU General Public License v2.0.", + -1, 82, 28, 200, 48 + LTEXT "https://codeberg.org/Nixietab/EOSSDK-Holos", + IDC_ABOUT_LINK, 82, 80, 200, 10, SS_NOTIFY + DEFPUSHBUTTON "OK", IDOK, 235, 104, 50, 16 +END