aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan200101 <sentrycraft123@gmail.com>2024-05-05 16:51:48 +0200
committerJan200101 <sentrycraft123@gmail.com>2024-05-05 16:51:48 +0200
commit54365b02ef3e36a631354aa1af3bd33f90db3cf7 (patch)
treeb7f02a04c28437a1c44c427c440fe3f0750bed04
downloadSouthRPC-54365b02ef3e36a631354aa1af3bd33f90db3cf7.tar.gz
SouthRPC-54365b02ef3e36a631354aa1af3bd33f90db3cf7.zip
Zig rework
-rw-r--r--.gitignore4
-rw-r--r--LICENSE22
-rw-r--r--README.md5
-rw-r--r--build.zig35
-rw-r--r--src/class.zig300
-rw-r--r--src/engine.zig50
-rw-r--r--src/interface.zig34
-rw-r--r--src/interfaces/PluginCallbacks001.zig105
-rw-r--r--src/interfaces/PluginId001.zig65
-rw-r--r--src/main.zig16
-rw-r--r--src/methods/execute_command.zig53
-rw-r--r--src/methods/execute_squirrel.zig42
-rw-r--r--src/methods/list_methods.zig18
-rw-r--r--src/northstar.zig19
-rw-r--r--src/server.zig183
-rw-r--r--src/squirrel.zig29
-rw-r--r--src/sys.zig48
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
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..207309d
--- /dev/null
+++ b/LICENSE
@@ -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);
+ }
+}