From 0df84ef8a999440f1f8d862fbaf1635c25527cb8 Mon Sep 17 00:00:00 2001 From: IonutParau Date: Mon, 19 May 2025 19:25:46 +0200 Subject: [PATCH] initial --- .gitignore | 2 + README.md | 21 ++++ build.zig | 79 +++++++++++++++ build.zig.zon | 72 +++++++++++++ src/engine.zig | 6 ++ src/engine/component.zig | 74 ++++++++++++++ src/engine/computer.zig | 212 +++++++++++++++++++++++++++++++++++++++ src/engine/universe.zig | 18 ++++ src/main.zig | 11 ++ 9 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/engine.zig create mode 100644 src/engine/component.zig create mode 100644 src/engine/computer.zig create mode 100644 src/engine/universe.zig create mode 100644 src/main.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3389c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/ +zig-out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e4e744 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# NeoNucleus + +The core of NeoComputers. + +It provides: +- the computer model and state implementation +- architecture system +- (NOT NOW) basic component implementations +- (NOT NOW) standard emulator +- (NOT NOW) some extra components + +The library does not provide: +- The sandbox (equivalent to OpenComputer's `machine.lua`) +- Default architectures +- Default host interop (as in, the vtables that control the basic component's internals, such as the filesystem implementation) + +The emulator *will* (as its gonna be made after the engine is functional) provide: +- A simple Lua sandbox +- Very simple workspaces +- Ocelot components for debug +- Headless mode (single computer, uses actual terminal for a teletypewriter). diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..9144697 --- /dev/null +++ b/build.zig @@ -0,0 +1,79 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const engineStatic = b.addStaticLibrary(.{ + .name = "neonucleus", + .root_source_file = b.path("src/engine.zig"), + .target = target, + .optimize = optimize, + }); + + const install = b.getInstallStep(); + + b.installArtifact(engineStatic); + + const engineShared = b.addSharedLibrary(.{ + .name = "neonucleus", + .root_source_file = b.path("src/engine.zig"), + .target = target, + .optimize = optimize, + }); + + b.installArtifact(engineShared); + + const engineStep = b.step("engine", "Builds the engine as a static library"); + engineStep.dependOn(&engineStatic.step); + engineStep.dependOn(install); + + const sharedStep = b.step("shared", "Builds the engine as a shared library"); + sharedStep.dependOn(&engineShared.step); + sharedStep.dependOn(install); + + const emulator = b.addExecutable(.{ + .name = "neunucleus", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // forces us to link in everything too + emulator.linkLibrary(engineStatic); + + b.installArtifact(emulator); + b.step("emulator", "Builds the emulator").dependOn(&emulator.step); + + const run_cmd = b.addRunArtifact(emulator); + + run_cmd.step.dependOn(install); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the emulator"); + run_step.dependOn(&run_cmd.step); + + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/engine.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..9b9d954 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "oc_reimagined", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/engine.zig b/src/engine.zig new file mode 100644 index 0000000..9863511 --- /dev/null +++ b/src/engine.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const testing = std.testing; + +pub const Computer = @import("engine/computer.zig"); +pub const Component = @import("engine/component.zig"); +pub const Universe = @import("engine/universe.zig"); diff --git a/src/engine/component.zig b/src/engine/component.zig new file mode 100644 index 0000000..a3e7f2e --- /dev/null +++ b/src/engine/component.zig @@ -0,0 +1,74 @@ +address: []const u8, +slot: isize, +allocator: Allocator, +userdata: *anyopaque, +vtable: *const VTable, +computer: *const Computer, + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Component = @This(); +const Computer = @import("computer.zig"); + +pub const Method = struct { + callback: Callback, + userdata: *anyopaque, + doc: []const u8, + direct: bool, + + pub const Callback = *const fn(componentUser: *anyopaque, callbackUser: *anyopaque, computer: *const Computer) callconv(.C) c_int; +}; + +// not an extern struct because of how C API will work +pub const VTable = struct { + componentType: []const u8, + methods: std.StringHashMap(Method), + resetBudget: ?*const fn(userdata: *anyopaque) callconv(.C) void, + passive: ?*const fn(userdata: *anyopaque) callconv(.C) void, + teardown: ?*const fn(userdata: *anyopaque) callconv(.C) void, + + pub fn init(ctype: []const u8, allocator: Allocator) VTable { + return VTable { + .componentType = ctype, + .methods = std.StringHashMap(Method).init(allocator), + .resetBudget = null, + .teardown = null, + }; + } +}; + +pub fn init(allocator: Allocator, address: []const u8, slot: isize, vtable: *const VTable, userdata: *anyopaque) !Component { + const ourAddr = try allocator.dupe(u8, address); + errdefer allocator.free(ourAddr); + + return Component { + .address = ourAddr, + .slot = slot, + .allocator = allocator, + .userdata = userdata, + .vtable = vtable, + }; +} + +pub fn resetBudget(self: *const Component) void { + self.vtable.resetBudget(self.userdata); +} + +pub fn passive(self: *const Component) void { + self.vtable.passive(self.userdata); +} + +pub fn invoke(self: *const Component, method: []const u8) c_int { + const res = self.vtable.methods.get(method); + if(res) |f| { + return f.callback(self.userdata, f.userdata, self.computer); + } + + // no such method + return 0; +} + +pub fn deinit(self: Component) void { + self.vtable.teardown(self.userdata); + self.allocator.free(self.address); +} diff --git a/src/engine/computer.zig b/src/engine/computer.zig new file mode 100644 index 0000000..fefc14a --- /dev/null +++ b/src/engine/computer.zig @@ -0,0 +1,212 @@ +address: []const u8, +time: f64, +allocator: Allocator, +components: std.StringHashMap(Component), +userdata: *anyopaque, +universe: *Universe, +stack: std.ArrayList(Value), +userError: Error, +isUserErrorAllocated: bool, +architectureData: *anyopaque, +architecture: Universe.Architecture, +architectures: std.ArrayList(Universe.Architecture), +state: State, + +pub fn init(address: []const u8, allocator: Allocator, userdata: *anyopaque, architecture: Universe.Architecture, universe: *Universe) Computer { + const ourAddr = try allocator.dupe(u8, address); + errdefer allocator.free(ourAddr); + + var c = Computer { + .address = ourAddr, + .time = 0, + .allocator = allocator, + .components = std.StringHashMap(Component).init(allocator), + .userdata = userdata, + .universe = universe, + .args = std.ArrayList(Value).init(allocator), + .ret = std.ArrayList(Value).init(allocator), + .userError = Error {.none = 0}, + .isUserErrorAllocated = false, + .architectureData = undefined, + .architecture = architecture, + .architectures = std.ArrayList(Universe.Architecture).init(allocator), + .state = State.running, + }; + c.architectureData = architecture.setup(architecture.udata, &c); + return c; +} + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Computer = @This(); +const Component = @import("component.zig"); +const Universe = @import("universe.zig"); + +pub const Error = union(enum) { + none, + raw: [*c]const u8, + allocated: [*c]const u8, +}; + +pub const State = enum { + running, + closing, + rebooting, + blackout, + overworked, +}; + +pub const Value = union(enum) { + nil, + integer: i64, + number: f64, + cstring: [*c]const u8, + string: []const u8, + + pub fn toInteger(self: Value) i64 { + return switch(self) { + .integer => |i| i, + .number => |f| @intFromFloat(f), + else => 0, + }; + } + + pub fn toNumber(self: Value) f64 { + return switch(self) { + .integer => |i| @floatFromInt(i), + .number => |f| f, + else => 0, + }; + } + + pub fn toString(self: Value) ?[]const u8 { + return switch(self) { + .string => |s| s, + .cstring => |c| std.mem.span(c), + else => null, + }; + } + + pub fn toCString(self: Value) ?[*c]const u8 { + // normal strings are NOT CAST because it could be a safety violation + return switch(self) { + .cstring => |c| c, + else => null, + }; + } + + pub fn initNil() Value { + return Value {.nil = .{}}; + } + + pub fn initInteger(i: i64) Value { + return Value {.integer = i}; + } + + pub fn initNumber(f: f64) Value { + return Value {.number = f}; + } + + pub fn initCString(cstr: [*c]const u8, allocator: Allocator) !Value { + const span = std.mem.span(cstr); + const mem = try allocator.dupeZ(u8, span); + return Value {.cstring = mem}; + } + + pub fn initString(str: []const u8, allocator: Allocator) !Value { + const mem = try allocator.dupe(u8, str); + return Value {.string = mem}; + } + + pub fn deinit(self: Value, allocator: Allocator) void { + switch(self) { + .string => |s| allocator.free(s), + .cstring => |c| allocator.free(c), + else => {}, + } + } +}; + +// Error handling + +pub fn clearError(self: *Computer) void { + switch(self.userError) { + .allocated => |c| { + self.allocator.free(c); + }, + else => {}, + } + + self.userError = Error {.none = .{}}; +} + +pub fn setCError(self: *Computer, err: [*c]const u8) void { + self.clearError(); + self.userError = Error {.raw = err}; +} + +pub fn setError(self: *Computer, err: [*c]const u8) void { + self.clearError(); + const maybeBuf = self.allocator.dupeZ(u8, std.mem.span(err)); + if(maybeBuf) |buf| { + self.userError = Error {.allocated = buf}; + } else |e| { + _ = e; + self.setCError("out of memory"); + } +} + +pub fn getError(self: *const Computer) ?[*c]const u8 { + return switch(self.userError) { + .none => null, + .raw => |c| c, + .allocated => |c| c, + }; +} + +// Component functions + +pub fn invoke(self: *Computer, address: []const u8, method: []const u8) void { + self.setError(null); + if(self.components.get(address)) |c| { + c.invoke(method); + return; + } + self.setError("no such component"); +} + +// end of epilogue, just deletes everything +pub fn resetCall(self: *Computer) void { + for(self.stack.items) |element| { + element.deinit(self.allocator); + } + + // retain capacity for speed + self.stack.clearRetainingCapacity(); +} + +pub fn deinit(self: Computer) void { + // just to be safe + defer self.components.deinit(); + defer self.stack.deinit(); + defer self.architectures.deinit(); + + // absolutely destroy everything + // burn it all to the ground + // leave no evidence behind + self.resetCall(); + + self.architecture.demolish(self.architecture.udata, self.architectureData); + + self.clearError(); + + var compIter = self.components.iterator(); + while(compIter.next()) |entry| { + entry.value_ptr.deinit(); + } +} + +pub fn process(self: *Computer) void { + self.clearError(); + self.architecture.tick(self.architecture.udata, self.architectureData, self); +} diff --git a/src/engine/universe.zig b/src/engine/universe.zig new file mode 100644 index 0000000..7b5b332 --- /dev/null +++ b/src/engine/universe.zig @@ -0,0 +1,18 @@ +allocator: Allocator, +components: std.StringHashMap(Computer), +host: Host, + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Computer = @import("computer.zig"); + +pub const Architecture = struct { + name: [*c]const u8, + udata: *anyopaque, + setup: *const fn(udata: *anyopaque, computer: *Computer) callconv(.C) *anyopaque, + tick: *const fn(udata: *anyopaque, context: *anyopaque, computer: *Computer) callconv(.C) void, + demolish: *const fn(udata: *anyopaque, context: *anyopaque) callconv(.C) void, +}; + +pub const Host = struct { +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..9c24ef2 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +pub fn main() !void { + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Emulator is not even close to working.\n", .{}); + + try bw.flush(); // don't forget to flush! +}