Native mods
A native mod is a DLL, for what the engine cannot do yet at all, or for what a Lua mod can do but too slowly - reading text off the screen, say. It is a module, the way Garry's Mod binary modules are: a Lua mod says require("name") and calls its functions like any other. Lua is the only thing it sees. It never talks to the engine, so what a module can reach inside MCCP is what a Lua mod could already reach, plus its own code.
A mod is either Lua or native, never both: the module and the Lua mod that uses it are two mods, the Lua one listing the native one under dependencies. Native mods run on Windows (x64) for now.
filemod.txt
The source folder (or the repository's root) holds a mod.txt that says what it is and which version this is. The version is stated once, here, and is what the submission goes in as.
kind = native
version = 1.0.0
Without kind = native a folder is a Lua mod and is packed and published as one.
A GitHub repository does not need one. MCCP looks at the repository as soon as you have typed its address: if it has a mod.txt at the top, that is used and the panel says which version it states; if not, a box appears holding a mod.txt to edit, filled in to start from, and that is what the version is read from. A repository's own mod.txt always wins over a typed one.
exportMCCP_MODULE_OPEN() · MCCP_MODULE_CLOSE()
Everything a module needs is one header, mccp_module.h. Nothing to link: MCCP hands the module an interface of virtual functions (mccp::Lua, the LUA in every function), so it never touches Lua's own library and keeps working when MCCP updates its Lua.
MCCP_MODULE_OPEN runs on the first require of the module, not when MCCP starts: build what require returns - usually a table of functions - and return how many values you pushed. MCCP_MODULE_CLOSE is optional and runs once at exit, while Lua is still whole, so references you kept can be freed there. A function Lua can call is a LUA_FUNCTION: read its arguments, push its results, return how many.
Here is the whole way through: Lua passes a frame in, the module reads its pixels in place and answers with a string. Put an OCR library where the comment is and it is a text reader.
// pixels.cpp - built as pixels.dll, dropped in bin/
#include "mccp_module.h"
#include <cstdio>
#include <string>
// pixels.read(frame) -> a string about the frame
LUA_FUNCTION(Read) {
mccp::Frame f;
if (!LUA->ToFrame(1, &f)) return LUA->ArgError(1, "frame expected");
// f.data points at the frame itself: no copy was made. This is where
// an OCR library would read f.width x f.height pixels, f.pitch apart.
std::string text = std::to_string(f.width) + "x" + std::to_string(f.height) +
" " + f.format + ", first pixels:";
for (int i = 0; i < 8; ++i) text += " " + std::to_string(f.data[i]);
LUA->PushString(text.c_str(), text.size());
return 1; // one value back to Lua
}
MCCP_MODULE_OPEN() { // the first require("pixels")
LUA->CreateTable();
LUA->PushCFunction(Read);
LUA->SetField(-2, "read");
return 1; // what require gives back
}
Build it as a 64-bit DLL (MSVC or clang-cl) with the header on the include path. A DLL your module needs can sit beside it in bin/: its own folder is searched first.
functionrequire(name) → any
Opens bin/<name>.dll as a module and returns what its open returned (true if it returned nothing). The first call opens it; every later one, from any mod, returns the same value. The name is the DLL's file name without .dll, in any case.
-- addons/reader/lua/autorun/reader.lua
local pixels = require("pixels") -- bin/pixels.dll
hook.Add("OnFrameReceived", "reader", function(frame)
print(pixels.read(frame))
-- 1920x1080 SDL_PIXELFORMAT_NV12, first pixels: 16 16 16 17 16 16 16 16
end)
Only native modules can be required. Lua files can't: put them in lua/autorun. When there is nothing to open, the error says why - not in bin/, downloaded and waiting for the restart, switched off by the crash guard, not a module, or built for a newer MCCP.
interfacemccp::Lua
What LUA-> offers. It follows Lua's own C API, so its documentation answers the details.
| Top() · Pop(n) · Push(idx) | How many values are on the stack; remove n; push a copy of one. Indices work as in Lua: 1 is the first argument, -1 the top. |
| GetType(idx) · IsType(idx, type) · GetTypeName(type) | Lua's type numbers, as mccp::TNUMBER, mccp::TSTRING…; past the top is TNONE. |
| GetNumber · GetInteger · GetBool · GetString | Read without failing: the wrong type is 0 or nullptr. GetBool is Lua's truth (only nil and false are false). GetString(idx, &len) handles zero bytes; its pointer lives while the value stays on the stack. |
| CheckNumber · CheckInteger · CheckString | Read an argument that must be there: the wrong type fails the call with Lua's own wording (bad argument #2 to 'add' (number expected, got string)). Return right away after one fails; until then they give 0 or "", never a null pointer. |
| PushNil · PushBool · PushNumber · PushInteger · PushString(s, len?) · PushCFunction(fn) | Values for Lua. PushString with a length takes any bytes, zeros included. |
| CreateTable · GetField(idx, key) · SetField(idx, key) · GetTable(idx) · SetTable(idx) | Tables, metamethods included. SetField sets t[key] to the top value and pops it; GetTable and SetTable take the key (and value) from the top, as in Lua. |
| PushGlobals() | The mods' shared globals, the same _G every Lua mod sees. |
| Call(nargs, nresults) | Call Lua: the function, then its arguments, on top. Always protected: true and the results, or false and the error message. nresults -1 keeps them all. |
| ReferenceCreate() · ReferencePush(ref) · ReferenceFree(ref) | Keep a Lua value between calls (a callback, a table): create pops it and returns a number to push it back by. Free it when you are done. |
| ArgError(arg, msg) · Error(msg) | Fail the call with a Lua error, which the calling mod can pcall. They return 0 rather than jumping, so write return LUA->ArgError(1, "..."). |
| ToFrame(idx, &frame) · ToBuffer(idx, &buffer) | MCCP's own values, without a copy. A OnFrameReceived frame gives data, size, width, height, pitch and format (writing the bytes changes what is shown, as frame:setByte does); a buffer.new buffer gives data and size. false for anything else, and for a frame kept past its hook. |
| Log(text) | A line in MCCP's log, under the module's name. |
rulesThreads, errors, crashes
Only on the thread that called you. LUA is valid during the call it was handed to and nowhere else: never keep it, never use it from a thread of your own. A module function runs while a frame waits for it, so slow work belongs on your own thread, with a function Lua can poll for the result:
// Slow work on your own thread; Lua asks for the answer.
LUA_FUNCTION(Start) { /* copy the pixels you need, hand them to a worker */ return 0; }
LUA_FUNCTION(Poll) {
std::string done;
if (!takeResult(done)) return 0; // nothing yet: Lua gets nil
LUA->PushString(done.c_str(), done.size());
return 1;
}
Frames and buffers are valid only during the call that got them. Copy what a worker thread needs before you return.
Errors from ArgError, Error or a failed Check* end the call with a Lua error once your function returns - nothing jumps out of your code, so its destructors run. A C++ exception that leaves your function is caught and becomes a Lua error too.
Crashes - an access violation, a division by zero, a stack overflow - are caught as well, in every call into a module: open, its functions and close. The mod that called gets a Lua error (pixels.dll crashed (access violation)) and MCCP keeps running. The module is then switched off until MCCP restarts: after a crash its memory can't be trusted, so it is not called again, not even to close. A crash on a thread the module started itself can't be caught - nothing called it, so there is nowhere to return to - and takes MCCP down.
workflowSubmitting for review
You upload the source, never a DLL. In Upload a mod, pick the folder in addons/ (its .gitignore is honoured the way git would, so build output stays home) or a public GitHub repository (a private one is refused: add the files instead, or make it public). For a repository, the newest commit of the branch in the URL is what is reviewed, and its mod.txt is read at that commit (or typed into the panel, when it has none). A changelog is required: it is the reviewer's first read and the changelog entry once approved.
A reviewer reads the source and builds the DLL themselves. Prebuilt binaries in the source are fine to leave in for them, but expect to be asked where they came from, and to have them rebuilt and replaced. Nobody can download the mod until a build is approved, and you cannot publish it before that. You get an email when it is approved or declined; reviewers get one for every new submission.
A decline comes with a reason. Answer it from the mod's Edit panel: reply in words, reply pointing at the repository's newest commit (GitHub submissions), or submit new source. Once approved, Publish on the same panel makes it public. Submitting a new version starts a new review; until it is approved, the last approved build is what people download, and public stays public.
folderbin/
Native mods live in bin/ beside addons/, never in it. Downloads are checked against the hash of the reviewer's build before anything is kept, then wait as <name>.dll.new until the next start; a DLL in use is never replaced under itself. Uninstalling one that is running removes it at the next start.
Restart. A DLL loads at startup or not at all. A download that brings one restarts MCCP by itself, once, after everything it came with has arrived: the native mod first in the chain, the Lua mods that need it after it, then a single restart.
Crash guard. If MCCP goes down while loading a DLL, the next start leaves that DLL off (bin/<name>.disabled) and says so in the log and in My mods. A new version of it clears that, and so does deleting the file.
Testing your own. Put your build in bin/ by hand and start MCCP. It loads like any other, and the log notes it did not come from the site.