nightcoop_sdk.lua
cet/nightcoop_sdk.lua · 925 lines · 37.4 KB · Download
Lua helper for Cyber Engine Tweaks mods: registration, polling in onUpdate.
The SDK and its documentation are in English.
-- SPDX-License-Identifier: MIT — Copyright (c) 2026 NightCoop (see sdk/LICENSE)
--[[
nightcoop_sdk.lua -- helper for Cyber Engine Tweaks mods that use the NightCoop SDK (version 1).
Copy this file into your mod folder next to init.lua (bin/x64/plugins/cyber_engine_tweaks/mods/<YourMod>/) and load it:
local Sdk = require("nightcoop_sdk")
local mod = Sdk.new("yourname.yourmod", "Your Mod", "1.0.0", 1, Sdk.FLAG.NEEDED_BY_ALL)
registerForEvent("onInit", function() mod:register() end)
registerForEvent("onUpdate", function(dt)
mod:poll(function(ev)
if ev.kind == Sdk.EVENT.STATE and ev.key == "count" then
print("count is now " .. ev.data)
end
end)
end)
If NightCoop is not installed, or the player switched the SDK off ("sdk" switch), every function here does nothing
and returns a neutral value (see each function); your mod keeps working as a single-player mod.
How events reach you: Lua cannot be a Listen target, so the mod polls in onUpdate. poll() takes the next queued item of
your mod with NightCoopSdk.PollEvent(id) when redscript is installed (the event class is scripted), otherwise with
NightCoopSdk.PollEventText(id), which works without redscript. Either way the handler gets the same table:
ev.kind Sdk.EVENT.*
ev.peer Int: who it is about (session event), the sender (message), who set it (state), the owner (entity state)
ev.a Int: session event (Sdk.SESSION.*), channel (message), entity-state key, 1/0 present (shared spawn),
1/0 we lead it now (spawn leader; ev.peer — the new leader)
ev.key String: shared-state key (state events); "" otherwise
ev.removed Bool: the state key / entity-state key was removed or cleared
ev.data String: the payload as raw bytes ("" if none or removed)
ev.entityHash String: decimal EntityID hash of OUR mirror/copy (entity-state and shared-spawn events); "0" if none
ev.entity EntityID userdata, only when the event came as an object (redscript installed); else nil.
Without it, use Sdk.entityOfRef(ev.ref).
ev.ref Uint64 (cdata) NCSDK_PACK_REF value: kind 8 | owner 24 | id 32 (see Sdk.splitRef)
ev.source "object" or "text"
ev.request Int: the request number of NPC_REQUEST / NPC_RESULT events
ev.effect Int: NPC_RESULT — Sdk.NPC.* of the effect, 0 — a general request
ev.object the NightCoopSdkEvent itself (source "object" only; pose fields of kind 5 are there)
Not covered by the text path: the pose fields of partner-pose events (kind 5). Use the object path for those.
]]
local Sdk = {}
Sdk.__index = Sdk
-- ------------------------------------------------------------------------------------------------ constants
Sdk.PEER_HOST = 0
Sdk.PEER_NONE = -1
Sdk.PEER_ALL = -2 -- send(): every peer that has the mod
-- Result codes of the Int32 functions (NightCoopSdkResult in redscript, NCSDK_E_* in C).
Sdk.RESULT = {
OK = 0,
BAD_ARGUMENT = -1,
NOT_NOW = -2,
LIMIT = -3,
UNSUPPORTED = -4,
SWITCHED_OFF = -5,
DENIED = -6,
UNKNOWN = -7,
TOO_SMALL = -8,
}
-- Mod flags (add them: FLAG.NEEDED_BY_ALL + FLAG.HOST_AUTHORITY). Declare what the mod uses (USES_*, SPAWNS_ENTITIES for
-- shared spawns, CHANGES_QUESTS for KeepFactLocal): an undeclared use answers Denied (-6), see sdk/SECURITY.md.
Sdk.FLAG = {
NEEDED_BY_ALL = 1,
LOCAL_ONLY = 2,
CHANGES_WORLD = 4,
CHANGES_QUESTS = 8,
ADDS_ITEMS = 16,
SPAWNS_ENTITIES = 32,
HOST_AUTHORITY = 64,
USES_CHANNELS = 256,
USES_SHARED_STATE = 512,
USES_ENTITY_STATE = 1024,
USES_POSE = 2048,
USES_ACTIONS = 4096,
USES_NPC_REQUESTS = 8192, -- npc effects and requests (sdk/docs/npc-effects.md)
USES_WORLD_TIME = 67108864, -- worldSlow
USES_SAVE = 32768, -- mod:scriptableScope
USES_MENU = 65536, -- mod:menuItemSet, mod:menuItemRemove
USES_VOTES = 131072, -- mod:voteStart, mod:voteCancel
USES_WORLD_ASK = 8388608, -- mod:worldAsk
CHANGES_NPC_LOGIC = 1048576, -- NPCs go to the nearest player only if they have the mod
USES_EFFECTS = 2097152, -- permission for mod:showEffect, attachEffect, playSound
USES_REWARDS = 4194304, -- permission for mod:grantReward
USES_HOOKS = 262144, -- hookDeclare
USES_FALLBACKS = 16384, -- fallbackSet
}
-- mod:showEffect flags (C NCSDK_FX_*).
Sdk.FX = { IGNORE_TIME_DILATION = 1, BY_NAME = 2 }
-- kinds of content a partner may not have (fallbackSet, contentHere)
Sdk.FALLBACK = { VEHICLE = 1, ITEM = 2, WEATHER = 3, RADIO = 4, PHOTO_POSE = 5 }
-- where a mod without the SDK lies (peerHasKnownMod) — the kinds of NightCoop's mods list (logs/nightcoop-mods.txt)
Sdk.KNOWN_MOD = { RED4EXT = 0, REDSCRIPT = 1, CET = 2, ARCHIVE = 3, REDMOD = 4, TWEAK = 5, ASI = 6 }
Sdk.LOG = { TRACE = 0, INFO = 1, WARN = 2, ERROR = 3 }
Sdk.TONE = { QUIET = 0, INFO = 1, WAIT = 2, URGENT = 3 }
-- ev.kind
Sdk.EVENT = { SESSION = 1, MESSAGE = 2, STATE = 3, ENTITY_STATE = 4, PARTNER_POSE = 5, SHARED_SPAWN = 6,
-- the co-op menu and votes
MENU_ITEM = 11, -- ev.key — the item key of your mod the player pressed in the co-op menu
VOTE_RESULT = 12, -- ev.key — the vote key, ev.a — the winner (-1 none), ev.value — Sdk.VOTE_END.*, ev.data — votes
-- per option (one byte each), then the number of voters
SPAWN_LEADER = 9, REWARD = 18, -- shared spawns, rewards
}
-- we lead the NPC and a player asks this mod (ev.request; answer with mod:npcReply), and the
-- answer to our own request (ev.a = result, ev.request, ev.effect)
Sdk.EVENT.NPC_REQUEST = 7
Sdk.EVENT.NPC_RESULT = 8
-- effects on NPCs a partner leads (sdk/docs/npc-effects.md)
Sdk.NPC = { REQUEST = 0, STATUS = 1, REMOVE_STATUS = 2, DAMAGE = 3, IMPULSE = 4, SLOW = 5, ATTITUDE = 6, SIDE = 7, KILL = 8,
FOLLOW = 9, GOD_MODE = 10, FAKE_DEATH = 11, DISPOSE = 12, HIDE = 13 }
Sdk.GOD_MODE = { MORTAL = 0, IMMORTAL = 1, INVULNERABLE = 2 }
Sdk.NPC_FLAG = {
SKIP_LOCAL = 1, -- status: your mod did it on our copy itself
PERCENT = 2, -- damage: value is percent of max health
NONLETHAL = 4, -- damage: can defeat, not kill
REACT = 8, -- damage without an attack record: a hit reaction of reactType
ALL_PLAYERS = 16, -- attitude: to every player
SHOW_ALWAYS = 32, -- status: on the partners' copies whatever its type
NO_ANIM = 64, -- kill: no death animation
NO_RAGDOLL = 128, -- kill: no ragdoll
}
Sdk.NPC_SIDE = { JOIN = 0, ALL_ENEMIES = 1, OWN = 2 }
Sdk.ATTITUDE = { FRIENDLY = 0, NEUTRAL = 1, HOSTILE = 2 }
-- shareEntity flags (C NCSDK_SHARE_*). HANDOFF — an NPC goes to the player nearest to it, as an NPC of the world;
-- ev SPAWN_LEADER tells who leads it then.
Sdk.SHARE = { FOLLOW = 1, ANIMATE = 2, HANDOFF = 4 }
-- ev.a of a SESSION event
Sdk.SESSION = {
START = 1,
END = 2,
PEER_JOIN = 3,
PEER_LEAVE = 4,
PEER_WORLD_IN = 5,
PEER_WORLD_OUT = 6,
PEER_MOD = 7, -- the player has this mod (same id and protocol)
PEER_NO_MOD = 8,
-- co-op events, their value in ev.value
PARTNER_DOWN = 9, -- value 1 fell, 0 got up
TEAM_DOWN = 10, -- every player of the host's world is down: 1 the host loads its last save, 0 no save
PARTNER_VEHICLE = 11, -- value 1 in (passenger), 2 at the wheel, 0 out; ev.ref — the vehicle
NPC_KILLED = 12, -- ev.peer — the killer (may be us), ev.ref — the npc
WORLD_PAUSE = 13, -- value 1 every player is in a menu, 0 the world runs again
HOST_SAVE = 14, -- value 1 the host saved, 2 the host loaded a save
TIME_SKIP = 15, -- value — the world's game time after the host's skip, seconds
WORLD_ANSWER = 32, -- the answer to mod:worldAsk: value — Sdk.WORLD_ANSWER.*, peer — who asked
}
-- mod:worldAsk kinds and answers
Sdk.WORLD = { TIME_OF_DAY = 1, WEATHER = 2 }
Sdk.WORLD_ANSWER = { REFUSED = 0, DONE = 1, FAILED = 2, BUSY = 3 }
-- mod:scriptableScope scope
Sdk.SCOPE = { DEFAULT = 0, WORLD = 1, CHARACTER = 2 }
-- mod:menuItemSet flags (add them)
Sdk.MENU = { DISABLED = 1, IN_SESSION = 2, HOST_ONLY = 4, IN_WORLD = 8 }
-- ev.value of a VOTE_RESULT event
Sdk.VOTE_END = { DECIDED = 1, FORCED = 2, TIMED_OUT = 3, CANCELLED = 4, ABORTED = 5 }
-- Sdk.partner(peer).flags (test with Sdk.has(flags, Sdk.PARTNER.DOWN))
Sdk.PARTNER = { IN_WORLD = 1, DOWN = 2, COMBAT = 4, VEHICLE = 8, WHEEL = 16, SCENE = 32, MENU = 64, LINK_LOST = 128, US = 256 }
-- Sdk.partner(peer).where
Sdk.WHERE = { UNKNOWN = 0, HOST_WORLD = 1, MAIN_MENU = 2, LOADING = 3, JOINING = 4, OWN_WORLD = 5 }
-- kind of a network reference (see Sdk.splitRef)
Sdk.NET = { NONE = 0, PLAYER = 1, VEHICLE = 2, NPC = 3, SPAWN = 4 }
-- ------------------------------------------------------------------------------------------------ the class
-- NightCoopSdk is a native class registered by NightCoop.dll. Look it up lazily and never cache a failure for long:
-- before the game is ready the lookup may fail.
local cls = nil
local lastProbe = -1e9
local function sdk()
if cls then
return cls
end
local now = os.clock()
if now - lastProbe < 5 then
return nil
end
lastProbe = now
local ok, c = pcall(function() return NightCoopSdk end)
if ok and c ~= nil then
local okv, v = pcall(c.Version)
if okv and type(v) == "number" and v > 0 then
cls = c
end
end
return cls
end
-- Call NightCoopSdk.<name>(...). Returns the first result, or nil when the SDK is absent or the call failed.
local function call(name, ...)
local c = sdk()
if not c then
return nil
end
local ok, r = pcall(c[name], ...)
if ok then
return r
end
return nil
end
local function code(r)
if r == nil then
return Sdk.RESULT.UNSUPPORTED
end
return r
end
-- Is NightCoop here with its SDK switched on?
function Sdk.available()
return sdk() ~= nil
end
-- ------------------------------------------------------------------------------------------------ session (static)
function Sdk.inSession() return call("InSession") == true end
function Sdk.isHost() return call("IsHost") == true end
function Sdk.inWorld() return call("InWorld") == true end
function Sdk.inHostWorld() return call("InHostWorld") == true end
function Sdk.localPeer() return call("LocalPeer") or Sdk.PEER_NONE end
function Sdk.peers() return call("Peers") or {} end
function Sdk.peerName(peer) return call("PeerName", peer) or "" end
function Sdk.peerInOurWorld(peer) return call("PeerInOurWorld", peer) == true end
-- A text a partner sent, safe to show or log (valid UTF-8, no control, invisible or direction characters, at most
-- maxBytes, 0 — 256). Without NightCoop: the text cut to maxBytes bytes (no cleaning — nothing to show it with anyway).
function Sdk.cleanText(text, maxBytes)
local r = call("CleanText", tostring(text or ""), maxBytes or 0)
if type(r) == "string" then return r end
local s = tostring(text or "")
local max = (maxBytes and maxBytes > 0) and maxBytes or 256
return s:sub(1, max)
end
-- ------------------------------------------------------------------------------------------------ hooks
-- sdk/docs/do-not-hook.md. Your mod hooks a game function (Override / Observe) — tell NightCoop, it warns the player like for
-- the hooks it finds itself: flags Sdk.HOOK.REPLACES (the original does not run) or Sdk.HOOK.WRAPS (it runs). Needs
-- FLAG.USES_HOOKS. Returns 1 — co-op relies on it now, 0 — not, or a negative Sdk.RESULT. Without NightCoop: 0.
Sdk.HOOK = { REPLACES = 1, WRAPS = 2 }
Sdk.HOOK_AREA = {
PLAYER = 1, NPC = 2, POPULATION = 4, VEHICLES = 8, QUESTS = 16, TIME = 32, SAVES = 64, MENU = 128, WORLD = 256,
DEVICES = 512, OTHER = 1024,
}
function Sdk.hookDeclare(id, class, fn, flags)
return call("HookDeclare", id, class, fn, flags or Sdk.HOOK.REPLACES) or 0
end
-- Does co-op rely on Class.fn now? A mask of Sdk.HOOK_AREA (0 — no). Use it to call the original (the last argument of
-- your Override) where co-op needs it.
function Sdk.hookCoopUses(class, fn)
return call("HookCoopUses", class, fn) or 0
end
-- Keys NightCoop reads itself: { {name=, vk=, ik=, action=, when=, rebindable=}, ... } (empty without NightCoop).
function Sdk.coopKeys()
local out = {}
for _, line in ipairs(call("CoopKeys") or {}) do
local name, vk, ik, action, when, rebind = tostring(line):match("^([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)$")
if name then
out[#out + 1] = { name = name, vk = tonumber(vk), ik = ik, action = action, when = when, rebindable = rebind == "1" }
end
end
return out
end
-- Pick a label in the game's UI language: ru for Russian, en for English, NightCoop's translation of en for the others.
function Sdk.pick(ru, en)
return call("Pick", ru, en) or en
end
-- ------------------------------------------------------------------------------------------------ entities (static)
-- EntityID -> Uint64 network reference (cdata); nil if NightCoop does not sync the entity.
function Sdk.entityRef(entity)
local r = call("EntityRef", entity)
if r == nil or r == 0 then
return nil
end
return r
end
-- Uint64 reference (cdata or decimal string) -> our local EntityID, or nil if it is not here now.
function Sdk.entityOfRef(ref)
local u = Sdk.toU64(ref)
if u == nil then
return nil
end
return call("EntityOfRef", u)
end
-- 1 mine, 0 a partner's, RESULT.UNKNOWN (-7) not synced by NightCoop (then it is purely local: yours).
function Sdk.entityIsMine(entity)
return code(call("EntityIsMine", entity))
end
-- The body of a partner in our world, or nil.
function Sdk.partnerBody(peer)
return call("PartnerBody", peer)
end
-- ------------------------------------------------------------------------------------------------ partners
-- Is the bit set? (Lua 5.1 / LuaJIT without bit ops in plain Lua)
function Sdk.has(flags, bit)
return flags ~= nil and bit ~= nil and flags >= 0 and math.floor(flags / bit) % 2 == 1
end
-- What a player is doing (a partner, or ourselves): { flags = Sdk.PARTNER bits, where = Sdk.WHERE.*, health = 0..1 or
-- -1, level = 0 unknown, vehicle = Uint64 ref or nil }, or nil (no session, no such player).
function Sdk.partner(peer)
local flags = call("PartnerFlags", peer)
if flags == nil or flags < 0 then
return nil
end
local vehicle = call("PartnerVehicle", peer)
if vehicle == 0 then
vehicle = nil
end
return {
flags = flags,
where = call("PartnerWhere", peer) or Sdk.WHERE.UNKNOWN,
health = call("PartnerHealth", peer) or -1,
level = call("PartnerLevel", peer) or 0,
vehicle = vehicle,
}
end
-- ------------------------------------------------------------------------------------------------ shared spawns
-- Recipe for CET mods (exEntitySpawner leaves no mark NightCoop could see): right after you spawn, either share the entity
-- (partners get a copy or a mirror) or keep it local (NightCoop's NPC and vehicle rules never touch it). See
-- sdk/docs/shared-spawns.md. Needs the mod flag SPAWNS_ENTITIES.
-- Who leads the entity now (peer number); Sdk.PEER_NONE (-1) — not synced by NightCoop (purely local: yours).
function Sdk.entityLeader(entity)
local r = call("EntityLeader", entity)
if r == nil then
return Sdk.PEER_NONE
end
return r
end
-- ------------------------------------------------------------------------------------------------ uint64 helpers
-- A decimal string (or number, or cdata) as a 64-bit integer. In LuaJIT (Cyber Engine Tweaks) the result is a uint64
-- cdata and loses nothing; elsewhere a plain number (lossy above 2^53).
function Sdk.toU64(v)
if type(v) == "cdata" then
return v
end
local s = tostring(v)
if not s:match("^%d+$") then
return nil
end
local loader = loadstring or load
if loader then
local okl, f = pcall(loader, "return " .. s .. "ULL")
if okl and f then
local ok, r = pcall(f)
if ok then
return r
end
end
end
return tonumber(s)
end
local U56 = Sdk.toU64("72057594037927936")
local U32 = Sdk.toU64("4294967296")
local U24 = Sdk.toU64("16777216")
-- Unpack a network reference (NCSDK_PACK_REF): returns kind, owner, id as plain numbers (owner 0xFFFFFF = none).
-- Needs LuaJIT 64-bit integers; returns nil elsewhere.
function Sdk.splitRef(ref)
local u = Sdk.toU64(ref)
if u == nil or type(u) ~= "cdata" then
return nil
end
local ok, kind, owner, id = pcall(function()
return tonumber(u / U56), tonumber((u / U32) % U24), tonumber(u % U32)
end)
if not ok then
return nil
end
return kind, owner, id
end
-- ------------------------------------------------------------------------------------------------ event decoding
local function unhex(h)
if h == nil or h == "" then
return ""
end
return (h:gsub("%x%x", function(x) return string.char(tonumber(x, 16)) end))
end
local function splitTabs(line)
local out, n, pos = {}, 0, 1
while true do
local i = string.find(line, "\t", pos, true)
n = n + 1
if not i then
out[n] = string.sub(line, pos)
break
end
out[n] = string.sub(line, pos, i - 1)
pos = i + 1
end
return out
end
-- PollEventText line: kind \t peer \t a \t entityHash \t ref \t removed \t keyHex \t dataHex [\t value]
local function eventFromText(line)
line = line:gsub("[\r\n]+$", "")
local f = splitTabs(line)
local kind = tonumber(f[1])
if not kind or kind == 0 then
return nil
end
local ev = {
kind = kind,
peer = tonumber(f[2]) or Sdk.PEER_NONE,
a = tonumber(f[3]) or 0,
entityHash = f[4] or "0",
ref = Sdk.toU64(f[5] or "0"),
removed = (f[6] == "1"),
key = unhex(f[7]),
data = unhex(f[8]),
value = tonumber(f[9]) or 0,
source = "text",
}
-- NPC_REQUEST / NPC_RESULT: key is "<request>/<effect>"
if ev.kind == Sdk.EVENT.NPC_REQUEST or ev.kind == Sdk.EVENT.NPC_RESULT then
local r, e = string.match(ev.key, "^(%d+)/(%-?%d+)$")
ev.request = tonumber(r) or 0
ev.effect = tonumber(e) or 0
end
return ev
end
local function bytesToString(t)
local parts, n = {}, #t
local i = 1
while i <= n do
local j = math.min(i + 255, n)
parts[#parts + 1] = string.char((table.unpack or unpack)(t, i, j))
i = j + 1
end
return table.concat(parts)
end
local function hashOf(entityId)
local ok, h = pcall(function() return entityId.hash end)
if not ok or h == nil then
return "0"
end
return (tostring(h):gsub("U?LL$", ""))
end
local function eventFromObject(o)
local ok, ev = pcall(function()
local kind = o.kind
if type(kind) ~= "number" or kind == 0 then
return nil
end
local data = o.data or ""
if data == "" and o.bytes ~= nil and #o.bytes > 0 then
data = bytesToString(o.bytes)
end
return {
kind = kind,
peer = o.peer,
a = o.a,
key = o.key or "",
removed = (o.removed == true),
data = data,
entity = o.entity,
entityHash = hashOf(o.entity),
ref = o.ref,
value = o.value or 0,
source = "object",
object = o,
request = o.request or 0,
effect = o.effect or 0,
}
end)
if ok then
return ev
end
return nil
end
local function scriptedEventsExist()
local ok, c = pcall(function() return NightCoopSdkEvent end)
return ok and c ~= nil
end
-- ------------------------------------------------------------------------------------------------ a registered mod
-- Sdk.new(id, name, version, protocol, flags) -> mod object. Nothing is registered until :register().
-- id stable unique id, ASCII [a-z0-9._-], 3..64 bytes
-- protocol your own wire version: data goes only between players with equal numbers
-- flags sum of Sdk.FLAG.*
function Sdk.new(id, name, version, protocol, flags)
return setmetatable({
id = id,
name = name or id,
version = version or "0",
protocol = protocol or 1,
flags = flags or 0,
registered = false,
_lastRegister = -1e9,
_objectEvents = nil,
}, Sdk)
end
-- Register (or update) the mod. Safe to call again. Returns true when registered; false when the SDK is absent or
-- refused (bad id, or the id is registered by a C plugin). The reason is in NightCoop's log.
function Sdk:register()
local r = call("RegisterMod", self.id, self.name, self.version, self.protocol, self.flags)
self.registered = (r == true)
return self.registered
end
function Sdk:unregister()
call("UnregisterMod", self.id)
self.registered = false
end
-- Call from onUpdate. Registers the mod if that has not worked yet (retries every 5 s), then hands up to maxPerFrame
-- (default 32) queued events to handler(ev). A handler error is logged and does not stop the loop.
-- Returns how many events were handled.
function Sdk:poll(handler, maxPerFrame)
if not sdk() then
self.registered = false
return 0
end
if not self.registered then
local now = os.clock()
if now - self._lastRegister < 5 then
return 0
end
self._lastRegister = now
if not self:register() then
return 0
end
end
if self._objectEvents == nil then
self._objectEvents = scriptedEventsExist()
end
local limit = maxPerFrame or 32
local handled = 0
while handled < limit do
local ev = nil
-- PollEvent only when the scripted event class exists: without it the call could not build the object.
if self._objectEvents then
local o = call("PollEvent", self.id)
if o ~= nil then
ev = eventFromObject(o)
end
end
if ev == nil then
local line = call("PollEventText", self.id)
if type(line) == "string" and line ~= "" then
ev = eventFromText(line)
end
end
if ev == nil then
break
end
handled = handled + 1
local ok, err = pcall(handler, ev)
if not ok then
self:log(Sdk.LOG.ERROR, "event handler failed: " .. tostring(err))
end
end
return handled
end
-- ------------------------------------------------------------------------------------------------ log, screen
function Sdk:log(level, text)
call("Log", self.id, level, tostring(text))
end
-- A one-off message at the bottom of the screen. Returns true if it was shown.
function Sdk:showMessage(text, seconds, tone)
return call("ShowMessage", self.id, tostring(text), seconds or 3.0, tone or Sdk.TONE.INFO) == true
end
-- ------------------------------------------------------------------------------------------------ session
-- 1 the peer has this mod (same id and protocol), 0 not, RESULT.NOT_NOW (-2) not known yet.
function Sdk:peerHasMod(peer)
return code(call("PeerHasMod", self.id, peer))
end
function Sdk:peerModVersion(peer)
return call("PeerModVersion", self.id, peer) or ""
end
-- ------------------------------------------------------------------------------------------------ channels
-- to: a peer number, Sdk.PEER_HOST (0) or Sdk.PEER_ALL (-2). text: up to 1024 bytes unreliable, 16384 reliable.
-- Returns how many peers it went to (0: nobody has the mod) or a negative RESULT code.
function Sdk:send(to, channel, text, reliable)
return code(call("Send", self.id, to, channel, tostring(text), reliable and true or false))
end
-- ------------------------------------------------------------------------------------------------ shared state
-- Host: sets and spreads. Guest: asks the host; the value changes when ev STATE comes back. "" removes the key.
function Sdk:setState(key, value)
return code(call("SetState", self.id, key, tostring(value)))
end
function Sdk:removeState(key)
return code(call("RemoveState", self.id, key))
end
-- The current value, or nil if the key is not set.
function Sdk:getState(key)
if call("HasState", self.id, key) ~= true then
return nil
end
return call("GetState", self.id, key)
end
-- ------------------------------------------------------------------------------------------------ entity state
-- Owner only. entity: an EntityID (for example vehicle:GetEntityID()). key: 0..65535, yours. stream = true: unreliable,
-- only the newest value matters. Partners get ev ENTITY_STATE on their mirror/copy.
function Sdk:setEntityState(entity, key, value, stream)
return code(call("SetEntityState", self.id, entity, key, tostring(value), stream and true or false))
end
function Sdk:clearEntityState(entity, key)
return code(call("ClearEntityState", self.id, entity, key))
end
-- ------------------------------------------------------------------------------------------------ effects on NPCs
-- An effect on an NPC NightCoop syncs is done by its OWNER with the game, as an action of our player (sdk/docs/npc-effects.md).
-- Register with Sdk.FLAG.USES_NPC_REQUESTS. npc: an EntityID (npc:GetEntityID()) — our local copy or our own NPC.
-- Returns 0 — done here now (ours, or not synced); > 0 — the request number (ev NPC_RESULT with ev.request comes back);
-- < 0 — a RESULT code. record / status / attack: a TweakDBID or its name ("BaseStatusEffect.Burning").
-- Points and vectors: Vector4 or {x, y, z} in world space.
local function tdbid(v)
if v == nil or v == "" then
return TweakDBID.new()
end
if type(v) == "string" then
return TweakDBID.new(v)
end
return v
end
local function vec4(v)
if v == nil then
return Vector4.new(0, 0, 0, 0)
end
if type(v) == "table" and v.x == nil and v[1] ~= nil then
return Vector4.new(v[1], v[2], v[3], 0)
end
if type(v) == "table" and v.x ~= nil then
return Vector4.new(v.x, v.y, v.z, 0)
end
return v
end
-- kind: Sdk.NPC.*; the generic form of the calls below.
function Sdk:npcEffect(npc, kind, flags, mode, record, value, seconds, point, vector)
return code(call("NpcEffect", self.id, npc, kind, flags or 0, mode or 0, tdbid(record), value or 0, seconds or 0,
vec4(point), vec4(vector)))
end
-- flags: Sdk.NPC_FLAG.SKIP_LOCAL when your mod already put the status on our copy itself.
function Sdk:npcStatus(npc, status, flags)
return code(call("NpcStatus", self.id, npc, tdbid(status), flags or 0))
end
function Sdk:npcRemoveStatus(npc, status, flags)
return code(call("NpcRemoveStatus", self.id, npc, tdbid(status), flags or 0))
end
-- value: health points (Sdk.NPC_FLAG.PERCENT: percent of max health); attack: an Attack record or nil (health written
-- directly; with NPC_FLAG.REACT a reaction of reactType); point nil — the chest; dir nil — from us.
function Sdk:npcDamage(npc, attack, value, point, dir, flags, reactType)
return code(call("NpcDamage", self.id, npc, tdbid(attack), value, vec4(point), vec4(dir), flags or 0, reactType or 0))
end
-- Ragdoll and push: impulse (world) at point; radius 0 — 1 m.
function Sdk:npcImpulse(npc, point, impulse, radius)
return code(call("NpcImpulse", self.id, npc, vec4(point), vec4(impulse), radius or 0))
end
-- Its own time: dilation 0..1 for seconds (<= 120); 0 — frozen, 1 — back to normal.
function Sdk:npcSlow(npc, dilation, seconds)
return code(call("NpcSlow", self.id, npc, dilation, seconds or 0))
end
-- attitude: Sdk.ATTITUDE.*, to us (flags Sdk.NPC_FLAG.ALL_PLAYERS: to every player).
function Sdk:npcAttitude(npc, attitude, flags)
return code(call("NpcAttitude", self.id, npc, attitude, flags or 0))
end
-- side: Sdk.NPC_SIDE.* (JOIN our side, ALL_ENEMIES, OWN back to its group).
function Sdk:npcSide(npc, side)
return code(call("NpcSide", self.id, npc, side))
end
-- Kill it, we are the killer (flags NO_ANIM, NO_RAGDOLL).
function Sdk:npcKill(npc, flags)
return code(call("NpcKill", self.id, npc, flags or 0))
end
-- Follow us (an AI follower role, as companion mods; it stays with the NPC at its next owner); false — no role.
function Sdk:npcFollow(npc, follow)
return code(call("NpcFollow", self.id, npc, follow and true or false))
end
-- God mode: Sdk.GOD_MODE.* (stays with the NPC at its next owner).
function Sdk:npcGodMode(npc, mode)
return code(call("NpcGodMode", self.id, npc, mode or 0))
end
-- Fake death (its AI plays dying, as AMM "Fake Die"), dispose (as AMM "Despawn"), hide for good.
function Sdk:npcFakeDeath(npc) return self:npcEffect(npc, Sdk.NPC.FAKE_DEATH) end
function Sdk:npcDispose(npc) return self:npcEffect(npc, Sdk.NPC.DISPOSE) end
function Sdk:npcHide(npc) return self:npcEffect(npc, Sdk.NPC.HIDE) end
-- The general request: the owner's copy of this mod gets ev NPC_REQUEST (ev.entity / ev.entityHash — its NPC, ev.a — the
-- channel, ev.data — text) and answers with npcReply(ev.request, result, text); we get ev NPC_RESULT. Returns the request
-- number (> 0; also for our own NPC: then the request comes to us) or a negative RESULT code.
function Sdk:npcRequest(npc, channel, text)
return code(call("NpcRequest", self.id, npc, channel, tostring(text or "")))
end
-- This mod slows (or stops) OUR world: dilation 0..1 for seconds (<= 120); 1 — over. Declare first, then slow the world
-- with the game yourself. Partners near us get it as an implant's slow-down; a declared stop is no loading screen.
function Sdk:worldSlow(dilation, seconds)
return code(call("WorldSlow", self.id, dilation, seconds or 0))
end
-- The owner's answer: result 0 or a negative RESULT code; text up to 1024 bytes.
function Sdk:npcReply(request, result, text)
return code(call("NpcReply", self.id, request, result or 0, tostring(text or "")))
end
-- ------------------------------------------------------------------------------------------------ the save, the co-op menu, votes
-- Declare the flags (Sdk.FLAG.USES_SAVE / USES_MENU / USES_VOTES).
-- A ScriptableSystem of your mod (its class name as RTTI has it) is the world's (Sdk.SCOPE.WORLD: a guest entering the
-- host's world by save takes the host's state of it) or each player's own (CHARACTER, the default); DEFAULT drops it.
function Sdk:scriptableScope(className, scope)
return code(call("ScriptableScope", self.id, tostring(className), scope or Sdk.SCOPE.DEFAULT))
end
-- Several quest facts each player's own at once: returns how many were accepted (KeepFactsLocal; without it —
-- KeepFactLocal for each).
function Sdk:keepFactsLocal(facts, on)
local list = {}
for _, fact in ipairs(facts or {}) do
list[#list + 1] = tostring(fact)
end
local r = call("KeepFactsLocal", self.id, list, on ~= false)
if r ~= nil then
return r
end
local n = 0
for _, fact in ipairs(list) do
if code(call("KeepFactLocal", self.id, fact, on ~= false)) == Sdk.RESULT.OK then
n = n + 1
end
end
return n
end
-- An item of your mod in NightCoop's co-op menu (Esc -> «Co-op» -> «Mods…»): a press comes as ev.kind EVENT.MENU_ITEM
-- with ev.key. ru / en — the label; flags — sum of Sdk.MENU.*.
function Sdk:menuItemSet(key, ru, en, flags)
return code(call("MenuItemSet", self.id, tostring(key), tostring(ru or en or ""), tostring(en or ru or ""), flags or 0))
end
function Sdk:menuItemRemove(key)
return code(call("MenuItemRemove", self.id, tostring(key)))
end
-- Host only: ask every player of the host's world in NightCoop's choice window. optionsRu / optionsEn — Lua arrays of the
-- same length (2..8); the end comes as EVENT.VOTE_RESULT with ev.key on every machine that has the mod.
function Sdk:voteStart(key, questionRu, questionEn, optionsRu, optionsEn)
return code(call("VoteStart", self.id, tostring(key), tostring(questionRu or questionEn or ""),
tostring(questionEn or questionRu or ""), optionsRu or optionsEn or {}, optionsEn or optionsRu or {}))
end
function Sdk:voteCancel(key)
return code(call("VoteCancel", self.id, tostring(key)))
end
-- Ask the host to change the world: kind Sdk.WORLD.TIME_OF_DAY (value — seconds of the day) or WEATHER (text — a weather
-- state, value — blend seconds). vote — decide by a vote of every player. The answer: SESSION.WORLD_ANSWER. Returns the ask's
-- number (> 0) or a RESULT.
function Sdk:worldAsk(kind, value, text, vote)
return code(call("WorldAsk", self.id, kind, value or 0, tostring(text or ""), vote and true or false))
end
-- The votes of a VOTE_RESULT event: { votes = { per option }, voters = n }.
function Sdk.voteCounts(ev)
local data = ev and ev.data or ""
local out = { votes = {}, voters = 0 }
if #data == 0 then
return out
end
for i = 1, #data - 1 do
out.votes[i] = string.byte(data, i)
end
out.voters = string.byte(data, #data)
return out
end
-- ------------------------------------------------------------------------------------------------ shared spawns
-- Share an entity your mod spawned. flags: Sdk.SHARE.* added (FOLLOW + HANDOFF). An NPC: partners near it see it with its
-- animation; with HANDOFF it goes to the player nearest to it. A vehicle: partners get NightCoop's mirror with seats, doors
-- and driver, whoever drives leads it. A prop: a copy (FOLLOW: it follows your transform). If the class has no
-- ShareEntityFlags, the plain ShareEntity is called (FOLLOW and ANIMATE only).
function Sdk:shareEntity(entity, flags)
local f = flags or 0
local r = call("ShareEntityFlags", self.id, entity, f)
if r ~= nil then
return r
end
local follow = (f % 2) == 1
local animate = (math.floor(f / 2) % 2) == 1
return code(call("ShareEntity", self.id, entity, follow, animate))
end
function Sdk:unshareEntity(entity)
return code(call("UnshareEntity", self.id, entity))
end
-- Keep the entity purely local: NightCoop never shares, adopts, hides or removes it. on = false — back to normal.
function Sdk:keepLocal(entity, on)
return code(call("KeepLocal", self.id, entity, on ~= false))
end
-- Show a world effect (an .effect resource path, picture and sound, no damage) to the partners in our world, fire and
-- forget. Play it on your own machine yourself. position: Vector4, orientation: Quaternion (nil — no rotation).
-- Needs Sdk.FLAG.USES_EFFECTS. Returns how many partners got it (0 — nobody) or a negative RESULT code.
function Sdk:showEffect(path, position, orientation, flags)
local rot = orientation or Quaternion.new(0, 0, 0, 1)
return code(call("ShowEffect", self.id, tostring(path), position, rot, flags or 0))
end
-- An effect on a synced entity (a player, a vehicle, an NPC, a shared spawn), on the partners' mirror or copy of it. effect:
-- an .effect resource path attached to slot ("" — the root), or with Sdk.FX.BY_NAME the name of an effect of the entity's
-- template. Returns the effect number (for stopEffect) or a negative RESULT code.
function Sdk:attachEffect(entity, effect, slot, flags)
return code(call("AttachEffect", self.id, entity, tostring(effect), tostring(slot or ""), flags or 0))
end
function Sdk:stopEffect(number, breakLoop)
return code(call("StopEffect", self.id, number, breakLoop and true or false))
end
-- A sound event (as GameObject.PlaySound) on the partners' mirror or copy of a synced entity; stop = true stops it.
function Sdk:playSound(entity, event, stop)
return code(call("PlaySoundOn", self.id, entity, tostring(event), stop and true or false))
end
-- A reward for a partner (to — a peer number, Sdk.PEER_ALL — everyone in our world): quantity of an item record
-- ("Items.money") created in their inventory, exactly once. Needs Sdk.FLAG.USES_REWARDS. Returns the first grant number
-- (> 0), 0 — nobody in our world, or a negative RESULT code; the answer comes as ev REWARD (entityHash = the grant number,
-- a = NCSDK result).
function Sdk:grantReward(to, record, quantity)
return code(call("GrantReward", self.id, to, tostring(record), quantity or 1))
end
-- Lead an NPC for a while (possession, mind control): entity — our body of a synced NPC (a partner's NPC's copy here). Its
-- owner hands it over; NightCoop keeps it ours for seconds (1..600; -1 — until you call with 0; 0 — release now). The outcome
-- comes as ev SPAWN_LEADER for it (a = 1 ours now, 0 not handed over). Needs the mod flag USES_NPC_REQUESTS (8192).
function Sdk:leaseNpc(entity, seconds)
return code(call("LeaseNpc", self.id, entity, seconds or 0))
end
-- ------------------------------------------------------------------------------------------------ content a partner may not have
-- sdk/docs/content-fallbacks.md
-- What a partner WITHOUT your mod sees instead of your content: kind Sdk.FALLBACK.*, name — your record / state / station /
-- pose animation, fallback — the game's own of the same kind ("" or nil clears). Needs FLAG.USES_FALLBACKS.
-- mod:fallbackSet(Sdk.FALLBACK.VEHICLE, "Vehicle.v_my_sedan", "Vehicle.v_standard2_archer_hella_player")
function Sdk:fallbackSet(kind, name, fallback)
return code(call("FallbackSet", self.id, kind, tostring(name or ""), tostring(fallback or "")))
end
-- Is this content in THIS game: 1 yes, 0 no, RESULT.NOT_NOW (cannot tell yet), RESULT.UNSUPPORTED (weather states).
function Sdk.contentHere(kind, name)
return code(call("ContentHere", kind, tostring(name or "")))
end
-- Does the partner have a mod without the SDK (kind Sdk.KNOWN_MOD.*, its folder or file name as in nightcoop-mods.txt):
-- 1 yes, 0 no, RESULT.NOT_NOW (the mods check with them is not over).
function Sdk.peerHasKnownMod(peer, kind, name)
return code(call("PeerHasKnownMod", peer, kind, tostring(name or "")))
end
return Sdk