diff options
author | Jan200101 <sentrycraft123@gmail.com> | 2024-05-05 16:51:48 +0200 |
---|---|---|
committer | Jan200101 <sentrycraft123@gmail.com> | 2024-05-05 16:51:48 +0200 |
commit | 54365b02ef3e36a631354aa1af3bd33f90db3cf7 (patch) | |
tree | b7f02a04c28437a1c44c427c440fe3f0750bed04 | |
download | SouthRPC-54365b02ef3e36a631354aa1af3bd33f90db3cf7.tar.gz SouthRPC-54365b02ef3e36a631354aa1af3bd33f90db3cf7.zip |
Zig rework
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | LICENSE | 22 | ||||
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | build.zig | 35 | ||||
-rw-r--r-- | src/class.zig | 300 | ||||
-rw-r--r-- | src/engine.zig | 50 | ||||
-rw-r--r-- | src/interface.zig | 34 | ||||
-rw-r--r-- | src/interfaces/PluginCallbacks001.zig | 105 | ||||
-rw-r--r-- | src/interfaces/PluginId001.zig | 65 | ||||
-rw-r--r-- | src/main.zig | 16 | ||||
-rw-r--r-- | src/methods/execute_command.zig | 53 | ||||
-rw-r--r-- | src/methods/execute_squirrel.zig | 42 | ||||
-rw-r--r-- | src/methods/list_methods.zig | 18 | ||||
-rw-r--r-- | src/northstar.zig | 19 | ||||
-rw-r--r-- | src/server.zig | 183 | ||||
-rw-r--r-- | src/squirrel.zig | 29 | ||||
-rw-r--r-- | src/sys.zig | 48 |
17 files changed, 1028 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb2faea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +zig-out +zig-cache +.vs + @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) Jan Drögehoff + +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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..55724db --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# SouthRPC + +SouthRPC is a plugin for [Northstar](https://northstar.tf) which implements RPC over HTTP through JSON-RPC 2.0. +This gives external applications the ability to run code within Titanfall 2. + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..ab7f300 --- /dev/null +++ b/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ + .default_target = .{ + .cpu_arch = .x86_64, + .os_tag = .windows, + .abi = .msvc, + }, + }); + + const optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .Debug, + }); + + const lib = b.addSharedLibrary(.{ + .name = "ZigPlugin", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); + + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_main_tests = b.addRunArtifact(main_tests); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); +} diff --git a/src/class.zig b/src/class.zig new file mode 100644 index 0000000..de8002e --- /dev/null +++ b/src/class.zig @@ -0,0 +1,300 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const Fn = std.builtin.Type.Fn; +const StructField = std.builtin.Type.StructField; + +pub fn Class(comptime base: anytype, comptime members: anytype) type { + const BaseType = @TypeOf(base); + const base_type_info = @typeInfo(BaseType); + + if (base_type_info != .Struct or base_type_info.Struct.is_tuple != true) { + @compileError("expected tuple base argument, found " ++ @typeName(BaseType)); + } + + const MembersType = @TypeOf(members); + const members_type_info = @typeInfo(MembersType); + + if (members_type_info != .Struct or members_type_info.Struct.is_tuple != false) { + @compileError("expected struct members argument, found " ++ @typeName(MembersType)); + } + + const ClassStruct = @Type(.{ + .Struct = .{ + .layout = .Extern, + .fields = comptime blk: { + var class_fields = struct { + fields: []const StructField = &[_]StructField{}, + + fn add_virtual(self: anytype, comptime field: StructField) void { + self.add_field(.{ + .name = "vtable", + .type = @Type(.{ + .Struct = .{ + .layout = .Extern, + .fields = &[_]StructField{ + field, + }, + .decls = &.{}, + .is_tuple = false, + }, + }), + .default_value = null, + .is_comptime = false, + .alignment = 0, + }); + } + + fn add_field(comptime self: anytype, comptime field: StructField) void { + if (std.mem.eql(u8, field.name, "vtable")) { + const vtable_index: ?comptime_int = for (0.., self.fields) |i, f| { + if (std.mem.eql(u8, f.name, "vtable")) break i; + } else null; + + if (vtable_index) |i| { + const old_vtable_pointer = self.fields[i]; + const OldVTablePointerType = old_vtable_pointer.type; + const old_vtable_pointer_type_info = @typeInfo(OldVTablePointerType); + if (old_vtable_pointer_type_info != .Pointer) { + @compileError("expected vtable to be pointer, found " ++ @typeName(OldVTablePointerType)); + } + + const OldVTableType = old_vtable_pointer_type_info.Pointer.child; + const old_vtable_type_info = @typeInfo(OldVTableType); + if (old_vtable_type_info != .Struct or old_vtable_type_info.Struct.is_tuple != false) { + @compileError("expected vtable to be pointer to struct, found pointer to " ++ @typeName(OldVTableType)); + } + + const FieldType = field.type; + const field_type_info = @typeInfo(FieldType); + if (field_type_info != .Struct or field_type_info.Struct.is_tuple != false) { + @compileError("expected new vtable addition to be struct, found " ++ @typeName(FieldType)); + } + + self.fields = &[_]StructField{ + .{ + .name = old_vtable_pointer.name, + .type = @Type(.{ + .Pointer = .{ + .size = old_vtable_pointer_type_info.Pointer.size, + .is_const = old_vtable_pointer_type_info.Pointer.is_const, + .is_volatile = old_vtable_pointer_type_info.Pointer.is_volatile, + .alignment = old_vtable_pointer_type_info.Pointer.alignment, + .address_space = old_vtable_pointer_type_info.Pointer.address_space, + .child = @Type(.{ + .Struct = .{ + .layout = old_vtable_type_info.Struct.layout, + .backing_integer = old_vtable_type_info.Struct.backing_integer, + .fields = old_vtable_type_info.Struct.fields ++ field_type_info.Struct.fields, + .decls = old_vtable_type_info.Struct.decls, + .is_tuple = old_vtable_type_info.Struct.is_tuple, + }, + }), + .is_allowzero = old_vtable_pointer_type_info.Pointer.is_allowzero, + .sentinel = old_vtable_pointer_type_info.Pointer.sentinel, + }, + }), + .default_value = old_vtable_pointer.default_value, + .is_comptime = old_vtable_pointer.is_comptime, + .alignment = old_vtable_pointer.alignment, + }, + } ++ self.fields[1..]; + } else { + const FieldType = field.type; + const field_type_info = @typeInfo(FieldType); + + // If we get vtable pointer lets pretend its the real deal + if (field_type_info == .Pointer) { + const VTableType = field_type_info.Pointer.child; + const vtable_info = @typeInfo(VTableType); + if (vtable_info != .Struct or vtable_info.Struct.is_tuple != false) { + @compileError("expected vtable to be pointer to struct, found ponter to " ++ @typeName(VTableType)); + } + + self.fields = self.fields ++ &[_]StructField{field}; + return; + } else if (field_type_info != .Struct or field_type_info.Struct.is_tuple != false) { + @compileError("expected vtable to be struct, found " ++ @typeName(FieldType)); + } + + self.fields = &[_]StructField{ + .{ + .name = field.name, + .type = @Type(.{ + .Pointer = .{ + .size = .One, + .is_const = true, + .is_volatile = false, + .alignment = 0, + .address_space = .generic, + .child = @Type(.{ + .Struct = .{ + .layout = .Extern, + .fields = field_type_info.Struct.fields, + .decls = &.{}, + .is_tuple = false, + }, + }), + .is_allowzero = false, + .sentinel = null, + }, + }), + .default_value = null, + .is_comptime = false, + .alignment = 0, + }, + } ++ self.fields; + } + return; + } + + for (self.fields) |f| { + if (std.mem.eql(u8, f.name, field.name)) { + @compileError("duplicate field " ++ f.name); + } + } + self.fields = self.fields ++ &[_]StructField{field}; + } + }{}; + + // Deal with inheritance first + for (base) |b| { + const b_type_info = @typeInfo(b); + + if (b_type_info != .Struct or b_type_info.Struct.is_tuple != false) { + @compileError("expected struct base, found " ++ @typeName(b)); + } else if (b_type_info.Struct.layout != .Extern) { + @compileError("expected extern struct, found " ++ @tagName(b_type_info.Struct.layout)); + } + + for (b_type_info.Struct.fields) |f| { + class_fields.add_field(f); + } + } + + // Deal with all new members + for (members_type_info.Struct.fields) |member_field| { + const member_field_info = @typeInfo(member_field.type); + const member = @field(members, member_field.name); + + if (member_field_info != .Struct or member_field_info.Struct.is_tuple != false) { + @compileError("expected struct"); + } + + const member_type = member.type; + const member_default_value = if (@hasField(@TypeOf(member), "default_value")) &@as(member_type, member.default_value) else null; + const member_virtual = if (@hasField(@TypeOf(member), "virtual")) member.virtual else false; + + if (member_virtual) { + class_fields.add_virtual(.{ + .name = member_field.name, + .type = member_type, + .default_value = member_default_value, + .is_comptime = false, + .alignment = 0, + }); + + continue; + } + + class_fields.add_field(.{ + .name = member_field.name, + .type = member_type, + .default_value = member_default_value, + .is_comptime = false, + .alignment = 0, + }); + } + + break :blk class_fields.fields; + }, + .decls = members_type_info.Struct.decls, + .is_tuple = false, + }, + }); + + //_ = ClassStruct; + return ClassStruct; +} + +pub fn print_struct(comptime tag: std.builtin.Type, comptime prefix: []const u8) void { + if (tag != .Struct) { + @compileError("bruh"); + } + + std.debug.print("{s}struct len {} layout {}\n", .{ + prefix, + tag.Struct.fields.len, + tag.Struct.layout, + }); + + inline for (1.., tag.Struct.fields) |i, field| { + std.debug.print("{s}{}: \"{s}\" {} = {any} ({}, {})\n", .{ + prefix ++ "\t", + i, + field.name, + field.type, + field.default_value, + field.is_comptime, + field.alignment, + }); + + const t = @typeInfo(field.type); + if (t == .Struct) { + print_struct(t, prefix ++ "\t"); + } else if (t == .Pointer) { + const p = @typeInfo(t.Pointer.child); + if (p == .Struct) { + print_struct(p, prefix ++ "\t"); + } + } + } + std.debug.print("\n", .{}); +} + +fn compare_struct_type(comptime a: std.builtin.Type, comptime b: std.builtin.Type) !void { + if (a != .Struct or b != .Struct) { + return error.TestUnexpectedResult; + } + + try std.testing.expectEqual(a.Struct.layout, b.Struct.layout); + try std.testing.expectEqual(a.Struct.backing_integer, b.Struct.backing_integer); + try std.testing.expect(a.Struct.fields.len == b.Struct.fields.len); + try std.testing.expectEqual(a.Struct.decls.len, b.Struct.decls.len); + try std.testing.expectEqual(a.Struct.is_tuple, b.Struct.is_tuple); + + inline for (0..@min(a.Struct.fields.len, b.Struct.fields.len)) |i| { + const field_a = a.Struct.fields[i]; + const field_b = b.Struct.fields[i]; + + try std.testing.expect(std.mem.eql(u8, field_a.name, field_b.name)); + try std.testing.expectEqual(field_a.type, field_b.type); + if (field_a.default_value != null and field_b.default_value != null) { + const default_value_a = @as(*const field_a.type, @alignCast(@ptrCast(field_a.default_value))); + const default_value_b = @as(*const field_b.type, @alignCast(@ptrCast(field_b.default_value))); + + try std.testing.expectEqual(default_value_a.*, default_value_b.*); + } else { + try std.testing.expectEqual(field_a.default_value, field_b.default_value); + } + try std.testing.expectEqual(field_a.is_comptime, field_b.is_comptime); + try std.testing.expectEqual(field_a.alignment, field_b.alignment); + } +} + +test "basic layout" { + const struct_layout = extern struct { + len: usize = 0xCAFE, + field: f32, + }; + + const class_layout = Class(.{}, .{ + .len = .{ .type = usize, .default_value = 0xCAFE }, + .field = .{ .type = f32 }, + }); + + try compare_struct_type( + @typeInfo(struct_layout), + @typeInfo(class_layout), + ); +} diff --git a/src/engine.zig b/src/engine.zig new file mode 100644 index 0000000..e58c1f2 --- /dev/null +++ b/src/engine.zig @@ -0,0 +1,50 @@ +pub const ECommandTarget_t = enum(c_int) { + CBUF_FIRST_PLAYER = 0, + CBUF_LAST_PLAYER = 1, // MAX_SPLITSCREEN_CLIENTS - 1, MAX_SPLITSCREEN_CLIENTS = 2 + CBUF_SERVER, // CBUF_LAST_PLAYER + 1 + + CBUF_COUNT, +}; + +pub const cmd_source_t = enum(c_int) { + // Added to the console buffer by gameplay code. Generally unrestricted. + kCommandSrcCode, + + // Sent from code via engine->ClientCmd, which is restricted to commands visible + // via FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS. + kCommandSrcClientCmd, + + // Typed in at the console or via a user key-bind. Generally unrestricted, although + // the client will throttle commands sent to the server this way to 16 per second. + kCommandSrcUserInput, + + // Came in over a net connection as a clc_stringcmd + // host_client will be valid during this state. + // + // Restricted to FCVAR_GAMEDLL commands (but not convars) and special non-ConCommand + // server commands hardcoded into gameplay code (e.g. "joingame") + kCommandSrcNetClient, + + // Received from the server as the client + // + // Restricted to commands with FCVAR_SERVER_CAN_EXECUTE + kCommandSrcNetServer, + + // Being played back from a demo file + // + // Not currently restricted by convar flag, but some commands manually ignore calls + // from this source. FIXME: Should be heavily restricted as demo commands can come + // from untrusted sources. + kCommandSrcDemoFile, + + // Invalid value used when cleared + kCommandSrcInvalid = -1, +}; + +pub const CbufType = struct { + GetCurrentPlayer: *const fn () callconv(.C) ECommandTarget_t, + AddText: *const fn (ECommandTarget_t, [*:0]const u8, cmd_source_t) callconv(.C) void, + Execute: *const fn () callconv(.C) void, +}; + +pub var Cbuf: ?CbufType = null; diff --git a/src/interface.zig b/src/interface.zig new file mode 100644 index 0000000..530d8cf --- /dev/null +++ b/src/interface.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +const PluginCallbacks001 = @import("interfaces/PluginCallbacks001.zig").plugin_interface; +const PluginId001 = @import("interfaces/PluginId001.zig").plugin_interface; + +pub const interfaces = .{ + PluginCallbacks001, + PluginId001, +}; + +pub const InterfaceStatus = enum(c_int) { + IFACE_OK = 0, + IFACE_FAILED, +}; + +pub export fn CreateInterface(name_ptr: [*:0]const u8, status_ptr: ?*InterfaceStatus) callconv(.C) *allowzero void { + const name = std.mem.span(name_ptr); + + inline for (interfaces) |interface| { + if (std.mem.eql(u8, interface.name, name)) { + if (status_ptr) |status| { + status.* = .IFACE_OK; + } + + return interface.func(); + } + } + + if (status_ptr) |status| { + status.* = .IFACE_FAILED; + } + + return @ptrFromInt(0); +} diff --git a/src/interfaces/PluginCallbacks001.zig b/src/interfaces/PluginCallbacks001.zig new file mode 100644 index 0000000..17d687d --- /dev/null +++ b/src/interfaces/PluginCallbacks001.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const windows = std.os.windows; + +const Class = @import("../class.zig").Class; + +const interface = @import("../interface.zig"); +const northstar = @import("../northstar.zig"); +const squirrel = @import("../squirrel.zig"); +const sys = @import("../sys.zig"); +const server = @import("../server.zig"); +const engine = @import("../engine.zig"); + +const CSquirrelVM = squirrel.CSquirrelVM; + +pub const plugin_interface = .{ + .name = "PluginCallbacks001", + .func = CreatePluginCallbacks, +}; + +pub fn CreatePluginCallbacks() *void { + return @ptrCast( + @constCast( + &IPluginCallbacks{ + .vtable = &.{ + .Init = Init, + .Finalize = Finalize, + .Unload = Unload, + .OnSqvmCreated = OnSqvmCreated, + .OnSqvmDestroyed = OnSqvmDestroyed, + .OnLibraryLoaded = OnLibraryLoaded, + .RunFrame = RunFrame, + }, + }, + ), + ); +} + +pub const IPluginCallbacks = Class(.{}, .{ + .Init = .{ .type = *const fn (*anyopaque, windows.HMODULE, *northstar.NorthstarData, u8) callconv(.C) void, .virtual = true }, + .Finalize = .{ .type = *const fn (*anyopaque) callconv(.C) void, .virtual = true }, + .Unload = .{ .type = *const fn (*anyopaque) callconv(.C) bool, .virtual = true }, + .OnSqvmCreated = .{ .type = *const fn (*anyopaque, *CSquirrelVM) callconv(.C) void, .virtual = true }, + .OnSqvmDestroyed = .{ .type = *const fn (*anyopaque, *CSquirrelVM) callconv(.C) void, .virtual = true }, + .OnLibraryLoaded = .{ .type = *const fn (*anyopaque, windows.HMODULE, [*:0]const u8) callconv(.C) void, .virtual = true }, + .RunFrame = .{ .type = *const fn (*anyopaque) callconv(.C) void, .virtual = true }, +}); + +pub fn Init(self: *anyopaque, module: windows.HMODULE, data: *northstar.NorthstarData, reloaded: u8) callconv(.C) void { + _ = self; + + northstar.init(module, data); + sys.init(); + + if (reloaded != 0) { + server.stop(); + } + + if (!server.running) { + server.start() catch std.log.err("Failed to start HTTP Server", .{}); + } + + std.log.info("Loaded", .{}); +} + +pub fn Finalize(self: *anyopaque) callconv(.C) void { + _ = self; +} + +pub fn Unload(self: *anyopaque) callconv(.C) bool { + _ = self; + + server.stop(); + + return true; +} + +pub fn OnSqvmCreated(self: *anyopaque, c_sqvm: *CSquirrelVM) callconv(.C) void { + _ = self; + + std.log.info("created {s} sqvm", .{@tagName(c_sqvm.context)}); +} + +pub fn OnSqvmDestroyed(self: *anyopaque, c_sqvm: *CSquirrelVM) callconv(.C) void { + _ = self; + + std.log.info("destroyed {s} sqvm", .{@tagName(c_sqvm.context)}); +} + +pub fn OnLibraryLoaded(self: *anyopaque, module: windows.HMODULE, name_ptr: [*:0]const u8) callconv(.C) void { + _ = self; + + const name = std.mem.span(name_ptr); + + if (std.mem.eql(u8, name, "engine.dll")) { + engine.Cbuf = .{ + .GetCurrentPlayer = @ptrFromInt(@intFromPtr(module) + 0x120630), + .AddText = @ptrFromInt(@intFromPtr(module) + 0x1203B0), + .Execute = @ptrFromInt(@intFromPtr(module) + 0x1204B0), + }; + } +} + +pub fn RunFrame(self: *anyopaque) callconv(.C) void { + _ = self; +} diff --git a/src/interfaces/PluginId001.zig b/src/interfaces/PluginId001.zig new file mode 100644 index 0000000..2246b8b --- /dev/null +++ b/src/interfaces/PluginId001.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const Class = @import("../class.zig").Class; + +pub const plugin_interface = .{ + .name = "PluginId001", + .func = CreatePluginId, +}; + +fn CreatePluginId() *void { + return @ptrCast( + @constCast( + &IPluginId{ + .vtable = &.{ + .GetString = GetString, + .GetField = GetField, + }, + }, + ), + ); +} + +pub const IPluginId = Class(.{}, .{ + .GetString = .{ .type = *const fn (*anyopaque, PluginString) callconv(.C) ?[*:0]const u8, .virtual = true }, + .GetField = .{ .type = *const fn (*anyopaque, PluginField) callconv(.C) i64, .virtual = true }, +}); + +const PluginString = enum(c_int) { + ID_NAME = 0, + ID_LOG_NAME, + ID_DEPENDENCY_NAME, + _, +}; + +const PluginField = enum(c_int) { + ID_CONTEXT = 0, + _, +}; + +const PluginContext = enum(i64) { + PCTX_DEDICATED = 0x1, // load on dedicated servers + PCTX_CLIENT = 0x2, // load on clients + _, +}; + +pub fn GetString(self: *anyopaque, prop: PluginString) callconv(.C) ?[*:0]const u8 { + _ = self; + + switch (prop) { + .ID_NAME => return @import("root").PLUGIN_NAME, + .ID_LOG_NAME => return @import("root").LOG_NAME, + .ID_DEPENDENCY_NAME => return @import("root").DEPENDENCY_NAME, + else => return null, + } +} + +pub fn GetField(self: *anyopaque, prop: PluginField) callconv(.C) i64 { + _ = self; + + switch (prop) { + .ID_CONTEXT => { + return @intFromEnum(PluginContext.PCTX_DEDICATED) | @intFromEnum(PluginContext.PCTX_CLIENT); + }, + else => return 0, + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ef5efab --- /dev/null +++ b/src/main.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +pub const PLUGIN_NAME = "Zig Plugin"; +pub const LOG_NAME = "ZIGPLUGIN"; +pub const DEPENDENCY_NAME = "ZigPlugin"; + +pub const std_options = struct { + // Set the log level to info + pub const log_level = .info; + // Define logFn to override the std implementation + pub const logFn = @import("sys.zig").log; +}; + +comptime { + _ = @import("interface.zig"); +} diff --git a/src/methods/execute_command.zig b/src/methods/execute_command.zig new file mode 100644 index 0000000..c253738 --- /dev/null +++ b/src/methods/execute_command.zig @@ -0,0 +1,53 @@ +const std = @import("std"); + +const engine = @import("../engine.zig"); + +const Allocator = std.mem.Allocator; + +pub const method = .{ + .name = "execute_command", + .func = execute_command, +}; + +fn execute_command(allocator: Allocator, params: ?std.json.Value) !?std.json.Value { + if (params == null) { + return error.InvalidParameters; + } + + const command = blk: { + const command_item = switch (params.?) { + .array => param_blk: { + const command_array = params.?.array; + + if (command_array.capacity < 1) { + return error.NoCommand; + } + + break :param_blk command_array.items[0]; + }, + + .object => param_blk: { + const command_object = params.?.object; + + break :param_blk command_object.get("command") orelse return error.NoCommand; + }, + + else => return error.NoCommand, + }; + + if (command_item != .string) { + return error.InvalidCommand; + } + + break :blk try allocator.dupeZ(u8, command_item.string); + }; + + if (engine.Cbuf) |Cbuf| { + const cur_player = Cbuf.GetCurrentPlayer(); + Cbuf.AddText(cur_player, command, .kCommandSrcCode); + } else { + return error.NoEngine; + } + + return null; +} diff --git a/src/methods/execute_squirrel.zig b/src/methods/execute_squirrel.zig new file mode 100644 index 0000000..91a975f --- /dev/null +++ b/src/methods/execute_squirrel.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +const squirrel = @import("../squirrel.zig"); + +const Allocator = std.mem.Allocator; + +pub const method = .{ + .name = "execute_squirrel", + .func = execute_squirrel, +}; + +fn execute_squirrel(allocator: Allocator, params: ?std.json.Value) !?std.json.Value { + if (params == null or params.? != .object) { + return error.InvalidParameters; + } + + const object = params.?.object; + + const context = blk: { + const context_object = object.get("context"); + if (context_object != null and context_object.? == .string) { + const context_string = context_object.?.string; + + if (std.mem.eql(u8, context_string, "server")) { + break :blk squirrel.ScriptContext.SC_SERVER; + } else if (std.mem.eql(u8, context_string, "client")) { + break :blk squirrel.ScriptContext.SC_CLIENT; + } else if (std.mem.eql(u8, context_string, "ui")) { + break :blk squirrel.ScriptContext.SC_UI; + } + + return error.InvalidContext; + } + + break :blk squirrel.ScriptContext.SC_UI; + }; + _ = context; + + return .{ + .string = "test", + }; +} diff --git a/src/methods/list_methods.zig b/src/methods/list_methods.zig new file mode 100644 index 0000000..b396100 --- /dev/null +++ b/src/methods/list_methods.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +const server = @import("../server.zig"); +const RpcMethods = server.RpcMethods; + +const Allocator = std.mem.Allocator; + +pub const method = .{ + .name = "list_methods", + .func = list_methods, +}; + +fn list_methods(allocator: Allocator, params: ?std.json.Value) !?std.json.Value { + _ = allocator; + _ = params; + + return null; +} diff --git a/src/northstar.zig b/src/northstar.zig new file mode 100644 index 0000000..8c659a7 --- /dev/null +++ b/src/northstar.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const windows = std.os.windows; + +const interface = @import("interface.zig"); + +pub const NorthstarData = extern struct { + handle: ?windows.HMODULE, +}; + +pub var data: NorthstarData = .{ + .handle = null, +}; + +pub var create_interface: ?*const fn ([*:0]const u8, ?*const interface.InterfaceStatus) callconv(.C) *anyopaque = null; + +pub fn init(ns_module: windows.HMODULE, init_data: *NorthstarData) void { + create_interface = @ptrCast(windows.kernel32.GetProcAddress(ns_module, "CreateInterface")); + data.handle = init_data.*.handle; +} diff --git a/src/server.zig b/src/server.zig new file mode 100644 index 0000000..4dc6d42 --- /dev/null +++ b/src/server.zig @@ -0,0 +1,183 @@ +const std = @import("std"); + +const http = std.http; +const Server = http.Server; +const Thread = std.Thread; + +const max_header_size = 8192; + +pub var running = false; +var server_thread: ?Thread = null; + +var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 12 }){}; +var allocator = gpa.allocator(); + +const execute_command = @import("methods/execute_command.zig").method; +const list_methods = @import("methods/list_methods.zig").method; + +pub const RpcMethods = .{ + execute_command, + list_methods, +}; + +const JsonRpcRequest = struct { + jsonrpc: []const u8, + method: []const u8, + params: ?std.json.Value = null, + id: ?std.json.Value = null, +}; + +const JsonRpcError = struct { + code: isize, + message: []const u8, + data: ?std.json.Value = null, +}; + +const JsonRpcResponse = struct { + jsonrpc: []const u8, + result: ?std.json.Value = null, + @"error": ?JsonRpcError = null, + id: ?std.json.Value = null, +}; + +fn handleRequest(res: *Server.Response) !void { + std.log.info("{s} {s} {s}", .{ @tagName(res.request.method), @tagName(res.request.version), res.request.target }); + + if (!std.mem.startsWith(u8, res.request.target, "/rpc")) { + res.status = .not_found; + try res.do(); + return; + } + + const body = try res.reader().readAllAlloc(allocator, 8192); + defer allocator.free(body); + + const resp = blk: { + var response: JsonRpcResponse = .{ + .jsonrpc = "2.0", + }; + + const parsed = std.json.parseFromSlice(JsonRpcRequest, allocator, body, .{}) catch |err| { + std.log.err("Failed to parse request body {}", .{err}); + + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + + response.@"error" = .{ + .code = -32700, + .message = "Parse error", + }; + + break :blk response; + }; + defer parsed.deinit(); + + var request = parsed.value; + + if (request.id) |request_id| { + if (request_id != .integer and request_id != .string) { + request.id = null; + } + } + response.id = request.id; + + if (request.params) |request_params| { + if (request_params != .object and request_params != .array) { + response.@"error" = .{ + .code = -32602, + .message = "Invalid params", + }; + + break :blk response; + } + } + + inline for (RpcMethods) |method| { + if (std.mem.eql(u8, method.name, request.method)) { + response.result = method.func(allocator, request.params) catch |err| method_blk: { + response.@"error" = .{ + .code = -32603, + .message = @errorName(err), + }; + break :method_blk null; + }; + break; + } + } else { + response.@"error" = .{ + .code = -32601, + .message = "Method not found", + }; + } + + break :blk response; + }; + + const json_resp = try std.json.stringifyAlloc(allocator, resp, .{}); + res.transfer_encoding = .{ .content_length = json_resp.len }; + + try res.do(); + try res.writeAll(json_resp); + try res.finish(); +} + +fn runServer(srv: *Server) !void { + outer: while (running) { + var res = try srv.accept(.{ + .allocator = allocator, + .header_strategy = .{ .dynamic = max_header_size }, + }); + defer res.deinit(); + + while (res.reset() != .closing) { + res.wait() catch |err| switch (err) { + error.HttpHeadersInvalid => continue :outer, + error.EndOfStream => continue, + else => return err, + }; + + try handleRequest(&res); + } + } +} + +fn serverThread(addr: std.net.Address) !void { + var server = Server.init(allocator, .{ .reuse_address = true }); + + try server.listen(addr); + defer server.deinit(); + + defer _ = gpa.deinit(); + + runServer(&server) catch |err| { + std.log.err("server error: {}\n", .{err}); + + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + + _ = gpa.deinit(); + std.os.exit(1); + }; +} + +pub fn start() !void { + if (server_thread == null) { + var addr = try std.net.Address.parseIp("127.0.0.1", 26505); + + running = true; + server_thread = try Thread.spawn(.{}, serverThread, .{addr}); + + std.log.info("Started HTTP Server on {}", .{addr}); + } +} + +pub fn stop() void { + if (server_thread) |thread| { + running = false; + thread.join(); + + std.log.info("Stopped HTTP Server", .{}); + } +} diff --git a/src/squirrel.zig b/src/squirrel.zig new file mode 100644 index 0000000..dd369c7 --- /dev/null +++ b/src/squirrel.zig @@ -0,0 +1,29 @@ +const std = @import("std"); +const windows = std.os.windows; + +const Class = @import("class.zig").Class; + +pub const ScriptContext = enum(c_int) { + SC_SERVER, + SC_CLIENT, + SC_UI, +}; + +pub const SQObject = extern struct { + type: c_int, + structNumber: c_int, + value: *void, +}; + +pub const CSquirrelVM = Class(.{}, .{ + .unknown = .{ .type = void, .virtual = true }, + + .sqvm = .{ .type = void }, + .gap_10 = .{ .type = [8]u8 }, + .unkObj = .{ .type = SQObject }, + .gap_30 = .{ .type = [12]u8 }, + .context = .{ .type = ScriptContext }, + .gap_40 = .{ .type = [648]u8 }, + .formatString = .{ .type = *const fn (i64, [*]const u8, ...) callconv(.C) [*]u8 }, + .gap_2D0 = .{ .type = [24]u8 }, +}); diff --git a/src/sys.zig b/src/sys.zig new file mode 100644 index 0000000..3c0afec --- /dev/null +++ b/src/sys.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const windows = std.os.windows; + +const interface = @import("interface.zig"); +const northstar = @import("northstar.zig"); + +const Class = @import("class.zig").Class; + +const CSys = Class(.{}, .{ + .log = .{ .type = *const fn (*anyopaque, ?windows.HMODULE, LogLevel, [*:0]const u8) callconv(.C) void, .virtual = true }, + .unload = .{ .type = *const fn (*anyopaque, ?windows.HMODULE) callconv(.C) void, .virtual = true }, + .reload = .{ .type = *const fn (*anyopaque, ?windows.HMODULE) callconv(.C) void, .virtual = true }, +}); + +var sys: ?*CSys = null; + +pub const LogLevel = enum(c_int) { LOG_INFO, LOG_WARN, LOG_ERR }; + +pub fn init() void { + sys = @ptrCast(@alignCast(northstar.create_interface.?("NSSys001", null))); +} + +pub fn log( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + _ = scope; + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + + const log_level: LogLevel = switch (level) { + .err => LogLevel.LOG_ERR, + .warn => LogLevel.LOG_WARN, + .info => LogLevel.LOG_INFO, + .debug => LogLevel.LOG_INFO, + }; + + const msg = std.fmt.allocPrintZ(allocator, format, args) catch unreachable; + + if (sys) |s| { + s.*.vtable.log(s, northstar.data.handle, log_level, msg); + } +} |