Jump to content

Grimwar/VSig reference

From Vellocet Developer Community
Revision as of 00:13, 1 October 2026 by DocumentationBot (talk | contribs) (Version 1)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This is the detailed VSig reference for Grimwar. Start with the door tutorial or the language basics. Examples below show individual language features; keep the required refs and modules in your map when combining them.

Refs and groups

A ref can match one hierarchy path or a wildcard:

ref main_door: func_door match BuildingA/Door
ref spawn_doors: func_door match SpawnDoors/Door*

group exits:
    main_door
    spawn_doors

on round.post_round:
    exits.close

An input sent to a wildcard ref or group broadcasts to every match. Property reads and state capture require exactly one entity.

Stable entity names make refs readable and keep wildcard matches predictable. VSig refs use the object hierarchy and entity class.

Entity lifecycle and operational state

Every concrete map entity exposes the same terminal lifecycle:

Member Kind Meaning
kill Input Permanently remove this entity instance for the current map load.
killed Output Fire once immediately before the runtime is removed.

kill is distinct from domain actions such as a damageable entity's break. A broken entity can be healed or reset; a killed entity returns only when the scene or map is loaded again. The server synchronizes terminal state to current clients and late joiners and records it in demo entity state. Client-local presentation entities use their owning client runtime.

The SDK calls this operation kill, not destroy. It does not expose hierarchy destruction because a Unity Transform hierarchy is an authoring relationship, not an entity-ownership boundary.

Entities that declare the Operational capability also expose:

Member Kind Meaning
enable Input Make the entity operational.
disable Input Make the entity non-operational.
toggle Input Switch operational state.
enabled Output Fire on an actual disabled-to-enabled transition.
disabled Output Fire on an actual enabled-to-disabled transition.
is_enabled Property Read the current operational state.

Repeated enable or disable calls succeed without emitting duplicate transition outputs. Operational state never calls GameObject.SetActive and never changes Unity's component enabled property. The entity remains registered and applies its own behavior. For example, a disabled func_damageable ignores gameplay damage but still accepts heal, reset, break, operational inputs, and kill.

Keep more precise verbs when they carry different meaning. Doors remain open/close, damageables remain break/reset, and continuous movers remain start/stop.

Activator, caller, and self

Every entity signal carries immutable execution context:

Name Meaning
activator Gameplay object that began the chain, such as the participant touching a trigger.
caller Map entity that invoked the current entity.
self Entity whose output handler or input is currently executing.

Use the procedural targets !activator, !caller, and !self:

ref entry_trigger: trigger_multiple match entry_trigger
ref first_gate: func_door match first_gate

on entry_trigger.start_touch:
    first_gate.open

on first_gate.open:
    !caller.disable
    !self.kill

If a player activates entity A and A invokes B, B receives the original player as activator, A as caller, and B as self. Each later entity invocation advances caller and self while preserving the original activator. Procedures, waits, scheduled blocks, and delayed entity outputs preserve their originating context. Autonomous and global signals have no activator or caller.

Targets resolve separately for every execution; there is no global last activator. A missing target produces a bounded warning and does nothing. Player and gameplay-object activators remain opaque provenance handles, so !activator.kill cannot kill a player or arbitrary GameObject. Clients cannot forge handles or invoke server-owned entities through this path.

Values and expressions

Primitive types are bool, int, float, and string. Entity state uses the state type declared by its marker contract. Durations accept seconds or milliseconds, including 1.5s and 250ms.

seed 1847

const max_score: int = 10
var score: int = 0
var title: string = "Arena"

on button.pressed:
    score = clamp(score + 1, 0, max_score)
    label.set_text("{0}: {1}/{2}", title, score, max_score)

Strings must be quoted. Declare and initialize map variables and module state before use. Handler parameters and procedure parameters are immutable; assign mutable data to a map var or module state.

Operators

Precedence, high to low Operators Notes
Grouping (expression) Parentheses override normal precedence.
Unary -value, not value, !value Numeric negation and logical negation.
Multiplicative *, /, % Division or remainder by zero produces an invalid expression value.
Additive +, - + concatenates when either operand is a string; otherwise both operands must be numeric.
Comparison ==, !=, <, <=, >, >= Ordering requires numbers. Non-numeric equality compares invariant text without case sensitivity.
Logical AND and, && Both operands are evaluated.
Logical OR or, || Both operands are evaluated.

Falsey values are false, numeric zero, an empty string, null, and a missing value. Nonzero numbers, nonempty strings, and entity-state values are truthy.

Built-in functions

These 20 expression functions are available:

Function Result and constraints
abs(value) Absolute numeric value.
bool(value) Truthiness conversion using the rules above. A nonempty string, including "false", is truthy.
ceil(value) Smallest integer greater than or equal to the numeric value.
choose(condition, when_true, when_false) Returns the second or third value according to the condition's truthiness.
clamp(value, minimum, maximum) Numeric value constrained to the supplied bounds.
convar(name) Reads one content-accessible console variable. Use a quoted name such as convar("sv_gravity"). The server allowlist and sv_allow_content_commands policy apply.
float(value) Converts an integer, float, or invariant numeric string to a float.
floor(value) Greatest integer less than or equal to the numeric value.
format(pattern [, value1 [, value2]]) Invariant composite formatting. Example: format("{0}: {1:0.0}", name, score).
int(value) Converts an integer, float, or invariant integer string to an integer. Float conversion truncates toward zero.
lerp(a, b, t) Unclamped linear interpolation from a to b. Values of t outside 0..1 extrapolate.
max(value1 [, value2]) Maximum of one or more numeric arguments.
min(value1 [, value2]) Minimum of one or more numeric arguments.
random() Random float from zero, inclusive, to one, exclusive.
random(maximum) Random float between zero and maximum; reversed bounds are normalized.
random(minimum, maximum) Random float between the two bounds; reversed bounds are normalized.
random_int(maximum) Random integer from zero, inclusive, to maximum, exclusive. Maximum must be greater than zero.
random_int(minimum, maximum) Random integer from minimum, inclusive, to maximum, exclusive. Maximum must be greater than minimum.
remap(value, input_min, input_max, output_min, output_max) Unclamped mapping between numeric ranges. Equal input bounds are invalid.
round(value) Rounds a numeric value to the nearest integer using the runtime's midpoint rule.
shuffle(key, count) Returns every integer in 0..count-1 once in random order, then refills and reshuffles that key's bag. Count must be positive.
string(value) Converts a value to invariant text.
time() Current scaled game time in seconds.

The table has 20 function names; overload rows show the accepted forms of random and random_int. A wrong argument count or incompatible value reports a runtime diagnostic and produces no usable value.

Without seed, each map runtime starts with fresh random entropy. A declared seed makes random, random_int, and shuffle refills repeatable for the same execution order. Use a stable string key for a shared shuffle bag. Inside a module, shuffle(self, count) gives each expanded module instance its own bag.

Statement reference

Form Purpose
target.input or target.input(arguments) Invoke an entity, group, or module input.
call procedure(arguments) Run a top-level procedure and wait for it to return.
local_input(arguments) Invoke an input or procedure inside the current module instance.
name = expression Assign a map var or module state. Constants and parameters are immutable.
if / elif / else Run one conditional branch.
match / case / default Run the first equal case or the default branch.
wait duration Suspend only the current handler, procedure, or scheduled block.
schedule name after duration: Start or replace one named delayed task.
schedule name every duration [times count]: Start or replace one named repeating task.
cancel name Stop a named task if it exists.
emit output(arguments) Emit a declared module output.
server_command(expression) Run one allowlisted command from server authority.

Conditions and matching

on counter.changed(value: float):
    if value >= 10:
        door.open
    elif value > 0:
        light.on
    else:
        light.off

    match int(value):
        case 1:
            label.set_text("one")
        case 2:
            label.set_text("two")
        default:
            label.set_text("many")

Indentation defines the statement block. A type mismatch, unknown name, invalid call, missing ref, or wrong handler payload is a compile error.

Choose server or client authority

Handlers run on the server unless they begin with client. Use server handlers for rules, collision, objectives, and state changes. Use client handlers only for presentation entities whose contract allows client access.

ref trigger: trigger_multiple match Trigger
ref door: func_door match Door
ref local_audio: env_ambient match LocalAudio

server on trigger.start_touch:
    door.open

client on door.open:
    local_audio.play

Calls made by a handler keep that handler's authority. The compiler checks every entity input and output against its access rules. server_command() works only from server authority.

Procedures, waits, and schedules

def announce(message: string, delay: float = 0s):
    wait delay
    label.set_text(message)

on round.live:
    call announce("Fight!", 0.25s)
    schedule heartbeat every 1s times 10:
        light.toggle

on round.post_round:
    cancel heartbeat

Procedures and wait

A procedure declaration uses def, typed parameters, and optional defaults. call runs it inline: the caller resumes only after the procedure and all of its waits complete. Direct or indirect recursive calls are rejected at runtime.

wait duration evaluates a non-negative duration once and suspends only the current execution chain. Other handlers and named schedules continue. A zero duration resumes on the next frame. A wait has no task name and cannot be cancelled independently; put the work in a named schedule when it needs a cancellation handle.

Schedule syntax

One-shot form:

schedule open_warning after 750ms:
    warning_light.on
    call announce("Door opening")

Repeating forms:

schedule heartbeat every 1s:
    light.toggle

schedule countdown every 1s times 10:
    counter.subtract(1)

The schedule name must be an identifier. The interval and optional count are expressions, so they can read constants, variables, module state, parameters, and built-in functions. after cannot use times.

Rule Runtime behavior
First execution Both after and every wait for the interval before running the block. Neither form runs immediately.
Duration Negative or non-numeric durations stop the task with a diagnostic. Zero waits until the next frame.
Repeat count A positive integer runs exactly that many times. Zero schedules nothing. A negative integer repeats until cancellation or map unload. Omitting times from every is the same as an unlimited negative count.
Dynamic interval An every interval is re-evaluated before each iteration. every random(0.5s, 1.5s) therefore chooses a new delay each time.
Deadline timing Repeating schedules anchor each short wait to the previous scaled-time deadline, so ordinary frame overshoot does not accumulate into timer drift.
Long overruns If a block, nested procedure, or wait runs past its next deadline, the scheduler coalesces the missed time and waits one fresh interval. It does not burst missed executions in one frame.
Replacement Starting a schedule automatically cancels the task with the same name before registering the new one.
Cancellation cancel name stops the delay or running block. Cancelling a missing task is safe.
Locals and context The task copies the handler or procedure's parameter values and preserves authority plus !activator, !caller, and !self. Map variables and module state remain live shared state.
Module scope Schedule names declared inside a module are private to each expanded module instance. Two instances can both run timer_loop without replacing each other.
Lifetime All tasks stop when the map VSig runtime unloads.

This replacement behavior makes debounce straightforward:

on trigger.trigger:
    schedule close_after_quiet after 2s:
        door.close

Every new trigger restarts the two-second delay because it replaces close_after_quiet.

Milestone timelines

Use parallel one-shot schedules when a stage has actions at specific times. Each deadline is measured from the event that starts the stage; it does not depend on a repeating timer's frame-measured elapsed value.

ref zombie_teleport: trigger_teleport match ZombieTeleport
ref spawn_door: func_door match SpawnDoor
ref zombie_door: func_door match ZombieDoor

on round.pre_round:
    schedule island_intro after 1s:
        server_command("gm_message major Defend this island for 90 seconds")

    schedule island_warning after 80s:
        server_command("gm_message major 10 seconds until the gates open")

    schedule island_open after 90s:
        server_command("gm_message major The gates are open. GOGOGO")
        zombie_teleport.enable
        spawn_door.open

    schedule island_release after 115s:
        zombie_teleport.disable
        zombie_door.open

Running this handler again replaces all four named tasks and restarts the timeline. Give unrelated timelines distinct task names. Use an explicit guard such as a relay or latch when a repeatable entity output must not restart a stage.

Cancel round-owned repeating work on round.post_round or another lifecycle event that always closes the activity:

on round.post_round:
    cancel heartbeat
    cancel countdown

Reusable modules

A module contains parameters, private state, inputs, outputs, procedures, and handlers:

module capture_counter(start: float = 0, max_value: float = 10):
    state value: float = start
    output changed(value: float)
    output reached(value: float)

    input add(amount: float = 1):
        value = min(value + amount, max_value)
        emit changed(value)
        if value >= max_value:
            emit reached(value)

    input reset:
        value = start
        emit changed(value)

use capture_counter as captures(max_value = 3)

ref capture_point: game_capture_point
ref exit: func_door

on capture_point.captured:
    captures.add

on captures.reached(value: float):
    exit.open

Module parameters are immutable configuration. State is private mutable data; read it as instance.state_name. Inputs are typed procedures. Outputs carry typed payloads and work like entity outputs.

Put shared modules in .vsiglib files and import only the required module.

Standard logic library

The twelve standard logic modules are built into VSig. No library file, copy, or import is required. They work in both Vertex maps and existing Unity-authored maps. use creates an instance with its own state:

use counter as captures(max_value = 5)

Give every instance a unique alias; using the same alias twice is an error. Explicitly declared or imported custom modules take precedence over built-ins with the same name. Game-specific modules such as spawn_door_set still need their own library import.

Existing imports remain compatible, including Logic.vsiglib, Packages/com.vellocet.sdk/Runtime/VSig/Libraries/Logic.vsiglib, and Packages/com.vellocet.vertex/Runtime/VSig/Libraries/Logic.vsiglib. You can remove those imports when using the standard modules. An explicitly supplied custom library at one of those paths keeps its own definitions.

These modules are compiled logic. They do not create GameObjects or network objects. Recompile a map after changing its script so its embedded program is updated.

Module Use
counter Add, subtract, set, clamp, and report a numeric value.
accumulator Maintain an unclamped numeric total.
relay Gate, delay, or limit a string signal to one trigger.
timer Emit indexed ticks at fixed or randomized intervals.
selector Route a string to one of eight configured cases.
random_selector Pick a zero-based random or shuffled index.
sequence Walk an integer range once or in a loop.
latch Model idempotent set and clear transitions.
toggle Store, flip, or directly set a boolean value.
state_machine Track named state transitions and their previous value.
debounce Emit only after a quiet delay following the latest trigger.
cooldown Accept one trigger, then reject triggers until ready again.

counter

counter(start: float = 0, min_value: float = 0, max_value: float = 1, step: float = 1, clamp_value: bool = true)

The exported value state starts at start. Each input that applies a value emits changed(value). It then emits min_reached(value) when the result is at or below min_value and max_reached(value) when it is at or above max_value. When clamp_value is true, applied values are clamped to the configured range.

Input Behavior
add(amount: float = step) Add amount to the current value.
subtract(amount: float = step) Subtract amount from the current value.
set(next_value: float) Replace the current value.
reset Apply the configured start value.

accumulator

accumulator(start: float = 0) exports its current value. add(amount: float), set(next_value: float), and reset update that value and emit changed(value: float). It does not clamp or emit threshold signals; use counter when a range matters.

relay

relay(enabled_on_load: bool = true, trigger_once: bool = false, delay: float = 0) exports enabled and fired state. An accepted trigger(value: string = "") emits triggered(value: string) immediately or after delay. A new delayed trigger replaces the instance's pending delayed emission.

Input Behavior
trigger(value: string = "") Emit only while enabled and, when trigger_once is true, only before the instance has fired.
enable Accept later triggers.
disable Reject later triggers and cancel a pending delayed emission.
reset Clear fired and cancel a pending delayed emission without changing enabled.

timer

timer(interval: float = 1s, times: int = -1, lower_bound: float = interval, upper_bound: float = interval, start_on_load: bool = false)

Use timer.tick for recurring gameplay milestones. Its one-based integer index advances exactly once for every delivered tick. The elapsed value remains available for display, progress, and diagnostics, but it is measured scaled frame time and is not guaranteed to equal a whole number.

ref gate: func_door match Gate

use timer as gate_timer(interval = 1s, times = 90, start_on_load = false)

on round.live:
    gate_timer.restart

on gate_timer.tick(index: int, elapsed: float, remaining: int):
    if index == 80:
        server_command("gm_message major 10 seconds until the gates open")
    if index == 90:
        gate.open

on round.post_round:
    gate_timer.reset

Do not compare elapsed with exact equality, such as elapsed == 90, because a frame may deliver the tick at 90.016. The compiler reports VSIG_TIMER_ELAPSED_EQUALITY when a timer elapsed parameter is used with ==, !=, or as the direct value of a match block.

Input Behavior
start Start a reset timer or resume a stopped timer from its preserved elapsed time, index, and remaining count. Calling it while running is safe and does nothing.
stop Cancel the active wait while preserving elapsed time, index, and remaining count.
restart Cancel, reset to index zero, and start from the beginning.
reset Cancel and restore elapsed time and index to zero without starting.
fire Emit tick immediately and advance the index without consuming a scheduled repeat.
toggle Stop a running timer or start/resume a stopped timer.
set_interval(value: float) Set one fixed non-negative interval for later waits.
set_random_bounds(lower: float, upper: float) Set normalized non-negative bounds; each wait samples a fresh interval.
set_times(value: int) Set the repeat count used by the next reset. When the timer is stopped, also replace its current remaining count. A negative count repeats until stopped or unloaded.

A finite timer emits finished() after its last scheduled tick(index: int, elapsed: float, remaining: int). Call restart, not start, to run a completed timer again. The exported running, remaining, tick_index, and elapsed states report status. started_at, current_interval, current_lower_bound, current_upper_bound, and repeat_count expose the backing schedule configuration; prefer the timer inputs when changing it.

selector

selector(case_1: string = "", case_2: string = "", case_3: string = "", case_4: string = "", case_5: string = "", case_6: string = "", case_7: string = "", case_8: string = "") compares select(value: string) with the configured cases in order. The first match emits selected(index: int, value: string) with a one-based case index. No match emits defaulted(value: string). The module has no state.

random_selector

random_selector(count: int = 2, shuffle_mode: bool = false) exposes pick and emits selected(index: int) with a zero-based index from 0 through count - 1. The exported last state starts at -1 and records the last emitted index. count must be positive.

With shuffle_mode = false, each pick is independent. With shuffle_mode = true, the instance emits every index once in randomized order before refilling its private shuffle bag.

sequence

sequence(length: int = 1, start_index: int = 0, loop: bool = false) exports its current index. While index < length, next emits step(index: int) and advances the index. Reaching the exclusive length emits finished(); a looping sequence then returns to start_index. reset returns to start_index without emitting an output.

latch

latch(initial: bool = false) exports active state and represents idempotent set/clear transitions.

Input Outputs
set When inactive, set active and emit latched() followed by changed(true). When already active, emit nothing.
clear When active, clear active and emit released() followed by changed(false). When already inactive, emit nothing.
reset Restore initial and emit changed(active).

toggle

toggle(initial: bool = false) exports boolean value state. flip inverts it, set(value_to_set: bool) replaces it, and reset restores initial. Every input emits changed(value: bool), including set when the supplied value already matches.

state_machine

state_machine(initial: string = "") exports current and previous state. set(next: string) does nothing when next equals current. A real transition updates both states, then emits exited(previous: string), entered(current: string), and changed(previous: string, current: string) in that order. reset records the old current value as previous, restores initial, and emits only changed(previous, current).

debounce

debounce(delay: float = 0.1s) exposes trigger(value: string = "") and cancel. Each trigger replaces the pending wait. If no newer trigger or cancellation arrives during delay, the module emits fired(value: string) with the latest payload. It has no exported state.

cooldown

cooldown(duration: float = 1s, ready_on_load: bool = true) exports ready state. While ready, trigger(value: string = "") clears readiness, emits fired(value: string), waits for duration, restores readiness, and emits became_ready(). Triggers received while not ready do nothing. reset cancels the wait, sets ready, and emits became_ready() immediately.

Shipped spatial helper

Grimwar's Order and Union spawn-door prefabs include a logic_script host for spawn_door_set. Place one from Vertex's Prefabs browser. It binds the trigger, matching doors, and audio under that instance; you do not need a source-level import in the map script.

The helper exposes restore_initial_state, enable, and disable. Give the host a unique explicit alias if the map script needs to call it. Keep the assembly's expected child hierarchy when editing it.

Spatial modules and logic_script

A spatial module declares local refs and binds each instance to a scene hierarchy:

module gate:
    ref trigger: trigger_multiple
    ref door: func_door

    on trigger.start_touch:
        door.open

    input reset:
        door.close

use gate as exits match Gates/Exit_*

A wildcard use creates one module instance per matching root and a broadcast group named by the authored alias.

Place a logic_script marker when a map should configure a module without a source-level use. Set the .vsiglib, module name, alias, arguments, and local-ref binding mode on the marker. The compiler expands it into the same form as a written use. It does not become a runtime entity.

Map and round lifecycle

Lifecycle sources need no scene marker:

once on map.ready:
    doors.close

on round.warmup:
    warmup_lights.on

on round.pre_round:
    round_timer.reset

on round.live:
    round_timer.start

client on round.overtime:
    overtime_alarm.play

on round.post_round:
    cancel heartbeat

on round.intermission:
    doors.open

Available lifecycle sources are map.ready, round.warmup, round.pre_round, round.live, round.overtime, round.post_round, and round.intermission.

Use once on map.ready to capture an authored entity's initial state or perform setup that must run once per map load.

Safe server commands

server_command() sends one command through the server's content-accessible command boundary:

once on map.ready:
    server_command("mp_roundtime 3")
    server_command("sv_gravity -6")
    server_command("say Map rules loaded")

on door.open:
    server_command("gm_message major DOOR HAS BEEN OPENED")

The server allows a curated set of match timing, chat or HUD messages, round completion, friendly fire, movement tuning, class enforcement, and weapon-slot rules. It rejects semicolons, newlines, secret-like identifiers, and commands without the ContentAccessible flag. The runtime restores changed console variables when the map unloads.

Server operators can disable map commands with sv_allow_content_commands 0. convar() reads use the same allowlist.

Snapshot a participant zone

trigger_multiple.execute captures the participants currently inside the volume and emits executed with an opaque snapshot token. Use it for extraction zones and other one-shot outcomes that a VMod must award to specific occupants.

Name the trigger and the entity that completes the objective, then connect them in the scene script:

ref extraction_zone: trigger_multiple match ExtractionZone
ref extraction_door: func_door match ExtractionDoor

server on extraction_door.close:
    extraction_zone.execute

Set Allowed Activators and Allowed Teams on the trigger so the snapshot includes only the intended participants. The close output occurs after the door finishes closing. execute still emits an empty snapshot when nobody is inside, which lets the game mode resolve a failed extraction deterministically.

The subscribed VMod must call context.VSig.GetParticipantSnapshot(token) during the matching callback. The token expires when signal dispatch returns. See Send signals to VSig for the callback and objective-award example.

Signals shared with VMod

VMods can emit a source under mod:<mod-id>. Handle that signal like any other output:

on mod:community.hello.round_started(mode_id: string):
    status_label.set_text("Mode: {0}", mode_id)

on mod:community.hello.stage.opened(index: int, gate_name: string):
    gate_counter.set(index)

The first handler receives output round_started from source mod:community.hello. The second receives output opened from child source mod:community.hello.stage.

A VMod can subscribe to a map entity or module output by source alias and output name. This exchange carries scalar values only. It does not expose scene objects to VMod code.

Compile and test

Use the Code workspace to resolve syntax and binding errors, then build through Play in Game or Export add-on. The build validates against the entities captured for that map and embeds the compiled program. Save external source edits before starting the build.

Check wildcard matches, exercise round handlers more than once, and cancel repeating schedules at their intended lifecycle boundary. Editing loose source beside an installed package does not change the compiled map.