Initial commit
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
// Native platform layer: Win32 window + OpenGL 3.3 core context, via Jai's Window_Creation / Input / GL modules.
|
||||
// The project is a plain text file (default: project.taskforge in the working directory, or the path given
|
||||
// on the command line), so it can live in your game's repository.
|
||||
|
||||
#import "Window_Creation";
|
||||
#import "Input";
|
||||
#import "GL";
|
||||
#import "File";
|
||||
#import "File_Utilities";
|
||||
#import "Clipboard";
|
||||
#import "System";
|
||||
#import "Windows";
|
||||
#import "Thread";
|
||||
#import "Curl";
|
||||
|
||||
tf_user32 :: #system_library "user32";
|
||||
SetProcessDpiAwarenessContext_ :: (value: *void) -> s32 #foreign tf_user32 "SetProcessDpiAwarenessContext";
|
||||
GetDpiForWindow_ :: (hwnd: HWND) -> u32 #foreign tf_user32 "GetDpiForWindow";
|
||||
MsgWaitForMultipleObjects_ :: (count: u32, handles: *HANDLE, wait_all: s32, milliseconds: u32, wake_mask: u32) -> u32 #foreign tf_user32 "MsgWaitForMultipleObjects";
|
||||
QS_ALLINPUT_ :: 0x04FF;
|
||||
|
||||
project_path: string;
|
||||
server_url: string; // Set with --server; empty = local project file.
|
||||
native_window: HWND;
|
||||
|
||||
main :: () {
|
||||
args := get_command_line_arguments();
|
||||
project_path = "project.taskforge";
|
||||
i := 1;
|
||||
while i < args.count {
|
||||
if args[i] == "--sim" && i + 1 < args.count {
|
||||
sim_script = args[i + 1];
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if args[i] == "--server" && i + 1 < args.count {
|
||||
server_url = copy_string(args[i + 1]);
|
||||
while server_url.count && server_url[server_url.count - 1] == #char "/" server_url.count -= 1;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
project_path = copy_string(args[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
SetProcessDpiAwarenessContext_(cast(*void) -4); // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
native_window = create_window(1440, 900, "TaskForge", background_color_rgb = .[0.067, 0.075, 0.094]);
|
||||
_, hdc := wgl_create_context(native_window, null, 3, 3);
|
||||
gl_load(*gl);
|
||||
#if OS == .WINDOWS {
|
||||
if wglSwapIntervalEXT wglSwapIntervalEXT(1);
|
||||
}
|
||||
|
||||
exe_dir := path_strip_filename(get_path_of_running_executable());
|
||||
font_data, ok := read_entire_file(tprint("%fonts.bin", exe_dir));
|
||||
if !ok {
|
||||
log_error("Missing fonts.bin next to the executable. Build with: jai build.jai");
|
||||
return;
|
||||
}
|
||||
if server_url.count curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
if !app_init(font_data, server_url.count > 0) {
|
||||
log_error("Failed to initialize.");
|
||||
return;
|
||||
}
|
||||
|
||||
input: Frame_Input;
|
||||
idle_frames := 0;
|
||||
last_mouse_x, last_mouse_y := -1.0, -1.0;
|
||||
running := true;
|
||||
while running {
|
||||
update_window_events();
|
||||
reset_per_frame_input(*input);
|
||||
|
||||
for event: events_this_frame {
|
||||
if event.type == {
|
||||
case .QUIT;
|
||||
running = false;
|
||||
case .KEYBOARD;
|
||||
button := -1;
|
||||
if event.key_code == {
|
||||
case .MOUSE_BUTTON_LEFT; button = MOUSE_LEFT;
|
||||
case .MOUSE_BUTTON_RIGHT; button = MOUSE_RIGHT;
|
||||
case .MOUSE_BUTTON_MIDDLE; button = MOUSE_MIDDLE;
|
||||
}
|
||||
if button >= 0 {
|
||||
if event.key_pressed {
|
||||
input.mouse_down[button] = true;
|
||||
input.mouse_pressed[button] = true;
|
||||
} else {
|
||||
input.mouse_down[button] = false;
|
||||
input.mouse_released[button] = true;
|
||||
}
|
||||
} else if event.key_pressed {
|
||||
key: Key_Press;
|
||||
key.key = cast(u32) event.key_code;
|
||||
if event.shift_pressed key.mods |= MOD_SHIFT;
|
||||
if event.ctrl_pressed key.mods |= MOD_CTRL;
|
||||
if event.alt_pressed key.mods |= MOD_ALT;
|
||||
array_add(*input.keys, key);
|
||||
}
|
||||
case .TEXT_INPUT;
|
||||
if event.utf32 >= 32 && event.utf32 != 127 array_add(*input.chars, event.utf32);
|
||||
case .MOUSE_WHEEL;
|
||||
if event.typical_wheel_delta {
|
||||
lines := -cast(float) event.wheel_delta / cast(float) event.typical_wheel_delta;
|
||||
input.wheel_y += lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input.mods = 0;
|
||||
if input_button_states[Key_Code.SHIFT] & .DOWN input.mods |= MOD_SHIFT;
|
||||
if input_button_states[Key_Code.CTRL] & .DOWN input.mods |= MOD_CTRL;
|
||||
if input_button_states[Key_Code.ALT] & .DOWN input.mods |= MOD_ALT;
|
||||
|
||||
rect: RECT;
|
||||
GetClientRect(native_window, *rect);
|
||||
pw := rect.right - rect.left;
|
||||
ph := rect.bottom - rect.top;
|
||||
dpi := GetDpiForWindow_(native_window);
|
||||
input.dpr = ifx dpi then dpi / 96.0 else 1.0;
|
||||
|
||||
if pw <= 0 || ph <= 0 {
|
||||
// Minimized.
|
||||
MsgWaitForMultipleObjects_(0, null, 0, 250, QS_ALLINPUT_);
|
||||
continue;
|
||||
}
|
||||
input.width = pw / input.dpr;
|
||||
input.height = ph / input.dpr;
|
||||
input.time = seconds_since_init();
|
||||
|
||||
if sim_script.count {
|
||||
if !simulate_frame(*input, pw, ph) running = false;
|
||||
} else {
|
||||
mx, my := get_mouse_pointer_position(native_window, false);
|
||||
input.mouse_x = mx / input.dpr;
|
||||
input.mouse_y = my / input.dpr;
|
||||
// If a button was released outside the window we never got the event.
|
||||
if input.mouse_down[MOUSE_LEFT] && !(input_button_states[Key_Code.MOUSE_BUTTON_LEFT] & .DOWN) {
|
||||
input.mouse_down[MOUSE_LEFT] = false;
|
||||
input.mouse_released[MOUSE_LEFT] = true;
|
||||
}
|
||||
}
|
||||
|
||||
poll_http_jobs();
|
||||
frame_start := seconds_since_init();
|
||||
wants_more := app_frame(*input);
|
||||
if sim_screenshot.count {
|
||||
save_screenshot(sim_screenshot, pw, ph);
|
||||
sim_screenshot = "";
|
||||
}
|
||||
SwapBuffers(hdc);
|
||||
|
||||
// Vsync isn't guaranteed (drivers may ignore the swap interval), so cap the frame rate ourselves.
|
||||
MIN_FRAME_TIME :: 1.0 / 120.0;
|
||||
elapsed := seconds_since_init() - frame_start;
|
||||
if elapsed < MIN_FRAME_TIME sleep_milliseconds(cast(s32) ((MIN_FRAME_TIME - elapsed) * 1000) + 1);
|
||||
if sim_script.count continue;
|
||||
|
||||
moved := input.mouse_x != last_mouse_x || input.mouse_y != last_mouse_y;
|
||||
last_mouse_x, last_mouse_y = input.mouse_x, input.mouse_y;
|
||||
if events_this_frame.count || moved || wants_more idle_frames = 0;
|
||||
else idle_frames += 1;
|
||||
|
||||
// A task app should not burn a CPU core: sleep until input arrives (or the caret needs to blink).
|
||||
if idle_frames > 2 MsgWaitForMultipleObjects_(0, null, 0, xx ifx http_jobs.count then 15 else 500, QS_ALLINPUT_);
|
||||
}
|
||||
|
||||
app_shutdown();
|
||||
}
|
||||
|
||||
platform_load_project :: () -> string, bool {
|
||||
if !file_exists(project_path) return "", false;
|
||||
text, ok := read_entire_file(project_path);
|
||||
return text, ok;
|
||||
}
|
||||
|
||||
platform_save_project :: (data: string) -> bool {
|
||||
// Write to a temp file first so a crash mid-write can't destroy the project.
|
||||
temp_path := tprint("%.tmp", project_path);
|
||||
if !write_entire_file(temp_path, data) return false;
|
||||
return file_move(temp_path, project_path);
|
||||
}
|
||||
|
||||
platform_storage_description :: () -> string {
|
||||
return tprint("Saved to %", path_filename(project_path));
|
||||
}
|
||||
|
||||
platform_server_label :: () -> string {
|
||||
return server_url;
|
||||
}
|
||||
|
||||
//
|
||||
// HTTP (server mode): each request runs libcurl on its own thread; results are picked up by the main loop.
|
||||
//
|
||||
|
||||
Http_Job :: struct {
|
||||
tag: Http_Tag;
|
||||
url: string;
|
||||
body: string;
|
||||
token: string;
|
||||
status: int;
|
||||
response: String_Builder;
|
||||
thread: Thread;
|
||||
}
|
||||
|
||||
http_jobs: [..] *Http_Job;
|
||||
|
||||
platform_http_post :: (tag: Http_Tag, path: string, body: string, token: string) {
|
||||
job := New(Http_Job);
|
||||
job.tag = tag;
|
||||
job.url = sprint("%1%2", server_url, path);
|
||||
job.body = copy_string(body);
|
||||
job.token = copy_string(token);
|
||||
thread_init(*job.thread, http_thread_proc);
|
||||
job.thread.data = job;
|
||||
thread_start(*job.thread);
|
||||
array_add(*http_jobs, job);
|
||||
}
|
||||
|
||||
http_thread_proc :: (thread: *Thread) -> s64 {
|
||||
job := cast(*Http_Job) thread.data;
|
||||
curl := curl_easy_init();
|
||||
if !curl return 0;
|
||||
defer curl_easy_cleanup(curl);
|
||||
|
||||
write :: (data: *u8, size: u64, count: u64, builder: *String_Builder) -> u64 #c_call {
|
||||
n := size * count;
|
||||
push_context { append(builder, data, cast(s64) n); }
|
||||
return n;
|
||||
}
|
||||
|
||||
headers: *curl_slist;
|
||||
headers = curl_slist_append(headers, "Content-Type: text/plain; charset=utf-8");
|
||||
if job.token.count headers = curl_slist_append(headers, temp_c_string(tprint("Authorization: Bearer %", job.token)));
|
||||
defer curl_slist_free_all(headers);
|
||||
|
||||
curl_easy_setopt(curl, .URL, temp_c_string(job.url));
|
||||
curl_easy_setopt(curl, .POST, cast(s64) 1);
|
||||
curl_easy_setopt(curl, .POSTFIELDS, job.body.data);
|
||||
curl_easy_setopt(curl, .POSTFIELDSIZE, cast(s64) job.body.count);
|
||||
curl_easy_setopt(curl, .HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, .WRITEFUNCTION, write);
|
||||
curl_easy_setopt(curl, .WRITEDATA, *job.response);
|
||||
curl_easy_setopt(curl, .TIMEOUT, cast(s64) 20);
|
||||
curl_easy_setopt(curl, .CONNECTTIMEOUT, cast(s64) 10);
|
||||
|
||||
if curl_easy_perform(curl) == .OK {
|
||||
code: s64;
|
||||
curl_easy_getinfo(curl, .RESPONSE_CODE, *code);
|
||||
job.status = code;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
poll_http_jobs :: () {
|
||||
i := 0;
|
||||
while i < http_jobs.count {
|
||||
job := http_jobs[i];
|
||||
if !thread_is_done(*job.thread) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
thread_deinit(*job.thread);
|
||||
array_ordered_remove_by_index(*http_jobs, i);
|
||||
response := builder_to_string(*job.response);
|
||||
http_on_response(job.tag, job.status, response);
|
||||
free(response);
|
||||
free(job.url);
|
||||
free(job.body);
|
||||
free(job.token);
|
||||
free(job);
|
||||
}
|
||||
}
|
||||
|
||||
// The session token is kept per user profile, tied to the server it belongs to.
|
||||
session_path :: () -> string {
|
||||
home := get_home_directory();
|
||||
dir := tprint("%/AppData/Roaming/TaskForge", home);
|
||||
make_directory_if_it_does_not_exist(dir);
|
||||
return tprint("%/session.txt", dir);
|
||||
}
|
||||
|
||||
platform_load_session :: () -> string {
|
||||
text, ok := read_entire_file(session_path(), log_errors = false);
|
||||
if !ok return "";
|
||||
lines := split(text, "\n",, temp);
|
||||
if lines.count < 2 || lines[0] != server_url return "";
|
||||
return copy_string(trim(lines[1]));
|
||||
}
|
||||
|
||||
platform_save_session :: (token: string) {
|
||||
write_entire_file(session_path(), tprint("%\n%\n", server_url, token));
|
||||
}
|
||||
|
||||
platform_clipboard_get :: () -> string {
|
||||
text := os_clipboard_get_text();
|
||||
result := copy_temporary_string(text);
|
||||
free(text);
|
||||
return result;
|
||||
}
|
||||
|
||||
platform_clipboard_set :: (text: string) {
|
||||
os_clipboard_set_text(text);
|
||||
}
|
||||
|
||||
platform_set_cursor :: (kind: Cursor_Kind) {
|
||||
name: *s16;
|
||||
if kind == {
|
||||
case .HAND; name = cast(*s16) IDC_HAND;
|
||||
case .TEXT; name = cast(*s16) IDC_IBEAM;
|
||||
case .GRAB; name = cast(*s16) IDC_SIZEALL;
|
||||
case; name = cast(*s16) IDC_ARROW;
|
||||
}
|
||||
SetCursor(LoadCursorW(null, name));
|
||||
}
|
||||
|
||||
platform_export_project :: (data: string) {}
|
||||
platform_request_import :: () {}
|
||||
|
||||
//
|
||||
// Test harness: --sim "<commands>" drives the app with scripted input, one command per frame, separated by ';'.
|
||||
// move X Y | down | up | click X Y | type TEXT | key CODE [MODS] | wheel LINES | wait N | shot FILE.bmp | quit
|
||||
// Coordinates are logical pixels.
|
||||
//
|
||||
|
||||
sim_script: string;
|
||||
sim_screenshot: string;
|
||||
sim_wait: int;
|
||||
sim_pending_up: bool;
|
||||
|
||||
simulate_frame :: (input: *Frame_Input, pw: s32, ph: s32) -> bool {
|
||||
if sim_pending_up {
|
||||
sim_pending_up = false;
|
||||
input.mouse_down[MOUSE_LEFT] = false;
|
||||
input.mouse_released[MOUSE_LEFT] = true;
|
||||
return true;
|
||||
}
|
||||
if sim_wait > 0 {
|
||||
sim_wait -= 1;
|
||||
return true;
|
||||
}
|
||||
while sim_script.count && (sim_script[0] == #char ";" || sim_script[0] == #char " ") advance(*sim_script);
|
||||
if !sim_script.count return false;
|
||||
|
||||
end := find_index_from_left(sim_script, #char ";");
|
||||
if end < 0 end = sim_script.count;
|
||||
command := substring(sim_script, 0, end);
|
||||
advance(*sim_script, end);
|
||||
|
||||
parts := split(command, " ",, allocator = temp);
|
||||
if !parts.count return true;
|
||||
if parts[0] == {
|
||||
case "move";
|
||||
input.mouse_x = string_to_float(parts[1]);
|
||||
input.mouse_y = string_to_float(parts[2]);
|
||||
case "down";
|
||||
input.mouse_down[MOUSE_LEFT] = true;
|
||||
input.mouse_pressed[MOUSE_LEFT] = true;
|
||||
case "up";
|
||||
input.mouse_down[MOUSE_LEFT] = false;
|
||||
input.mouse_released[MOUSE_LEFT] = true;
|
||||
case "click";
|
||||
input.mouse_x = string_to_float(parts[1]);
|
||||
input.mouse_y = string_to_float(parts[2]);
|
||||
input.mouse_down[MOUSE_LEFT] = true;
|
||||
input.mouse_pressed[MOUSE_LEFT] = true;
|
||||
sim_pending_up = true;
|
||||
case "type";
|
||||
text := substring(command, 5, command.count);
|
||||
for 0..text.count-1 array_add(*input.chars, cast(u32) text[it]);
|
||||
case "key";
|
||||
k: Key_Press;
|
||||
k.key = cast(u32) string_to_int(parts[1]);
|
||||
if parts.count > 2 k.mods = cast(u32) string_to_int(parts[2]);
|
||||
array_add(*input.keys, k);
|
||||
case "wheel";
|
||||
input.wheel_y += string_to_float(parts[1]);
|
||||
case "wait";
|
||||
sim_wait = string_to_int(parts[1]);
|
||||
case "shot";
|
||||
sim_screenshot = copy_string(parts[1]);
|
||||
case "quit";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
save_screenshot :: (path: string, w: s32, h: s32) {
|
||||
pixels := NewArray(w * h * 4, u8,, allocator = temp);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, xx w, xx h, GL_BGRA, GL_UNSIGNED_BYTE, pixels.data);
|
||||
|
||||
// 32-bit BMP, bottom-up rows (which is what glReadPixels gives us).
|
||||
b: String_Builder;
|
||||
put :: (b: *String_Builder, value: $T) {
|
||||
v := value;
|
||||
append(b, cast(*u8) *v, size_of(T));
|
||||
}
|
||||
append(*b, "BM");
|
||||
put(*b, cast(u32) (54 + pixels.count));
|
||||
put(*b, cast(u32) 0);
|
||||
put(*b, cast(u32) 54);
|
||||
put(*b, cast(u32) 40);
|
||||
put(*b, cast(s32) w);
|
||||
put(*b, cast(s32) h);
|
||||
put(*b, cast(u16) 1);
|
||||
put(*b, cast(u16) 32);
|
||||
put(*b, cast(u32) 0);
|
||||
put(*b, cast(u32) pixels.count);
|
||||
put(*b, cast(s32) 2835);
|
||||
put(*b, cast(s32) 2835);
|
||||
put(*b, cast(u32) 0);
|
||||
put(*b, cast(u32) 0);
|
||||
append(*b, pixels.data, pixels.count);
|
||||
write_entire_file(path, *b);
|
||||
}
|
||||
Reference in New Issue
Block a user