cenker's mod - Manual

Version 2.1.0

A multiplayer game that hosts multiple mini-games and enables community mod creation

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.

Offline Access: Please download this manual from your browser (Save Page As) to access it offline while modding.
LSP/Code Completion Support: The file 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.

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.

Lua Environment: The game uses Lua 5.4 with the following standard libraries available:
  • base - Basic functions (print, type, pairs, ipairs, etc.)
  • table - Table manipulation
  • string - String operations
  • math - Mathematical functions
Other Lua libraries (io, os, debug, etc.) are NOT available for security reasons. Use the provided API functions instead (e.g., get_os_time() for time, save_json() for file I/O).

Key Features

Example Mods

The game comes with three example mods that demonstrate different gameplay styles:

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)
Note: The 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:

Variable Declaration Rules: Variables at the top of the script (before any function, comment, or blank line) must be primitive types (numbers, strings, booleans). These special configuration variables are parsed before script execution. See the list of special variables below.
-- 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.

Entity Global Scope: Each entity has its own separate global Lua environment. Variables declared without 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.
Important Physics Limitations:
  • 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", {})
Best Practice: Use short singleton names (like -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
Security Warning: ALL variables in STATIC (1) and DYNAMIC (2) entities are sent from host to clients when players join. This includes all script variables. Be careful with sensitive data - store it in non-networked entities or use local-only variables (declared with local).
Note: DYNAMIC entities automatically sync position, rotation, and linear velocity. STATIC entities stay in place and don't sync movement.

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
Critical Requirements for Network Functions:
  • 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
Security Rule: Always validate player actions on the host before applying them! Clients should only send requests, and the host should validate and execute the actual game logic to prevent cheating.

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
Performance Tip - Direct Variable Access: For certain built-in variables like 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.

Performance Warning: _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.

Modder-Friendly Input System: Players can customize their key bindings in the game settings. As a modder, you don't need to do anything special - just use the input names (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
Note: Some inputs like ESC (Select, Back, Share button to open in-game menu), SPACE (Start, Menu, Options for UI selection), and R3 (virtual mouse click) are reserved for system use as default keybinds and cannot be listened to by modders. The game also features a virtual mouse system that allows gamepad users to interact with UI elements seamlessly.

_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.

Important: 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.

Automatic Command Interception: The system now checks for mod commands (registered via add_command) before calling this function. If a message matches a registered command, this handler will not be triggered for that message.
Pre-formatted Message: The message argument is already formatted with the player's nickname and color (e.g., [color=#66FF99]username[/color]: actual message).
Chaining Logic:
  • 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

Critical 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 local keyword cannot be accessed with get_value or set_value. Only global entity variables can be accessed this way.
  • Return Values: run_function and get_value cannot 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

-- 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.

Note: To upload or update your mod, click the Submit button located in the top right corner of the editor interface.

Setting Up Description and Icon

The Workshop item automatically uses specific files from your mod directory:

Mapping description and icon

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:

Updating Your Mod: If you are updating an existing mod, you should select the same Category and Genre as before. Don't forget to include your Change Notes so users know what's new!
Configuring change notes and categories

The Upload Process

During the upload, the game will communicate with Steam servers. A progress window will appear.

Do Not Close: "cenker's mod" must remain open throughout the entire upload process. Closing the game will interrupt the transfer.
Upload in progress

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.

Upload successful

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!