Welcome to KissMP Documentation!

It's currently in development, so expect it to be a little junky

Building

First, download and install a Rust toolchain

After, clone the KissMP repository

git clone https://github.com/TheHellBox/KISS-multiplayer.git
cd KISS-multiplayer

Now you are ready to build the server and bridge.

Server

cd kissmp-server
cargo run --release

or

cargo run -p kissmp-server --release

Bridge

cd kissmp-bridge
cargo run --release

or

cargo run -p kissmp-bridge --release

Hosting

Hosting a server with KissMP is very easy.

  • The server software was included in your download of KissMP, simply extract the "kissmp-server" directory to where you would like to set up your server.
  • Run the kissmp-server executable, it will generate a config file that you can edit.
  • Edit the config.json file to set the level, player limit, whether it's public, etc.
  • That's basically all there is to it.

How do I connect to my server?

If your server is running on your own PC, connect using 127.0.0.1 as the address. Otherwise, follow the steps below.

How do others connect to my server?

First of all, make sure that the port specified in your config.json is forwarded (How To Port Forward - General Guide to Multiple Router Brands).

If enabled in your config, your server will show up in the server list and others can just click the Connect button. Otherwise:

  • If you're not using any networking software like Hamachi, people connect to your server with your public IP address (https://www.whatismyip.com).
  • If you're using networking software like Hamachi, use the IP address assigned to you by that software.

How do i change the level/map?

To change what level the server is set on, simply specify your desired maps level path in your server configs map field.

The easiest way to get the path of a level is by loading into the level in singeplayer and executing print(getMissionFilename()) in the console.

If the map is modded, make sure to include it in your servers mods folder. See the instructions below on adding mods.

How do i add mods or addons to my server?

See Installing Mods and Addons.


Having issues with setting up your server? Have a look at Troubleshooting

Installing Mods and Addons

Mods

Mods add additional content to the game and are downloaded for all players connecting to the server. A mod could for example add a new level or vehicle.

Installation

Your server will automatically create a mods folder after you run it, in there, simply place all of the mods you want your players to download when they join your server.

If you prefer the speed of pre-downloading your servers mods through an external service like Google Drive, simply put your pre-downloaded mods into the kissmp_mods folder.
The kissmp_mods folder can be found in the same directory as your BeamNGs mods folder.

Addons

Addons are scripts that run on the server and are not downloaded to any players.
With addons, servers are able to do all kinds of things (like gamemodes, commands, etc).

If you would like to get started with creating Addons for KissMP, see Server side Lua API.
A community mantained collection of addons is available here.

Installation

Just like with the mods folder, the addons folder is created automatically by your server.
Most of the time you should just be able to drag addons into your addons folder, but if that doesn't work, make sure that the folder structure matches the structure below.
KissMP addons use main.lua as their entrypoint and addons should follow the structure of:
/addons/ADDON_NAME/main.lua

Troubleshooting

I can connect to my server using its IP address, but it's not showing up on the server list!

There are a few reasons as to why this may happen.

  • You may not have show_in_server_list set to true in your config.
  • Server is not up to date with the latest KissMP-server version.
  • Servers with inappropriate words in their name will automatically be blocked.
  • To avoid abuse, there is a limit of 10 servers per IP address.
  • Servers may not exceed the character limit of 64 characters for its name and 256 characters for its description.

My friends can't to connect to my server!

This is probably the result of incorrect port forwarding or firewall issues. Make sure to follow the port forwarding guide from Server Hosting

I have another issue that is not listed here!

If more help is needed, we are usually able to provide help at our Discord server.

Introduction

KissMP server uses lua as its language for creating addons.

Keep in mind that the server in KissMP doesn't do much, most of the stuff is done by clients. Server just passes some data between clients.

However, that doesn't mean that the server is limited to what it's able to do. You can still control quite a lot of things, mostly by dictating what clients should do.

For this reason, lots of stuff can be done with the connection:sendLua() method. For example, the built in vehicle:setPositionRotation method uses sendLua as its backend.
You can display UI, messages, modify input and change time just by sending small lua commands.

Creating an addon

Create new folder in the /addons/ directory with any name. Create a file called main.lua in there, this file will get executed when the server starts.

Server also supports hot-reloading, so lua addons will reload automatically when saved without needing to restart the server.

Hooks

You can register a hook by running

hooks.register("HookName", "Subname", function(arguments)
    return value
end)

Keep in mind that the subname has to be unique.

Default hooks include:

  • OnChat(int client_id, string message) returns string - modified message

  • Tick()

  • OnStdIn(string input)

  • OnVehicleRemoved(vehicle_id, client_id)

  • OnVehicleSpawned(vehicle_id, client_id)

  • OnVehicleResetted(vehicle_id, client_id)

  • OnPlayerConnected(client_id)

  • OnPlayerDisconnected(client_id)

Vehicles

A vehicle object represents a vehicle that was spawned by a client.

Vehicle objects are stored in the global table vehicles and a specific vehicle can be obtained using its vehicle ID with vehicles[vehicle_id].

Vehicle objects have the following methods:

  • getTransform()
  • getData()
  • remove()
    • Returns: null
  • reset()
    • Returns: null
  • setPositionRotation(x, y, z, xr, yr, zr, w)
    • Note: Rotation is in quaternion form.
    • Returns: null
  • sendLua(string lua_command)
    • Returns: null

Vehicle Data

A vehicle data object holds information about a vehicle and can be obtained through the getData() method on a vehicle object.

List of methods available for vehicle data:

  • getInGameID()
    • Returns: Integer
  • getID()
  • getColor()
    • Returns: Table
  • getPalete0()
    • Returns: Table
  • getPalete1()
    • Returns: Table
  • getPlate()
    • Returns: String
  • getName()
    • Returns: String
  • getOwner()
  • getPartsConfig()
    • Returns: String (JSON)

Transform

A transform object holds information about a vehicles transform (position, rotation, etc) and can be obtained through the getTransform() method on a vehicle object.

Transform object has following methods:

  • getPosition()
    • Returns: Table (Vector3)
  • getRotation()
    • Returns: Table (Quaternion)
  • getVelocity()
    • Returns: Table (Vector3)
  • getAngularVelocity()
    • Returns: Table (Vector3)

Connections

A connection object represents a player connected to the server.

Connections are stored in the global table connections and a specific connection can be obtained using its client ID with connections[client_id].

List of methods a connection object has:

  • getID()
  • getIpAddr()
    • Returns: String
  • getSecret()
    • Note: Returns a client unique identifier. Keep the server identifier the same if you want persistent client secrets between different servers. WARNING: NEVER EXPOSE TO CLIENT SIDE!
    • Returns: String
  • getCurrentVehicle()
  • getName()
    • Returns: String
  • sendChatMessage(string message)
    • Returns: null
  • kick(string reason)
    • Returns: null
  • sendLua(string lua_command)
    • Note: WARNING: You should always make sure to sanitize any form of user input inside of sendLua to avoid clients being vulnerable to arbitrary code injections.
      For example client:sendLua('ui_message("'..message..'")') would be vulnerable if message is ") Evil code here--.
      The admin system example has an example of sanitization in the cmd_parse function.
    • Returns: null

Global functions

  • send_message_broadcast(string)
  • encode_json(table)
  • encode_json_pretty(table)
  • decode_json(string)

Globals

  • SERVER_TICKRATE
  • SERVER_NAME
  • MAX_PLAYERS
  • MAX_VEHICLES_PER_CLIENT
  • MPSC_CHANNEL_SENDER
  • hooks
  • vehicles
  • connections

Examples

Basic example of commands

function string.startswith(input, start)
   return string.sub(input,1,string.len(start))==start
end

hooks.register("OnChat", "HomeCommand", function(client_id, message)
    local vehicle_id = connections[client_id]:getCurrentVehicle()
    if not vehicles[vehicle_id] then return end
    local vehicle = vehicles[vehicle_id]
    if message == "/home" then
      vehicle:setPositionRotation(0, 0, 0, 0, 0, 0, 1)
    end
    if message == "/reset" then
      vehicle:reset()
    end
    if message == "/remove" then
      vehicle:remove()
    end
    if message == "/kick_me" then
      connections[client_id]:kick("Kick reason")
    end
    if string.startswith(message, "/send_me_lua") then
      local message = message:gsub("%/send_me_lua", "")
      connections[client_id]:sendLua(message)
    end
    if string.startswith(message, "/send_me_msg") then
      local message = message:gsub("%/send_me_msg", "")
      connections[client_id]:sendChatMessage(message)
    end
end)

Vote-kick system

local vote = {
  victim = nil,
  votes = {},
  end_time = 0
}

local function startswith(input, start)
   return string.sub(input,1,string.len(start))==start
end

local function count_players()
  local i = 0
  for _, _ in pairs(connections) do
    i = i + 1
  end
  return i
end

hooks.register("OnChat", "VoteKick", function(client_id, message)
    local initiator = connections[client_id]
    if startswith(message, "/votekick") then
      if not vote.victim then
        local victim = message:gsub("%/votekick ", "")
        for _, client in pairs(connections) do
          if victim == client:getName() then
            vote.victim = client:getID()
            vote.end_time = os.clock() + 30
            send_message_broadcast(initiator:getName().." has started a vote to kick "..client:getName())
            send_message_broadcast("Type /vote to vote")
            local votes_needed = count_players() / 2
            send_message_broadcast(math.floor(votes_needed).." votes are needed")
          else
            initiator:sendChatMessage("No such player")
          end
        end
      else
        initiator:sendChatMessage("Wait until the current vote ends")
      end
    end
    if startswith(message, "/vote") then
      if not vote.votes[initiator:getID()] then
        vote.votes[initiator:getID()] = true
      else
        initiator:sendChatMessage("You have already voted!")
      end
    end
end)

hooks.register("Tick", "VoteTimer", function(client_id, message)
    if vote.victim and (os.clock() > vote.end_time) then
      local votes_count = 0
      for _, _ in pairs(vote.votes) do
        votes_count = votes_count + 1
      end
      if votes_count > (count_players() / 2) then
        local victim = connections[vote.victim]
        if victim then
          victim:kick("You have been kicked by vote results")
          send_message_broadcast(victim:getName().." has been kicked by vote results")
        end
      else
        send_message_broadcast("Vote has failed")
      end
      vote.victim = nil
      vote.votes = {}
    end
end)

Vehicle list

hooks.register("OnStdIn", "ListVehiclesCommand", function(input)
    if input == "/list_vehicles" then
      for vehicle_id, vehicle in pairs(vehicles) do
        local position = vehicle:getTransform():getPosition()
        print("Vehicle "..vehicle_id..": "..position[1]..", "..position[2]..", "..position[3])
      end
    end
end)

Here's a simple admin system. It's probably not suitable for big servers, and only serves as an example, but it can still be used in its original form

KSA = {}

KSA.ban_list = {}
KSA.player_roles = {}

KSA.commands = {
  kick = {
    roles = {admin = true, superadmin = true},
    exec = function(executor, args)
      if not args[1] then executor:sendChatMessage("No arguments provided") end
      for id, client in pairs(connections) do
        if client:getName() == args[1] then
          client:kick("You have been kicked. Reason: "..(args[2] or "No reason provided"))
          return
        end
      end
    end
  },
  ban = {
    roles = {admin = true, superadmin = true},
    exec = function(executor, args)
      if not args[1] then executor:sendChatMessage("No arguments provided") end
      for id, client in pairs(connections) do
        if client:getName() == args[1] then
          KSA.ban(client:getSecret(), client:getName(), client:getID(), tonumber(args[2]) or math.huge)
          return
        end
      end     
    end
  },
  promote = {
    roles = {superadmin = true},
    exec = function(executor, args)
      if not args[1] then executor:sendChatMessage("No arguments provided") end
      for id, client in pairs(connections) do
        if client:getName() == args[1] then
          KSA.promote(client:getSecret(), args[2] or "user")
          return
        end
      end
    end
  }
}

  -- Created by Dummiesman
local function cmd_parse(cmd)
  local parts = {}
  local len = cmd:len()
  local escape_sequence_stack = 0
  local in_quotes = false

  local cur_part = ""
  for i=1,len,1 do
     local char = cmd:sub(i,i)
     if escape_sequence_stack > 0 then escape_sequence_stack = escape_sequence_stack + 1 end
     local in_escape_sequence = escape_sequence_stack > 0
     if char == "\\" then
        escape_sequence_stack = 1
     elseif char == " " and not in_quotes then
        table.insert(parts, cur_part)
        cur_part = ""
     elseif char == '"'and not in_escape_sequence then
        in_quotes = not in_quotes
     else
        cur_part = cur_part .. char
     end
     if escape_sequence_stack > 1 then escape_sequence_stack = 0 end
  end
  if cur_part:len() > 0 then
    table.insert(parts, cur_part)
  end
  return parts
end

local function load_roles()
  local file = io.open("./ksa_roles.json", "r")
  if not file then return end
  KSA.player_roles = decode_json(file:read("*a"))
end

local function save_roles()
  local file = io.open("./ksa_roles.json", "w")
  local content = encode_json_pretty(KSA.player_roles)
  if not content then return end
  file:write(content)
end

local function load_banlist()
  local file = io.open("./ksa_banlist.json", "r")
  if not file then return end
  KSA.ban_list = decode_json(file:read("*a"))
end

local function save_banlist()
  local file = io.open("./ksa_banlist.json", "w")
  local content = encode_json_pretty(KSA.ban_list)
  if not content then return end
  file:write(content)
end

function KSA.ban(secret, name, client_id, time)
  local time = time or math.huge()
  KSA.ban_list[secret] = {
    name = name,
    unban_time = os.time() + (time * 60)
  }
  connections[client_id]:kick("You've been banned on this server.")
  save_banlist()
end

function KSA.unban(secret)
  KSA.ban_list[secret] = nil
  save_banlist()
end

function KSA.promote(secret, new_role)
  KSA.player_roles[secret] = new_role
  save_roles()
end

hooks.register("OnPlayerConnected", "CheckBanList", function(client_id)
    local secret = connections[client_id]:getSecret()
    local ban = KSA.ban_list[secret]
    if not ban then return end
    local remaining = ban.unban_time - os.time()
    if remaining < 0 then
      KSA.unban(secret)
      return
    end
    connections[client_id]:kick("You've been banned on this server. Time remaining: "..tostring(remaining / 60).." min")
end)

hooks.register("OnStdIn", "KSA_Run_Lua", function(str)
    if string.sub(str, 1, 7) == "run_lua" then
      load(string.sub(str, 9, #str))()
    end
end)

hooks.register("OnStdIn", "KSA_Promote", function(str)
    if not string.sub(str, 1, 9) == "set_super" then return end
    local target = string.sub(str, 11, #str)
    print(target)
    for id, client in pairs(connections) do
      if client:getName() == target then
        KSA.promote(client:getSecret(), "superadmin")
      end
    end
end)

hooks.register("OnChat", "KSA_Process_Commands", function(client_id, str)
    if not string.sub(str, 1, 4) == "/ksa" then return end
    local args = cmd_parse(str, " ")
    table.remove(args, 1)
    local base = table.remove(args, 1)
    local executor = connections[client_id]
    local command = KSA.commands[base]
    if not command.roles[KSA.player_roles[executor:getSecret()] or "user"] then
      executor:sendChatMessage("KSA: You're not allowed to use this command")
      return
    end
    if not command then
      executor:sendChatMessage("KSA: Command not found")
      return
    end
    command.exec(executor, args)
    return ""
end)

load_roles()
load_banlist()