Copyright © 2023-Present Durmuş Cenker Cennet (cenullum)
https://www.cenullum.com
cenker's mod is built with Godot Engine, Lua 5.4, and Weasel Games Godot Lua API.
cenkers_mod_stub.lua (included in
every mod folder) contains detailed API type definitions and documentation comments for all available
functions. You can use this file with external editors that support luaLSP for features
like code suggestions and code completion, as there is currently
no built-in LSP support in cenker's mod.
Table of Contents
1. Introduction
cenker's mod is not a game engine. It is a package that has multiple multiplayer mods within it. Users can create and host their own mods using Lua 5.4 scripting. cenker's mod handles networking, physics, rendering, and input management, allowing you to focus on creating unique experiences.
- base - Basic functions (print, type, pairs, ipairs, etc.)
- table - Table manipulation
- string - String operations
- math - Mathematical functions
get_os_time() for time, save_json() for file I/O).
Key Features
- Multiplayer Support: Built-in Steam networking with host-client architecture
- Lua 5.4 Scripting: Write game logic in Lua with full API access
- Entity System: Flexible entity-component architecture
- Network Synchronization: Automatic entity synchronization across clients
- Physics System: 2D physics for collisions and movement (max velocity: 300)
- UI Framework: Dynamic panel and widget creation system
- Audio System: 2D positional audio with effects
- Voice Chat: Integrated voice communication system
Example Mods
The game comes with three example mods that demonstrate different gameplay styles:
- Football: A competitive team-based sports game
- Hook Up: A physics-based platformer with grappling hook mechanics and fishing minigame
- Campfire Survivors: A cooperative survival game with wave-based enemies
2. Getting Started
Mod Structure
Each mod follows this directory structure:
mods/YourModName/
├── general/
│ ├── scripts/ # Lua entity scripts
│ │ ├── user.lua # Player entity (required)
│ │ ├── world.lua # World singleton (recommended)
│ │ └── ... # Your custom entities
│ ├── images/ # PNG textures
│ └── sounds/ # OGG audio files
├── maps/ # Tilemap definitions
└── cenkers_mod_stub.lua # API type definitions (optional)
user.lua script is mandatory for player entities. The
world.lua singleton is recommended for managing game state.
Your First Entity
Create a simple entity in general/scripts/my_entity.lua:
-- Set entity properties at the top (MUST be primitive values)
network_mode = 2 -- DYNAMIC: synced continuously
z_index = 5 -- Rendering order (must be between -999 and 999)
-- Called every frame
function _process(delta, inputs)
-- Your game logic here
if IS_LOCAL then
-- Only execute on local player's machine
end
if IS_HOST then
-- Only execute on host machine
end
return inputs
end
-- Initialize entity
set_image({
parent_name = name,
name = "my_sprite",
image_path = "my_texture", -- loads my_texture.png
scale = Vector2(32, 32)
})
set_collision({
parent_name = name,
shape = "circle",
size = 16
})
3. Game Architecture
3.1 Entities
Entities are the fundamental building blocks of your game. Each entity is a Lua script that defines behavior and properties. The game automatically creates a RigidBody2D node for each entity.
local are global to that entity only. There is no shared global table
(_G) between entities - each entity's globals are isolated from other entities.
- Maximum velocity for any entity is 300 (linear velocity magnitude is capped)
- Z-index values must be between -999 and 999
Entity Properties
| Property | Type | Description | Default |
|---|---|---|---|
network_mode |
integer | 0=NONE (local), 1=STATIC (sync on join), 2=DYNAMIC (continuous sync) | 0 |
speed |
number | Movement speed multiplier | 15 |
mass |
number | Physics mass | 1 |
gravity_scale |
number | Gravity multiplier (0.0 = no gravity) | 0.0 |
linear_damp |
number | Movement friction | 1.0 |
bounce |
number | Bounciness (0.0-1.0) | 0 |
friction |
number | Surface friction | 1 |
Example: Football Player
-- From Football mod: user.lua
bounce = 0.8
friction = 0.1
lock_rotation = true
add_tag(name, "user")
function _process(delta, inputs)
-- Handle player input
if inputs["key_7"] then
hit_value = math.min(hit_value + 100*delta, 100)
end
return inputs
end
3.2 Singletons
Singletons are special entities that exist once per game and manage global state. They are prefixed with
- and short names are recommended for network efficiency.
-- Define as singleton at the top of your script
singleton_name = "-game_manager"
-- Singletons are accessible from any entity
run_function("-game_manager", "start_game", {})
-gm, -ui) to
reduce network traffic.
3.3 Network Modes
Network synchronization is controlled by the network_mode variable:
| Mode | Value | Description | Use Case |
|---|---|---|---|
| NONE | 0 | Local only, not synchronized | Client-side UI, visual effects |
| STATIC | 1 | Synchronized only when players join. Movement is NOT synced. | Static world objects, decorations that don't move |
| DYNAMIC | 2 | Continuously synchronized. Position, rotation, and velocity are synced automatically. | Players, projectiles, moving objects |
local).
Network Functions
Network functions use suffixes to control execution:
| Suffix | Who Can Call | Where It Runs | With Steam ID Parameter |
|---|---|---|---|
_HOST |
Host or Client | Host Only | No effect |
_ALL |
Host Only | All Clients (Including Host) | Runs only on host and specific client |
_CLIENT |
Host Only | All Clients (Excluding Host) | Runs only on specific client |
- Function name MUST end with
_HOST,_ALL, or_CLIENT - First parameter MUST be named
sender_id(the Steam ID of who called the function) - Network functions without these suffixes will NOT work
function my_function_HOST(sender_id, other_params)
-- sender_id is the Steam ID of who called this
end
Network Function Examples
Example 1: Simple Weapon System
-- BAD: Client directly modifies gameplay
function fire_weapon()
if IS_LOCAL then
spawn_entity_local({t = "bullet"}) -- Client can cheat!
end
end
-- GOOD: Client requests, host validates
function fire_weapon()
if IS_LOCAL then
run_network_function(name, "request_fire_weapon_HOST", {})
end
end
function request_fire_weapon_HOST(sender_id)
if sender_id ~= name then return end -- Verify sender
-- Check cooldown using Unix time (seconds)
local current_time = get_os_time() -- Returns Unix timestamp in seconds
if current_time - last_fire_time < 1 then return end -- 1 second cooldown
last_fire_time = current_time
-- Spawn on host
spawn_entity_host({
t = "bullet",
p = get_value("", sender_id, "position"),
owner = sender_id
})
end
Example 2: Hook System (from Hook Up Mod)
-- From Hook Up mod: user.lua
-- Client requests to fire hook
function fire_hook_to_mouse()
if IS_LOCAL then
run_network_function(name, "request_fire_hook_HOST", {hook_power})
end
end
-- Host validates and processes the request
function request_fire_hook_HOST(sender_id, requested_power)
if sender_id ~= name then return end -- Validate sender
if hook_state ~= "READY" then return end -- Validate state
-- Clamp power to prevent cheating
local clamped_power = math.max(hook_power_min,
math.min(requested_power, hook_power_max))
-- Calculate and apply velocity
local direction = (mouse_position - position).normalized()
local velocity = direction * clamped_power
unfreeze_entity(hook_entity_name, true)
change_instantly({
entity_name = hook_entity_name,
linear_velocity = velocity,
position = position
})
-- Sync state to all clients
run_network_function(sender_id, "set_hook_state_ALL", {"FIRING"})
end
-- All clients update their hook state
function set_hook_state_ALL(sender_id, new_state)
hook_state = new_state
if new_state == "FIRING" then
run_function(hook_entity_name, "activate_hook", {})
if IS_LOCAL then
set_camera_target(hook_entity_name)
end
end
end
4. Lua API Reference
4.1 Global Variables
These variables are automatically available in every entity script:
| Variable | Type | Description |
|---|---|---|
name |
string | Unique entity name (for users, this is their Steam ID) |
script_name |
string | Script filename without .lua extension |
position |
Vector2 | Current world position |
rotation |
number | Current rotation in radians |
linear_velocity |
Vector2 | Current movement velocity |
IS_LOCAL |
boolean | True if this is the local player's entity |
IS_HOST |
boolean | True if running on host machine |
LOCAL_STEAM_ID |
string | Local player's Steam ID |
HOST_STEAM_ID |
string | Host's Steam ID |
MOD_PATH |
string | Absolute path to current mod directory |
position, rotation, linear_velocity, and
angular_velocity, you can read them directly by name without using
get_value(). However, you MUST still use set_value() when modifying them so
the game can detect and process the changes.
-- Reading: Direct access is faster
local pos = position -- Instead of get_value("", name, "position")
local vel = linear_velocity -- Instead of get_value("", name, "linear_velocity")
-- Writing: Must use set_value() for game to detect changes
set_value("", name, "position", Vector2(100, 200))
set_value("", name, "linear_velocity", Vector2(50, 0))
4.2 Callback Functions
Define these functions in your entity to receive events:
_process(delta, inputs)
Called every frame. Return modified inputs dictionary or nil.
_process runs every frame and consumes significant
processing power. Avoid calling expensive functions like set_label, set_image,
or set_progress_bar inside _process unless values actually change. Cache
previous values and only update UI when needed.
function _process(delta, inputs)
-- delta: time since last frame in seconds
-- inputs: table with the following keys:
-- key_1 to key_13: boolean (various buttons, see input table below)
-- stick_1: Vector2 -- left stick / WASD (movement)
-- stick_2: Vector2 -- right stick / mouse position in world space
if IS_LOCAL then
-- Check input
if inputs["key_6"] then -- Interact key (E / Square / Y)
-- Perform action
end
-- Movement using stick_1
local movement = inputs["stick_1"]
if movement.x ~= 0 or movement.y ~= 0 then
-- Apply movement
end
end
return inputs -- or nil to leave unchanged
end
Input System and Gamepad Support
The game comes with full gamepad integration and virtual mouse support. Players can use keyboard/mouse, Nintendo Switch controllers, Xbox controllers, or PlayStation controllers. The input mappings are automatically handled by the game.
key_1 through key_13, stick_1, stick_2) and the
game handles the rest. For detailed gamepad mappings, refer to gamepad.ods in the docs
folder.
| Input Name | Default PC Key | Xbox | PlayStation | Nintendo Switch | Suggested Use |
|---|---|---|---|---|---|
key_1 |
1 | D-Pad Right | D-Pad Right | D-Pad Right | Item selection |
key_2 |
2 | D-Pad Left | D-Pad Left | D-Pad Left | Item selection |
key_3 |
3 | D-Pad Down | D-Pad Down | D-Pad Down | Item selection |
key_4 |
Q | D-Pad Up | D-Pad Up | D-Pad Up | Switch previous item |
key_5 |
R | Y | Triangle | X | Reload |
key_6 |
E | X | Square | Y | Interact |
key_7 |
F | B | Circle | A | Dash |
key_8 |
SPACE | A | X | B | Jump |
key_9 |
RIGHT MOUSE | LB | L1 | L | Secondary fire/action |
key_10 |
SHIFT | LT | L2 | ZL | Sprint |
key_11 |
G | L3 (Left Stick Click) | L3 (Left Stick Click) | L3 (Left Stick Click) | Drop |
key_12 |
LEFT MOUSE | RB | R1 | R | Primary fire/action |
key_13 |
CTRL | RT | R2 | ZR | Crouch |
stick_1 |
WASD | Left Stick | Left Stick | Left Stick | Movement controls |
stick_2 |
Mouse Position | Right Stick | Right Stick | Right Stick | Mouse position in world space |
_on_user_initialized(steam_id, nickname)
Called when a user finishes joining and is ready to receive network calls.
function _on_user_initialized(steam_id, nickname)
if IS_HOST then
if steam_id == name then
-- This user just finished initializing
local player_color = generate_random_color()
run_network_function(name, "create_user_ALL", {player_color})
else
-- Send existing data to new player
run_network_function(name, "sync_state_CLIENT", {current_state}, steam_id)
end
end
end
_on_user_disconnected(steam_id, nickname)
Called when a user leaves the game.
function _on_user_disconnected(steam_id, nickname)
if steam_id == name then
-- Clean up this user's entities
destroy("", hook_entity_name)
destroy_line(hook_line_name)
end
end
add_command(entity_name, function_name, command_name, description, chat_executable)
Registers a command to the centralized command system. Commands are reset when the map is unloaded.
Note: Do not include the / prefix in the command_name; it is added
automatically for chat.
entity_name: The name of the singleton or entity (e.g.,"-cmd").function_name: The function inside that entity to execute.command_name: The name of the command without prefix (e.g.,"rules").description: A brief description (shown in/help). If your command has parameters, describe them here (e.g."Teleport to [x] [y]").chat_executable: Boolean. If true, it can be executed via chat with a/prefix.
add_command cannot execute network functions directly via
run_network_function. If you need to trigger network logic from a command, you must use a
local intermediate (wrapper) function as the callback, which then calls
run_network_function.
-- Typical usage (at top of script)
add_command("-cmd", "show_rules", "rules", "Shows Finding Liar game rules", true)
-- Users can now type '/rules' in chat or 'rules' in the console.
-- Callback function example:
function show_rules(sender_id, arg1, arg2)
-- arg1, arg2 etc. are strings passed from the chat/console
add_to_chat("Rules: Be nice!", false)
end
_on_chat_message_received(sender_id, nickname, message)
Called when a chat message is received. Supports chaining between multiple mods.
add_command) before calling this function.
If a message matches a registered command, this handler will not be triggered for that message.
message argument is already formatted with the
player's nickname and color (e.g., [color=#66FF99]username[/color]: actual message).
- Return
nil: Mod performs no action. Passes the current message to the next mod. - Return
"": Mod suppresses the message. The chain stops, and nothing is displayed. - Return string: Mod modifies the message. The new string is passed to the next mod.
function _on_chat_message_received(sender_id, nickname, message)
-- Example 1: Suppress messages from a specific user
if sender_id == "76561198..." then
return ""
end
-- Example 2: Profanity Filter (Censorship)
-- Replaces "your mom" with "***" (case-insensitive)
local censored_message = message:gsub("[Yy][Oo][Uu][Rr] [Mm][Oo][Mm]", "***")
-- Example 3: Emoji Replacement
-- Replaces ":)" with a smiling face symbol
local final_message = censored_message:gsub(":%s?%)", "😊")
-- Example 4: Add a custom prefix
return "[GLOBAL] " .. final_message
end
_on_gamepad_connection_changed(has_gamepad)
Called when gamepad is connected/disconnected or display style changes.
function _on_gamepad_connection_changed(has_gamepad)
if IS_LOCAL then
-- Refresh button prompts
set_label({
name = "tutorial_label",
text = "Press @key_7@ to interact\n@stick_1@ to move"
})
end
end
4.3 Entity Management
spawn_entity_local(config)
Spawn an entity locally (client-side only, not synced).
-- Spawn a local particle effect
local particle = spawn_entity_local({
t = "particle_effect", -- script name
p = Vector2(100, 100), -- position
r = 0, -- rotation in radians
v = Vector2(0, 0), -- linear velocity
a = 0, -- angular velocity
-- Custom properties:
duration = 2.0,
color = Color(1, 0, 0, 1)
})
spawn_entity_host(config)
Spawn a network-synchronized entity (Host only).
-- From Campfire Survivors: spawn monsters
if IS_HOST then
local monster = spawn_entity_host({
t = "monster",
p = spawn_position,
hp = 100,
damage = 10,
speed = 20
})
end
destroy(parent_name, entity_name, notify_network)
Destroy an entity and its children.
-- Destroy a networked entity
if IS_HOST then
destroy("", bullet_name, true) -- Notify clients
end
-- Destroy a local entity
destroy("", effect_name, false)
freeze_entity(entity_name, all)
Freeze an entity's physics (stops movement).
-- From Hook Up: freeze hook when attached
freeze_entity(hook_entity_name, true) -- Sync to all clients
4.4 Network Functions
run_network_function(entity_name, function_name, parameters, steam_id)
Execute a function across the network.
-- Host broadcasts to all clients
if IS_HOST then
run_network_function("user_steam_id", "update_score_ALL", {new_score})
end
-- Client sends request to host
if IS_LOCAL then
run_network_function(name, "request_action_HOST", {action_data})
end
-- Host sends to specific client
if IS_HOST then
run_network_function(name, "show_message_CLIENT",
{"Welcome!"}, target_steam_id)
end
Important: User Entity Names Are Steam IDs
In user scripts (user.lua), the name variable contains the player's Steam ID. This is why
callbacks like _on_user_initialized, _on_user_disconnected, and
_on_user_banned often check if steam_id == name - these callbacks run on ALL
user entities, so you need to verify which user the event belongs to.
-- These callbacks run on EVERY user entity
function _on_user_disconnected(steam_id, nickname)
if steam_id == name then -- Check if THIS user disconnected
-- Clean up this user's entities
destroy("", hook_entity_name)
stop_timer("shoot_timer"..name)
end
end
function _on_user_initialized(steam_id, nickname)
if steam_id == name then -- Check if THIS user just joined
-- Initialize this user
run_network_function(name, "setup_player_ALL", {})
end
end
Targeting User Entities: When calling network functions on user entities, use the player's Steam ID as the entity name:
-- target_steam_id is a specific player's Steam ID
run_network_function(target_steam_id, "change_team_ALL", {team_id})
-- In the user.lua script, 'name' equals the Steam ID
run_network_function(name, "update_health_ALL", {new_health})
4.5 UI System
create_panel(config)
Create a custom UI window/panel.
-- From Hook Up: inventory panel
local panel_name = create_panel({
name = "_inventory_panel",
title = "Inventory",
text = "Your items:",
resizable = true,
close = true,
minimum_size = Vector2(400, 300),
is_scrollable = true,
color = Color(0.2, 0.2, 0.2, 0.9)
})
add_button_to_panel(panel_name, config)
Add an interactive button to a panel.
add_button_to_panel("_upgrade_panel", {
entity_name = name,
function_name = "select_upgrade",
text = "Increase Speed (+20%)",
extra_args = {upgrade_type = "speed", value = 0.2},
color = Color(0.3, 0.5, 0.7)
})
set_label(config)
Create or update a text label.
-- From Campfire Survivors: health display
if IS_LOCAL then
set_label({
name = "_health_label",
text = string.format("Health: %d/%d", current_health, max_health),
position = Vector2(20, 20),
size = Vector2(200, 20),
font_size = 16,
font_color = Color(1, 1, 1, 1),
outline_size = 2,
outline_color = Color(0, 0, 0, 1)
})
end
Input Display Names
Use @key_N@ or @stick_N@ in text to show context-aware button prompts:
-- Shows keyboard key or gamepad button automatically
set_label({
text = "Press @key_7@ to interact\n@stick_1@ to move"
})
4.6 Audio System
set_audio(config)
Play audio with various options.
-- Play 2D positional sound
set_audio({
stream_path = "explosion", -- loads explosion.ogg
position = explosion_pos,
is_2d = true,
volume = 0, -- in decibels (-80 to 24)
pitch_scale = 1.0,
random_pitch = 0.2, -- randomize pitch by ±20%
max_distance = 500
})
-- Play global sound
set_audio({
stream_path = "background_music",
is_loop = true,
volume = -10,
bus = "Music"
})
5. Practical Examples
Example 1: Creating a Projectile (from Hook Up)
-- hook_projectile.lua
linear_damp = 0
gravity_scale = 0.5
lock_rotation = true
network_mode = 2 -- DYNAMIC: synced continuously
-- Owner data (set when spawned)
-- owner_name = "steam_id_of_player"
add_tag(name, "hook")
-- Create visuals and collision
function create_hook_components()
set_image({
parent_name = name,
name = "hook_image",
image_path = "hook",
modulate = modulate or Color(0.7, 0.7, 0.9, 1),
visible = false
})
set_collision({
parent_name = name,
name = "hook_collision",
shape = "circle",
size = 10,
collision_layer = {3},
collision_mask = {1, 4} -- Walls and water
})
end
create_hook_components()
function activate_hook()
set_image({
parent_name = name,
name = "hook_image",
visible = true
})
set_collision({
parent_name = name,
name = "hook_collision",
disabled = false
})
end
function deactivate_hook()
set_value("", name, "linear_velocity", Vector2(0, 0))
set_image({
parent_name = name,
name = "hook_image",
visible = false
})
set_collision({
parent_name = name,
name = "hook_collision",
disabled = true
})
end
-- Handle collision
function on_body_body_entered(collision_info)
if not IS_HOST then return end
local owner_state = get_value("", owner_name, "hook_state")
if owner_state ~= "FIRING" then return end
local other_entity = collision_info.body_name
if has_tag(other_entity, "user") then return end
-- Attach hook
freeze_entity(name, true)
run_network_function(owner_name, "set_hook_state_ALL", {"ATTACHED"})
end
-- Handle area collision (water detection)
function on_body_area_entered(area_name)
if not IS_HOST then return end
local owner_state = get_value("", owner_name, "hook_state")
if owner_state ~= "FIRING" then return end
if has_tag(area_name, "sea") or has_tag(area_name, "lake") then
run_network_function(owner_name, "set_hook_state_ALL", {"SEARCHING"})
-- Determine water type for fishing
local water_source = has_tag(area_name, "sea") and "sea" or "lake"
run_function("-fishing_game", "start_searching_for_player",
{owner_name, water_source})
end
end
Example 2: Stat System (from Campfire Survivors)
-- user.lua excerpt
-- Define default stats as global variables (each entity has its own globals)
movement_speed = 40
max_health = 100
current_health = 100
armor = 0
damage = 30
attack_speed = 1.0
projectile_count = 1
-- Note: Each entity has its own separate global scope
-- There is no shared _G table between entities
set_value("", name, "speed", movement_speed)
-- Called by upgrade system when stats change
function update_stats(stats)
-- Update global variables in this entity's scope
movement_speed = stats.movement_speed
max_health = stats.max_health
current_health = stats.current_health
armor = stats.armor
damage = stats.damage
attack_speed = stats.attack_speed
projectile_count = stats.projectile_count
-- Update derived values
set_value("", name, "speed", movement_speed)
if IS_LOCAL then
set_progress_bar({
name = "_health_progress_bar",
max_value = max_health,
value = current_health
})
end
-- Restart shooting timer with new attack speed
if IS_HOST then
stop_timer("shoot_timer"..name)
start_timer({
entity_name = name,
timer_id = "shoot_timer"..name,
wait_time = 1.0 / attack_speed,
function_name = "shoot_at_nearest_monster"
})
end
end
-- Shooting function
function shoot_at_nearest_monster()
if is_dead then return end
local nearest = get_nearest_entity_by_tag(name, "monster")
if not nearest or not nearest.name then return end
for i = 1, projectile_count do
local angle_offset = 0
if projectile_count > 1 then
angle_offset = (i - 1) * (2 * math.pi / projectile_count)
end
spawn_entity_host({
t = "bullet",
p = position,
r = nearest.angle + angle_offset,
steam_id = name,
damage = damage,
speed = projectile_speed,
penetration = projectile_penetration
})
end
end
Example 3: Team System (from Football)
-- user.lua excerpt
team = 0 -- 0=spectator, 1=red, 2=blue
function change_team_ALL(sender_id, _team)
team = _team
if team == 0 then -- Spectator
if IS_LOCAL then
set_camera_target("*ball")
end
delete_visuals()
return
end
-- Create player visuals
collision_name = set_collision({
parent_name = name,
shape = "circle",
size = 16
})
if IS_LOCAL then
set_camera_target(name)
end
nickname_label_name = set_label({
parent_name = name,
text = nickname,
position = Vector2(-256, 16),
size = Vector2(512, 16),
horizontal_alignment = 1
})
image_name = set_image({
parent_name = name,
image_path = is_avatar_loaded and name or "",
scale = Vector2(32, 32)
})
-- Apply team color
local color = team == 1 and Color(1, 0, 0, 1) or Color(0, 0, 1, 1)
set_shader({
parent_name = name,
image_name = image_name,
shader_name = "circle",
outline_color = color
})
reset_position()
end
function reset_position()
if not IS_HOST then return end
local spawn_area = team == 1 and "red_spawn_area" or "blue_spawn_area"
local new_pos = get_random_position_in_polygon(spawn_area)
change_instantly({
entity_name = name,
position = new_pos,
linear_velocity = Vector2(0, 0),
rotation = 0,
angular_velocity = 0
})
end
6. Best Practices
Important Limitations and Rules
- Max Velocity: Entity velocity is capped at 300. Values above this are unnecessary.
- Z-Index Range: All z_index values must be between -999 and 999.
- User Data Types in Tables: Vector2 and Color types CANNOT be used as table keys - you will get errors. Use strings or numbers as keys instead.
- Local Variables: Variables declared with
localkeyword cannot be accessed withget_valueorset_value. Only global entity variables can be accessed this way. - Return Values:
run_functionandget_valuecannot return multiple values directly. To return multiple values, wrap them in a table. - Top Variables Must Be Primitive: Variables declared at the top of the script
(before any function, comment, or blank line) must be primitive types (numbers, strings,
booleans). Special configuration variables like
network_mode,singleton_name,area_radius, etc. are parsed before script execution.
-- BAD: Vector2 as table key
local my_table = {}
my_table[Vector2(10, 20)] = "value" -- ERROR!
-- GOOD: String as table key
local my_table = {}
my_table["10,20"] = "value"
-- BAD: Returning multiple values
function get_position_and_health()
return position.x, position.y, health -- Only first value returned!
end
-- GOOD: Return table with multiple values
function get_position_and_health()
return {x = position.x, y = position.y, health = health}
end
-- BAD: Local variable with get_value
local secret_value = 100
local val = get_value("", name, "secret_value") -- Returns nil!
-- GOOD: Global variable with get_value
secret_value = 100
local val = get_value("", name, "secret_value") -- Works!
Performance Optimization
- Minimize network calls: Batch updates instead of sending individual values
- Use local entities for effects: Particles and UI don't need to be synced
- Cache frequently accessed values: Store entity variables locally instead of calling
get_valuerepeatedly - Use appropriate network modes: Static entities don't need continuous sync
-- BAD: Repeated get_value calls
function _process(delta, inputs)
local target_pos = get_value("", target_name, "position")
local target_health = get_value("", target_name, "health")
local target_team = get_value("", target_name, "team")
-- Expensive repeated lookups every frame!
end
-- GOOD: Cache values when they change
target_pos_cache = Vector2(0, 0)
target_health_cache = 100
target_team_cache = 1
function on_target_updated_ALL(sender_id, pos, health, team)
target_pos_cache = pos
target_health_cache = health
target_team_cache = team
end
function _process(delta, inputs)
-- Use cached values - much faster!
local distance = (target_pos_cache - position).length()
end
-- BAD: Repeated network calls
for i = 1, 10 do
run_network_function("user", "update_value_ALL", {i})
end
-- GOOD: Single batched call
local batch = {}
for i = 1, 10 do
table.insert(batch, i)
end
run_network_function("user", "update_batch_ALL", {batch})
Entity Lifecycle
-- Proper cleanup on disconnect
function _on_user_disconnected(steam_id, nickname)
if steam_id == name then
-- Clean up all associated entities
destroy("", hook_entity_name)
destroy("", cursor_name)
destroy_line(hook_line_name)
stop_timer("shoot_timer"..name)
end
end
Error Handling
-- Always validate entity existence
local target_pos = get_value("", target_name, "position")
if not target_pos then
print("Warning: Target entity not found: " .. target_name)
return
end
-- Check for nil values
local nearest = get_nearest_entity_by_tag(name, "enemy")
if nearest and nearest.name and nearest.name ~= "" then
-- Safe to use nearest.name
end
Debugging
-- Use print for debugging (shows in console)
print("DEBUG: Player fired weapon at position:", position.x, position.y)
-- Use chat for in-game debugging
if IS_HOST then
add_to_chat("[DEBUG] Score updated: " .. score, false)
end
7. Uploading To Steam Workshop
Once your mod is ready, you can share it with the community by uploading it to the Steam Workshop. The entire process is handled directly within the cenker's mod editor.
Setting Up Description and Icon
The Workshop item automatically uses specific files from your mod directory:
- icon.png: This will be the preview image seen on the Workshop.
- description.txt: The contents of this file will become the mod's Workshop description.
Your local files are mapped to the Workshop UI automatically.
Categories and Genres
When you click Submit, you'll need to configure your mod's categorization:
- Category: Choose between Mod (standalone experience) or Addon (extra content for other mods).
- Genre/Type:
- For Mods: Select the closest genre (Action, Strategy, Crafting, Sports, etc.).
- For Addons: Select the addon type. Addons are recommended to be compatible with other mods (e.g., new maps, moderation tools, or helper utilities).
The Upload Process
During the upload, the game will communicate with Steam servers. A progress window will appear.
Finalizing
Once successful, you'll receive a confirmation with your File ID. You can click "View on Workshop" to see your live project and manage it through the Steam interface.
Additional Resources
For complete API documentation, refer to cenkers_mod_stub.lua in your mod directory.
This file contains detailed type annotations and parameter descriptions for all available functions.
Study the included example mods (Football, Hook Up, Campfire Survivors) to see these concepts in action!