Custom keys

From Wiki G1R-MP G1 Remake Multiplayer
Jump to navigation Jump to search

Available from 0.1.9. Use matching updated server and clients.

Custom keys are new native inventory types, not renamed copies of the donor key. Each type has its own name, description and server identity, while reusing an approved native key model/icon. Multiple copies of the same type can be given, dropped and picked up normally. A locksmith system can issue additional copies through authorized server logic.

Server API

All functions above run in server Lua. Door IDs are local to the calling resource; a resource cannot alter or query another resource's door through these functions. A door can reference an active key defined by another resource. Client requests must still pass your gamemode's permission, payment and cooldown checks before server scripts grant items or alter locks.

Definition and copies

local key, reason = registerKeyDefinition("supertest_key", {
    name = "Test key", description = "Opens the workshop.",
    model = "cavalorn_key", value = 5, weight = 0.1, maxStack = 99
})
assert(key, reason)
-- Call this from authorized server gameplay, not a public client grant event.
local function issueCopies(playerId, quantity)
    if not isPlayerKeyDefinitionReady(playerId, key) then return false end
    return givePlayerItem(playerId, key, quantity)
end

The returned identity is ck:resource_name:supertest_key. Store this canonical string, not an engine/runtime numeric ID. Changing the resource name changes key identity. Repeating an identical registration is safe; changing an existing definition's metadata is rejected. Registration does not grant any items.

onPlayerKeyDefinitionReady(playerId, item) indicates an accepted client presentation acknowledgement for the current connection/definition revision. It is not proof of legitimate inventory, a reward entitlement, or visual confirmation by a human. The server checks its own quantity for every key-protected door operation. Check readiness immediately as well as listening for the event; a resource starting later may have missed the earlier notification.

Only cavalorn_key is currently an approved donor model. getKeyModels enumerates supported choices. This first custom-item family is keys: it does not create arbitrary custom weapons, armor, potions or new game assets. Two separately registered types remain different keys even when their model/icon is identical.

Shared door placement and access

local doors = {
    {id="workshop", prefab="io_oc_room_door", x=1045, y=-594, z=25.25, yaw=0}
}
local valid, reason = validateWorldDoors(doors)
assert(valid, reason)
assert(setWorldDoors(doors))
-- key is the canonical string returned by your registration code.
assert(setWorldDoorKey("workshop", key))
-- Optional owner-controlled changes:
assert(setWorldDoorLocked("workshop", false)) -- keep the binding, temporarily unlock
assert(setWorldDoorLocked("workshop", true))
local state = getWorldDoor("workshop")
assert(state and state.key == key)

Coordinates are metres; yaw is degrees. Native scale/collision remain original. Use getWorldDoorPrefabs or World door prefab catalog for allowed aliases. setWorldDoors replaces the complete set owned by this resource; omitted doors are removed, and an empty table removes all its doors. To edit one door, keep and submit the other placement rows too. Query results contain status fields: copy only id,prefab,x,y,z,yaw when building replacement rows. Passing status fields back is rejected.

An unchanged placement keeps its runtime identity. Moving/replacing it retains its key rule when the resource and local door ID stay the same, but recreates native/mechanism state. Removing the door removes its binding. Resource stop removes its doors. Bind locks again after intentionally removing and recreating a door.

setWorldDoorKey assigns one key type and enables its lock; possession of at least one server-owned copy authorizes operation. Keys are not consumed. One key type can open multiple doors. Dropping one of several copies retains access; dropping the last removes it. clearWorldDoorKey removes the custom requirement, whereas unlocking retains the assignment. Lock changes do not force an open door closed and do not interrupt an active exit/release animation. These APIs configure resource-placed doors, not arbitrary original map doors, chests or gates.

hasPlayerWorldDoorAccess checks the assigned key rule and authoritative inventory only. It does not check distance, death, busy state, current animation or whether the native client is ready to interact. It does not reserve a door. The core repeats its own checks when an operation arrives; an earlier true result cannot bypass a later key removal.

Placed doors use shared mechanism replication, including join-time state. A local editor native test is not a deployed shared door. The key-protected path blocks normal native initiation when the server keyring denies access and independently rejects unauthorized state changes on the server. Private keys, definitions, inventory and door rules cannot be created by a client packet. A modified client can fake its own visuals, not grant shared access.

Drop, pickup and persistence

For a custom key, the resource which registered the type receives the trusted onWorldItemDrop(objectId, playerId, item, quantity, serverTimeMs) persistence request. The server has already deducted the quantity and quarantined the new world object: it is not yet published or pickable.

-- In-memory example ONLY. A persistent gamemode must commit its DB change first.
addEventHandler("onWorldItemDrop", root, function(objectId, playerId, item, quantity)
    if eventOrigin ~= "server" or type(sourceResource) ~= "table"
        or sourceResource.name ~= "server" then return end
    local definition = getKeyDefinition(item)
    if not definition or definition.owner ~= "custom_keys" then return end
    -- No removePlayerItem here: the core already deducted quantity.
    local settled = completeWorldItemDrop(playerId, objectId, true)
    if not settled then outputDebugString("Drop settlement rejected", 1) end
end)

Replace custom_keys with your actual defining resource name. For durable inventory, use an idempotent database transaction and call completeWorldItemDrop(playerId, objectId, committed) with its known result. Capture the event arguments before asynchronous work; event globals are scoped to the current callback. Never acknowledge success before persistence succeeds, and never deduct the live server inventory again.

Only the captured persistence owner of that pending drop can settle it, and the player/object/revision/item/quantity must still match. Duplicate, wrong-owner and wrong-player completions return false. True publishes the object; false cancels it and rolls back the connected player's live quantity. A missing/failed request handler triggers cancellation. A handler that accepts work but never completes it leaves the drop pending: there is no automatic timeout that can safely guess an uncertain database result. Implement retry/recovery in your persistent gamemode; callbacks are not a replacement for durable transactions.

onWorldItemDropCommitted notifies running resources after publication; onWorldItemDropCancelled notifies the persistence owner after cancellation. Both are deferred server notifications. For custom keys, other resources do not receive the pre-commit onWorldItemDrop request. Existing ordinary-item drop persistence still uses gothic_rp and keeps its legacy observer notification behavior.

onWorldItemPickup runs after the core grants the quantity and removes the world item. Persist your own inventory/cache if required; do not grant the quantity again. A competing pickup cannot duplicate the same world object. spawnItem creates a single shared world item per accepted call, not a stack-size parameter; use givePlayerItem for multiple inventory copies. Drop uses the selected native quantity. No automatic SQL schema, economy, trade UI or locksmith job is installed by the key API.

Callbacks and safety

addEventHandler("onWorldDoorStateChange", root, function(id, playerId, open, previousOpen, revision)
    outputDebugString(id .. " open=" .. tostring(open) .. " revision=" .. revision)
end)
addEventHandler("onWorldDoorLockChange", root, function(id, key, locked, previousKey, previousLocked)
    outputDebugString(id .. " locked=" .. tostring(locked))
end)
addEventHandler("onWorldDoorAccessDenied", root, function(id, playerId, key, reason)
    outputDebugString(id .. ": denied " .. playerId .. " (" .. reason .. ")")
end)

The three door events go only to the placement owner. State-change events describe accepted open/closed transitions, not heartbeats, lease release, bootstrap or animation completion. Lock-change events describe actual binding/lock changes, not identical setter calls. Access-denied events describe server-rejected key permission attempts, at most one per player per second across all doors; an ordinary blocked client click may never send an attempt and therefore emits no server denial event.

These are notifications, not cancellable pre-operation hooks. They run later in the server loop, outside mechanism processing and lock setters. Code can query the latest state; it may already be newer than the event. Queues are bounded to 4096 pending notifications with up to 64 delivered per tick; overload is logged and can drop notifications. Resource stop discards its queued notifications. They are not a guaranteed audit/persistence log.

The key/door and item lifecycle event names are reserved: addEvent(name,true), client attempts, and Lua triggerEvent cannot forge these notifications. Use ordinary addEventHandler(name,root,callback). Event context is eventOrigin == "server" and sourceResource.name == "server". Readiness is still based on a client report and must not authorize inventory/rewards.

Limits, restart and qualification

  • Key resource name: 1..32 lower-case letters, digits, underscore or hyphen; local key ID: 1..28 of the same characters. Full identity is namespaced.
  • Name: 1..192 UTF-8 bytes, nonblank. Description: empty or up to 2048 UTF-8 bytes, nonblank if supplied. No controls, line breaks, NUL, malformed UTF-8 or whitespace-only text.
  • Value: integer 0..1000000 (default 0); weight: finite number 0..1000 (default 0.1); maxStack: integer 1..1000 (default 99). Unknown fields and model paths are rejected. All copies share the type settings.
  • At most 64 distinct key definitions per resource and 256 per server process, including inactive registrations. Stopping a resource does not free native type identities; identical re-registration is supported. Changing a recipe requires a planned restart/migration, not hot mutation of shared native classes.
  • Door local ID: 1..64 letters/digits/underscore/hyphen; owner name up to 96. At most 1024 placements per resource and 4096 overall. Serialized list at most 512000 bytes; finite positions within +/-100000 metres and yaw within +/-36000 degrees. Use only approved door aliases and exact placement fields.
  • No automatic persistence: re-register definitions and reapply placements/bindings from server data on restart. Restore inventories with canonical keys after registration/readiness. Stopping a key owner deactivates its types; other resources' doors referring to those types stay denied, not silently unlocked. Do not rely on single-player saves for server inventory.
  • Native key name/icon, creation/cleanup, ordinary drop/pickup and key admission were confirmed in local testing. Automated tests cover ownership, key quantities, wrong keys, forged/stale requests and callback routing. Complete multi-human observer/late-join visual qualification remains a separate test; documentation is not a claim that every door prefab and scenario has been visually tested.