From e06d6eec3743874d4d0146a8e401700b53877045 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sat, 27 Sep 2025 21:18:05 +0100 Subject: [PATCH] [Refactor] Implement scriptable Lua API Large architecture refactor of the scripting system. Previous implementation required Lua scripts to return "action tables" which were interpreted by a large and not very flexible at all if-else ladder in C++. While fine for the initial implementation, it's not scalable, and it makes it impossible for players to write their own meaningful tools. --- assets/scripts/bin/cat.lua | 1 + assets/scripts/bin/cd.lua | 2 +- assets/scripts/bin/echo.lua | 2 +- assets/scripts/bin/exit.lua | 4 +- assets/scripts/bin/ls.lua | 2 +- assets/scripts/bin/nmap.lua | 2 +- assets/scripts/bin/rm.lua | 2 +- assets/scripts/bin/ssh.lua | 2 +- common/src/command_processor.cpp | 202 ++++++++++--------------------- common/src/command_processor.h | 11 +- common/src/lua_api.cpp | 102 ++++++++++++++++ common/src/lua_api.h | 25 ++++ common/src/lua_processor.cpp | 27 ++++- common/src/lua_processor.h | 4 +- 14 files changed, 235 insertions(+), 153 deletions(-) create mode 100644 common/src/lua_api.cpp create mode 100644 common/src/lua_api.h diff --git a/assets/scripts/bin/cat.lua b/assets/scripts/bin/cat.lua index befe959..9a760d8 100644 --- a/assets/scripts/bin/cat.lua +++ b/assets/scripts/bin/cat.lua @@ -4,6 +4,7 @@ if not filename then return "" -- No arguments, return nothing. end +local current_dir = bettola.get_current_dir(context) local target_node = current_dir.children[filename] if not target_node then diff --git a/assets/scripts/bin/cd.lua b/assets/scripts/bin/cd.lua index fb47f56..4bb4fe1 100644 --- a/assets/scripts/bin/cd.lua +++ b/assets/scripts/bin/cd.lua @@ -4,4 +4,4 @@ if not target then return "" -- No argument, just return to prompt. end -return { action = "cd", target = target } +return bettola.cd(context, target) diff --git a/assets/scripts/bin/echo.lua b/assets/scripts/bin/echo.lua index 9c5127d..971ea15 100644 --- a/assets/scripts/bin/echo.lua +++ b/assets/scripts/bin/echo.lua @@ -18,7 +18,7 @@ if found_redirect then return "echo: syntax error: expected filename after '>'" end local content = table.concat(content_parts, " ") - return { action = "write_file", target = filename, content = content } + return bettola.write_file(context, filename, content) else return table.concat(arg, " ") end diff --git a/assets/scripts/bin/exit.lua b/assets/scripts/bin/exit.lua index 888bfd1..cb17fa3 100644 --- a/assets/scripts/bin/exit.lua +++ b/assets/scripts/bin/exit.lua @@ -1,6 +1,6 @@ -- /bin/exit - Disconnects from a remote session or cloes terminal window. if is_remote_session then - return { action = "disconnect" } + return bettola.disconnect(context) else - return { action = "close_terminal" } + return bettola.close_terminal(context) end diff --git a/assets/scripts/bin/ls.lua b/assets/scripts/bin/ls.lua index 7cc330d..063a90b 100644 --- a/assets/scripts/bin/ls.lua +++ b/assets/scripts/bin/ls.lua @@ -1,5 +1,5 @@ -- /bin/ls - Lists files in a directory. -local dir = current_dir -- Get directory object from C++. +local dir = bettola.get_current_dir(context) local output = "" -- Iterate over the 'children' map exposed via C++. diff --git a/assets/scripts/bin/nmap.lua b/assets/scripts/bin/nmap.lua index 55220ef..3974202 100644 --- a/assets/scripts/bin/nmap.lua +++ b/assets/scripts/bin/nmap.lua @@ -6,4 +6,4 @@ if not target_ip then end -- TODO: Add args such as -sV for version detection etc. -return { action = "scan", target = target_ip } +return bettola.nmap(context, target_ip) diff --git a/assets/scripts/bin/rm.lua b/assets/scripts/bin/rm.lua index e38e071..475865f 100644 --- a/assets/scripts/bin/rm.lua +++ b/assets/scripts/bin/rm.lua @@ -1,3 +1,3 @@ local file_to_remove = arg[1] if not file_to_remove then return "rm: missing operand" end -return { action = "rm", target = file_to_remove } +return bettola.rm(context, file_to_remove) diff --git a/assets/scripts/bin/ssh.lua b/assets/scripts/bin/ssh.lua index 1ccf3aa..7204b56 100644 --- a/assets/scripts/bin/ssh.lua +++ b/assets/scripts/bin/ssh.lua @@ -1,4 +1,4 @@ -- /bin/ssh - Connects to a remote host. local target = arg[1] if not target then return "ssh: missing operand" end -return { action = "ssh", target = target } +return bettola.ssh(context, target) diff --git a/common/src/command_processor.cpp b/common/src/command_processor.cpp index 2bde667..5ec6225 100644 --- a/common/src/command_processor.cpp +++ b/common/src/command_processor.cpp @@ -24,6 +24,25 @@ vfs_node* CommandProcessor::get_current_dir(void) { return _current_dir; } +Machine* CommandProcessor::get_home_machine(void) { return _home_machine; } +Machine* CommandProcessor::get_session_machine(void) { return _session_machine; } + +std::map& +CommandProcessor::get_world_machines(void) { + return _world_machines; +} + +void CommandProcessor::set_current_dir(vfs_node* node) { + _current_dir = node; +} + +void CommandProcessor::set_session_machine(Machine* machine) { + _session_machine = machine; + if(_session_machine) { + _current_dir = _session_machine->vfs_root; + } +} + std::string CommandProcessor::process_command(const std::string& command) { /* Trim trailing whitespace. */ std::string cmd = command; @@ -48,13 +67,9 @@ std::string CommandProcessor::process_command(const std::string& command) { if(root->children.count("bin") && root->children["bin"]->children.count(script_filename)) { vfs_node* script_node = root->children["bin"]->children[script_filename]; bool is_remote = (_session_machine != _home_machine); - sol::object result = _lua->execute(script_node->content, _current_dir, args, is_remote); - if(result.is()) { - return result.as(); - } else if(result.is()) { - return _handle_vfs_action(result.as()); - } - return "[Script returned an unexpected type]"; + sol::object result = _lua->execute(script_node->content, *this, args, is_remote); + return result.is() ? result.as() : "[Script returned an unexpected type]"; + } return "Unknown command: " + command_name + "\n"; } @@ -83,145 +98,54 @@ vfs_node* find_node_by_path(vfs_node* root, const std::string& path) { return current; } -std::string CommandProcessor::_handle_vfs_action(sol::table action) { - std::string action_name = action["action"].get_or(""); - /* Make the CoW check universal for any write operation. */ - if(action_name == "rm" || action_name == "write_file") { - if(!_session_machine->is_vfs_a_copy) { - /* VFS is shared, a copy is required. */ - if(_session_machine == _home_machine) { - /* We are modifying our own home machine. */ - fprintf(stderr, "CoW: Write attempt on player's home machine" - "Creating persistant copy.\n"); +void CommandProcessor::ensure_vfs_is_writable(void) { + if(!_session_machine->is_vfs_a_copy) { + /* VFS shared, copy required. */ + if(_session_machine == _home_machine) { + /* We are modifying our own home machine. */ + fprintf(stderr, "CoW: Write attempt on player's home machine." + "Creating persistent copy.\n"); + std::string original_path = get_full_path(_current_dir); + + vfs_node* new_vfs_root = copy_vfs_node(_home_machine->vfs_root, nullptr); + auto* new_machine = new Machine(_home_machine->id, _home_machine->hostname); + new_machine->vfs_root = new_vfs_root; + new_machine->services = _home_machine->services; + new_machine->is_vfs_a_copy = true; + + _home_machine = new_machine; + set_session_machine(new_machine); + _current_dir = find_node_by_path(_session_machine->vfs_root, original_path); + if(!_current_dir) { _current_dir = _session_machine->vfs_root; } + } else { + /* we are modifying a remote NPC machine. */ + std::string remote_ip = ""; + for(auto const& [ip, machine] : _world_machines) { + if(machine == _session_machine) { + remote_ip = ip; + break; + } + } + if(!remote_ip.empty()) { + fprintf(stderr, "CoW: Write attempt on remote machine '%s'." + "Creating persistent copy.\n", + remote_ip.c_str()); + std::string original_path = get_full_path(_current_dir); - vfs_node* new_vfs_root = copy_vfs_node(_home_machine->vfs_root, nullptr); - auto* new_machine = new Machine(_home_machine->id, _home_machine->hostname); - new_machine->vfs_root = new_vfs_root; - new_machine->services = _home_machine->services; - new_machine->is_vfs_a_copy = true; /* Mark as copy. */ + vfs_node* new_vfs_root = copy_vfs_node(_session_machine->vfs_root, nullptr); + auto* new_machine = new Machine(_session_machine->id, _session_machine->hostname); + new_machine->vfs_root = new_vfs_root; + new_machine->services = _session_machine->services; + new_machine->is_vfs_a_copy = true; + + _world_machines[remote_ip] = new_machine; + set_session_machine(new_machine); - _home_machine = new_machine; - _session_machine = new_machine; _current_dir = find_node_by_path(_session_machine->vfs_root, original_path); if(!_current_dir) { _current_dir = _session_machine->vfs_root; } - } else { - std::string remote_ip = ""; - /* Check if we are on a known remote system. */ - for(auto const& [ip, machine] : _world_machines) { - if(machine == _session_machine) { - remote_ip = ip; - break; - } - } - if(!remote_ip.empty()) { - fprintf(stderr, "CoW: Write attempt on remote machine '%s'. " - "Creating persistant copy.\n", - remote_ip.c_str()); - std::string original_path = get_full_path(_current_dir); - vfs_node* new_vfs_root = copy_vfs_node(_session_machine->vfs_root, nullptr); - auto* new_machine = new Machine(_session_machine->id, _session_machine->hostname); - new_machine->vfs_root = new_vfs_root; - new_machine->services = _session_machine->services; - new_machine->is_vfs_a_copy = true; /* Mark as copy. */ - - _world_machines[remote_ip] = new_machine; - _session_machine = new_machine; - - _current_dir = find_node_by_path(_session_machine->vfs_root, original_path); - if(!_current_dir) { _current_dir = _session_machine->vfs_root; } - } else { - return "Permission denied: Filesystem is read-only and not part of the world."; - } } } } - if(action_name == "rm") { - std::string path = action["target"].get_or(""); - if(path.empty()) return "rm: missing operand"; - - if(!_current_dir->children.count(path)) { - return std::string("rm: cannot remove '") + path + "': No such file or directory."; - } - vfs_node* target_node = _current_dir->children[path]; - if(target_node->type == DIR_NODE) { - return std::string("rm: cannot remove '") + path + "': Is a directory."; - } - _current_dir->children.erase(path); - delete target_node; - return ""; /* Success. */ - } else if(action_name == "write_file") { - std::string filename = action["target"].get_or(""); - std::string content = action["content"].get_or(""); - if(filename.empty()) { - return "write_file: missing filename"; - } - - if(_current_dir->children.count(filename)) { - vfs_node* target_node = _current_dir->children[filename]; - if(target_node->type == DIR_NODE) { - return std::string("cannot write to '") + filename + "': Is a directory."; - } - target_node->content = content; - } else { - vfs_node* new_file = new vfs_node { - .name = filename, .type = FILE_NODE, .content = content, - .parent = _current_dir - }; - _current_dir->children[filename] = new_file; - } - return ""; /* Success. */ - } else if(action_name == "ssh") { - std::string target_ip = action["target"].get_or(""); - if(_world_machines.count(target_ip)) { - _session_machine = _world_machines[target_ip]; - _current_dir = _session_machine->vfs_root; - return "Connected to " + target_ip; - } - return "ssh: Could not resolve hostname " + target_ip + ": Name or service not known"; - } else if(action_name == "cd") { - std::string target_dir_name = action["target"].get_or(""); - if(target_dir_name == "..") { - if(_current_dir->parent) { - _current_dir = _current_dir->parent; - } - } else if(_current_dir->children.count(target_dir_name)) { - vfs_node* target_node = _current_dir->children[target_dir_name]; - if(target_node->type == DIR_NODE) { - _current_dir = target_node; - } else { - return std::string("cd: not a directory: ") + target_dir_name; - } - } else { - return std::string("cd: no such file or directory: ") + target_dir_name; - } - return ""; /* Success. */ - } else if(action_name == "disconnect") { - _session_machine = _home_machine; - _current_dir = _home_machine->vfs_root; - return "Connection closed."; - } else if(action_name == "close_terminal") { - return "__CLOSE_CONNECTION__"; - } else if(action_name == "scan") { - std::string target_ip = action["target"].get_or(""); - if(!_world_machines.count(target_ip)) { - return std::string("nmap: Could not resolve host: ") + target_ip; - } - - Machine* target_machine = _world_machines[target_ip]; - if(target_machine->services.empty()) { - return std::string("No open ports for ") + target_ip; - } - - std::stringstream ss; - ss<<"Host: "<services) { - ss<& get_world_machines(void); + + void set_current_dir(vfs_node* node); + void set_session_machine(Machine* machine); + + void ensure_vfs_is_writable(void); private: - std::string _handle_vfs_action(sol::table action); Machine* _home_machine; Machine* _session_machine; vfs_node* _current_dir; diff --git a/common/src/lua_api.cpp b/common/src/lua_api.cpp new file mode 100644 index 0000000..525cd08 --- /dev/null +++ b/common/src/lua_api.cpp @@ -0,0 +1,102 @@ +#include + +#include "lua_api.h" +#include "command_processor.h" +#include "machine.h" +#include "vfs.h" + +namespace api { + +vfs_node* get_current_dir(CommandProcessor& context) { + return context.get_current_dir(); +} + +std::string rm(CommandProcessor& context, const std::string& filename) { + context.ensure_vfs_is_writable(); + vfs_node* current_dir = context.get_current_dir(); + if(!current_dir->children.count(filename)) { + return "rm: cannot remove '" + filename + "': No such file or directory."; + } + vfs_node* target_node = current_dir->children[filename]; + if(target_node->type == DIR_NODE) { + return "rm: cannot remove '" + filename + "': Is a directory."; + } + current_dir->children.erase(filename); + delete target_node; + return ""; +} + +std::string write_file(CommandProcessor& context, const std::string& filename, + const std::string& content) { + context.ensure_vfs_is_writable(); + vfs_node* current_dir = context.get_current_dir(); + if(current_dir->children.count(filename)) { + vfs_node* target_node = current_dir->children[filename]; + if(target_node->type == DIR_NODE) { + return "cannot write to '" + filename + "': Is a directory."; + } + target_node->content = content; + } else { + vfs_node* new_file = new vfs_node { + .name = filename, .type = FILE_NODE, .content = content, .parent = current_dir}; + current_dir->children[filename] = new_file; + } + return ""; +} + +std::string cd(CommandProcessor& context, const std::string& path) { + vfs_node* current_dir = context.get_current_dir(); + if(path == "..") { + if(current_dir->parent) { + context.set_current_dir(current_dir->parent); + } + } else if(current_dir->children.count(path)) { + vfs_node* target_node = current_dir->children[path]; + if(target_node->type == DIR_NODE) { + context.set_current_dir(target_node); + } else { + return "cd: not a directory: " + path; + } + } else { + return "cd: no such file or directory: " + path; + } + return ""; +} + +std::string ssh(CommandProcessor& context, const std::string& ip) { + if(context.get_world_machines().count(ip)) { + context.set_session_machine(context.get_world_machines().at(ip)); + return "Connected to " + ip; + } + return "ssh: Could not resolve hostname " + ip + ": Name or service not found"; +} + +std::string nmap(CommandProcessor& context, const std::string& ip) { + if(!context.get_world_machines().count(ip)) { + return "nmap: Could not resolve host: " + ip; + } + + Machine* target_machine = context.get_world_machines().at(ip); + if(target_machine->services.empty()) { + return "No open ports for " + ip; + } + + std::stringstream ss; + ss << "Host: " << ip << "\n"; + ss << "PORT\tSTATE\tSERVICE\n"; + for(auto const& [port, service_name] : target_machine->services) { + ss << port << "/tcp\t" << "open\t" << service_name << "\n"; + } + return ss.str(); +} + +std::string disconnect(CommandProcessor& context) { + context.set_session_machine(context.get_home_machine()); + return "Connection closed."; +} + +std::string close_terminal(CommandProcessor& context) { + return "__CLOSE_CONNECTION__"; +} + +} /* namespace api */ diff --git a/common/src/lua_api.h b/common/src/lua_api.h new file mode 100644 index 0000000..4c7b038 --- /dev/null +++ b/common/src/lua_api.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "vfs.h" + +class CommandProcessor; + +namespace api { + +/* FILESYSTEM ACTIONS. */ +vfs_node* get_current_dir(CommandProcessor& context); +std::string rm(CommandProcessor& context, const std::string& filename); +std::string write_file(CommandProcessor& context, const std::string& filename, + const std::string& content); +std::string cd(CommandProcessor& context, const std::string& path); + +/* NETWORK ACTIONS. */ +std::string ssh(CommandProcessor& context, const std::string& ip); +std::string nmap(CommandProcessor& context, const std::string& ip); +std::string disconnect(CommandProcessor& context); + +/* SYSTEM ACTIONS. */ +std::string close_terminal(CommandProcessor& context); + +} /* namespace api */ diff --git a/common/src/lua_processor.cpp b/common/src/lua_processor.cpp index da056d5..2bd05ea 100644 --- a/common/src/lua_processor.cpp +++ b/common/src/lua_processor.cpp @@ -1,8 +1,12 @@ -#include "lua_processor.h" #include #include #include +#include + +#include "lua_processor.h" #include "vfs.h" +#include "lua_api.h" +#include "command_processor.h" LuaProcessor::LuaProcessor(void) { _lua.open_libraries(sol::lib::base, sol::lib::string, sol::lib::io, sol::lib::table); @@ -12,16 +16,31 @@ LuaProcessor::LuaProcessor(void) { "name", &vfs_node::name, "type", &vfs_node::type, "children", &vfs_node::children, - "content", &vfs_node::content); } + "content", &vfs_node::content); + + /* Expose CommandProcessor to Lua. DON'T ALLOW SCRIPTS TO CREATE IT THOUGH! */ + _lua.new_usertype("CommandProcessor", sol::no_constructor); + + /* Create the 'bettola' API table. */ + sol::table bettola_api = _lua.create_named_table("bettola"); + bettola_api["rm"] = &api::rm; + bettola_api["write_file"] = &api::write_file; + bettola_api["get_current_dir"]= &api::get_current_dir; + bettola_api["cd"] = &api::cd; + bettola_api["ssh"] = &api::ssh; + bettola_api["nmap"] = &api::nmap; + bettola_api["disconnect"] = &api::disconnect; + bettola_api["close_terminal"] = &api::close_terminal; +} LuaProcessor::~LuaProcessor(void) {} -sol::object LuaProcessor::execute(const std::string& script, vfs_node* current_dir, +sol::object LuaProcessor::execute(const std::string& script, CommandProcessor& context, const std::vector& args, bool is_remote) { try { /* Pass C++ objects/points into the Lua env. */ _lua["is_remote_session"] = is_remote; - _lua["current_dir"] = current_dir; + _lua["context"] = &context; /* Create and populate the 'arg' table for the script. */ sol::table arg_table = _lua.create_table(); diff --git a/common/src/lua_processor.h b/common/src/lua_processor.h index b1b1bac..21d6fae 100644 --- a/common/src/lua_processor.h +++ b/common/src/lua_processor.h @@ -5,13 +5,15 @@ #include #include +class CommandProcessor; + class LuaProcessor { public: LuaProcessor(void); ~LuaProcessor(void); /* Executes a string of lua code and returns result as a string. */ - sol::object execute(const std::string& script, vfs_node* current_dir, + sol::object execute(const std::string& script, CommandProcessor& context, const std::vector& args, bool is_remote); private: sol::state _lua;