From 1ee804355c15312182d12ddd17885029dbe535b8 Mon Sep 17 00:00:00 2001 From: Peter Li Date: Sat, 17 May 2025 14:45:23 -0700 Subject: [PATCH] adding file reloading --- build/buildExternModule.zig | 0 engine/core/src/core.zig | 2 + engine/core/src/engine.zig | 16 + engine/core/src/extern/externModule.zig | 64 + engine/core/src/externObject.zig | 11 + engine/core/src/gameObject.zig | 1 + engine/core/tests/tests.zig | 2 +- extras/gameExtras/snippets.zig | 15 + extras/gameExtras/src/ecsEventDebugger.zig | 17 + lib/cimgui/src/cimgui.zig | 5 + lib/p2/src/p2.zig | 10 + lib/packer/build.zig | 5 + lib/packer/src/include/FileWatch.hpp | 1246 +++++++++++++++++ lib/packer/src/packerfs.zig | 78 ++ lib/packer/src/watcher.cpp | 17 + lib/packer/src/watcher.zig | 7 + lib/packer/tests/test.zig | 24 + projects/build.zig | 13 + projects/sampleGame/externGame/externGame.zig | 5 + projects/sampleGame/main.zig | 59 +- 20 files changed, 1590 insertions(+), 7 deletions(-) create mode 100644 build/buildExternModule.zig create mode 100644 engine/core/src/extern/externModule.zig create mode 100644 engine/core/src/externObject.zig create mode 100644 engine/core/src/gameObject.zig create mode 100644 extras/gameExtras/snippets.zig create mode 100644 extras/gameExtras/src/ecsEventDebugger.zig create mode 100644 lib/packer/src/include/FileWatch.hpp create mode 100644 lib/packer/src/watcher.cpp create mode 100644 lib/packer/src/watcher.zig create mode 100644 projects/sampleGame/externGame/externGame.zig diff --git a/build/buildExternModule.zig b/build/buildExternModule.zig new file mode 100644 index 0000000..e69de29 diff --git a/engine/core/src/core.zig b/engine/core/src/core.zig index 8ccf984..105312d 100644 --- a/engine/core/src/core.zig +++ b/engine/core/src/core.zig @@ -248,5 +248,7 @@ pub const console = @import("console.zig"); pub const configVars = @import("configVars.zig"); +pub const loopDelay = engine.loopDelay; + pub const getConfigVar = configVars.getConfigVar; pub const configVar = configVars.configVar; diff --git a/engine/core/src/engine.zig b/engine/core/src/engine.zig index 1ca11d5..42efa04 100644 --- a/engine/core/src/engine.zig +++ b/engine/core/src/engine.zig @@ -388,4 +388,20 @@ pub const NeonObjectParams = struct { isCore: bool = false, }; +const Src = std.builtin.SourceLocation; +pub fn loopDelay(comptime src: Src, interval: f64, dt: f64, comptime S: type, capture: S) void { + const C = struct { + pub const s = src; + pub var __timeleft: f64 = 0.0; + pub var __interval: f64 = 0.0; + }; + C.__interval -= dt; + + if (C.__timeleft <= 0.0) { + S.func(capture); + while (C.__timeleft < 0) + C.__timeleft += interval; + } +} + test "comptime registration implementation" {} diff --git a/engine/core/src/extern/externModule.zig b/engine/core/src/extern/externModule.zig new file mode 100644 index 0000000..2ca7f54 --- /dev/null +++ b/engine/core/src/extern/externModule.zig @@ -0,0 +1,64 @@ +// modules for managing loading dynamic libraries +// +// i want hot swapping +// +// 1. add a folder to the watch list + +pub const FileWatchEntry = struct { + path: []const u8, + stamp: i128 = 0, + cbCtx: ?*anyopaque, + + loadCallback: *const fn (*@This(), ?*anyopaque) void, + + // staging path is under .cache/modules// + pub fn copyToStaging(self: *@This()) !void { + const fileName = p2. + const dir = try std.fs.cwd().makePath(); + std.fs.cwd().copyFile(self.path, dir, "", options: CopyFileOptions) + } + + pub fn checkUpdateTime(self: *@This()) bool { + const stat = std.fs.cwd().statFile(self.path) catch false; + if (stat.mtime != self.stamp) { + self.stamp = stat.mtime; + } + } +}; + +pub const ModuleLoader = struct { + backingAllocator: std.mem.Allocator, + arena: std.heap.ArenaAllocator, + allocator: std.mem.Allocator = undefined, + + files: std.ArrayListUnmanaged(FileWatchEntry) = .{}, + + pub fn create(allocator: std.mem.Allocator) !*@This() { + const self = try allocator.create(@This()); + + self.* = .{ + .backingAllocator = allocator, + .arena = try std.heap.ArenaAllocator.init(allocator), + }; + + self.allocator = self.arena.allocator(); + + return self; + } + + pub fn destroy(self: *@This()) void { + self.arena.deinit(); + self.backingAllocator.destroy(self); + } + + pub fn checkForFileUpdates(self: *@This()) void { + for (self.files.items) |*f| { + if (f.checkTime()) { + f.loadCallback(f.cbCtx); + } + } + } +}; + +const std = @import("std"); +const p2 = @import("p2"); diff --git a/engine/core/src/externObject.zig b/engine/core/src/externObject.zig new file mode 100644 index 0000000..95e4118 --- /dev/null +++ b/engine/core/src/externObject.zig @@ -0,0 +1,11 @@ +// script objects. + +pub const ObjectTable = extern struct { + create: *const fn (allocator: *anyopaque) callconv(.C) ?*anyopaque, + tick: *const fn (*anyopaque, f64) callconv(.C) void, + destroy: *const fn (p: *anyopaque) callconv(.C) ?*anyopaque, +}; + +// eg. in C +// +// object Struc diff --git a/engine/core/src/gameObject.zig b/engine/core/src/gameObject.zig new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/core/src/gameObject.zig @@ -0,0 +1 @@ + diff --git a/engine/core/tests/tests.zig b/engine/core/tests/tests.zig index f4b4f7b..5fd3924 100644 --- a/engine/core/tests/tests.zig +++ b/engine/core/tests/tests.zig @@ -25,7 +25,7 @@ test "simple systems setup for core" { try test_consoleCommands(); try test_gameObjects(std.testing.allocator); - try generateRandomSamples(); + // try generateRandomSamples(); try memory.dumpTimeline("test-core-timeline.txt"); } diff --git a/extras/gameExtras/snippets.zig b/extras/gameExtras/snippets.zig new file mode 100644 index 0000000..9a1622e --- /dev/null +++ b/extras/gameExtras/snippets.zig @@ -0,0 +1,15 @@ +allocator: std.mem.allocator, + +pub fn create(allocator: std.mem.Allocator) !*@This() { + const self = try allocator.create(@This()); + + self.* = .{ + .allocator = allocator, + }; + + return self; +} + +const std = @import("std"); +const backlog = @import("Backlog"); +const core = backlog.core; diff --git a/extras/gameExtras/src/ecsEventDebugger.zig b/extras/gameExtras/src/ecsEventDebugger.zig new file mode 100644 index 0000000..0e31909 --- /dev/null +++ b/extras/gameExtras/src/ecsEventDebugger.zig @@ -0,0 +1,17 @@ +allocator: std.mem.allocator, + +pub fn create(allocator: std.mem.Allocator) !*@This() { + const self = try allocator.create(@This()); + + self.* = .{ + .allocator = allocator, + }; + + return self; +} + +const std = @import("std"); +const backlog = @import("Backlog"); +const core = backlog.core; +const rend = backlog.rend; +const ig = backlog.imgui.api; diff --git a/lib/cimgui/src/cimgui.zig b/lib/cimgui/src/cimgui.zig index c1778e4..a555160 100644 --- a/lib/cimgui/src/cimgui.zig +++ b/lib/cimgui/src/cimgui.zig @@ -4627,6 +4627,11 @@ pub inline fn textFmt(comptime fmt: []const u8, args: anytype) !void { textUnformatted(@ptrCast(buffer.ptr), buffer.ptr + buffer.len); } +pub inline fn textf(comptime f: []const u8, args: anytype) void { + const buffer = std.fmt.bufPrint(&textFmtBuffer, f, args) catch return; + textUnformatted(@ptrCast(buffer.ptr), buffer.ptr + buffer.len); +} + // pub inline fn textV(fmt: [*c]const u8, args: list) void { //igTextV // c.igTextV(fmt, args); // } diff --git a/lib/p2/src/p2.zig b/lib/p2/src/p2.zig index 119069d..79f187c 100644 --- a/lib/p2/src/p2.zig +++ b/lib/p2/src/p2.zig @@ -73,6 +73,16 @@ pub const Span = spans.Span; pub const shell = @import("utils/shell.zig"); +pub fn sharedLibName(comptime s: []const u8) []const u8 { + const os_tag = @import("builtin").os.tag; + if (os_tag == .linux) { + return s ++ ".so"; + } else if (os_tag == .macos) { + return s ++ ".dynlib"; + } else { + return s ++ ".dll"; + } +} comptime { std.testing.refAllDecls(utils); std.testing.refAllDecls(static_structures); diff --git a/lib/packer/build.zig b/lib/packer/build.zig index 4d80d6e..420bcff 100644 --- a/lib/packer/build.zig +++ b/lib/packer/build.zig @@ -10,8 +10,13 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, .root_source_file = b.path("src/packer.zig"), + .link_libc = true, + .link_libcpp = true, }); + mod.addIncludePath(b.path("src/include")); + mod.addCSourceFile(.{ .file = b.path("src/watcher.cpp"), .flags = &.{} }); + const p2mod = p2dep.module("p2"); mod.addImport("p2", p2mod); diff --git a/lib/packer/src/include/FileWatch.hpp b/lib/packer/src/include/FileWatch.hpp new file mode 100644 index 0000000..4eba08b --- /dev/null +++ b/lib/packer/src/include/FileWatch.hpp @@ -0,0 +1,1246 @@ +// MIT License +// +// Copyright(c) 2017 Thomas Monkman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef FILEWATCHER_H +#define FILEWATCHER_H + +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#include +#include +#include +#endif // WIN32 + +#if __unix__ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif // __unix__ + +#ifdef __linux__ +#include +#endif + +#if defined(__APPLE__) || defined(__MACH__) +#include +#include +#include +#include +#include +#include +#define FILEWATCH_PLATFORM_MAC 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef FILEWATCH_PLATFORM_MAC +extern "C" int __getdirentries64(int, char *, int, long *); +#endif // FILEWATCH_PLATFORM_MAC + +namespace filewatch { + enum class Event { + added, + removed, + modified, + renamed_old, + renamed_new + }; + + template + struct IsWChar { + static constexpr bool value = false; + }; + + template<> + struct IsWChar { + static constexpr bool value = true; + }; + + template + struct Invokable { + static Fn make() { + return (Fn*)0; + } + + template + static T defaultValue() { + return *(T*)0; + } + + static void call(int) { + make()(defaultValue()); + } + + static int call(long value); + + static constexpr bool value = std::is_same::value; + }; + +#define _FILEWATCH_TO_STRING(x) #x +#define FILEWATCH_TO_STRING(x) _FILEWATCH_TO_STRING(x) + + [[maybe_unused]] static const char* event_to_string(Event event) { + switch (event) { + case Event::added: + return FILEWATCH_TO_STRING(Event::added); + case Event::removed: + return FILEWATCH_TO_STRING(Event::removed); + case Event::modified: + return FILEWATCH_TO_STRING(Event::modified); + case Event::renamed_old: + return FILEWATCH_TO_STRING(Event:renamed_old); + case Event::renamed_new: + return FILEWATCH_TO_STRING(Event::renamed_new); + } + assert(false); + } + + template + static typename std::enable_if::value, bool>::type + isParentOrSelfDirectory(const StringType& path) { + return path == L"." || path == L".."; + } + + template + static typename std::enable_if::value, bool>::type + isParentOrSelfDirectory(const StringType& path) { + return path == "." || path == ".."; + } + + /** + * \class FileWatch + * + * \brief Watches a folder or file, and will notify of changes via function callback. + * + * \author Thomas Monkman + * + */ + template + class FileWatch + { + typedef typename StringType::value_type C; + typedef std::basic_string> UnderpinningString; + typedef std::basic_regex> UnderpinningRegex; + + public: + + FileWatch(StringType path, UnderpinningRegex pattern, std::function callback) : + _path(absolute_path_of(path)), + _pattern(pattern), + _callback(callback), + _directory(get_directory(path)) + { + init(); + } + + FileWatch(StringType path, std::function callback) : + FileWatch(path, UnderpinningRegex(_regex_all), callback) {} + + ~FileWatch() { + destroy(); + } + + FileWatch(const FileWatch& other) : FileWatch(other._path, other._callback) {} + + FileWatch& operator=(const FileWatch& other) + { + if (this == &other) { return *this; } + + destroy(); + _path = other._path; + _callback = other._callback; + _directory = get_directory(other._path); + init(); + return *this; + } + + // Const memeber varibles don't let me implent moves nicely, if moves are really wanted std::unique_ptr should be used and move that. + FileWatch(FileWatch&&) = delete; + FileWatch& operator=(FileWatch&&) & = delete; + + private: + static constexpr C _regex_all[] = { '.', '*', '\0' }; + static constexpr C _this_directory[] = { '.', '/', '\0' }; + + struct PathParts + { + PathParts(StringType directory, StringType filename) : directory(directory), filename(filename) {} + StringType directory; + StringType filename; + }; + const StringType _path; + + UnderpinningRegex _pattern; + + static constexpr std::size_t _buffer_size = { 1024 * 256 }; + + // only used if watch a single file + StringType _filename; + + std::function _callback; + + std::thread _watch_thread; + + std::condition_variable _cv; + std::mutex _callback_mutex; + std::vector> _callback_information; + std::thread _callback_thread; + + std::promise _running; + std::atomic _destory = { false }; + bool _watching_single_file = { false }; + +#pragma mark "Platform specific data" +#ifdef _WIN32 + HANDLE _directory = { nullptr }; + HANDLE _close_event = { nullptr }; + + const DWORD _listen_filters = + FILE_NOTIFY_CHANGE_SECURITY | + FILE_NOTIFY_CHANGE_CREATION | + FILE_NOTIFY_CHANGE_LAST_ACCESS | + FILE_NOTIFY_CHANGE_LAST_WRITE | + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_ATTRIBUTES | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_FILE_NAME; + + const std::unordered_map _event_type_mapping = { + { FILE_ACTION_ADDED, Event::added }, + { FILE_ACTION_REMOVED, Event::removed }, + { FILE_ACTION_MODIFIED, Event::modified }, + { FILE_ACTION_RENAMED_OLD_NAME, Event::renamed_old }, + { FILE_ACTION_RENAMED_NEW_NAME, Event::renamed_new } + }; +#endif // WIN32 + +#if __unix__ + struct FolderInfo { + int folder; + int watch; + }; + + FolderInfo _directory; + + const std::uint32_t _listen_filters = IN_MODIFY | IN_CREATE | IN_DELETE; + + const static std::size_t event_size = (sizeof(struct inotify_event)); +#endif // __unix__ + +#if FILEWATCH_PLATFORM_MAC + struct FileState + { + int fd; + uint32_t nlink; + time_t last_modification; + + FileState(int fd, uint32_t nlink, time_t lt) + : fd(fd), nlink(nlink), + last_modification(lt) + { + + } + FileState(const FileState&) = delete; + FileState& operator=(const FileState&) = delete; + FileState(FileState&& other) : fd(other.fd), nlink(other.nlink), last_modification(other.last_modification) + { + other.fd = -1; + } + + FileState invalidate_and_clone() { + int fd = this->fd; + + this->fd = -1; + return FileState {fd, nlink, last_modification}; + } + + ~FileState() + { + if (fd != -1) { + close(fd); + } + } + }; + std::unordered_map _directory_snapshot{}; + bool _previous_event_is_rename = false; + CFRunLoopRef _run_loop = nullptr; + int _file_fd = -1; + struct timespec _last_modification_time = {}; + FSEventStreamRef _directory; + // fd for single file +#endif // FILEWATCH_PLATFORM_MAC + + void init() + { +#ifdef _WIN32 + _close_event = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!_close_event) { + throw std::system_error(GetLastError(), std::system_category()); + } +#endif // WIN32 + + _callback_thread = std::thread([this]() { + try { + callback_thread(); + } catch (...) { + try { + _running.set_exception(std::current_exception()); + } + catch (...) {} // set_exception() may throw too + } + }); + + _watch_thread = std::thread([this]() { + try { + monitor_directory(); + } catch (...) { + try { + _running.set_exception(std::current_exception()); + } + catch (...) {} // set_exception() may throw too + } + }); + + std::future future = _running.get_future(); + future.get(); //block until the monitor_directory is up and running + } + + void destroy() + { + _destory = true; + _running = std::promise(); + +#ifdef _WIN32 + SetEvent(_close_event); +#elif __unix__ + inotify_rm_watch(_directory.folder, _directory.watch); +#elif FILEWATCH_PLATFORM_MAC + if (_run_loop) { + CFRunLoopStop(_run_loop); + } +#endif // __unix__ + + _cv.notify_all(); + _watch_thread.join(); + _callback_thread.join(); + +#ifdef _WIN32 + CloseHandle(_directory); +#elif __unix__ + close(_directory.folder); +#elif FILEWATCH_PLATFORM_MAC + FSEventStreamStop(_directory); + FSEventStreamInvalidate(_directory); + FSEventStreamRelease(_directory); + _directory = nullptr; +#endif // FILEWATCH_PLATFORM_MAC + } + + const PathParts split_directory_and_file(const StringType& path) const + { + const auto predict = [](C character) { +#ifdef _WIN32 + return character == C('\\') || character == C('/'); +#elif __unix__ || FILEWATCH_PLATFORM_MAC + return character == C('/'); +#endif // __unix__ + }; + + UnderpinningString path_string = path; + const auto pivot = std::find_if(path_string.rbegin(), path_string.rend(), predict).base(); + //if the path is something like "test.txt" there will be no directory part, however we still need one, so insert './' + const StringType directory = [&]() { + const auto extracted_directory = UnderpinningString(path_string.begin(), pivot); + return (extracted_directory.size() > 0) ? extracted_directory : UnderpinningString(_this_directory); + }(); + const StringType filename = UnderpinningString(pivot, path_string.end()); + return PathParts(directory, filename); + } + + bool pass_filter(const UnderpinningString& file_path) + { + if (_watching_single_file) { + const UnderpinningString extracted_filename = { split_directory_and_file(file_path).filename }; + //if we are watching a single file, only that file should trigger action + return extracted_filename == _filename; + } + return std::regex_match(file_path, _pattern); + } + +#ifdef _WIN32 + template DWORD GetFileAttributesX(const char* lpFileName, Args... args) { + return GetFileAttributesA(lpFileName, args...); + } + template DWORD GetFileAttributesX(const wchar_t* lpFileName, Args... args) { + return GetFileAttributesW(lpFileName, args...); + } + + template HANDLE CreateFileX(const char* lpFileName, Args... args) { + return CreateFileA(lpFileName, args...); + } + template HANDLE CreateFileX(const wchar_t* lpFileName, Args... args) { + return CreateFileW(lpFileName, args...); + } + + HANDLE get_directory(const StringType& path) + { + auto file_info = GetFileAttributesX(path.c_str()); + + if (file_info == INVALID_FILE_ATTRIBUTES) + { + throw std::system_error(GetLastError(), std::system_category()); + } + _watching_single_file = (file_info & FILE_ATTRIBUTE_DIRECTORY) == false; + + const StringType watch_path = [this, &path]() { + if (_watching_single_file) + { + const auto parsed_path = split_directory_and_file(path); + _filename = parsed_path.filename; + return parsed_path.directory; + } + else + { + return path; + } + }(); + + HANDLE directory = CreateFileX( + watch_path.c_str(), // pointer to the file name + FILE_LIST_DIRECTORY, // access (read/write) mode + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // share mode + nullptr, // security descriptor + OPEN_EXISTING, // how to create + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, // file attributes + HANDLE(0)); // file with attributes to copy + + if (directory == INVALID_HANDLE_VALUE) + { + throw std::system_error(GetLastError(), std::system_category()); + } + return directory; + } + + void convert_wstring(const std::wstring& wstr, std::string& out) + { + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); + out.resize(size_needed, '\0'); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &out[0], size_needed, NULL, NULL); + } + + void convert_wstring(const std::wstring& wstr, std::wstring& out) + { + out = wstr; + } + + void monitor_directory() + { + std::vector buffer(_buffer_size); + DWORD bytes_returned = 0; + OVERLAPPED overlapped_buffer{ 0 }; + + overlapped_buffer.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!overlapped_buffer.hEvent) { + std::cerr << "Error creating monitor event" << std::endl; + } + + std::array handles{ overlapped_buffer.hEvent, _close_event }; + + auto async_pending = false; + _running.set_value(); + do { + std::vector> parsed_information; + ReadDirectoryChangesW( + _directory, + buffer.data(), static_cast(buffer.size()), + TRUE, + _listen_filters, + &bytes_returned, + &overlapped_buffer, NULL); + + async_pending = true; + + switch (WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE)) + { + case WAIT_OBJECT_0: + { + if (!GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE)) { + throw std::system_error(GetLastError(), std::system_category()); + } + async_pending = false; + + if (bytes_returned == 0) { + break; + } + + FILE_NOTIFY_INFORMATION *file_information = reinterpret_cast(&buffer[0]); + do + { + std::wstring changed_file_w{ file_information->FileName, file_information->FileNameLength / sizeof(file_information->FileName[0]) }; + UnderpinningString changed_file; + convert_wstring(changed_file_w, changed_file); + if (pass_filter(changed_file)) + { + parsed_information.emplace_back(StringType{ changed_file }, _event_type_mapping.at(file_information->Action)); + } + + if (file_information->NextEntryOffset == 0) { + break; + } + + file_information = reinterpret_cast(reinterpret_cast(file_information) + file_information->NextEntryOffset); + } while (true); + break; + } + case WAIT_OBJECT_0 + 1: + // quit + break; + case WAIT_FAILED: + break; + } + //dispatch callbacks + { + std::lock_guard lock(_callback_mutex); + _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end()); + } + _cv.notify_all(); + } while (_destory == false); + + if (async_pending) + { + //clean up running async io + CancelIo(_directory); + GetOverlappedResult(_directory, &overlapped_buffer, &bytes_returned, TRUE); + } + } +#endif // WIN32 + +#if __unix__ + + bool is_file(const StringType& path) const + { + struct stat statbuf = {}; + if (stat(path.c_str(), &statbuf) != 0) + { + throw std::system_error(errno, std::system_category()); + } + return S_ISREG(statbuf.st_mode); + } + + FolderInfo get_directory(const StringType& path) + { + const auto folder = inotify_init(); + if (folder < 0) + { + throw std::system_error(errno, std::system_category()); + } + + _watching_single_file = is_file(path); + + const StringType watch_path = [this, &path]() { + if (_watching_single_file) + { + const auto parsed_path = split_directory_and_file(path); + _filename = parsed_path.filename; + return parsed_path.directory; + } + else + { + return path; + } + }(); + + const auto watch = inotify_add_watch(folder, watch_path.c_str(), IN_MODIFY | IN_CREATE | IN_DELETE); + if (watch < 0) + { + throw std::system_error(errno, std::system_category()); + } + return { folder, watch }; + } + + void monitor_directory() + { + std::vector buffer(_buffer_size); + + _running.set_value(); + while (_destory == false) + { + const auto length = read(_directory.folder, static_cast(buffer.data()), buffer.size()); + if (length > 0) + { + int i = 0; + std::vector> parsed_information; + while (i < length) + { + struct inotify_event *event = reinterpret_cast(&buffer[i]); // NOLINT + if (event->len) + { + const UnderpinningString changed_file{ event->name }; + if (pass_filter(changed_file)) + { + if (event->mask & IN_CREATE) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::added); + } + else if (event->mask & IN_DELETE) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::removed); + } + else if (event->mask & IN_MODIFY) + { + parsed_information.emplace_back(StringType{ changed_file }, Event::modified); + } + } + } + i += event_size + event->len; + } + //dispatch callbacks + { + std::lock_guard lock(_callback_mutex); + _callback_information.insert(_callback_information.end(), parsed_information.begin(), parsed_information.end()); + } + _cv.notify_all(); + } + } + } +#endif // __unix__ + +#if FILEWATCH_PLATFORM_MAC + static StringType absolute_path_of(const StringType& path) { + char buf[PATH_MAX]; + int fd = open((const char*)path.c_str(), O_RDONLY); + const char* str = buf; + struct stat stat; + mbstate_t state; + + assert(fd != -1); + fcntl(fd, F_GETPATH, buf); + fstat(fd, &stat); + + if (stat.st_mode & S_IFREG || stat.st_mode & S_IFLNK) { + size_t len = strlen(buf); + + for (size_t i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + buf[i] = '\0'; + break; + } + } + } + close(fd); + + if (IsWChar::value) { + size_t needed = mbsrtowcs(nullptr, &str, 0, &state) + 1; + StringType s; + + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &str, s.size(), &state); + return s; + } + return StringType {buf}; + } +#elif defined(__unix__) + static StringType absolute_path_of(const StringType& path) { + char buf[PATH_MAX]; + const char* str = buf; + struct stat stat; + mbstate_t state; + + realpath((const char*)path.c_str(), buf); + ::stat((const char*)path.c_str(), &stat); + + if (stat.st_mode & S_IFREG || stat.st_mode & S_IFLNK) { + size_t len = strlen(buf); + + for (size_t i = len - 1; i >= 0; i--) { + if (buf[i] == '/') { + buf[i] = '\0'; + break; + } + } + } + + if (IsWChar::value) { + size_t needed = mbsrtowcs(nullptr, &str, 0, &state) + 1; + StringType s; + + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &str, s.size(), &state); + return s; + } + return StringType {buf}; + } +#elif _WIN32 + static StringType absolute_path_of(const StringType& path) { + constexpr size_t size = IsWChar::value? MAX_PATH : 32767 * sizeof(wchar_t); + char buf[size]; + + DWORD length = IsWChar::value? + GetFullPathNameW((LPCWSTR)path.c_str(), + size / sizeof(TCHAR), + (LPWSTR)buf, + nullptr) : + GetFullPathNameA((LPCSTR)path.c_str(), + size / sizeof(TCHAR), + buf, + nullptr); + return StringType{(C*)buf, length}; + } +#endif + +#if FILEWATCH_PLATFORM_MAC + static StringType utf8StringToUtf32String(const char* buffer) { + mbstate_t state{}; + StringType s{}; + + size_t needed = mbsrtowcs(nullptr, &buffer, 0, &state) + 1; + s.reserve(needed); + mbsrtowcs((wchar_t*)&s[0], &buffer, s.size(), &state); + return s; + } + + template::value>> + static void walkDirectory(const StringType& path, Fn callback) { + int fd = open(path.c_str(), O_RDONLY); + char buf[1024]; + long basep = 0; + + if (fd == -1) { + return; + } + + int ret = __getdirentries64(fd, buf, sizeof(buf), &basep); + + while (ret > 0) { + char* current = buf; + int offset = 0; + + while (offset < ret) { + struct dirent* dirent = (struct dirent*)current; + StringType name = IsWChar::value? + utf8StringToUtf32String(dirent->d_name) + : StringType(dirent->d_name); + + callback(std::move(name)); + current += dirent->d_reclen; + offset += dirent->d_reclen; + } + ret = __getdirentries64(fd, buf, sizeof(buf), &basep); + } + close(fd); + } + + static StringType nameofFd(int fd) { + size_t len = 0; + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + + len = strnlen(buf, MAXPATHLEN); + for (int i = len - 1; i >= 0; i--) { + if(buf[i] == '/') { + return StringType{buf + i + 1, len - i - 1}; + } + } + return StringType{buf, len}; + } + + static StringType fullPathOfFd(int fd) { + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + return StringType{(C*)buf}; + } + + static StringType pathOfFd(int fd) { + size_t len = 0; + char buf[MAXPATHLEN]; + + if (fcntl(fd, F_GETPATH, buf) == -1) { + return StringType{}; + } + if (IsWChar::value) { + return utf8StringToUtf32String(buf); + } + + len = strnlen(buf, MAXPATHLEN); + for (int i = len - 1; i >= 0; i--) { + if(buf[i] == '/') { + return StringType{buf, static_cast(i)}; + } + } + return StringType{buf, len}; + } + + static bool fdIsRemoved(int fd) { + char buf[MAXPATHLEN]; + return fcntl(fd, F_GETPATH, buf) == -1; + } + + FileState makeFileState(const StringType& path) { + int fd = openFile(path); + struct stat stat; + + fstat(fd, &stat); + + return FileState { + openFile(path), + stat.st_nlink, + stat.st_mtimespec.tv_sec + }; + } + + static StringType filenameOf(const StringType& file) { + for (int i = file.size() - 1; i >= 0; i--) { + if(file[i] == '/') { + return file.substr(i + 1); + } + } + return file; + } + + static bool isInDirectory(const StringType& file, const StringType& path) { + if (file.size() < path.size()) { + return false; + } + return strncmp(file.data(), path.data(), path.size()) == 0; + } + + PathParts splitPath(const StringType& path) { + PathParts split = split_directory_and_file(path); + + if (split.directory.size() > 0 && split.directory[split.directory.size() - 1] == '/') { + split.directory.erase(split.directory.size() - 1); + } + return split; + } + + StringType fullPathOf(const StringType& file) { + return _path + '/' + file; + } + + int openFile(const StringType& file) { + int fd = open(fullPathOf(file).c_str(), O_RDONLY); + assert(fd != -1); + return fd; + } + + void walkAndSeeChanges() { + struct RenamedPair { + StringType old; + StringType current; + }; + struct EventInfo { + StringType file; + struct timespec time; + Event event; + }; + std::unordered_map newSnapshot{}; + std::vector events{}; + + for (auto& entry : _directory_snapshot) { + struct stat stat; + + fstat(entry.second.fd, &stat); + if (fdIsRemoved(entry.second.fd)) { + events.push_back(EventInfo { + .event = Event::removed, + .file = entry.first, + .time = stat.st_ctimespec + }); + continue; + } + + StringType fullPath = fullPathOfFd(entry.second.fd); + PathParts pathPair = splitPath(fullPath); + + if (pathPair.directory != _path) { + events.push_back(EventInfo { + .event = Event::removed, + .file = entry.first, + .time = stat.st_ctimespec + }); + continue; + } + if (entry.first != pathPair.filename) { + events.push_back(EventInfo { + .event = Event::renamed_old, + .file = entry.first, + .time = stat.st_ctimespec + }); + events.push_back(EventInfo { + .event = Event::renamed_new, + .file = pathPair.filename, + .time = stat.st_ctimespec + }); + } + else { + if (stat.st_mtimespec.tv_sec > entry.second.last_modification) { + entry.second.last_modification = stat.st_mtimespec.tv_sec; + events.push_back(EventInfo { + .event = Event::modified, + .file = pathPair.filename, + .time = stat.st_mtimespec + }); + } + } + newSnapshot.insert(std::make_pair(std::move(pathPair.filename), + std::move(entry.second.invalidate_and_clone()))); + } + + walkDirectory(_path, [&](StringType file) { + if (isParentOrSelfDirectory(file) || !std::regex_match(file, _pattern)) { + return; + } + if (newSnapshot.count(file) == 0) { + FileState state = makeFileState(file); + struct stat stat; + + fstat(state.fd, &stat); + events.push_back(EventInfo { + .event = Event::added, + .file = file, + .time = stat.st_mtimespec + }); + newSnapshot.insert(std::make_pair(file, std::move(state))); + } + }); + + std::swap(_directory_snapshot, newSnapshot); + + std::sort(events.begin(), events.end(), [] (const EventInfo& a, EventInfo& b) { + if (a.time.tv_sec == b.time.tv_sec) { + return a.time.tv_nsec < b.time.tv_nsec; + } + return a.time.tv_sec < b.time.tv_sec; + }); + + { + std::lock_guard lock(_callback_mutex); + + for (const auto& event : events) { + _callback_information.push_back(std::make_pair(event.file, event.event)); + } + } + _cv.notify_all(); + } + + void seeSingleFileChanges() { + struct EventInfo { + StringType file; + Event event; + }; + + int eventCount = 1; + EventInfo eventInfos[2]; + + if (fdIsRemoved(_file_fd)) { + eventInfos[0].event = Event::removed; + eventInfos[0].file = _filename; + } + else { + StringType absPath = pathOfFd(_file_fd); + PathParts split = splitPath(absPath); + + if (split.directory != _path) { + eventInfos[0].event = Event::removed; + eventInfos[0].file = _filename; + } + else if (split.filename != _filename) { + eventInfos[0].event = Event::renamed_old; + eventInfos[0].file = std::move(_filename); + eventInfos[1].event = Event::renamed_new; + eventInfos[1].file = split.filename; + eventCount = 2; + _filename = std::move(split.filename); + } + else { + struct stat stat; + + fstat(_file_fd, &stat); + + if (stat.st_mtimespec.tv_sec > _last_modification_time.tv_sec) { + eventInfos[0].event = Event::modified; + eventInfos[0].file = _filename; + _last_modification_time = stat.st_mtimespec; + } + else if (stat.st_mtimespec.tv_nsec > _last_modification_time.tv_nsec) { + eventInfos[0].event = Event::modified; + eventInfos[0].file = _filename; + _last_modification_time = stat.st_mtimespec; + } + else { + return; + } + } + } + + { + std::lock_guard lock(_callback_mutex); + for (int i = 0; i < eventCount; i++) { + _callback_information.push_back( + std::make_pair(eventInfos[i].file, eventInfos[i].event)); + } + } + _cv.notify_all(); + } + + void notify(CFStringRef path, const FSEventStreamEventFlags flags) { + CFIndex pathLength = CFStringGetLength(path); + CFIndex written = 0; + char buffer[PATH_MAX + 1]; + + CFStringGetBytes(path, + CFRange { + .location = 0, + .length = pathLength, + }, + IsWChar::value? kCFStringEncodingUTF32 : kCFStringEncodingUTF8, + 0, + false, + (UInt8*)buffer, + PATH_MAX, + &written); + + buffer[written] = 0; + + StringType absolutePath{(const C*)buffer, static_cast(pathLength)}; + PathParts pathPair = splitPath(absolutePath); + + if (_watching_single_file && pathPair.filename != _filename) { + return; + } + if (pathPair.directory != _path || !std::regex_match(pathPair.filename, _pattern)) { + return; + } + + Event event = Event::modified; + if (_previous_event_is_rename) { + event = Event::renamed_new; + _directory_snapshot.insert(std::make_pair(pathPair.filename, + std::move(makeFileState(pathPair.filename)))); + _previous_event_is_rename = false; + } + else if (flags & kFSEventStreamEventFlagItemRenamed) { + const auto state = _directory_snapshot.find(pathPair.filename); + assert(state != _directory_snapshot.end()); + StringType fdPath = pathOfFd(state->second.fd); + + // moved/delete to Trash folder + if (!isInDirectory(absolutePath, fdPath)) { + event = Event::removed; + _directory_snapshot.erase(pathPair.filename); + } + else { + event = Event::renamed_old; + _previous_event_is_rename = true; + } + } + else if (flags & kFSEventStreamEventFlagItemCreated) { + _directory_snapshot.insert(std::make_pair(pathPair.filename, + std::move(makeFileState(pathPair.filename)))); + event = Event::added; + } + else if (flags & kFSEventStreamEventFlagItemRemoved) { + _directory_snapshot.erase(pathPair.filename); + event = Event::removed; + } + + { + std::lock_guard lock(_callback_mutex); + _callback_information.push_back(std::make_pair(std::move(pathPair.filename), event)); + } + _cv.notify_all(); + } + + static void handleFsEvent(__attribute__((unused)) ConstFSEventStreamRef streamFef, + void* clientCallBackInfo, + size_t numEvents, + CFArrayRef eventPaths, + const FSEventStreamEventFlags* eventFlags, + __attribute__((unused)) const FSEventStreamEventId* eventIds) { + FileWatch* self = (FileWatch*)clientCallBackInfo; + + for (size_t i = 0; i < numEvents; i++) { + FSEventStreamEventFlags flag = eventFlags[i]; + CFStringRef path = (CFStringRef)CFArrayGetValueAtIndex(eventPaths, i); + + if (self->_watching_single_file) { + self->seeSingleFileChanges(); + } + else if (flag & kFSEventStreamEventFlagMustScanSubDirs) { + self->walkAndSeeChanges(); + } + else { + self->notify(path, flag); + } + } + } + + FSEventStreamRef openStream(const StringType& directory) { + CFStringEncoding encoding = IsWChar::value? + kCFStringEncodingUTF32 : kCFStringEncodingASCII; + CFStringRef path = CFStringCreateWithBytes(kCFAllocatorDefault, + (const UInt8*)directory.data(), + directory.size(), + encoding, + false); + CFArrayRef paths = CFArrayCreate( + kCFAllocatorDefault, + (const void**)&path, + 1, + nullptr); + FSEventStreamContext context { + .info = (void*)this + }; + FSEventStreamRef event = FSEventStreamCreate( + kCFAllocatorDefault, + (FSEventStreamCallback)handleFsEvent, + &context, + paths, + kFSEventStreamEventIdSinceNow, + 0, + kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagUseCFTypes); + + CFRelease(path); + CFRelease(paths); + return event; + } + + FSEventStreamRef openStreamForDirectory(const StringType& directory) { + FSEventStreamRef stream = openStream(directory); + walkDirectory(directory, [this] (StringType path) mutable { + if (!isParentOrSelfDirectory(path) && std::regex_match(path, _pattern)) { + _directory_snapshot.insert(std::make_pair(std::move(path), + std::move(makeFileState(path)))); + } + }); + return stream; + } + + FSEventStreamRef openStreamForFile(const StringType& file) { + PathParts split = splitPath(file); + + _watching_single_file = true; + _filename = std::move(split.filename); + _file_fd = openFile(file); + return openStreamForDirectory(split.directory); + } + + FSEventStreamRef get_directory(const StringType& directory) { + struct stat stat; + + ::stat((const char*)directory.c_str(), &stat); + if (stat.st_mode & S_IFDIR) { + return openStreamForDirectory(directory); + } + _last_modification_time = stat.st_mtimespec; + return openStreamForFile(directory); + } + + void monitor_directory() { + _run_loop = CFRunLoopGetCurrent(); + FSEventStreamScheduleWithRunLoop(_directory, + _run_loop, + kCFRunLoopDefaultMode); + FSEventStreamStart(_directory); + _running.set_value(); + CFRunLoopRun(); + } +#endif // FILEWATCH_PLATFORM_MAC + + void callback_thread() + { + while (_destory == false) { + std::unique_lock lock(_callback_mutex); + if (_callback_information.empty() && _destory == false) { + _cv.wait(lock, [this] { return _callback_information.size() > 0 || _destory; }); + } + decltype(_callback_information) callback_information = {}; + std::swap(callback_information, _callback_information); + lock.unlock(); + + for (const auto& file : callback_information) { + if (_callback) { + try + { + _callback(file.first, file.second); + } + catch (const std::exception&) + { + } + } + } + } + } + }; + + template constexpr typename FileWatch::C FileWatch::_regex_all[]; + template constexpr typename FileWatch::C FileWatch::_this_directory[]; +} +#endif diff --git a/lib/packer/src/packerfs.zig b/lib/packer/src/packerfs.zig index 3d80af3..ed6eec3 100644 --- a/lib/packer/src/packerfs.zig +++ b/lib/packer/src/packerfs.zig @@ -53,6 +53,16 @@ pub const Settings = struct { contentFolderExtraPaths: []const []const u8 = &[_][]const u8{}, // By default, this will mount the content/ folder on disk. }; +pub const WatchCallback = struct { + func: *const fn ([]const u8, ?*anyopaque) void, + ctx: ?*anyopaque = null, + path: []const u8, + + pub inline fn call(self: *@This(), path: []const u8) void { + self.func(path, self.ctx); + } +}; + pub const PackerFS = struct { allocator: std.mem.Allocator, fileHeaders: std.ArrayListUnmanaged(PackedFileEntry) = .{}, @@ -67,6 +77,8 @@ pub const PackerFS = struct { lock: std.Thread.Mutex = .{}, + fileWatchCallbacks: std.ArrayListUnmanaged(WatchCallback) = .{}, + pub const PakMounting = struct { filePath: []const u8, // path to the file bytes: []align(8) u8 = undefined, @@ -114,6 +126,48 @@ pub const PackerFS = struct { } }; + pub fn addFileChangedCallback( + self: *@This(), + path: []const u8, + cb: *const fn ([]const u8, ?*anyopaque) void, + ctx: ?*anyopaque, + ) !void { + const x = WatchCallback{ + .func = cb, + .ctx = ctx, + .path = path, + }; + + try self.fileWatchCallbacks.append(self.allocator, x); + } + + pub fn watchCallback(path: [*c]const u8, ctx: ?*anyopaque) callconv(.C) void { + const self: *@This() = @ptrCast(@alignCast(ctx)); + // std.debug.print("WE GOT OURSELFS A FUCKIN CALLBAKC FOR A PATH MOTHERF- {s} {p} {d}\n", .{ path, ctx.?, self.pakMountings.items.len }); + + for (self.fileWatchCallbacks.items) |*watch| { + // std.debug.print("checking {s} {s}\n", .{ watch.path, path }); + if (std.mem.eql(u8, watch.path, std.mem.span(path))) { + watch.call(std.mem.span(path)); + } + // std.debug.print("registered callback: {s}\n", .{watch.path}); + } + } + + pub fn watchPath(self: *@This(), path: []const u8) void { + // std.debug.print("self = {x}\n", .{@intFromPtr(self)}); + const p = self.stringArena.allocator().dupeZ(u8, path) catch return; + const watch = watcher.createWatchPoint( + p, + watchCallback, + self, + ) orelse { + std.debug.print("Unable to create watch point, {s}\n", .{p}); + return; + }; + _ = watch; + } + // todo: implement load memory to file then-map setup. // for systems which do not support mmap @@ -164,6 +218,26 @@ pub const PackerFS = struct { return @intCast(self.fileHeaders.items.len); } + pub fn resolveContentFile(self: *@This(), allocator: std.mem.Allocator, path: []const u8) !?[]u8 { + for (self.contentPaths.items) |contentPath| { + // std.debug.print("{s}\n", .{contentPath}); + const fullPath = try std.fmt.allocPrint(self.stringArena.allocator(), "{s}/{s}", .{ contentPath, path }); + defer self.stringArena.allocator().free(fullPath); + + var exists: bool = true; + + std.fs.cwd().access(fullPath, .{}) catch |err| { + exists = if (err == error.FileNotFound) false else true; + }; + + if (exists) { + return try allocator.dupe(u8, fullPath); + } + } + + return null; + } + pub fn fileExists(self: *@This(), path: []const u8) bool { self.lock.lock(); defer self.lock.unlock(); @@ -231,6 +305,7 @@ pub const PackerFS = struct { return self.stringArena.allocator(); } + // if skipLoading is set bytes will contain an empty path fn loadFileDirect(self: *@This(), basePath: []const u8, path: []const u8) !?PackerBytesMapping { // if we made it to this function we can assume that the file does not exist in existing pak mounting references. // nor does it exist in self.fileHandlesByName @@ -347,6 +422,7 @@ pub const PackerFS = struct { self.contentPaths.deinit(self.allocator); self.stringArena.deinit(); self.lock.unlock(); + self.fileWatchCallbacks.deinit(self.allocator); self.allocator.destroy(self); } @@ -414,3 +490,5 @@ pub const PackerFS = struct { return .{ .allocator = allocator, .data = rv, .sources = sourcePath }; } }; + +const watcher = @import("watcher.zig"); diff --git a/lib/packer/src/watcher.cpp b/lib/packer/src/watcher.cpp new file mode 100644 index 0000000..8289d91 --- /dev/null +++ b/lib/packer/src/watcher.cpp @@ -0,0 +1,17 @@ +#include + +extern "C" void* createWatchPoint( + const char* watchPath, + void(*cb)(const char*, void*), + void* ctx +){ + auto* watch = new filewatch::FileWatch( + watchPath, + [cb, ctx] (const std::string& path, const filewatch::Event change_type) { + std::cout << event_to_string(change_type) << std::endl; + cb(path.c_str(), ctx); + } + ); + + return watch; +} diff --git a/lib/packer/src/watcher.zig b/lib/packer/src/watcher.zig new file mode 100644 index 0000000..df90c0c --- /dev/null +++ b/lib/packer/src/watcher.zig @@ -0,0 +1,7 @@ +pub const CallbackType = *const fn ([*c]const u8, ?*anyopaque) callconv(.C) void; + +pub extern fn createWatchPoint( + [*c]const u8, + CallbackType, + ?*anyopaque, +) callconv(.C) ?*anyopaque; diff --git a/lib/packer/tests/test.zig b/lib/packer/tests/test.zig index 6d642ba..1a3a5c9 100644 --- a/lib/packer/tests/test.zig +++ b/lib/packer/tests/test.zig @@ -116,3 +116,27 @@ test "packerfs_test" { std.debug.print("littleEndian {any}", .{packer.littleEndian}); } + +fn fileChangedCb(pathChanged: []const u8, ctx: ?*anyopaque) void { + _ = ctx; + std.debug.print("pathChanged {s}\n", .{pathChanged}); +} + +test "packer file watch" { + var fs = try PackerFS.init(std.testing.allocator, .{}); + defer fs.destroy(); + std.fs.cwd().deleteFile("test_output/test2.txt") catch {}; + try fs.addContentPath("test_output"); + + fs.watchPath("test_output"); + try fs.addFileChangedCallback("test2.txt", fileChangedCb, null); + + var file = try std.fs.cwd().createFile("test_output/test2.txt", .{}); + std.time.sleep(100000000); + + std.debug.print("writing to file\n", .{}); + // var file = try std.fs.cwd().openFile("test_output/test2.txt", .{ .mode = .read_write }); + try file.writeAll("what the fuck bro"); + std.time.sleep(400000000); + // std.time.sleep(5000000000); +} diff --git a/projects/build.zig b/projects/build.zig index 9d458ff..9780f03 100644 --- a/projects/build.zig +++ b/projects/build.zig @@ -21,4 +21,17 @@ pub fn build(b: *std.Build) void { blbuild.addExtraModule(sampleGame, "videoplayer"); blbuild.addExtraModule(sampleGame, "doomplayer"); blbuild.addExtraModule(sampleGame, "bsp"); + + const sampleGameExtern = b.addSharedLibrary(.{ + .root_source_file = b.path("sampleGame/externGame/externGame.zig"), + .link_libc = true, + .optimize = optimize, + .target = target, + .name = "externGame", + }); + + const installExtern = b.addInstallArtifact(sampleGameExtern, .{ + .dest_dir = .{ .override = .{ .custom = "modules" } }, + }); + b.getInstallStep().dependOn(&installExtern.step); } diff --git a/projects/sampleGame/externGame/externGame.zig b/projects/sampleGame/externGame/externGame.zig new file mode 100644 index 0000000..9bb240b --- /dev/null +++ b/projects/sampleGame/externGame/externGame.zig @@ -0,0 +1,5 @@ +pub export fn add(a: i32, b: i32) i32 { + return a + b + 32000; +} + +const std = @import("std"); diff --git a/projects/sampleGame/main.zig b/projects/sampleGame/main.zig index 0acdf09..dc0a4a8 100644 --- a/projects/sampleGame/main.zig +++ b/projects/sampleGame/main.zig @@ -28,7 +28,9 @@ rendererDebugger: *extras.RendererDebug = undefined, tbMap: ?*bsp.maploader.TBMap = null, -fileWatch: f64 = 3.0, +addFunc: ?*const fn (i32, i32) callconv(.C) i32 = undefined, + +modules: std.AutoHashMapUnmanaged(u32, []const u8) = .{}, pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This()); @@ -172,6 +174,14 @@ pub fn loadMap(self: *@This()) void { core.engine_log("map loaded", .{}); } +fn moduleChangedCallback(pathChanged: []const u8, ctx: ?*anyopaque) void { + const self: *@This() = @ptrCast(@alignCast(ctx)); + + core.engine_log("moduleChanged {s}", .{pathChanged}); + var name = core.MakeName(pathChanged); + self.modules.put(self.allocator, name.handle(), name.utf8()) catch {}; +} + pub fn prepare(self: *@This()) !void { core.engine_log(">>>>>>> game prepare", .{}); var z = core.tracy.ZoneN(@src(), "PREPARING GAME"); @@ -180,6 +190,11 @@ pub fn prepare(self: *@This()) !void { try script.loadTypes("scripts"); try script.runScriptFile("scripts/prepare.lua"); + core.fs().watchPath("zig-out/modules"); + try core.fs().addFileChangedCallback(core.sharedLibName("externGame"), moduleChangedCallback, self); + + try self.tryLoadExtern("externGame"); + self.rendererDebugger = try extras.RendererDebug.create(self.allocator); //try core.createObject(extras.RendererDebug, .{}); self.objectSpawner = try extras.ObjectSpawner.create(self.allocator); @@ -361,17 +376,39 @@ pub fn getMouseMovement(self: *@This()) core.Vector2f { return rv; } +pub fn tryLoadExtern(self: *@This(), gameName: []const u8) !void { + var buf = std.mem.zeroes([256]u8); + + const os_tag = @import("builtin").os.tag; + + var suffix: []const u8 = "dll"; + if (os_tag == .linux) { + suffix = "so"; + } else if (os_tag == .macos) { + suffix = "dynlib"; + } + + const path = try std.fmt.bufPrint(&buf, "zig-out/bin/{s}.{s}", .{ gameName, suffix }); + var lib = try std.DynLib.open(path); + // std.debug.print("path: {s}", .{path}); + + self.addFunc = lib.lookup(@TypeOf(self.addFunc.?), "add"); +} + pub fn tick(self: *@This(), dt: f64) void { const fdt: f32 = @floatCast(dt); _ = ig.dockSpaceOverViewport(ig.getMainViewport(), .{ .passthru_central_node = true }, null); - self.loadMap2() catch unreachable; + // self.loadMap2() catch unreachable; - self.fileWatch -= dt; - if (self.fileWatch < 0) { - self.fileWatch = 3.0; - } + core.loopDelay(@src(), 1.0, dt, struct { + s: @TypeOf(self), + + pub fn func(c: @This()) void { + c.s.tryLoadExtern("externGame") catch unreachable; + } + }, .{ .s = self }); const z1 = tracy.ZoneN(@src(), "inputDebugger"); if (!self.mouseLook) { @@ -441,6 +478,15 @@ pub fn tick(self: *@This(), dt: f64) void { ig.textFmt("- f2 to route all inputs to the doom player", .{}) catch return; ig.textFmt("- movingLight checkbox makes the light stop moving with you", .{}) catch return; + ig.textf("1 + 2 = {d}", .{self.addFunc.?(1, 2)}); + + { + ig.textf("modules dirty: ", .{}); + var i = self.modules.iterator(); + while (i.next()) |n| { + ig.textf("{s} pending reload", .{n.value_ptr.*}); + } + } if (ig.smallButton("reload map")) { self.loadMap2() catch unreachable; } @@ -472,6 +518,7 @@ pub fn tick(self: *@This(), dt: f64) void { } pub fn deinit(self: *@This()) void { + self.modules.deinit(self.allocator); self.rendererDebugger.destroy(); DoomPlayer.DoomCanvas.cleanupDoom(); self.fpcamera.destroy();