├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── KISSMultiplayer ├── art │ └── shapes │ │ └── kissmp_playermodels │ │ ├── base_nb.dae │ │ ├── base_nb_head.dae │ │ └── main.materials.json ├── lua │ ├── ge │ │ └── extensions │ │ │ ├── core │ │ │ └── input │ │ │ │ └── actions │ │ │ │ └── kissmp.json │ │ │ ├── kissconfig.lua │ │ │ ├── kissmods.lua │ │ │ ├── kissmp │ │ │ └── ui │ │ │ │ ├── chat.lua │ │ │ │ ├── download.lua │ │ │ │ ├── main.lua │ │ │ │ ├── names.lua │ │ │ │ └── tabs │ │ │ │ ├── create_server.lua │ │ │ │ ├── direct_connect.lua │ │ │ │ ├── favorites.lua │ │ │ │ ├── server_list.lua │ │ │ │ └── settings.lua │ │ │ ├── kissplayers.lua │ │ │ ├── kissrichpresence.lua │ │ │ ├── kisstransform.lua │ │ │ ├── kissui.lua │ │ │ ├── kissutils.lua │ │ │ ├── kissvoicechat.lua │ │ │ ├── network.lua │ │ │ └── vehiclemanager.lua │ └── vehicle │ │ └── extensions │ │ └── kiss_mp │ │ ├── kiss_couplers.lua │ │ ├── kiss_electrics.lua │ │ ├── kiss_gearbox.lua │ │ ├── kiss_input.lua │ │ ├── kiss_nodes.lua │ │ ├── kiss_transforms.lua │ │ └── kiss_vehicle.lua ├── scripts │ └── kiss_mp │ │ └── modScript.lua └── ui │ └── This is here because BeamNG would not stop mounting the damn mod in the levels folder ├── LICENSE ├── README.md ├── docs ├── book │ └── .nojekyll └── src │ ├── SUMMARY.md │ ├── building.md │ ├── introduction.md │ ├── srv_hosting │ ├── hosting.md │ ├── mods_and_addons.md │ └── troubleshooting.md │ └── srv_lua │ ├── admin_system_example.md │ ├── connection.md │ ├── examples.md │ ├── global_functions.md │ ├── globals.md │ ├── hooks.md │ ├── lua_api.md │ ├── transform.md │ ├── vehicle_data.md │ └── vehicles.md ├── kissmp-bridge ├── .gitignore ├── Cargo.toml └── src │ ├── discord.rs │ ├── http_proxy.rs │ ├── main.rs │ └── voice_chat.rs ├── kissmp-master ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── kissmp-server ├── .cargo │ └── config ├── .gitignore ├── Cargo.toml └── src │ ├── config.rs │ ├── events.rs │ ├── file_transfer.rs │ ├── incoming.rs │ ├── lib.rs │ ├── lua.rs │ ├── main.rs │ ├── outgoing.rs │ └── server_vehicle.rs └── shared ├── Cargo.toml └── src ├── lib.rs └── vehicle ├── electrics.rs ├── gearbox.rs ├── mod.rs ├── transform.rs └── vehicle_meta.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build_bridge: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Install alsa dev 22 | if: runner.os == 'Linux' 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get install libasound2-dev 26 | 27 | - uses: Swatinem/rust-cache@v1 28 | 29 | - name: Build 30 | run: cargo build -p kissmp-bridge --verbose --release 31 | 32 | - name: Store Artifacts 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: build_results_bridge 36 | path: | 37 | ./target/release/kissmp-bridge 38 | ./target/release/kissmp-bridge.exe 39 | 40 | build_server: 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | matrix: 44 | os: [ubuntu-20.04, windows-latest] 45 | steps: 46 | - uses: actions/checkout@v2 47 | 48 | - uses: Swatinem/rust-cache@v1 49 | 50 | - name: Build 51 | run: cargo build -p kissmp-server --verbose --release 52 | 53 | - name: Store Artifacts 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: build_results_server 57 | path: | 58 | ./target/release/kissmp-server 59 | ./target/release/kissmp-server.exe 60 | 61 | build_master: 62 | runs-on: ${{ matrix.os }} 63 | strategy: 64 | matrix: 65 | os: [ubuntu-20.04] 66 | steps: 67 | - uses: actions/checkout@v2 68 | 69 | - uses: Swatinem/rust-cache@v1 70 | 71 | - name: Build 72 | run: cargo build -p kissmp-master --verbose --release 73 | 74 | - name: Store Artifacts 75 | uses: actions/upload-artifact@v3 76 | with: 77 | name: build_results_master_server 78 | path: | 79 | ./target/release/kissmp-master 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /kissmp-server/target/ 5 | /kissmp-bridge/target/ 6 | /docs/book/ 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | # We're ignoring the nested .locks so CDing and building doesn't result in accidential commits 10 | */Cargo.lock 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "kissmp-bridge", 4 | "kissmp-server", 5 | "kissmp-master", 6 | "shared" 7 | ] 8 | default-members = [ 9 | "kissmp-bridge", 10 | "kissmp-server" 11 | ] 12 | -------------------------------------------------------------------------------- /KISSMultiplayer/art/shapes/kissmp_playermodels/main.materials.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_nb_body": { 3 | "name": "base_nb_body", 4 | "mapTo": "base_nb_body", 5 | "class": "material", 6 | "persistentId": "be389ad5-9cc7-47d1-a6fb-854af1f84e35", 7 | "Stages": [ 8 | { 9 | "diffuseColor": [ 10 | 0.800000012, 11 | 0.800000012, 12 | 0.800000012, 13 | 1 14 | ], 15 | "instanceDiffuse": true 16 | }, 17 | {}, 18 | {}, 19 | {} 20 | ], 21 | "translucentBlendOp": "None" 22 | }, 23 | "base_nb_face": { 24 | "name": "base_nb_face", 25 | "mapTo": "base_nb_face", 26 | "class": "material", 27 | "persistentId": "4bd6fc2b-d72b-4a17-95c6-dbc729ec19cc", 28 | "Stages": [ 29 | { 30 | "diffuseColor": [ 31 | 0.800000012, 32 | 0.800000012, 33 | 0.800000012, 34 | 1 35 | ], 36 | "instanceDiffuse": true 37 | }, 38 | {}, 39 | {}, 40 | {} 41 | ], 42 | "order_simset": 0, 43 | "translucentBlendOp": "None" 44 | }, 45 | "base_nb_mask": { 46 | "name": "base_nb_mask", 47 | "mapTo": "base_nb_mask", 48 | "class": "material", 49 | "persistentId": "edc7c5c6-6f39-4e4d-8a89-407c4f8c8328", 50 | "Stages": [ 51 | { 52 | "diffuseColor": [ 53 | 0.100000012, 54 | 0.100000012, 55 | 0.100000012, 56 | 1 57 | ] 58 | }, 59 | {}, 60 | {}, 61 | {} 62 | ], 63 | "doubleSided": true, 64 | "translucentBlendOp": "None" 65 | } 66 | } -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/core/input/actions/kissmp.json: -------------------------------------------------------------------------------- 1 | { 2 | "voicechat":{"cat":"general", "order": 0, "ctx": "tlua", "onDown" :"kissvoicechat.start_vc()", "onUp": "kissvoicechat.end_vc()", "isBasic":true, "title": "KissMP Voice Chat", "desc": "KissMP Voice Chat" }, 3 | "kmp_focus_chat":{"cat":"general", "order": 0, "ctx": "tlua", "onDown" :"kissui.chat.focus_chat = true", "isBasic":true, "title": "KissMP Chat", "desc": "Focus chat text entry" }, 4 | "kmp_ui":{"cat":"general", "order": 0, "ctx": "tlua", "onDown" :"kissui.toggle_ui()", "isBasic":true, "title": "Toggle KissMP UI", "desc": "Toggle KissMP UI" }, 5 | } 6 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissconfig.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local function generate_base_secret() 5 | math.randomseed(os.time() + os.clock()) 6 | local result = "" 7 | for i=0,64 do 8 | local char = string.char(32 + math.random() * 96) 9 | result = result..char 10 | end 11 | return result 12 | end 13 | 14 | local function save_config() 15 | local secret = network.base_secret or "None" 16 | if secret == "None" then 17 | secret = generate_base_secret() 18 | end 19 | local result = { 20 | name = ffi.string(kissui.player_name), 21 | addr = ffi.string(kissui.addr), 22 | show_nametags = kissui.show_nametags[0], 23 | show_drivers = kissui.show_drivers[0], 24 | window_opacity = kissui.window_opacity[0], 25 | enable_view_distance = kissui.enable_view_distance[0], 26 | view_distance = kissui.view_distance[0], 27 | base_secret_v2 = secret 28 | } 29 | local file = io.open("./settings/kissmp_config.json", "w") 30 | file:write(jsonEncode(result)) 31 | io.close(file) 32 | end 33 | 34 | local function load_config() 35 | local file = io.open("./settings/kissmp_config.json", "r") 36 | if not file then 37 | if Steam and Steam.isWorking and Steam.accountLoggedIn then 38 | kissui.player_name = imgui.ArrayChar(32, Steam.playerName) 39 | end 40 | return 41 | end 42 | local content = file:read("*a") 43 | local config = jsonDecode(content or "") 44 | if not config then return end 45 | 46 | if config.name ~= nil then 47 | kissui.player_name = imgui.ArrayChar(32, config.name) 48 | end 49 | if config.addr ~= nil then 50 | kissui.addr = imgui.ArrayChar(128, config.addr) 51 | end 52 | if config.show_nametags ~= nil then 53 | kissui.show_nametags[0] = config.show_nametags 54 | end 55 | if config.show_drivers ~= nil then 56 | kissui.show_drivers[0] = config.show_drivers 57 | end 58 | if config.window_opacity ~= nil then 59 | kissui.window_opacity[0] = config.window_opacity 60 | end 61 | if config.view_distance ~= nil then 62 | kissui.view_distance[0] = config.view_distance 63 | end 64 | if config.enable_view_distance ~= nil then 65 | kissui.enable_view_distance[0] = config.enable_view_distance 66 | end 67 | if config.base_secret_v2 ~= nil then 68 | network.base_secret = config.base_secret_v2 69 | end 70 | io.close(file) 71 | end 72 | 73 | local function init() 74 | load_config() 75 | if #FS:findFiles("/mods/", "kissmultiplayer.zip", 1000) == 0 then 76 | kissui.incorrect_install = true 77 | end 78 | end 79 | 80 | M.save_config = save_config 81 | M.load_config = load_config 82 | M.onExtensionLoaded = init 83 | 84 | return M 85 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmods.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.mods = {} 3 | 4 | local function is_special_mod(mod_path) 5 | local special_mods = {"kissmultiplayer.zip", "translations.zip"} 6 | local mod_path_lower = string.lower(mod_path) 7 | for _, special_mod in pairs(special_mods) do 8 | if string.endswith(mod_path_lower, special_mod) then 9 | return true 10 | end 11 | end 12 | return false 13 | end 14 | 15 | local function get_mod_name(name) 16 | local name = string.lower(name) 17 | name = name:gsub('.zip$', '') 18 | return "kissmp_mods"..name 19 | end 20 | 21 | local function deactivate_mod(name) 22 | local filename = "/kissmp_mods/"..name 23 | if FS:isMounted(filename) then 24 | FS:unmount(filename) 25 | end 26 | core_vehicles.clearCache() 27 | end 28 | 29 | local function is_app_mod(path) 30 | local pattern = "([^/]+)%.zip$" 31 | if string.sub(path, -4) ~= ".zip" then 32 | pattern = "([^/]+)$" 33 | end 34 | 35 | path = string.match(path, pattern) 36 | local mod = core_modmanager.getModDB(path) 37 | if not mod then return false end 38 | 39 | return mod.modType == "app" 40 | end 41 | 42 | local function deactivate_all_mods() 43 | for k, mod_path in pairs(FS:findFiles("/mods/", "*.zip", 1000)) do 44 | if not is_special_mod(mod_path) and not is_app_mod(mod_path) then 45 | FS:unmount(string.lower(mod_path)) 46 | end 47 | end 48 | for k, mod_path in pairs(FS:findFiles("/kissmp_mods/", "*.zip", 1000)) do 49 | FS:unmount(mod_path) 50 | end 51 | for k, mod_path in pairs(FS:directoryList("/mods/unpacked/", "*", 1)) do 52 | if not is_app_mod(mod_path) then 53 | FS:unmount(mod_path.."/") 54 | end 55 | end 56 | core_vehicles.clearCache() 57 | end 58 | 59 | local function mount_mod(name) 60 | --local mode = mode or "added" 61 | --extensions.core_modmanager.workOffChangedMod("/kissmp_mods/"..name, mode) 62 | if FS:fileExists("/kissmp_mods/"..name) then 63 | FS:mount("/kissmp_mods/"..name) 64 | else 65 | files = FS:findFiles("/mods/", name, 1000) 66 | if files[1] then 67 | FS:mount(files[1]) 68 | else 69 | kissui.chat.add_message("Failed to mount mod "..name..", file not found", kissui.COLOR_RED) 70 | end 71 | end 72 | core_vehicles.clearCache() 73 | end 74 | 75 | local function mount_mods(list) 76 | for _, mod in pairs(list) do 77 | -- Demount mod in case it was mounted before, to refresh it 78 | deactivate_mod(mod) 79 | mount_mod(mod) 80 | --activate_mod(mod) 81 | end 82 | core_vehicles.clearCache() 83 | end 84 | 85 | local function update_status(mod) 86 | local search_results = FS:findFiles("/kissmp_mods/", mod.name, 1) 87 | local search_results2 = FS:findFiles("/mods/", mod.name, 99) 88 | 89 | for _, v in pairs(search_results2) do 90 | table.insert(search_results, v) 91 | end 92 | 93 | if not search_results[1] then 94 | mod.status = "missing" 95 | else 96 | local len = FS:stat(search_results[1]).filesize 97 | if len ~= mod.size then 98 | mod.status = "different" 99 | else 100 | mod.status = "ok" 101 | end 102 | end 103 | end 104 | 105 | local function update_status_all() 106 | for name, mod in pairs(M.mods) do 107 | update_status(mod) 108 | end 109 | end 110 | 111 | local function set_mods_list(mod_list) 112 | M.mods = {} 113 | for _, mod in pairs(mod_list) do 114 | local mod_name = mod[1] 115 | local mod_table = { 116 | name = mod_name, 117 | size = mod[2], 118 | status = "unknown" 119 | } 120 | M.mods[mod_name] = mod_table 121 | end 122 | end 123 | 124 | local function open_file(name) 125 | if not string.endswith(name, ".zip") then return end 126 | if not FS:directoryExists("/kissmp_mods/") then 127 | FS:directoryCreate("/kissmp_mods/") 128 | end 129 | local path = "/kissmp_mods/"..name 130 | print(path) 131 | local file = io.open(path, "wb") 132 | return file 133 | end 134 | 135 | M.open_file = open_file 136 | M.check_mods = check_mods 137 | M.is_special_mod = is_special_mod 138 | M.mount_mod = mount_mod 139 | M.mount_mods = mount_mods 140 | M.deactivate_all_mods = deactivate_all_mods 141 | M.set_mods_list = set_mods_list 142 | M.update_status_all = update_status_all 143 | M.update_status = update_status 144 | 145 | return M 146 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/chat.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local set_column_offset = false 5 | local should_draw_unread_count = false 6 | local unread_message_count = 0 7 | local prev_chat_scroll_max = 0 8 | local message_buffer = imgui.ArrayChar(128) 9 | 10 | M.focus_chat = false 11 | M.chat = { 12 | {text = "KissMP chat", has_color = false} 13 | } 14 | 15 | -- Server list update and search 16 | -- spairs from https://stackoverflow.com/a/15706820 17 | local function spairs(t, order) 18 | local keys = {} 19 | for k in pairs(t) do keys[#keys+1] = k end 20 | if order then 21 | table.sort(keys, function(a,b) return order(t, a, b) end) 22 | else 23 | table.sort(keys) 24 | end 25 | local i = 0 26 | return function() 27 | i = i + 1 28 | if keys[i] then 29 | return keys[i], t[keys[i]] 30 | end 31 | end 32 | end 33 | 34 | local function send_current_chat_message() 35 | local message = ffi.string(message_buffer) 36 | local message_trimmed = message:gsub("^%s*(.-)%s*$", "%1") 37 | if message_trimmed:len() == 0 then return end 38 | 39 | network.send_data( 40 | { 41 | Chat = message_trimmed 42 | }, 43 | true 44 | ) 45 | message_buffer = imgui.ArrayChar(128) 46 | end 47 | 48 | local function draw_player_list() 49 | imgui.BeginGroup(); 50 | imgui.Text("Player list:") 51 | imgui.BeginChild1("PlayerList", imgui.ImVec2(0, 0), true) 52 | if network.connection.connected then 53 | for _, player in spairs(network.players, function(t,a,b) return t[b].name:lower() > t[a].name:lower() end) do 54 | imgui.Text(player.name.."("..player.ping.." ms)") 55 | end 56 | end 57 | imgui.EndChild() 58 | imgui.EndGroup() 59 | end 60 | 61 | local function draw() 62 | if not kissui.gui.isWindowVisible("Chat") then return end 63 | imgui.PushStyleVar2(imgui.StyleVar_WindowMinSize, imgui.ImVec2(100, 100)) 64 | 65 | local window_title = "Chat" 66 | if unread_message_count > 0 and should_draw_unread_count then 67 | window_title = window_title .. " (" .. tostring(unread_message_count) .. ")" 68 | end 69 | window_title = window_title .. "###chat" 70 | 71 | imgui.SetNextWindowBgAlpha(kissui.window_opacity[0]) 72 | if imgui.Begin(window_title) then 73 | local content_width = imgui.GetWindowContentRegionWidth() 74 | imgui.BeginChild1("ChatWindowUpperContent", imgui.ImVec2(0, -30), true) 75 | local upper_content_width = imgui.GetWindowContentRegionWidth() 76 | imgui.Columns(2, "###chat_columns") 77 | 78 | if not set_column_offset then 79 | -- Imgui doesn't have a "first time" method for this, so we track it ourselves.. 80 | local column_position = upper_content_width - 175 81 | if column_position > 0 then 82 | imgui.SetColumnOffset(1, column_position) 83 | end 84 | set_column_offset = true 85 | end 86 | 87 | -- Draw messages 88 | imgui.BeginChild1("Scrolling", imgui.ImVec2(0, 0), false) 89 | 90 | for _, message in pairs(M.chat) do 91 | imgui.PushTextWrapPos(0) 92 | if message.user_name ~= nil then 93 | local color = imgui.ImVec4(message.user_color[1], message.user_color[2], message.user_color[3], message.user_color[4]) 94 | imgui.TextColored(color, "%s", (message.user_name:sub(1, 16))..":") 95 | imgui.SameLine() 96 | end 97 | if message.has_color then 98 | imgui.TextColored(imgui.ImVec4(message.color.r or 1, message.color.g or 1, message.color.b or 1, message.color.a or 1), "%s", message.text) 99 | else 100 | imgui.Text("%s", message.text) 101 | end 102 | imgui.PopTextWrapPos() 103 | end 104 | 105 | -- Scroll to bottom and clear unreads 106 | local scroll_to_bottom = imgui.GetScrollY() >= prev_chat_scroll_max 107 | if scroll_to_bottom then 108 | imgui.SetScrollY(imgui.GetScrollMaxY()) 109 | unread_message_count = 0 110 | end 111 | prev_chat_scroll_max = imgui.GetScrollMaxY() 112 | imgui.EndChild() 113 | 114 | -- Draw player list 115 | imgui.NextColumn() 116 | draw_player_list() 117 | 118 | -- End UpperContent 119 | imgui.EndChild() 120 | 121 | -- Draw chat textbox 122 | local content_width = imgui.GetWindowContentRegionWidth() 123 | local button_width = 75 124 | local textbox_width = content_width - (button_width * 1.075) 125 | 126 | imgui.Spacing() 127 | 128 | imgui.PushItemWidth(textbox_width) 129 | if M.focus_chat then 130 | imgui.SetKeyboardFocusHere(0) 131 | M.focus_chat = false 132 | end 133 | if imgui.InputText("##chat", message_buffer, 128, imgui.InputTextFlags_EnterReturnsTrue) then 134 | send_current_chat_message() 135 | imgui.SetKeyboardFocusHere(-1) 136 | end 137 | imgui.PopItemWidth() 138 | imgui.SameLine() 139 | if imgui.Button("Send", imgui.ImVec2(button_width, -1)) then 140 | send_current_chat_message() 141 | end 142 | imgui.PopItemWidth() 143 | end 144 | imgui.End() 145 | imgui.PopStyleVar() 146 | should_draw_unread_count = true 147 | end 148 | 149 | local function add_message(message, color, sent_by) 150 | unread_message_count = unread_message_count + 1 151 | should_draw_unread_count = false 152 | local user_color 153 | local user_name 154 | if sent_by ~= nil then 155 | if network.players[sent_by] then 156 | local r,g,b,a = kissplayers.get_player_color(sent_by) 157 | user_color = {r,g,b,a} 158 | user_name = network.players[sent_by].name 159 | end 160 | end 161 | local has_color = color ~= nil and type(color) == 'table' 162 | local message_table = { 163 | text = message, 164 | has_color = has_color, 165 | user_color = user_color, 166 | user_name = user_name 167 | } 168 | if has_color then 169 | message_table.color = color 170 | end 171 | 172 | table.insert(M.chat, message_table) 173 | end 174 | 175 | M.draw = draw 176 | M.add_message = add_message 177 | 178 | return M 179 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/download.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local function bytes_to_mb(bytes) 5 | return (bytes / 1024) / 1024 6 | end 7 | 8 | local function draw(gui) 9 | if not kissui.show_download then return end 10 | 11 | if not kissui.gui.isWindowVisible("Downloads") then return end 12 | imgui.SetNextWindowBgAlpha(kissui.window_opacity[0]) 13 | imgui.PushStyleVar2(imgui.StyleVar_WindowMinSize, imgui.ImVec2(300, 300)) 14 | imgui.SetNextWindowViewport(imgui.GetMainViewport().ID) 15 | if imgui.Begin("Downloading Required Mods") then 16 | imgui.BeginChild1("DownloadsScrolling", imgui.ImVec2(0, -30), true) 17 | 18 | -- Draw a list of all the downloads, and finish by drawing a total/max size 19 | local total_size = 0 20 | local downloaded_size = 0 21 | 22 | local content_width = imgui.GetWindowContentRegionWidth() 23 | local split_width = content_width * 0.495 24 | 25 | imgui.PushItemWidth(content_width / 2) 26 | if network.downloads_status then 27 | for _, download_status in pairs(network.downloads_status) do 28 | local text_size = imgui.CalcTextSize(download_status.name) 29 | local extra_size = split_width - text_size.x 30 | 31 | imgui.Text(download_status.name) 32 | if extra_size > 0 then 33 | imgui.SameLine() 34 | imgui.Dummy(imgui.ImVec2(extra_size, -1)) 35 | end 36 | imgui.SameLine() 37 | imgui.ProgressBar(download_status.progress, imgui.ImVec2(split_width, 0)) 38 | 39 | local mod = kissmods.mods[download_status.name] 40 | total_size = total_size + mod.size 41 | downloaded_size = downloaded_size + (mod.size * download_status.progress) 42 | end 43 | end 44 | imgui.EndChild() 45 | 46 | total_size = bytes_to_mb(total_size) 47 | downloaded_size = bytes_to_mb(downloaded_size) 48 | local progress = downloaded_size / total_size 49 | local progress_text = tostring(math.floor(downloaded_size)) .. "MB / " .. tostring(math.floor(total_size)) .. "MB" 50 | 51 | content_width = imgui.GetWindowContentRegionWidth() 52 | split_width = content_width * 0.495 53 | local text_size = imgui.CalcTextSize(progress_text) 54 | local extra_size = split_width - text_size.x 55 | 56 | imgui.Text(progress_text) 57 | if extra_size > 0 then 58 | imgui.SameLine() 59 | imgui.Dummy(imgui.ImVec2(extra_size, -1)) 60 | end 61 | imgui.SameLine() 62 | if imgui.Button("Cancel###cancel_download", imgui.ImVec2(split_width, -1)) then 63 | network.cancel_download() 64 | kissui.show_download = false 65 | network.disconnect() 66 | end 67 | end 68 | imgui.End() 69 | imgui.PopStyleVar() 70 | end 71 | 72 | M.draw = draw 73 | 74 | return M 75 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/main.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local function draw(dt) 5 | kissui.tabs.favorites.draw_add_favorite_window(gui) 6 | if kissui.show_download then return end 7 | 8 | if not kissui.gui.isWindowVisible("KissMP") then return end 9 | imgui.SetNextWindowBgAlpha(kissui.window_opacity[0]) 10 | imgui.PushStyleVar2(imgui.StyleVar_WindowMinSize, imgui.ImVec2(300, 300)) 11 | imgui.SetNextWindowViewport(imgui.GetMainViewport().ID) 12 | if imgui.Begin("KissMP "..network.VERSION_STR) then 13 | imgui.Text("Player name:") 14 | imgui.InputText("##name", kissui.player_name) 15 | if network.connection.connected then 16 | if imgui.Button("Disconnect") then 17 | network.disconnect() 18 | end 19 | end 20 | 21 | imgui.Dummy(imgui.ImVec2(0, 5)) 22 | 23 | if imgui.BeginTabBar("server_tabs##") then 24 | if imgui.BeginTabItem("Server List") then 25 | kissui.tabs.server_list.draw(dt) 26 | imgui.EndTabItem() 27 | end 28 | if imgui.BeginTabItem("Direct Connect") then 29 | kissui.tabs.direct_connect.draw() 30 | imgui.EndTabItem() 31 | end 32 | if imgui.BeginTabItem("Create server") then 33 | kissui.tabs.create_server.draw() 34 | imgui.EndTabItem() 35 | end 36 | if imgui.BeginTabItem("Favorites") then 37 | kissui.tabs.favorites.draw() 38 | imgui.EndTabItem() 39 | end 40 | if imgui.BeginTabItem("Settings") then 41 | kissui.tabs.settings.draw() 42 | imgui.EndTabItem() 43 | end 44 | imgui.EndTabBar() 45 | end 46 | end 47 | imgui.End() 48 | imgui.PopStyleVar() 49 | end 50 | 51 | local function init(m) 52 | m.tabs.server_list.refresh(m) 53 | m.tabs.favorites.load(m) 54 | m.tabs.favorites.update(m) 55 | m.tabs.server_list.update_filtered(m) 56 | end 57 | 58 | M.draw = draw 59 | M.init = init 60 | 61 | return M 62 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/names.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function draw() 4 | for id, player in pairs(network.players) do 5 | if id ~= network.connection.client_id and player.current_vehicle then 6 | local vehicle_id = vehiclemanager.id_map[player.current_vehicle] or -1 7 | local vehicle = be:getObjectByID(vehicle_id) 8 | local vehicle_position = vec3() 9 | if (not vehicle) or (kisstransform.inactive[vehicle_id]) then 10 | if kissplayers.players[player.current_vehicle] then 11 | vehicle_position = vec3(kissplayers.players[player.current_vehicle]:getPosition()) 12 | elseif kisstransform.raw_transforms[player.current_vehicle] then 13 | vehicle_position = vec3(kisstransform.raw_transforms[player.current_vehicle].position) 14 | end 15 | else 16 | vehicle_position = vec3(vehicle:getPosition()) 17 | end 18 | 19 | local local_position = getCameraPosition() 20 | local distance = vehicle_position:distance(vec3(local_position)) or 0 21 | vehicle_position.z = vehicle_position.z + 1.6 22 | debugDrawer:drawTextAdvanced( 23 | Point3F(vehicle_position.x, vehicle_position.y, vehicle_position.z), 24 | String(player.name.." ("..tostring(math.floor(distance)).."m)"), 25 | ColorF(1, 1, 1, 1), 26 | true, 27 | false, 28 | ColorI(0, 0, 0, 255) 29 | ) 30 | end 31 | end 32 | end 33 | 34 | M.draw = draw 35 | 36 | return M 37 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/create_server.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | local http = require("socket.http") 4 | 5 | M.map = "/levels/industrial/info.json" 6 | M.map_name = "Industrial" 7 | M.mods = {} 8 | M.server_name = imgui.ArrayChar(128, "Private KissMP server") 9 | M.max_players = imgui.IntPtr(8) 10 | M.port = imgui.IntPtr(3698) 11 | M.is_proton = imgui.BoolPtr(false) 12 | M.proton_path = imgui.ArrayChar(1024, "/home/") 13 | 14 | local forced_mods = {} 15 | local pre_forced_mods_state = {} 16 | 17 | local function to_non_lowered(path) 18 | local mods = FS:findFiles("/mods/", "*.zip", 1000) 19 | for k, v in pairs(mods) do 20 | if string.lower(v) == path then 21 | return v 22 | end 23 | end 24 | return path 25 | end 26 | 27 | local function host_server() 28 | local port = M.port[0] 29 | local mods_converted = {} 30 | for k, v in pairs(M.mods) do 31 | table.insert(mods_converted, v) 32 | end 33 | if #mods_converted == 0 then 34 | mods_converted = nil 35 | end 36 | local config = { 37 | name = ffi.string(M.server_name), 38 | max_players = M.max_players[0], 39 | map = M.map, 40 | mods = mods_converted, 41 | port = port 42 | } 43 | local b, _, _ = http.request("http://127.0.0.1:3693/host/"..jsonEncode(config)) 44 | if b == "ok" then 45 | local player_name = ffi.string(kissui.player_name) 46 | network.connect("127.0.0.1:"..port, player_name) 47 | end 48 | end 49 | 50 | local function find_map_real_path(map_path) 51 | local patterns = {"info.json", "*.mis"} 52 | local found_file = map_path 53 | 54 | for _,pattern in pairs(patterns) do 55 | local files = FS:findFiles(map_path, pattern, 1) 56 | if #files > 0 then 57 | found_file = files[1] 58 | break 59 | end 60 | end 61 | print(found_file) 62 | return FS:virtual2Native(found_file) 63 | end 64 | 65 | local function change_map(map_info, title) 66 | -- deactivate mods that were activated by last map selection 67 | for k,v in pairs(forced_mods) do 68 | if not pre_forced_mods_state[k] then 69 | M.mods[k] = nil 70 | end 71 | end 72 | forced_mods = {} 73 | pre_forced_mods_state = {} 74 | 75 | -- 76 | local map_path = map_info.misFilePath 77 | print(map_path) 78 | M.map = map_path 79 | M.map_name = title or map_info.levelName 80 | 81 | local native = find_map_real_path(map_path) 82 | print(native) 83 | local _, zip_end = string.find(native, ".zip") 84 | local _, is_mod = string.find(native, "mods") 85 | if zip_end and is_mod then 86 | local mod_file = string.sub(native, 1, zip_end) 87 | print(mod_file) 88 | local virtual = to_non_lowered(FS:native2Virtual(mod_file)) 89 | 90 | pre_forced_mods_state[virtual] = (M.mods[virtual] ~= nil) 91 | M.mods[virtual] = FS:virtual2Native(virtual) 92 | forced_mods[virtual] = true 93 | end 94 | end 95 | 96 | local function checkbox(id, checked, allow_click) 97 | if allow_click == nil then allow_click = allow_click or true end 98 | 99 | if not allow_click then imgui.PushStyleVar1(imgui.StyleVar_Alpha, 0.70) end 100 | local return_value = imgui.Checkbox(id, checked) 101 | if not allow_click then imgui.PopStyleVar() end 102 | 103 | if allow_click then return return_value else return false end 104 | end 105 | 106 | local function draw() 107 | imgui.Text("Server name:") 108 | imgui.InputText("##host_server_name", M.server_name) 109 | 110 | imgui.Text("Max players:") 111 | if imgui.InputInt("###host_max_players", M.max_players) then 112 | M.max_players[0] = math.max(1, math.min(255, M.max_players[0])) 113 | end 114 | 115 | imgui.Text("Map:") 116 | if imgui.BeginCombo("###host_map", M.map_name) then 117 | for k, v in pairs(core_levels.getList()) do 118 | local title = v.title 119 | if title:find("^levels.") then 120 | title = v.levelName 121 | end 122 | if imgui.Selectable1(title.."###host_map_s_"..k) then 123 | change_map(v, title) 124 | end 125 | end 126 | imgui.EndCombo() 127 | end 128 | 129 | imgui.Text("Port:") 130 | if imgui.InputInt("###host_port", M.port) then 131 | M.port[0] = math.max(0, math.min(65535, M.port[0])) 132 | end 133 | 134 | local mods = FS:findFiles("/mods/", "*.zip", 1000) 135 | imgui.Text("Mods:") 136 | imgui.BeginChild1("###Mods", imgui.ImVec2(0, -30), true) 137 | for k, v in pairs(mods) do 138 | if not kissmods.is_special_mod(v) then 139 | local forced = forced_mods[v] or false 140 | local checked = imgui.BoolPtr(M.mods[v] ~= nil or forced) 141 | 142 | if checkbox(v.."###host_mod"..k, checked, not forced) then 143 | if checked[0] and not M.mods[v] then 144 | M.mods[v] = FS:virtual2Native(v) 145 | elseif not checked[0] then 146 | M.mods[v] = nil 147 | end 148 | end 149 | end 150 | end 151 | imgui.EndChild() 152 | 153 | if imgui.Button("Create Server", imgui.ImVec2(-1, 0)) then 154 | host_server() 155 | end 156 | end 157 | 158 | M.draw = draw 159 | 160 | return M 161 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/direct_connect.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local function draw() 5 | imgui.Text("Server address:") 6 | imgui.InputText("##addr", kissui.addr) 7 | imgui.SameLine() 8 | if imgui.Button("Connect") then 9 | local addr = ffi.string(kissui.addr) 10 | local player_name = ffi.string(kissui.player_name) 11 | kissconfig.save_config() 12 | network.connect(addr, player_name) 13 | end 14 | end 15 | 16 | M.draw = draw 17 | 18 | return M 19 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/favorites.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local add_favorite_addr = imgui.ArrayChar(128) 5 | local add_favorite_name = imgui.ArrayChar(64, "KissMP Server") 6 | 7 | M.favorite_servers = {} 8 | 9 | -- Server list update and search 10 | -- spairs from https://stackoverflow.com/a/15706820 11 | local function spairs(t, order) 12 | local keys = {} 13 | for k in pairs(t) do keys[#keys+1] = k end 14 | if order then 15 | table.sort(keys, function(a,b) return order(t, a, b) end) 16 | else 17 | table.sort(keys) 18 | end 19 | local i = 0 20 | return function() 21 | i = i + 1 22 | if keys[i] then 23 | return keys[i], t[keys[i]] 24 | end 25 | end 26 | end 27 | 28 | local function save_favorites() 29 | local file = io.open("./settings/kissmp_favorites.json", "w") 30 | file:write(jsonEncode(M.favorite_servers)) 31 | io.close(file) 32 | end 33 | 34 | local function load_favorites(m) 35 | local kissui = kissui or m 36 | local file = io.open("./settings/kissmp_favorites.json", "r") 37 | if file then 38 | local content = file:read("*a") 39 | M.favorite_servers = jsonDecode(content) or {} 40 | io.close(file) 41 | end 42 | end 43 | 44 | local function update_favorites(m) 45 | local kissui = kissui or m 46 | local update_count = 0 47 | for addr, server in pairs(M.favorite_servers) do 48 | if not server.added_manually then 49 | local server_from_list = kissui.tabs.server_list.server_list[addr] 50 | local server_found_in_list = server_from_list ~= nil 51 | 52 | if server_found_in_list then 53 | server.name = server_from_list.name 54 | server.description = server_from_list.description 55 | update_count = update_count + 1 56 | end 57 | end 58 | end 59 | 60 | if update_count > 0 then 61 | save_favorites(m) 62 | end 63 | end 64 | 65 | -- Favorites tab things 66 | local function add_server_to_favorites(addr, server) 67 | M.favorite_servers[addr] = { 68 | name = server.name, 69 | description = server.description, 70 | added_manually = false 71 | } 72 | save_favorites() 73 | end 74 | 75 | local function add_direct_server_to_favorites(addr, name) 76 | M.favorite_servers[addr] = { 77 | name = name, 78 | added_manually = true 79 | } 80 | save_favorites() 81 | end 82 | 83 | local function remove_server_from_favorites(addr) 84 | M.favorite_servers[addr] = nil 85 | save_favorites() 86 | end 87 | 88 | local function draw_add_favorite_window() 89 | if not kissui.gui.isWindowVisible("Add Favorite") then return end 90 | 91 | local display_size = imgui.GetIO().DisplaySize 92 | imgui.SetNextWindowPos(imgui.ImVec2(display_size.x / 2, display_size.y / 2), imgui.Cond_Always, imgui.ImVec2(0.5, 0.5)) 93 | 94 | imgui.SetNextWindowBgAlpha(kissui.window_opacity[0]) 95 | if imgui.Begin("Add Favorite", kissui.gui.getWindowVisibleBoolPtr("Add Favorite"), bit.bor(imgui.WindowFlags_NoScrollbar ,imgui.WindowFlags_NoResize, imgui.WindowFlags_AlwaysAutoResize)) then 96 | imgui.Text("Name:") 97 | imgui.SameLine() 98 | imgui.PushItemWidth(-1) 99 | imgui.InputText("##favorite_name", add_favorite_name) 100 | imgui.PopItemWidth() 101 | 102 | imgui.Text("Address:") 103 | imgui.SameLine() 104 | imgui.PushItemWidth(-1) 105 | imgui.InputText("##favorite_addr", add_favorite_addr) 106 | imgui.PopItemWidth() 107 | 108 | imgui.Dummy(imgui.ImVec2(0, 5)) 109 | 110 | local content_width = imgui.GetWindowContentRegionWidth() 111 | local button_width = content_width * 0.495 112 | 113 | if imgui.Button("Add", imgui.ImVec2(button_width, 0)) then 114 | local addr = ffi.string(add_favorite_addr) 115 | local name = ffi.string(add_favorite_name) 116 | 117 | if addr:len() > 0 and name:len() > 0 then 118 | add_direct_server_to_favorites(addr, name) 119 | end 120 | 121 | kissui.gui.hideWindow("Add Favorite") 122 | end 123 | imgui.SameLine() 124 | if imgui.Button("Cancel", imgui.ImVec2(button_width, 0)) then 125 | kissui.gui.hideWindow("Add Favorite") 126 | end 127 | end 128 | imgui.End() 129 | end 130 | 131 | local function draw_server_description(description) 132 | local min_height = 64 133 | local rect_color = imgui.GetColorU322(imgui.ImVec4(0.15, 0.15, 0.15, 1)) 134 | 135 | local bg_size = imgui.CalcTextSize(description, nil, false, imgui.GetWindowContentRegionWidth()) 136 | bg_size.y = math.max(min_height, bg_size.y) 137 | bg_size.x = imgui.GetWindowContentRegionWidth() 138 | 139 | local cursor_pos_before = imgui.GetCursorPos() 140 | imgui.Dummy(bg_size) 141 | local r_min = imgui.GetItemRectMin() 142 | local r_max = imgui.GetItemRectMax() 143 | local cursor_pos_after = imgui.GetCursorPos() 144 | 145 | imgui.ImDrawList_AddRectFilled(imgui.GetWindowDrawList(), r_min, r_max, rect_color) 146 | 147 | imgui.SetCursorPos(cursor_pos_before) 148 | imgui.Text(description) 149 | imgui.SetCursorPos(cursor_pos_after) 150 | imgui.Spacing(2) 151 | end 152 | 153 | local function draw() 154 | --draw_list_search_and_filters(true) 155 | 156 | local favorites_count = 0 157 | 158 | imgui.BeginChild1("Scrolling", imgui.ImVec2(0, -30), true) 159 | for addr, server in spairs(M.favorite_servers, function(t,a,b) return t[b].name:lower() > t[a].name:lower() end) do 160 | local server_from_list = kissui.tabs.server_list.server_list[addr] 161 | local server_found_in_list = server_from_list ~= nil 162 | favorites_count = favorites_count + 1 163 | 164 | local header = server.name 165 | if server.added_manually then 166 | header = header.." [USER]" 167 | elseif server_found_in_list then 168 | header = header.." ["..server_from_list.player_count.."/"..server_from_list.max_players.."]" 169 | else 170 | header = header.." [OFFLINE]" 171 | end 172 | header = header .. "###server_header_" .. tostring(favorites_count) 173 | 174 | if imgui.CollapsingHeader1(header) then 175 | imgui.PushTextWrapPos(0) 176 | imgui.Text("Address: "..addr) 177 | 178 | if server_found_in_list then 179 | imgui.Text("Map: "..server_from_list.map) 180 | end 181 | 182 | if server.description and server.description:len() > 0 then 183 | draw_server_description(server.description) 184 | end 185 | 186 | imgui.PopTextWrapPos() 187 | if imgui.Button("Connect###connect_button_" .. tostring(favorites_count)) then 188 | kissconfig.save_config() 189 | local player_name = ffi.string(kissui.player_name) 190 | network.connect(addr, player_name) 191 | end 192 | imgui.SameLine() 193 | if imgui.Button("Remove from Favorites###remove_favorite_button_" .. tostring(favorites_count)) then 194 | remove_server_from_favorites(addr) 195 | end 196 | end 197 | end 198 | 199 | imgui.PushTextWrapPos(0) 200 | if favorites_count == 0 then 201 | imgui.Text("Favorites list is empty") 202 | end 203 | imgui.PopTextWrapPos() 204 | 205 | imgui.EndChild() 206 | 207 | local content_width = imgui.GetWindowContentRegionWidth() 208 | local button_width = content_width * 0.495 209 | 210 | if imgui.Button("Refresh List", imgui.ImVec2(button_width, 0)) then 211 | kissui.tabs.server_list.refresh() 212 | kissui.tabs.server_list.update_filtered() 213 | end 214 | imgui.SameLine() 215 | if imgui.Button("Add", imgui.ImVec2(button_width, 0)) then 216 | kissui.gui.showWindow("Add Favorite") 217 | end 218 | end 219 | 220 | M.draw = draw 221 | M.draw_add_favorite_window = draw_add_favorite_window 222 | M.load = load_favorites 223 | M.update = update_favorites 224 | M.add_server_to_favorites = add_server_to_favorites 225 | 226 | return M 227 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/server_list.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | local http = require("socket.http") 4 | local VERSION_PRTL = "0.6.0" 5 | 6 | local filter_servers_notfull = imgui.BoolPtr(false) 7 | local filter_servers_notempty = imgui.BoolPtr(false) 8 | local filter_servers_online = imgui.BoolPtr(false) 9 | 10 | local prev_search_text = "" 11 | local prev_filter_notfull = false 12 | local prev_filter_notempty = false 13 | local prev_filter_online = false 14 | 15 | local search_buffer = imgui.ArrayChar(64) 16 | local time_since_filters_change = 0 17 | local filter_queued = false 18 | 19 | local filtered_servers = {} 20 | local filtered_favorite_servers = {} 21 | local next_bridge_status_update = 0 22 | 23 | M.server_list = {} 24 | 25 | -- Server list update and search 26 | -- spairs from https://stackoverflow.com/a/15706820 27 | local function spairs(t, order) 28 | local keys = {} 29 | for k in pairs(t) do keys[#keys+1] = k end 30 | if order then 31 | table.sort(keys, function(a,b) return order(t, a, b) end) 32 | else 33 | table.sort(keys) 34 | end 35 | local i = 0 36 | return function() 37 | i = i + 1 38 | if keys[i] then 39 | return keys[i], t[keys[i]] 40 | end 41 | end 42 | end 43 | 44 | local function filter_server_list(list, term, filter_notfull, filter_notempty, filter_online, m) 45 | local kissui = kissui or m 46 | local return_servers = {} 47 | 48 | local term_trimmed = term:gsub("^%s*(.-)%s*$", "%1") 49 | local term_lower = term_trimmed:lower() 50 | local textual_search = term_trimmed:len() > 0 51 | 52 | for addr, server in pairs(list) do 53 | local server_from_list = M.server_list[addr] 54 | local server_found_in_list = server_from_list ~= nil 55 | 56 | local discard = false 57 | if textual_search and not discard then 58 | local name_lower = server.name:lower() 59 | discard = discard or not string.find(name_lower, term_lower) 60 | end 61 | if filter_notfull and server_found_in_list and not discard then 62 | discard = discard or server_from_list.player_count >= server_from_list.max_players 63 | end 64 | if filter_notempty and server_found_in_list and not discard then 65 | discard = discard or server_from_list.player_count == 0 66 | end 67 | if filter_online and not discard then 68 | discard = discard or not server_found_in_list 69 | end 70 | 71 | if not discard then 72 | return_servers[addr] = server 73 | end 74 | end 75 | 76 | return return_servers 77 | end 78 | 79 | local function update_filtered_servers(m) 80 | local kissui = kissui or m 81 | local term = ffi.string(search_buffer) 82 | local filter_notfull = filter_servers_notfull[0] 83 | local filter_notempty = filter_servers_notempty[0] 84 | local filter_online = filter_servers_online[0] 85 | 86 | filtered_servers = filter_server_list(M.server_list, term, filter_notfull, filter_notempty, filter_online, m) 87 | --filtered_favorite_servers = filter_server_list(kissui.tabs.favorites.favorite_servers, term, filter_notfull, filter_online, m) 88 | end 89 | 90 | local function refresh_server_list(m) 91 | local kissui = kissui or m 92 | local b, _, _ = http.request("http://127.0.0.1:3693/check") 93 | if b and b == "ok" then 94 | kissui.bridge_launched = true 95 | end 96 | local b, _, _ = http.request("http://127.0.0.1:3693/"..kissui.master_addr.."/"..VERSION_PRTL) 97 | if b then 98 | M.server_list = jsonDecode(b) or {} 99 | end 100 | end 101 | 102 | local function draw_list_search_and_filters(show_online_filter) 103 | imgui.Text("Search:") 104 | imgui.SameLine() 105 | imgui.PushItemWidth(-1) 106 | imgui.InputText("##server_search", search_buffer) 107 | imgui.PopItemWidth() 108 | 109 | imgui.Text("Filters:") 110 | imgui.SameLine() 111 | 112 | imgui.Checkbox("Not Full", filter_servers_notfull) 113 | 114 | imgui.SameLine() 115 | 116 | imgui.Checkbox("Not Empty", filter_servers_notempty) 117 | 118 | if show_online_filter then 119 | imgui.SameLine() 120 | imgui.Checkbox("Online", filter_servers_online) 121 | end 122 | end 123 | 124 | local function draw_server_description(description) 125 | local min_height = 64 126 | local rect_color = imgui.GetColorU322(imgui.ImVec4(0.15, 0.15, 0.15, 1)) 127 | 128 | local bg_size = imgui.CalcTextSize(description, nil, false, imgui.GetWindowContentRegionWidth()) 129 | bg_size.y = math.max(min_height, bg_size.y) 130 | bg_size.x = imgui.GetWindowContentRegionWidth() 131 | 132 | local cursor_pos_before = imgui.GetCursorPos() 133 | imgui.Dummy(bg_size) 134 | local r_min = imgui.GetItemRectMin() 135 | local r_max = imgui.GetItemRectMax() 136 | local cursor_pos_after = imgui.GetCursorPos() 137 | 138 | imgui.ImDrawList_AddRectFilled(imgui.GetWindowDrawList(), r_min, r_max, rect_color) 139 | 140 | imgui.SetCursorPos(cursor_pos_before) 141 | imgui.Text(description) 142 | imgui.SetCursorPos(cursor_pos_after) 143 | imgui.Spacing(2) 144 | end 145 | 146 | local function draw(dt) 147 | -- Search update 148 | local search_text = ffi.string(search_buffer) 149 | local filter_notfull = filter_servers_notfull[0] 150 | local filter_notempty = filter_servers_notempty[0] 151 | local filter_online = filter_servers_online[0] 152 | 153 | if search_text ~= prev_search_text or filter_notfull ~= prev_filter_notfull or filter_notempty ~= prev_filter_notempty or filter_online ~= prev_filter_online then 154 | time_since_filters_change = 0 155 | filter_queued = true 156 | end 157 | 158 | prev_search_text = search_text 159 | prev_filter_notfull = filter_notfull 160 | prev_filter_notempty = filter_notempty 161 | prev_filter_online = filter_online 162 | 163 | if time_since_filters_change > 0.5 and filter_queued then 164 | update_filtered_servers() 165 | filter_queued = false 166 | end 167 | 168 | time_since_filters_change = time_since_filters_change + dt 169 | 170 | draw_list_search_and_filters(false) 171 | 172 | local server_count = 0 173 | 174 | imgui.BeginChild1("Scrolling", imgui.ImVec2(0, -30), true) 175 | for addr, server in spairs(filtered_servers, function(t,a,b) return t[a].player_count > t[b].player_count end) do 176 | server_count = server_count + 1 177 | 178 | local header = server.name.." ["..server.player_count.."/"..server.max_players.."]" 179 | header = header .. "###server_header_"..tostring(server_count) 180 | 181 | if imgui.CollapsingHeader1(header) then 182 | imgui.PushTextWrapPos(0) 183 | imgui.Text("Address: "..addr) 184 | imgui.Text("Map: "..server.map) 185 | draw_server_description(server.description) 186 | imgui.PopTextWrapPos() 187 | if imgui.Button("Connect###connect_button_" .. tostring(server_count)) then 188 | kissconfig.save_config() 189 | local player_name = ffi.string(kissui.player_name) 190 | network.connect(addr, player_name) 191 | end 192 | 193 | local in_favorites_list = kissui.tabs.favorites.favorite_servers[addr] ~= nil 194 | if not in_favorites_list then 195 | imgui.SameLine() 196 | if imgui.Button("Add to Favorites###add_favorite_button_" .. tostring(server_count)) then 197 | kissui.tabs.favorites.add_server_to_favorites(addr, server) 198 | update_filtered_servers() 199 | end 200 | end 201 | end 202 | end 203 | 204 | imgui.PushTextWrapPos(0) 205 | if not kissui.bridge_launched then 206 | imgui.Text("Bridge is not launched. Please, launch the bridge and then hit 'Refresh list' button") 207 | elseif server_count == 0 then 208 | imgui.Text("Server list is empty") 209 | end 210 | imgui.PopTextWrapPos() 211 | 212 | imgui.EndChild() 213 | 214 | if imgui.Button("Refresh List", imgui.ImVec2(-1, 0)) then 215 | refresh_server_list() 216 | update_filtered_servers() 217 | end 218 | end 219 | 220 | M.draw = draw 221 | M.refresh = refresh_server_list 222 | M.update_filtered = update_filtered_servers 223 | 224 | return M 225 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissmp/ui/tabs/settings.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local imgui = ui_imgui 3 | 4 | local function draw() 5 | if imgui.Checkbox("Show Name Tags", kissui.show_nametags) then 6 | kissconfig.save_config() 7 | end 8 | if imgui.Checkbox("Show Players In Vehicles", kissui.show_drivers) then 9 | kissconfig.save_config() 10 | end 11 | imgui.Text("Window Opacity") 12 | imgui.SameLine() 13 | if imgui.SliderFloat("###window_opacity", kissui.window_opacity, 0, 1) then 14 | kissconfig.save_config() 15 | end 16 | if imgui.Checkbox("Enable view distance (Experimental)", kissui.enable_view_distance) then 17 | kissconfig.save_config() 18 | end 19 | if kissui.enable_view_distance[0] then 20 | if imgui.SliderInt("###view_distance", kissui.view_distance, 50, 1000) then 21 | kissconfig.save_config() 22 | end 23 | imgui.PushTextWrapPos(0) 24 | imgui.Text("Warning. This feature is experimental. It can introduce a small, usually unnoticeable lag spike when approaching nearby vehicles. It'll also block the ability to switch to far away vehicles") 25 | imgui.PopTextWrapPos() 26 | end 27 | end 28 | 29 | M.draw = draw 30 | 31 | return M 32 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissplayers.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.lerp_factor = 5 3 | M.players = {} 4 | M.players_in_cars = {} 5 | M.player_heads_attachments = {} 6 | M.player_transforms = {} 7 | 8 | local blacklist = { 9 | woodplanks = true, 10 | woodcrate = true, 11 | weightpad = true, 12 | wall = true, 13 | tv = true, 14 | tsfb = true, 15 | tube = true, 16 | trafficbarrel = true, 17 | tirewall = true, 18 | tirestacks = true, 19 | testroller = true, 20 | tanker = true, 21 | suspensionbridge = true, 22 | streetlight = true, 23 | shipping_container = true, 24 | sawhorse = true, 25 | rollover = true, 26 | rocks = true, 27 | roadsigns = true, 28 | piano = true, 29 | metal_ramp = true, 30 | metal_box = true, 31 | mattress = true, 32 | large_tilt = true, 33 | large_spinner = true, 34 | large_roller = true, 35 | large_hamster_wheel = true, 36 | large_crusher = true, 37 | large_cannon = true, 38 | large_bridge = true, 39 | large_angletester = true, 40 | kickplate = true, 41 | inflated_mat = true, 42 | haybale = true, 43 | gate = true, 44 | fridge = true, 45 | flipramp = true, 46 | flatbed = true, 47 | flail = true, 48 | couch = true, 49 | cones = true, 50 | christmas_tree = true, 51 | chair = true, 52 | cardboard_box = true, 53 | cannon = true, 54 | blockwall = true, 55 | barrier = true, 56 | barrels = true, 57 | ball = true, 58 | unicycle = true 59 | } 60 | 61 | local function get_player_color(id) 62 | math.randomseed(id) 63 | local r, g, b, a = 0.2 + math.random() * 0.8, 0.2 + math.random() * 0.8, 0.2 + math.random() * 0.8, 1 64 | math.randomseed(os.time()) 65 | return r, g, b, a 66 | end 67 | 68 | local function spawn_player(data) 69 | local player = createObject('TSStatic') 70 | player:setField("shapeName", 0, "/art/shapes/kissmp_playermodels/base_nb.dae") 71 | player:setField("dynamic", 0, "true") 72 | player.scale = Point3F(1, 1, 1) 73 | player:registerObject("player"..data.owner) 74 | player:setPosRot( 75 | data.position[1], data.position[2], data.position[3], 76 | data.rotation[1], data.rotation[2], data.rotation[3], data.rotation[4] 77 | ) 78 | local r, g, b, a = get_player_color(data.owner) 79 | player:setField('instanceColor', 0, string.format("%g %g %g %g", r, g, b, a)) 80 | vehiclemanager.id_map[data.server_id] = player:getID() 81 | vehiclemanager.server_ids[player:getID()] = data.server_id 82 | M.players[data.server_id] = player 83 | M.player_transforms[data.server_id] = { 84 | position = vec3(data.position), 85 | target_position = vec3(data.position), 86 | rotation = data.rotation, 87 | velocity = vec3(), 88 | time_past = 0 89 | } 90 | end 91 | 92 | local function update_players(dt) 93 | for id, data in pairs(M.player_transforms) do 94 | local player = M.players[id] 95 | if player and data then 96 | data.time_past = data.time_past + dt 97 | local old_position = data.position 98 | data.position = lerp(data.position, data.target_position + data.velocity * data.time_past, clamp(dt * M.lerp_factor, 0, 1)) 99 | local local_velocity = data.position - old_position 100 | local p = data.position + local_velocity * dt 101 | --player.position = m 102 | player:setPosRot( 103 | p.x, p.y, p.z, 104 | data.rotation[1], data.rotation[2], data.rotation[3], data.rotation[4] 105 | ) 106 | end 107 | end 108 | for id, player_data in pairs(network.players) do 109 | local vehicle = be:getObjectByID(vehiclemanager.id_map[player_data.current_vehicle or -1] or -1) 110 | if vehicle and (not blacklist[vehicle:getJBeamFilename()]) then 111 | local cam_node, _ = core_camera.getDriverData(vehicle) 112 | if cam_node and kisstransform.local_transforms[vehicle:getID()] then 113 | local p = vec3(vehicle:getNodePosition(cam_node)) + vec3(vehicle:getPosition()) 114 | local r = kisstransform.local_transforms[vehicle:getID()].rotation 115 | local hide = be:getPlayerVehicle(0) and (be:getPlayerVehicle(0):getID() == vehicle:getID()) and (vec3(getCameraPosition()):distance(p) < 2.5) 116 | hide = hide or (not kissui.show_drivers[0]) or kisstransform.inactive[vehicle:getID()] 117 | if (not M.players_in_cars[id]) and (not hide) then 118 | local player = createObject('TSStatic') 119 | player:setField("shapeName", 0, "/art/shapes/kissmp_playermodels/base_nb_head.dae") 120 | player:setField("dynamic", 0, "true") 121 | player.scale = Point3F(1, 1, 1) 122 | local r, g, b, a = get_player_color(id) 123 | player:setField('instanceColor', 0, string.format("%g %g %g %g", r, g, b, a)) 124 | player:registerObject("player_head"..id) 125 | M.players_in_cars[id] = player 126 | M.player_heads_attachments[id] = vehicle:getID() 127 | end 128 | if hide and M.players_in_cars[id] then 129 | M.players_in_cars[id]:delete() 130 | M.players_in_cars[id] = nil 131 | M.player_heads_attachments[id] = nil 132 | end 133 | p = p + vec3(vehicle:getVelocity()) * dt 134 | local player = M.players_in_cars[id] 135 | if player then 136 | player:setPosRot( 137 | p.x, p.y, p.z, 138 | r[1], r[2], r[3], r[4] 139 | ) 140 | end 141 | end 142 | else 143 | if M.players_in_cars[id] then 144 | M.players_in_cars[id]:delete() 145 | M.players_in_cars[id] = nil 146 | M.player_heads_attachments[id] = nil 147 | end 148 | end 149 | end 150 | for id, v in pairs(M.players_in_cars) do 151 | if not be:getObjectByID(M.player_heads_attachments[id] or -1) then 152 | v:delete() 153 | M.players_in_cars[id] = nil 154 | M.player_heads_attachments[id] = nil 155 | end 156 | end 157 | end 158 | 159 | M.spawn_player = spawn_player 160 | M.get_player_color = get_player_color 161 | M.onUpdate = update_players 162 | 163 | return M 164 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissrichpresence.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local http = require("socket.http") 3 | 4 | local function update() 5 | if (not network.connection.server_info) or (not network.connection.connected) then 6 | local _, _, _ = http.request("http://127.0.0.1:3693/rich_presence/none") 7 | Steam.clearRichPresence() 8 | return 9 | end 10 | 11 | local _, _, _ = http.request("http://127.0.0.1:3693/rich_presence/"..network.connection.server_info.name) 12 | Steam.setRichPresence("b", "KissMP - "..network.connection.server_info.name) 13 | end 14 | 15 | M.update = update 16 | return M 17 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kisstransform.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local generation = 0 4 | local timer = 0 5 | 6 | M.raw_transforms = {} 7 | M.received_transforms = {} 8 | M.local_transforms = {} 9 | M.raw_positions = {} 10 | M.inactive = {} 11 | 12 | M.threshold = 3 13 | M.rot_threshold = 2.5 14 | M.velocity_error_limit = 10 15 | 16 | M.hidden = {} 17 | 18 | local function update(dt) 19 | if not network.connection.connected then return end 20 | -- Get rotation/angular velocity from vehicle lua 21 | for i = 0, be:getObjectCount() do 22 | local vehicle = be:getObject(i) 23 | if vehicle and (not M.inactive[vehicle:getID()]) then 24 | vehicle:queueLuaCommand("kiss_vehicle.update_transform_info()") 25 | end 26 | end 27 | 28 | -- Don't apply velocity while paused. If we do, velocity gets stored up and released when the game resumes. 29 | local apply_velocity = not bullettime.getPause() 30 | for id, transform in pairs(M.received_transforms) do 31 | --apply_transform(dt, id, transform, apply_velocity) 32 | local vehicle = be:getObjectByID(id) 33 | local p = vec3(transform.position) 34 | if vehicle and apply_velocity and (not vehiclemanager.ownership[id]) then 35 | if ((p:distance(vec3(getCameraPosition())) > kissui.view_distance[0])) and kissui.enable_view_distance[0] then 36 | if (not M.inactive[id]) then 37 | vehicle:setActive(0) 38 | M.inactive[id] = true 39 | end 40 | else 41 | if M.inactive[id] then 42 | vehicle:setActive(1) 43 | M.inactive[id] = false 44 | end 45 | vehicle:queueLuaCommand("kiss_transforms.set_target_transform(\'"..jsonEncode(transform).."\')") 46 | vehicle:queueLuaCommand("kiss_transforms.update("..dt..")") 47 | end 48 | end 49 | end 50 | end 51 | 52 | local function update_vehicle_transform(data) 53 | local transform = data.transform 54 | transform.owner = data.vehicle_id 55 | transform.sent_at = data.sent_at 56 | 57 | local id = vehiclemanager.id_map[transform.owner or -1] or -1 58 | if vehiclemanager.ownership[id] then return end 59 | M.raw_positions[transform.owner or -1] = transform.position 60 | M.received_transforms[id] = transform 61 | 62 | local vehicle = be:getObjectByID(id) 63 | if vehicle and (not M.inactive[id]) then 64 | transform.time_past = clamp(vehiclemanager.get_current_time() - transform.sent_at, 0, 0.1) * 0.9 + 0.001 65 | vehicle:queueLuaCommand("kiss_transforms.set_target_transform(\'"..jsonEncode(transform).."\')") 66 | end 67 | end 68 | 69 | local function push_transform(id, t) 70 | M.local_transforms[id] = jsonDecode(t) 71 | end 72 | 73 | M.send_transform_updates = send_transform_updates 74 | M.send_vehicle_transform = send_vehicle_transform 75 | M.update_vehicle_transform = update_vehicle_transform 76 | M.push_transform = push_transform 77 | M.onUpdate = update 78 | 79 | return M 80 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissui.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local http = require("socket.http") 3 | 4 | local bor = bit.bor 5 | 6 | local main_window = require("kissmp.ui.main") 7 | M.chat = require("kissmp.ui.chat") 8 | M.download_window = require("kissmp.ui.download") 9 | local names = require("kissmp.ui.names") 10 | 11 | M.tabs = { 12 | server_list = require("kissmp.ui.tabs.server_list"), 13 | favorites = require("kissmp.ui.tabs.favorites"), 14 | settings = require("kissmp.ui.tabs.settings"), 15 | direct_connect = require("kissmp.ui.tabs.direct_connect"), 16 | create_server = require("kissmp.ui.tabs.create_server"), 17 | } 18 | 19 | M.dependencies = {"ui_imgui"} 20 | 21 | M.master_addr = "http://kissmp.online:3692/" 22 | M.bridge_launched = false 23 | 24 | M.show_download = false 25 | M.downloads_info = {} 26 | 27 | -- Color constants 28 | M.COLOR_YELLOW = {r = 1, g = 1, b = 0} 29 | M.COLOR_RED = {r = 1, g = 0, b = 0} 30 | 31 | M.force_disable_nametags = false 32 | 33 | local gui_module = require("ge/extensions/editor/api/gui") 34 | M.gui = {setupEditorGuiTheme = nop} 35 | local imgui = ui_imgui 36 | 37 | local ui_showing = false 38 | 39 | -- TODO: Move all this somewhere else. Some of settings aren't even related to UI 40 | M.addr = imgui.ArrayChar(128) 41 | M.player_name = imgui.ArrayChar(32, "Unknown") 42 | M.show_nametags = imgui.BoolPtr(true) 43 | M.show_drivers = imgui.BoolPtr(true) 44 | M.window_opacity = imgui.FloatPtr(0.8) 45 | M.enable_view_distance = imgui.BoolPtr(true) 46 | M.view_distance = imgui.IntPtr(300) 47 | 48 | local function show_ui() 49 | M.gui.showWindow("KissMP") 50 | M.gui.showWindow("Chat") 51 | M.gui.showWindow("Downloads") 52 | ui_showing = true 53 | end 54 | 55 | local function hide_ui() 56 | M.gui.hideWindow("KissMP") 57 | M.gui.hideWindow("Chat") 58 | M.gui.hideWindow("Downloads") 59 | M.gui.hideWindow("Add Favorite") 60 | ui_showing = false 61 | end 62 | 63 | local function toggle_ui() 64 | if not ui_showing then 65 | show_ui() 66 | else 67 | hide_ui() 68 | end 69 | end 70 | 71 | local function open_ui() 72 | main_window.init(M) 73 | gui_module.initialize(M.gui) 74 | M.gui.registerWindow("KissMP", imgui.ImVec2(256, 256)) 75 | M.gui.registerWindow("Chat", imgui.ImVec2(256, 256)) 76 | M.gui.registerWindow("Downloads", imgui.ImVec2(512, 512)) 77 | M.gui.registerWindow("Add Favorite", imgui.ImVec2(256, 128)) 78 | M.gui.registerWindow("Incorrect install detected", imgui.ImVec2(256, 128)) 79 | M.gui.hideWindow("Add Favorite") 80 | show_ui() 81 | end 82 | 83 | local function bytes_to_mb(bytes) 84 | return (bytes / 1024) / 1024 85 | end 86 | 87 | local function draw_incorrect_install() 88 | if imgui.Begin("Incorrect install detected") then 89 | imgui.Text("Incorrect KissMP install. Please, check if mod path is correct") 90 | end 91 | imgui.End() 92 | end 93 | 94 | local function onUpdate(dt) 95 | if getMissionFilename() ~= '' and not vehiclemanager.is_network_session then 96 | return 97 | end 98 | main_window.draw(dt) 99 | M.chat.draw() 100 | M.download_window.draw() 101 | if M.incorrect_install then 102 | draw_incorrect_install() 103 | end 104 | if (not M.force_disable_nametags) and M.show_nametags[0] then 105 | names.draw() 106 | end 107 | end 108 | 109 | M.onExtensionLoaded = open_ui 110 | M.onUpdate = onUpdate 111 | 112 | -- Backwards compatability 113 | M.add_message = M.chat.add_message 114 | M.draw_download = M.download_window.draw 115 | 116 | M.show_ui = show_ui 117 | M.hide_ui = hide_ui 118 | M.toggle_ui = toggle_ui 119 | 120 | return M 121 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissutils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.hooks = { 4 | internal = {} 5 | } 6 | 7 | M.hooks.clear = function() 8 | M.hooks.internal = {} 9 | end 10 | 11 | M.hooks.register = function(hook_name, subname, fn) 12 | if not M.hooks.internal[hook_name] then M.hooks.internal[hook_name] = {} end 13 | M.hooks.internal[hook_name][sub_name] = fn 14 | end 15 | 16 | M.hooks.call = function(hook_name, ...) 17 | for k, v in pairs(M.hooks.internal[hook_name]) do 18 | v(arg) 19 | end 20 | end 21 | 22 | local function onUpdate(dt) 23 | M.hooks.call("onUpdate", dt) 24 | end 25 | 26 | --M.onUpdate = onUpdate 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/kissvoicechat.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | M.el = vec3(0.08, 0, 0) 3 | M.er = vec3(-0.08, 0, 0) 4 | 5 | local function onUpdate() 6 | if not network.connection.connected then return end 7 | local position = vec3(getCameraPosition() or vec3()) 8 | local ear_left = M.el:rotated(quat(getCameraQuat())) 9 | local ear_right = M.er:rotated(quat(getCameraQuat())) 10 | local pl = position + ear_left 11 | local pr = position + ear_right 12 | --debugDrawer:drawSphere((pl + vec3(0, 2, 0):rotated(quat(getCameraQuat()))):toPoint3F(), 0.05, ColorF(0,1,0,0.8)) 13 | --debugDrawer:drawSphere((pr + vec3(0, 2, 0):rotated(quat(getCameraQuat()))):toPoint3F(), 0.05, ColorF(0,0,1,0.8)) 14 | network.send_data({ 15 | SpatialUpdate = {{pl.x, pl.y, pl.z}, {pr.x, pr.y, pr.z}} 16 | }) 17 | end 18 | 19 | local function start_vc() 20 | network.send_data('"StartTalking"') 21 | end 22 | 23 | 24 | local function end_vc() 25 | network.send_data('"EndTalking"') 26 | end 27 | 28 | M.onUpdate = onUpdate 29 | M.start_vc = start_vc 30 | M.end_vc = end_vc 31 | 32 | return M 33 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/ge/extensions/network.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.VERSION_STR = "0.6.0" 4 | 5 | M.downloads = {} 6 | M.downloading = false 7 | M.downloads_status = {} 8 | 9 | local current_download = nil 10 | 11 | local socket = require("socket") 12 | local messagepack = require("lua/common/libs/Lua-MessagePack/MessagePack") 13 | local ping_send_time = 0 14 | 15 | M.players = {} 16 | M.socket = socket 17 | M.base_secret = "None" 18 | M.connection = { 19 | tcp = nil, 20 | connected = false, 21 | client_id = 0, 22 | heartbeat_time = 1, 23 | timer = 0, 24 | tickrate = 33, 25 | mods_left = 0, 26 | ping = 0, 27 | time_offset = 0 28 | } 29 | 30 | local FILE_TRANSFER_CHUNK_SIZE = 16384; 31 | 32 | local message_handlers = {} 33 | 34 | local time_offset_smoother = { 35 | samples = {}, 36 | current_sample = 1, 37 | } 38 | 39 | time_offset_smoother.get = function(new_sample) 40 | if time_offset_smoother.current_sample < 30 then 41 | time_offset_smoother.samples[time_offset_smoother.current_sample] = new_sample 42 | else 43 | time_offset_smoother.current_sample = 0 44 | end 45 | time_offset_smoother.current_sample = time_offset_smoother.current_sample + 1 46 | local sum = 0 47 | local n = 0 48 | for _, v in pairs(time_offset_smoother.samples) do 49 | sum = sum + v 50 | n = n + 1 51 | end 52 | return sum / n 53 | end 54 | 55 | local function bytesToU32(str) 56 | local b1, b2, b3, b4 = str:byte(1, 4) 57 | return bit.bor( 58 | bit.lshift(b4, 24), 59 | bit.lshift(b3, 16), 60 | bit.lshift(b2, 8), 61 | b1 62 | ) 63 | end 64 | 65 | local function disconnect(data) 66 | local text = "Disconnected!" 67 | if data then 68 | text = text.." Reason: "..data 69 | end 70 | kissui.chat.add_message(text) 71 | M.connection.connected = false 72 | M.connection.tcp:close() 73 | M.players = {} 74 | kissplayers.players = {} 75 | kissplayers.player_transforms = {} 76 | kissplayers.players_in_cars = {} 77 | kissplayers.player_heads_attachments = {} 78 | kissrichpresence.update() 79 | --vehiclemanager.id_map = {} 80 | --vehiclemanager.ownership = {} 81 | --vehiclemanager.delay_spawns = false 82 | --kissui.force_disable_nametags = false 83 | --Lua:requestReload() 84 | --kissutils.hooks.clear() 85 | returnToMainMenu() 86 | end 87 | 88 | local function handle_disconnected(data) 89 | disconnect(data) 90 | end 91 | 92 | local function handle_file_transfer(data) 93 | kissui.show_download = true 94 | -- local file_len = ffi.cast("uint32_t*", ffi.new("char[?]", 5, data:sub(1, 4)))[0] 95 | local file_len = bytesToU32(data:sub(1, 4)) 96 | local file_name = data:sub(5, #data) 97 | local chunks = math.floor(file_len / FILE_TRANSFER_CHUNK_SIZE) 98 | 99 | current_download = { 100 | file_len = file_len, 101 | file_name = file_name, 102 | chunks = chunks, 103 | last_chunk = file_len - chunks * FILE_TRANSFER_CHUNK_SIZE, 104 | current_chunk = 0, 105 | file = kissmods.open_file(file_name) 106 | } 107 | M.downloading = true 108 | end 109 | 110 | local function handle_player_info(player_info) 111 | M.players[player_info.id] = player_info 112 | end 113 | 114 | local function check_lua(l) 115 | local filters = {"FS", "check_lua", "handle_lua", "handle_vehicle_lua", "network =", "network=", "message_handlers", "io%.write", "io%.open", "io%.close", "fileOpen", "fileExists", "removeDirectory", "removeFile", "io%."} 116 | for k, v in pairs(filters) do 117 | if string.find(l, v) ~= nil then 118 | kissui.chat.add_message("Possibly malicious lua command has been send, rejecting. Found: "..v) 119 | return false 120 | end 121 | end 122 | return true 123 | end 124 | 125 | local function handle_lua(data) 126 | if check_lua(data) then 127 | Lua:queueLuaCommand(data) 128 | end 129 | end 130 | 131 | local function handle_vehicle_lua(data) 132 | local id = data[1] 133 | local lua = data[2] 134 | local id = vehiclemanager.id_map[id or -1] or 0 135 | local vehicle = be:getObjectByID(id) 136 | if vehicle and check_lua(lua) then 137 | vehicle:queueLuaCommand(lua) 138 | end 139 | end 140 | 141 | local function handle_pong(data) 142 | local server_time = data 143 | local local_time = socket.gettime() 144 | local ping = local_time - ping_send_time 145 | if ping > 1 then return end 146 | local time_diff = server_time - local_time + (ping / 2) 147 | M.connection.time_offset = time_offset_smoother.get(time_diff) 148 | M.connection.ping = ping * 1000 149 | end 150 | 151 | local function handle_player_disconnected(data) 152 | local id = data 153 | M.players[id] = nil 154 | end 155 | 156 | local function handle_chat(data) 157 | kissui.chat.add_message(data[1], nil, data[2]) 158 | end 159 | 160 | local function onExtensionLoaded() 161 | message_handlers.VehicleUpdate = vehiclemanager.update_vehicle 162 | message_handlers.VehicleSpawn = vehiclemanager.spawn_vehicle 163 | message_handlers.RemoveVehicle = vehiclemanager.remove_vehicle 164 | message_handlers.ResetVehicle = vehiclemanager.reset_vehicle 165 | message_handlers.Chat = handle_chat 166 | message_handlers.SendLua = handle_lua 167 | message_handlers.PlayerInfoUpdate = handle_player_info 168 | message_handlers.VehicleMetaUpdate = vehiclemanager.update_vehicle_meta 169 | message_handlers.Pong = handle_pong 170 | message_handlers.PlayerDisconnected = handle_player_disconnected 171 | message_handlers.VehicleLuaCommand = handle_vehicle_lua 172 | message_handlers.CouplerAttached = vehiclemanager.attach_coupler 173 | message_handlers.CouplerDetached = vehiclemanager.detach_coupler 174 | message_handlers.ElectricsUndefinedUpdate = vehiclemanager.electrics_diff_update 175 | end 176 | 177 | local function send_data(raw_data, reliable) 178 | if type(raw_data) == "number" then 179 | print("NOT IMPLEMENTED. PLEASE REPORT TO KISSMP DEVELOPERS. CODE: "..raw_data) 180 | return 181 | end 182 | local data = "" 183 | -- Used in context of it being called from vehicle lua, where it's already encoded into json 184 | if type(raw_data) == "string" then 185 | data = raw_data 186 | else 187 | data = jsonEncode(raw_data) 188 | end 189 | if not M.connection.connected then return -1 end 190 | local len = #data 191 | local len = ffi.string(ffi.new("uint32_t[?]", 1, {len}), 4) 192 | if reliable then 193 | reliable = 1 194 | else 195 | reliable = 0 196 | end 197 | M.connection.tcp:send(string.char(reliable)..len) 198 | M.connection.tcp:send(data) 199 | end 200 | 201 | local function sanitize_addr(addr) 202 | -- Trim leading and trailing spaces that might occur during a copy/paste 203 | local sanitized = addr:gsub("^%s*(.-)%s*$", "%1") 204 | 205 | -- Check if port is missing, add default port if so 206 | if not sanitized:find(":") then 207 | sanitized = sanitized .. ":3698" 208 | end 209 | return sanitized 210 | end 211 | 212 | local function generate_secret(server_identifier) 213 | local secret = server_identifier..M.base_secret 214 | return hashStringSHA1(secret) 215 | end 216 | 217 | local function change_map(map) 218 | if FS:fileExists(map) or FS:directoryExists(map) then 219 | vehiclemanager.loading_map = true 220 | freeroam_freeroam.startFreeroam(map) 221 | else 222 | kissui.chat.add_message("Map file doesn't exist. Check if mod containing map is enabled", kissui.COLOR_RED) 223 | disconnect() 224 | end 225 | end 226 | 227 | local function connect(addr, player_name) 228 | if M.connection.connected then 229 | disconnect() 230 | end 231 | M.players = {} 232 | 233 | print("Connecting...") 234 | addr = sanitize_addr(addr) 235 | kissui.chat.add_message("Connecting to "..addr.."...") 236 | M.connection.tcp = socket.tcp() 237 | M.connection.tcp:settimeout(3.0) 238 | local connected, err = M.connection.tcp:connect("127.0.0.1", "7894") 239 | 240 | -- Send server address to the bridge 241 | local addr_lenght = ffi.string(ffi.new("uint32_t[?]", 1, {#addr}), 4) 242 | M.connection.tcp:send(addr_lenght) 243 | M.connection.tcp:send(addr) 244 | 245 | local connection_confirmed = M.connection.tcp:receive(1) 246 | if connection_confirmed then 247 | if connection_confirmed ~= string.char(1) then 248 | kissui.chat.add_message("Connection failed.", kissui.COLOR_RED) 249 | return 250 | end 251 | else 252 | kissui.chat.add_message("Failed to confirm connection. Check if bridge is running.", kissui.COLOR_RED) 253 | return 254 | end 255 | 256 | -- Ignore message type 257 | M.connection.tcp:receive(1) 258 | 259 | local len, _, _ = M.connection.tcp:receive(4) 260 | len = bytesToU32(len) 261 | 262 | local received, _, _ = M.connection.tcp:receive(len) 263 | print(received) 264 | local server_info = jsonDecode(received).ServerInfo 265 | if not server_info then 266 | print("Failed to fetch server info") 267 | return 268 | end 269 | print("Server name: "..server_info.name) 270 | print("Player count: "..server_info.player_count) 271 | 272 | M.connection.tcp:settimeout(0.0) 273 | M.connection.connected = true 274 | M.connection.client_id = server_info.client_id 275 | M.connection.server_info = server_info 276 | M.connection.tickrate = server_info.tickrate 277 | 278 | local steamid64 = nil 279 | if Steam and Steam.isWorking then 280 | steamid64 = Steam.getAccountIDStr() ~= "0" and Steam.getAccountIDStr() or nil 281 | end 282 | 283 | local client_info = { 284 | ClientInfo = { 285 | name = player_name, 286 | secret = generate_secret(server_info.server_identifier), 287 | steamid64 = steamid64, 288 | client_version = {0, 6} 289 | } 290 | } 291 | send_data(client_info, true) 292 | 293 | kissmods.set_mods_list(server_info.mods) 294 | kissmods.update_status_all() 295 | 296 | local missing_mods = {} 297 | local mod_names = {} 298 | for _, mod in pairs(kissmods.mods) do 299 | table.insert(mod_names, mod.name) 300 | if mod.status ~= "ok" then 301 | table.insert(missing_mods, mod.name) 302 | M.downloads_status[mod.name] = {name = mod.name, progress = 0} 303 | end 304 | end 305 | M.connection.mods_left = #missing_mods 306 | 307 | kissmods.deactivate_all_mods() 308 | for k, v in pairs(missing_mods) do 309 | print(k.." "..v) 310 | end 311 | if #missing_mods > 0 then 312 | -- Request mods 313 | send_data( 314 | { 315 | RequestMods = missing_mods 316 | }, 317 | true 318 | ) 319 | end 320 | vehiclemanager.loading_map = true 321 | if #missing_mods == 0 then 322 | kissmods.mount_mods(mod_names) 323 | change_map(server_info.map) 324 | end 325 | kissrichpresence.update() 326 | kissui.chat.add_message("Connected!") 327 | end 328 | 329 | local function send_messagepack(data_type, reliable, data) 330 | local data = data 331 | if type(data) == "string" then 332 | data = jsonDecode(data) 333 | end 334 | data = messagepack.pack(data) 335 | send_data(data_type, reliable, data) 336 | end 337 | 338 | local function on_finished_download() 339 | vehiclemanager.loading_map = true 340 | change_map(M.connection.server_info.map) 341 | end 342 | 343 | local function send_ping() 344 | ping_send_time = socket.gettime() 345 | send_data( 346 | { 347 | Ping = math.floor(M.connection.ping), 348 | }, 349 | false 350 | ) 351 | end 352 | 353 | local function cancel_download() 354 | --[[if not current_download then return end 355 | io.close(current_download.file) 356 | current_download = nil 357 | M.downloading = false]]-- 358 | for k, v in pairs(M.downloads) do 359 | M.downloads[k]:close() 360 | end 361 | end 362 | 363 | local function onUpdate(dt) 364 | if not M.connection.connected then return end 365 | if M.connection.timer < M.connection.heartbeat_time then 366 | M.connection.timer = M.connection.timer + dt 367 | else 368 | M.connection.timer = 0 369 | send_ping() 370 | end 371 | 372 | while true do 373 | local msg_type = M.connection.tcp:receive(1) 374 | if not msg_type then break end 375 | --print("msg_t"..string.byte(msg_type)) 376 | M.connection.tcp:settimeout(5.0) 377 | -- JSON data 378 | if string.byte(msg_type) == 1 then 379 | local data = M.connection.tcp:receive(4) 380 | local len = bytesToU32(data) 381 | local data, _, _ = M.connection.tcp:receive(len) 382 | M.connection.tcp:settimeout(0.0) 383 | local data_decoded = jsonDecode(data) 384 | for k, v in pairs(data_decoded) do 385 | if message_handlers[k] then 386 | message_handlers[k](v) 387 | end 388 | end 389 | elseif string.byte(msg_type) == 0 then -- Binary data 390 | M.downloading = true 391 | kissui.show_download = true 392 | local name_b = M.connection.tcp:receive(4) 393 | local len_n = bytesToU32(name_b) 394 | local name, _, _ = M.connection.tcp:receive(len_n) 395 | local chunk_n_b = M.connection.tcp:receive(4) 396 | local chunk_a_b = M.connection.tcp:receive(4) 397 | local read_size_b = M.connection.tcp:receive(4) 398 | local chunk_n = bytesToU32(chunk_n_b) 399 | local chunk_a = bytesToU32(chunk_a_b) 400 | local read_size = bytesToU32(read_size_b) 401 | local file_length = chunk_a 402 | local file_data, _, _ = M.connection.tcp:receive(read_size) 403 | M.downloads_status[name] = { 404 | name = name, 405 | progress = 0 406 | } 407 | M.downloads_status[name].progress = chunk_n * FILE_TRANSFER_CHUNK_SIZE / file_length 408 | local file = M.downloads[name] 409 | if not file then 410 | M.downloads[name] = kissmods.open_file(name) 411 | end 412 | M.downloads[name]:write(file_data) 413 | if read_size < FILE_TRANSFER_CHUNK_SIZE then 414 | M.downloading = false 415 | kissui.show_download = false 416 | kissmods.mount_mod(name) 417 | M.downloads[name]:close() 418 | M.downloads[name] = nil 419 | M.downloads_status = {} 420 | M.connection.mods_left = M.connection.mods_left - 1 421 | end 422 | if M.connection.mods_left <= 0 then 423 | on_finished_download() 424 | end 425 | M.connection.tcp:settimeout(0.0) 426 | break 427 | elseif string.byte(msg_type) == 2 then 428 | local len_b = M.connection.tcp:receive(4) 429 | local len = bytesToU32(len_b) 430 | local reason, _, _ = M.connection.tcp:receive(len) 431 | disconnect(reason) 432 | end 433 | end 434 | end 435 | 436 | local function get_client_id() 437 | return M.connection.client_id 438 | end 439 | 440 | M.get_client_id = get_client_id 441 | M.connect = connect 442 | M.disconnect = disconnect 443 | M.cancel_download = cancel_download 444 | M.send_data = send_data 445 | M.onUpdate = onUpdate 446 | M.send_messagepack = send_messagepack 447 | M.onExtensionLoaded = onExtensionLoaded 448 | 449 | return M 450 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_couplers.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local ownership = false 3 | local ignore_attachment = false 4 | local ignore_detachment = false 5 | 6 | local ignored_couplers = {} 7 | 8 | local function ignore_coupler_node(node) 9 | ignored_couplers[node] = true 10 | end 11 | 12 | local function attach_coupler(node) 13 | local node = v.data.nodes[node] 14 | obj:attachCoupler(node.cid, node.couplerTag or "", node.couplerStrength or 1000000, node.couplerRadius or 0.2, 0, node.couplerLatchSpeed or 0.3, node.couplerTargets or 0) 15 | ignore_attachment = true 16 | end 17 | 18 | local function detach_coupler(node) 19 | obj:detachCoupler(node, 0) 20 | ignore_detachment = true 21 | end 22 | 23 | local function onCouplerAttached(node_id, obj2_id, obj2_node_id) 24 | if not ownership then return end 25 | if ignored_couplers[node_id] then return end 26 | if ignore_attachment then 27 | ignore_attachment = false 28 | return 29 | end 30 | local data = { 31 | obj_a = obj:getID(), 32 | obj_b = obj2_id, 33 | node_a_id = node_id, 34 | node_b_id = obj2_node_id 35 | } 36 | obj:queueGameEngineLua("vehiclemanager.attach_coupler_inner(\'"..jsonEncode(data).."\')") 37 | end 38 | 39 | local function onCouplerDetached(node_id, obj2_id, obj2_node_id) 40 | if not ownership then return end 41 | if ignored_couplers[node_id] then return end 42 | if ignore_detachment then 43 | ignore_detachment = false 44 | return 45 | end 46 | local data = { 47 | obj_a = obj:getID(), 48 | obj_b = obj2_id, 49 | node_a_id = node_id, 50 | node_b_id = obj2_node_id 51 | } 52 | obj:queueGameEngineLua("vehiclemanager.detach_coupler_inner(\'"..jsonEncode(data).."\')") 53 | end 54 | 55 | local function kissUpdateOwnership(owned) 56 | ownership = owned 57 | end 58 | 59 | M.ignore_coupler_node = ignore_coupler_node 60 | M.onCouplerAttached = onCouplerAttached 61 | M.onCouplerDetached = onCouplerDetached 62 | M.kissUpdateOwnership = kissUpdateOwnership 63 | M.attach_coupler = attach_coupler 64 | M.detach_coupler = detach_coupler 65 | 66 | return M 67 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_electrics.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local prev_electrics = {} 3 | local prev_signal_electrics = {} 4 | local last_engine_state = true 5 | local engine_timer = 0 6 | local ownership = false 7 | 8 | local ignored_keys = { 9 | throttle = true, 10 | throttle_input = true, 11 | brake = true, 12 | brake_input = true, 13 | clutch = true, 14 | clutch_input = true, 15 | clutchRatio = true, 16 | parkingbrake = true, 17 | parkingbrake_input = true, 18 | steering = true, 19 | steering_input = true, 20 | regenThrottle = true, 21 | reverse = true, 22 | parking = true, 23 | lights = true, 24 | turnsignal = true, 25 | hazard = true, 26 | hazard_enabled = true, 27 | signal_R = true, 28 | signal_L = true, 29 | gear = true, 30 | gear_M = true, 31 | gear_A = true, 32 | gearIndex = true, 33 | exhaustFlow = true, 34 | engineLoad = true, 35 | airspeed = true, 36 | axle_FL = true, 37 | airflowspeed = true, 38 | watertemp = true, 39 | driveshaft_F = true, 40 | rpmspin = true, 41 | wheelspeed = true, 42 | oil = true, 43 | rpm = true, 44 | altitude = true, 45 | avgWheelAV = true, 46 | lowpressure = true, 47 | lowhighbeam = true, 48 | lowbeam = true, 49 | highbeam = true, 50 | oiltemp = true, 51 | rpmTacho = true, 52 | axle_FR = true, 53 | fuel_volume = true, 54 | driveshaft = true, 55 | fuel = true, 56 | engineThrottle = true, 57 | fuelCapacity = true, 58 | fuelVolume = true, 59 | turboSpin = true, 60 | turboRPM = true, 61 | turboBoost = true, 62 | virtualAirspeed = true, 63 | turboRpmRatio = true, 64 | lockupClutchRatio = true, 65 | abs = true, 66 | absActive = true, 67 | tcs = true, 68 | tcsActive = true, 69 | esc = true, 70 | escActive = true, 71 | brakelights = true, 72 | radiatorFanSpin = true, 73 | smoothShiftLogicAV = true, 74 | accXSmooth = true, 75 | accYSmooth = true, 76 | accZSmooth = true, 77 | trip = true, 78 | odometer = true, 79 | steeringUnassisted = true, 80 | boost = true, 81 | superchargerBoost = true, 82 | gearModeIndex = true, 83 | hPatternAxisX = true, 84 | hPatternAxisY = true, 85 | tirePressureControl_activeGroupPressure = true, 86 | reverse_wigwag_L = true, 87 | reverse_wigwag_R = true, 88 | highbeam_wigwag_L = true, 89 | highbeam_wigwag_R = true, 90 | lowhighbeam_signal_L = true, 91 | lowhighbeam_signal_R = true, 92 | brakelight_signal_L = true, 93 | brakelight_signal_R = true, 94 | isYCBrakeActive = true, 95 | isTCBrakeActive = true, 96 | isABSBrakeActive = true, 97 | dseWarningPulse = true, 98 | dseRollingOver = true, 99 | dseRollOverStopped = true, 100 | dseCrashStopped = true 101 | } 102 | 103 | local electrics_handlers = {} 104 | 105 | local function ignore_key(key) 106 | ignored_keys[key] = true 107 | end 108 | 109 | local function update_engine_state() 110 | if ownership then return end 111 | if not electrics.values.engineRunning then return end 112 | local engine_running = electrics.values.engineRunning > 0.5 113 | 114 | -- Trigger starter to swap the engine state 115 | if engine_running ~= last_engine_state then 116 | controller.mainController.setStarter(true) 117 | end 118 | end 119 | 120 | local function updateGFX(dt) 121 | engine_timer = engine_timer + dt 122 | if engine_timer > 5 then 123 | update_engine_state() 124 | engine_timer = engine_timer - 5 125 | end 126 | end 127 | 128 | local function send() 129 | local diff_count = 0 130 | local data = { 131 | diff = {} 132 | } 133 | for key, value in pairs(electrics.values) do 134 | if not ignored_keys[key] and type(value) == 'number' then 135 | if prev_electrics[key] ~= value then 136 | data.diff[key] = value 137 | diff_count = diff_count + 1 138 | end 139 | prev_electrics[key] = value 140 | end 141 | end 142 | local data = { 143 | ElectricsUndefinedUpdate = {obj:getID(), data} 144 | } 145 | if diff_count > 0 then 146 | print("=== ELECTRICS BEING SENT ===\n" .. jsonEncode(data)) 147 | obj:queueGameEngineLua("network.send_data(\'"..jsonEncode(data).."\', true)") 148 | end 149 | end 150 | 151 | local function apply_diff_signals(diff) 152 | local signal_left_input = diff["signal_left_input"] or prev_signal_electrics["signal_left_input"] or 0 153 | local signal_right_input = diff["signal_right_input"] or prev_signal_electrics["signal_right_input"] or 0 154 | local hazard_enabled = (signal_left_input > 0.5 and signal_right_input > 0.5) 155 | 156 | if hazard_enabled then 157 | electrics.set_warn_signal(1) 158 | else 159 | electrics.set_warn_signal(0) 160 | if signal_left_input > 0.5 then 161 | electrics.toggle_left_signal() 162 | elseif signal_right_input > 0.5 then 163 | electrics.toggle_right_signal() 164 | end 165 | end 166 | 167 | prev_signal_electrics["signal_left_input"] = signal_left_input 168 | prev_signal_electrics["signal_right_input"] = signal_right_input 169 | end 170 | 171 | local function set_drive_mode(electric_name, drive_mode_controller, desired_value) 172 | -- drive modes only allow applying them by the key, we'll cycle all of them and 173 | -- if it's not found it'll return to the previous state 174 | local currentDriveMode = drive_mode_controller.getCurrentDriveModeKey() 175 | while true do 176 | drive_mode_controller.nextDriveMode() 177 | if math.abs(electrics.values[electric_name] - desired_value) < 0.1 then break end 178 | if drive_mode_controller.getCurrentDriveModeKey() == currentDriveMode then break end 179 | end 180 | end 181 | 182 | local function update_advanced_coupler_state(coupler_control_controller, value) 183 | -- the value indicates "notattached" 184 | local is_open = value > 0.5 185 | if not is_open then 186 | coupler_control_controller.tryAttachGroupImpulse() 187 | else 188 | coupler_control_controller.detachGroup() 189 | end 190 | end 191 | 192 | local function apply_diff(data) 193 | local diff = jsonDecode(data) 194 | apply_diff_signals(diff) 195 | for k, v in pairs(diff) do 196 | electrics.values[k] = v 197 | 198 | local handler = electrics_handlers[k] 199 | if handler then handler(v) end 200 | end 201 | end 202 | 203 | local function onExtensionLoaded() 204 | -- Ignore powertrain electrics 205 | local devices = powertrain.getDevices() 206 | for _, device in pairs(devices) do 207 | if device.electricsName and device.visualShaftAngle then 208 | ignore_key(device.electricsName) 209 | end 210 | if device.electricsThrottleName then 211 | ignore_key(device.electricsThrottleName) 212 | end 213 | if device.electricsThrottleFactorName then 214 | ignore_key(device.electricsThrottleFactorName) 215 | end 216 | if device.electricsClutchRatio1Name then 217 | ignore_key(device.electricsClutchRatio1Name) 218 | end 219 | if device.electricsClutchRatio2Name then 220 | ignore_key(device.electricsClutchRatio2Name) 221 | end 222 | end 223 | 224 | -- Ignore common led electrics 225 | for i = 0, 10 do 226 | ignore_key("led"..tostring(i)) 227 | end 228 | 229 | -- Ignore controller electrics 230 | if v.data.controller and type(v.data.controller) == 'table' then 231 | for _, controller_data in pairs(v.data.controller) do 232 | if controller_data.fileName == "lightbar" and controller_data.modes then 233 | -- ignore lightbar electrics 234 | local modes = tableFromHeaderTable(controller_data.modes) 235 | for _, vm in pairs(modes) do 236 | local configEntries = tableFromHeaderTable(deepcopy(vm.config)) 237 | for _, j in pairs(configEntries) do 238 | ignore_key(j.electric) 239 | end 240 | end 241 | elseif controller_data.fileName == "jato" then 242 | -- ignore jato fuel 243 | ignore_key("jatofuel") 244 | elseif controller_data.fileName == "beaconSpin" and controller_data.electricsName then 245 | -- ignore beacon spin 246 | ignore_key(controller_data.electricsName) 247 | elseif controller_data.fileName == "driveModes" and controller_data.modes then 248 | -- register handlers for syncing drive modes 249 | for _, vm in pairs(controller_data.modes) do 250 | if vm.settings then 251 | for _, vs in pairs(vm.settings) do 252 | if vs[1] == "electricsValue" then 253 | local electric = vs[2].electricsName 254 | local drive_mode_controller = controller.getController(controller_data.name) 255 | electrics_handlers[electric] = function(v) set_drive_mode(electric, drive_mode_controller, v) end 256 | end 257 | end 258 | end 259 | end 260 | elseif controller_data.fileName == "advancedCouplerControl" then 261 | -- register handler for syncing advanced couplers 262 | local electric = controller_data.name .. "_notAttached" 263 | local coupler_control_controller = controller.getController(controller_data.name) 264 | electrics_handlers[electric] = function(v) update_advanced_coupler_state(coupler_control_controller, v) end 265 | 266 | -- ignore the related couplers, we'll manage them now 267 | for _, vn in pairs(tableFromHeaderTable(controller_data.couplerNodes)) do 268 | local cid1 = beamstate.nodeNameMap[vn.cid1] 269 | local cid2 = beamstate.nodeNameMap[vn.cid2] 270 | kiss_couplers.ignore_coupler_node(cid1) 271 | kiss_couplers.ignore_coupler_node(cid2) 272 | end 273 | end 274 | end 275 | end 276 | 277 | -- Ignore commonly used disp_* electrics used on vehicles with gear displays 278 | for k,v in pairs(electrics.values) do 279 | if type(k) == 'string' and k:sub(1,5) == "disp_" then 280 | ignored_keys[k] = true 281 | end 282 | end 283 | 284 | -- Ignore common extension/controller electrics 285 | if _G["4ws"] and type(_G["4ws"]) == 'table' then 286 | ignored_keys["4ws"] = true 287 | end 288 | 289 | -- Register handlers 290 | electrics_handlers["lights_state"] = function(v) electrics.setLightsState(v) end 291 | electrics_handlers["fog"] = function(v) electrics.set_fog_lights(v) end 292 | electrics_handlers["lightbar"] = function(v) electrics.set_lightbar_signal(v) end 293 | electrics_handlers["horn"] = function(v) electrics.horn(v > 0.5) end 294 | electrics_handlers["hasABS"] = function(v) 295 | if v > 0.5 then 296 | wheels.setABSBehavior("realistic") 297 | else 298 | wheels.setABSBehavior("off") 299 | end 300 | end 301 | electrics_handlers["engineRunning"] = function(v) 302 | last_engine_state = v > 0.5 303 | update_engine_state() 304 | engine_timer = 0 305 | end 306 | end 307 | 308 | local function kissUpdateOwnership(owned) 309 | ownership = owned 310 | end 311 | 312 | 313 | 314 | M.send = send 315 | M.apply = apply 316 | M.apply_diff = apply_diff 317 | M.ignore_key = ignore_key 318 | 319 | M.kissUpdateOwnership = kissUpdateOwnership 320 | 321 | M.onExtensionLoaded = onExtensionLoaded 322 | M.updateGFX = updateGFX 323 | 324 | return M 325 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_gearbox.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local mainController = nil 4 | local gearbox = nil 5 | 6 | local gearbox_is_sequential = false 7 | local vehicle_is_electric = false 8 | local gearbox_is_manual = false 9 | 10 | local last_requseted_gear = nil 11 | local sequential_lock = false 12 | local ownership = false 13 | local ownership_known = false 14 | local cooldown_timer = 0 15 | 16 | local function set_gear_indices(indices) 17 | if mainController and cooldown_timer <= 0 then 18 | local index = indices[1] 19 | local canShift = true 20 | 21 | -- there's a neutralRejectTimer that will lock sequentials into neutral if we try it more than once 22 | -- possibly a game bug 23 | if sequential_lock then 24 | canShift = false 25 | elseif index == 0 and gearbox_is_sequential then 26 | sequential_lock = true 27 | end 28 | 29 | if canShift then 30 | mainController.shiftToGearIndex(index, true) -- true for ignoring sequential bounds 31 | last_requseted_gear = index 32 | end 33 | end 34 | end 35 | 36 | local function get_gear_indices() 37 | local index = electrics.values.gearIndex 38 | 39 | -- convert gearIndex to values that shiftToGearIndex accepts 40 | if index == nil then index = 0 end 41 | if not gearbox_is_sequential and not gearbox_is_manual then 42 | if type(electrics.values.gear) == 'string' and string.sub(electrics.values.gear, 1, 1) == 'M' then 43 | index = 6 -- M1 is the best we can do 44 | elseif electrics.values.gear == "P" then 45 | index = 1 -- park 46 | elseif index >= 1 then 47 | index = 2 -- drive 48 | end 49 | end 50 | 51 | return {index, 0} 52 | end 53 | 54 | local function get_gearbox_data() 55 | local data = { 56 | vehicle_id = obj:getID(), 57 | lock_coef = gearbox and gearbox.lockCoef or 0, 58 | mode = gearbox and gearbox.mode or "none", 59 | gear_indices = get_gear_indices(), 60 | arcade = false 61 | } 62 | return data 63 | end 64 | 65 | local function apply(data) 66 | local data = jsonDecode(data) 67 | set_gear_indices(data.gear_indices) 68 | end 69 | 70 | local function updateGFX(dt) 71 | if not ownership_known or ownership then return end 72 | if cooldown_timer > 0 then 73 | cooldown_timer = cooldown_timer - clamp(dt, 0, 0.02) 74 | return 75 | end 76 | if sequential_lock and electrics.values.gearIndex == 0 then 77 | sequential_lock = false 78 | end 79 | if gearbox_is_manual and last_requseted_gear ~= 0 and electrics.values.gearIndex == 0 then 80 | electrics.values.clutchOverride = 1 81 | else 82 | electrics.values.clutchOverride = nil 83 | end 84 | end 85 | 86 | local function onReset() 87 | cooldown_timer = 0.2 88 | sequential_lock = false 89 | end 90 | 91 | local function onExtensionLoaded() 92 | mainController = controller.mainController 93 | vehicle_is_electric = tableSize(powertrain.getDevicesByType("electricMotor")) > 0 94 | gearbox = powertrain.getDevice("gearbox") 95 | 96 | -- Search for a gearbox if one wasn't found 97 | if not gearbox and not vehicle_is_electric then 98 | local devices = powertrain.getDevices() 99 | for _, device in pairs(devices) do 100 | if device.deviceCategories.gearbox and gearbox == nil then 101 | gearbox = device 102 | end 103 | end 104 | end 105 | 106 | if gearbox then 107 | gearbox_is_manual = gearbox.type == "manualGearbox" 108 | gearbox_is_sequential = gearbox.type == "sequentialGearbox" 109 | end 110 | end 111 | 112 | local function kissUpdateOwnership(owned) 113 | ownership = owned 114 | ownership_known = true 115 | if owned then return end 116 | if gearbox and gearbox_is_manual then 117 | gearbox.gearDamageThreshold = math.huge 118 | end 119 | end 120 | 121 | M.send = send 122 | M.apply = apply 123 | M.get_gearbox_data = get_gearbox_data 124 | M.onExtensionLoaded = onExtensionLoaded 125 | M.updateGFX = updateGFX 126 | M.onReset = onReset 127 | M.kissUpdateOwnership = kissUpdateOwnership 128 | 129 | return M 130 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_input.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function apply(data) 4 | local data = jsonDecode(data) 5 | input.event("throttle", data.throttle_input, 1) 6 | input.event("brake", data.brake_input, 2) 7 | input.event("parkingbrake", data.parkingbrake, 2) 8 | input.event("clutch", data.clutch, 1) 9 | input.event("steering", data.steering_input, 2, 0, 0) 10 | end 11 | 12 | local function kissUpdateOwnership(owned) 13 | if owned then return end 14 | hydros.enableFFB = false 15 | hydros.onFFBConfigChanged(nil) 16 | end 17 | 18 | M.apply = apply 19 | 20 | M.kissUpdateOwnership = kissUpdateOwnership 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_nodes.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function send() 4 | local nodes_table = { 5 | vehicle_id = obj:getID(), 6 | nodes = {} 7 | } 8 | for k, node in pairs(v.data.nodes) do 9 | local position = obj:getNodePosition(node.cid) 10 | table.insert(nodes_table.nodes, {position.x, position.y, position.z}) 11 | end 12 | obj:queueGameEngineLua("network.send_messagepack(4, false, \'"..jsonEncode(nodes_table).."\')") 13 | end 14 | 15 | local function apply(nodes) 16 | local nodes = jsonDecode(nodes) 17 | for node, pos in pairs(nodes) do 18 | node = tonumber(node) 19 | obj:setNodePosition(node, float3(pos[1], pos[2], pos[3])) 20 | local beam = v.data.beams[node] 21 | local beamPrecompression = beam.beamPrecompression or 1 22 | local deformLimit = type(beam.deformLimit) == 'number' and beam.deformLimit or math.huge 23 | obj:setBeam(-1, beam.id1, beam.id2, beam.beamStrength, beam.beamSpring, 24 | beam.beamDamp, type(beam.dampCutoffHz) == 'number' and beam.dampCutoffHz or 0, 25 | beam.beamDeform, deformLimit, type(beam.deformLimitExpansion) == 'number' and beam.deformLimitExpansion or deformLimit, 26 | beamPrecompression 27 | ) 28 | end 29 | end 30 | 31 | M.send = send 32 | M.apply = apply 33 | 34 | return M 35 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_transforms.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local cooldown_timer = 2 3 | 4 | M.received_transform = { 5 | position = vec3(0, 0, 0), 6 | rotation = quat(0, 0, 0, 1), 7 | velocity = vec3(0, 0, 0), 8 | angular_velocity = vec3(0, 0, 0), 9 | acceleration = vec3(0, 0, 0), 10 | angular_acceleration = vec3(0, 0, 0), 11 | sent_at = 0, 12 | time_past = 0 13 | } 14 | 15 | M.target_transform = { 16 | position = vec3(0, 0, 0), 17 | rotation = quat(0, 0, 0, 1), 18 | velocity = vec3(0, 0, 0), 19 | angular_velocity = vec3(0, 0, 0), 20 | acceleration = vec3(0, 0, 0), 21 | angular_acceleration = vec3(0, 0, 0), 22 | } 23 | 24 | M.force = 3 25 | M.ang_force = 100 26 | M.debug = false 27 | M.lerp_factor = 30.0 28 | 29 | local function predict(dt) 30 | M.target_transform.velocity = M.received_transform.velocity + M.received_transform.acceleration * M.received_transform.time_past 31 | local distance = M.target_transform.position:distance(vec3(obj:getPosition())) 32 | local p = M.received_transform.position + M.target_transform.velocity * M.received_transform.time_past 33 | if distance < 2 then 34 | M.target_transform.position = lerp(M.target_transform.position, p, clamp(M.lerp_factor * dt, 0.00001, 1)) 35 | else 36 | M.target_transform.position = p 37 | end 38 | 39 | --M.target_transform.angular_velocity = M.received_transform.angular_velocity + M.received_transform.angular_acceleration * M.received_transform.time_past 40 | --local rotation_delta = M.target_transform.angular_velocity * M.received_transform.time_past 41 | M.target_transform.rotation = quat(M.received_transform.rotation)-- * quatFromEuler(rotation_delta.x, rotation_delta.y, rotation_delta.z) 42 | end 43 | 44 | local function try_rude() 45 | local distance = M.target_transform.position:distance(vec3(obj:getPosition())) 46 | if distance > 6 then 47 | local p = M.target_transform.position 48 | obj:queueGameEngineLua("be:getObjectByID("..obj:getID().."):setPositionNoPhysicsReset(Point3F("..p.x..", "..p.y..", "..p.z.."))") 49 | return true 50 | end 51 | return false 52 | end 53 | 54 | local function draw_debug() 55 | obj.debugDrawProxy:drawSphere(0.3, M.target_transform.position:toFloat3(), color(0,255,0,100)) 56 | obj.debugDrawProxy:drawSphere(0.3, M.received_transform.position:toFloat3(), color(0,0,255,100)) 57 | end 58 | 59 | local function update(dt) 60 | if cooldown_timer > 0 then 61 | cooldown_timer = cooldown_timer - clamp(dt, 0, 0.02) 62 | return 63 | end 64 | if dt > 0.1 then return end 65 | M.received_transform.time_past = clamp(M.received_transform.time_past + dt, 0, 0.5) 66 | predict(dt) 67 | if try_rude() then return end 68 | 69 | if M.debug then 70 | draw_debug() 71 | end 72 | 73 | local force = M.force 74 | local ang_force = M.ang_force 75 | 76 | local c_ang = -math.sqrt(4 * ang_force) 77 | 78 | local velocity_difference = M.target_transform.velocity - vec3(obj:getVelocity()) 79 | local position_delta = M.target_transform.position - vec3(obj:getPosition()) 80 | --position_delta = position_delta:normalized() * math.pow(position_delta:length(), 2) 81 | local linear_force = (velocity_difference + position_delta * force) * dt * 5 82 | if linear_force:length() > 10 then 83 | linear_force = linear_force:normalized() * 10 84 | end 85 | 86 | local local_ang_vel = vec3( 87 | obj:getYawAngularVelocity(), 88 | obj:getPitchAngularVelocity(), 89 | obj:getRollAngularVelocity() 90 | ) 91 | 92 | local angular_velocity_difference = M.target_transform.angular_velocity - local_ang_vel 93 | local angle_delta = M.target_transform.rotation / quat(obj:getRotation()) 94 | local angular_force = angle_delta:toEulerYXZ() 95 | local angular_force = (angular_velocity_difference + angular_force * ang_force + c_ang * local_ang_vel) * dt 96 | if angular_force:length() > 25 then 97 | return 98 | end 99 | 100 | if angular_force:length() > 0.1 then 101 | kiss_vehicle.apply_linear_velocity_ang_torque( 102 | linear_force.x, 103 | linear_force.y, 104 | linear_force.z, 105 | angular_force.y, 106 | angular_force.z, 107 | angular_force.x 108 | ) 109 | elseif linear_force:length() > (dt * 15) then 110 | kiss_vehicle.apply_linear_velocity( 111 | linear_force.x, 112 | linear_force.y, 113 | linear_force.z 114 | ) 115 | end 116 | end 117 | 118 | local function set_target_transform(raw) 119 | local transform = jsonDecode(raw) 120 | local time_dif = clamp((transform.sent_at - M.received_transform.sent_at), 0.01, 0.1) 121 | 122 | M.received_transform.acceleration = (vec3(transform.velocity) - M.received_transform.velocity) / time_dif 123 | if M.received_transform.acceleration:length() > 5 then 124 | M.received_transform.acceleration = M.received_transform.acceleration:normalized() * 5 125 | end 126 | M.received_transform.angular_acceleration = (vec3(transform.angular_velocity) - M.received_transform.angular_velocity) / time_dif 127 | if M.received_transform.acceleration:length() > 5 then 128 | M.received_transform.angular_acceleration = M.received_transform.angular_acceleration:normalized() * 5 129 | end 130 | M.received_transform.position = vec3(transform.position) 131 | M.received_transform.rotation = quat(transform.rotation) 132 | M.received_transform.velocity = vec3(transform.velocity) 133 | M.received_transform.angular_velocity = vec3(transform.angular_velocity) 134 | M.received_transform.time_past = transform.time_past 135 | end 136 | 137 | local function onExtensionLoaded() 138 | M.received_transform.position = vec3(obj:getPosition()) 139 | M.target_transform.position = vec3(obj:getPosition()) 140 | M.received_transform.rotation = quat(obj:getRotation()) 141 | M.target_transform.rotation = quat(obj:getRotation()) 142 | cooldown_timer = 1.5 143 | end 144 | 145 | local function onReset() 146 | cooldown_timer = 0.2 147 | end 148 | 149 | M.set_target_transform = set_target_transform 150 | M.update = update 151 | M.onExtensionLoaded = onExtensionLoaded 152 | M.onReset = onReset 153 | 154 | return M 155 | -------------------------------------------------------------------------------- /KISSMultiplayer/lua/vehicle/extensions/kiss_mp/kiss_vehicle.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local parts_config = v.config 3 | local nodes = {} 4 | local ref_nodes = {} 5 | 6 | local last_node = 1 7 | local nodes_per_frame = 32 8 | 9 | local node_pos_thresh = 3 10 | local node_pos_thresh_sqr = node_pos_thresh * node_pos_thresh 11 | 12 | M.test_quat = quat(0.707, 0, 0, 0.707) 13 | 14 | local function onExtensionLoaded() 15 | local force = obj:getPhysicsFPS() 16 | 17 | local ref = { 18 | v.data.refNodes[0].left, 19 | v.data.refNodes[0].up, 20 | v.data.refNodes[0].back, 21 | v.data.refNodes[0].ref, 22 | } 23 | 24 | local total_mass = 0 25 | local inverse_rot = quat(obj:getRotation()):inversed() 26 | for _, node in pairs(v.data.nodes) do 27 | local node_mass = obj:getNodeMass(node.cid) 28 | local node_pos = inverse_rot * obj:getNodePosition(node.cid) 29 | table.insert( 30 | nodes, 31 | { 32 | node.cid, 33 | node_mass * force, 34 | true, 35 | node_pos 36 | } 37 | ) 38 | --M.test_nodes_sync[node.cid] = vec3(obj:getNodePosition(node.cid)) 39 | total_mass = total_mass + node_mass 40 | end 41 | 42 | for _, node in pairs(ref) do 43 | table.insert( 44 | ref_nodes, 45 | { 46 | node, 47 | total_mass * force / 4, 48 | true, 49 | inverse_rot * obj:getNodePosition(node) 50 | } 51 | ) 52 | end 53 | end 54 | 55 | -- NOTE: 56 | -- This is a temperary solution. It's not great. We made it to release the mod. 57 | -- A better solution will be used in future versions 58 | local function update_eligible_nodes() 59 | local inverse_rot = quat(obj:getRotation()):inversed() 60 | for k=last_node, math.min(#nodes , last_node + nodes_per_frame) do 61 | local node = nodes[k] 62 | local local_node_pos = inverse_rot * obj:getNodePosition(node[1]) 63 | local local_original_pos = node[4] 64 | node[3] = (local_node_pos - local_original_pos):squaredLength() < node_pos_thresh_sqr 65 | last_node = k 66 | end 67 | if last_node == #nodes then last_node = 1 end 68 | end 69 | 70 | local function update_transform_info() 71 | local r = quat(obj:getRotation()) 72 | local p = obj:getPosition() 73 | 74 | local throttle_input = electrics.values.throttle_input or 0 75 | local brake_input = electrics.values.brake_input or 0 76 | if electrics.values.gearboxMode == "arcade" and electrics.values.gearIndex < 0 then 77 | throttle_input, brake_input = brake_input, throttle_input 78 | end 79 | 80 | local input = { 81 | vehicle_id = obj:getID() or 0, 82 | throttle_input = throttle_input, 83 | brake_input = brake_input, 84 | clutch = electrics.values.clutch_input or 0, 85 | parkingbrake = electrics.values.parkingbrake_input or 0, 86 | steering_input = electrics.values.steering_input or 0, 87 | } 88 | local gearbox = kiss_gearbox.get_gearbox_data() 89 | local transform = { 90 | position = {p.x, p.y, p.z}, 91 | rotation = {r.x, r.y, r.z, r.w}, 92 | input = input, 93 | gearbox = gearbox, 94 | vel_pitch = obj:getPitchAngularVelocity(), 95 | vel_roll = obj:getRollAngularVelocity(), 96 | vel_yaw = obj:getYawAngularVelocity(), 97 | } 98 | obj:queueGameEngineLua("kisstransform.push_transform("..obj:getID()..", \'"..jsonEncode(transform).."\')") 99 | end 100 | 101 | local function apply_linear_velocity(x, y, z) 102 | local velocity = vec3(x, y, z) 103 | local force = float3(0, 0, 0) 104 | for k=1, #nodes do 105 | local node = nodes[k] 106 | if node[3] then 107 | local result = velocity * node[2] 108 | force:set(result.x, result.y, result.z) 109 | obj:applyForceVector(node[1], force) 110 | end 111 | end 112 | end 113 | 114 | local function apply_linear_velocity_ang_torque(x, y, z, pitch, roll, yaw) 115 | local velocity = vec3(x, y, z) 116 | local nodes = nodes 117 | -- 0.1 seems like the safe value we can use for low velocities 118 | -- NOTE: Doesn't work as well as expected 119 | if velocity:length() < 0.01 then 120 | --nodes = ref_nodes 121 | end 122 | local rot = vec3(pitch, roll, yaw):rotated(quat(obj:getRotation())) 123 | local node_position = vec3() 124 | local force = float3(0, 0, 0) 125 | for k=1, #nodes do 126 | local node = nodes[k] 127 | if node[3] then 128 | node_position:set(obj:getNodePosition(node[1])) 129 | local result = (velocity + node_position:cross(rot)) * node[2] 130 | force:set(result.x, result.y, result.z) 131 | obj:applyForceVector(node[1], force) 132 | end 133 | end 134 | end 135 | 136 | local function send_vehicle_config() 137 | local config = v.config 138 | local r = quat(obj:getRotation()) 139 | local p = obj:getPosition() 140 | local data = { 141 | position = {p.x, p.y, p.z}, 142 | rotation = {r.x, r.y, r.z, r.w}, 143 | } 144 | obj:queueGameEngineLua("vehiclemanager.send_vehicle_config_inner("..obj:getID()..", \'"..jsonEncode(config).."\', \'"..jsonEncode(data).."\')") 145 | end 146 | 147 | M.update_transform_info = update_transform_info 148 | M.apply_linear_velocity_ang_torque = apply_linear_velocity_ang_torque 149 | M.update_eligible_nodes = update_eligible_nodes 150 | M.apply_linear_velocity = apply_linear_velocity 151 | M.onExtensionLoaded = onExtensionLoaded 152 | M.set_reference = set_reference 153 | M.save_state = save_state 154 | M.send_vehicle_config = send_vehicle_config 155 | return M 156 | -------------------------------------------------------------------------------- /KISSMultiplayer/scripts/kiss_mp/modScript.lua: -------------------------------------------------------------------------------- 1 | print("Executing KissMP modScript...") 2 | loadJsonMaterialsFile("art/shapes/kissmp_playermodels/main.materials.json") 3 | 4 | load("kissplayers") 5 | registerCoreModule("kissplayers") 6 | 7 | load("vehiclemanager") 8 | registerCoreModule("vehiclemanager") 9 | 10 | load("kisstransform") 11 | registerCoreModule("kisstransform") 12 | 13 | load("kissui") 14 | registerCoreModule("kissui") 15 | 16 | load("kissmods") 17 | registerCoreModule("kissmods") 18 | 19 | load("kissrichpresence") 20 | registerCoreModule("kissrichpresence") 21 | 22 | load("network") 23 | registerCoreModule("network") 24 | 25 | load("kissconfig") 26 | registerCoreModule("kissconfig") 27 | 28 | load("kissvoicechat") 29 | registerCoreModule("kissvoicechat") 30 | 31 | --load("kissutils") 32 | --registerCoreModule("kissutils") 33 | -------------------------------------------------------------------------------- /KISSMultiplayer/ui/This is here because BeamNG would not stop mounting the damn mod in the levels folder: -------------------------------------------------------------------------------- 1 | {\rtf1} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KissMP 2 | ![alt text](https://i.imgur.com/kxocgKD.png) 3 | 4 | [KISS](https://en.wikipedia.org/wiki/KISS_principle) Multiplayer mod for BeamNG.drive ([Discord Channel](https://discord.gg/ANPsDkeVVF)) 5 | 6 | ## Main features 7 | - Cross platform, open source & free server written in Rust 8 | - QUIC-based networking (with help of quinn and tokio for async) 9 | - Server list with search and ability to save favorites 10 | - Automatic synchronization of your mods with the server 11 | - High overall performance which allows for more players to play on the same server 12 | - Low traffic usage 13 | - Lag compensation 14 | - In-game text chat 15 | - In-game **voice chat** 16 | - Lua API for creating server-side addons 17 | - Cross platform bridge (less Wine applications for Linux users) 18 | - Builtin server list 19 | 20 | ## Contributors 21 | - Dummiesman (most of the UI code, huge contributions to the core code) 22 | 23 | ## Installation 24 | - Drop KISSMultiplayer.zip into the /Documents/BeamNG.drive/mods folder. The archive name HAS to be named KISSMultiplayer.zip in order 25 | for the mod to work. 26 | - You can drop the bridge .exe file to any place you want. 27 | 28 | ## Usage 29 | - Launch the bridge. If everything is correct, it'll show you the text "Bridge is running!" in the console window. 30 | - Launch the game. After the launch, you should be able to see server list and chat windows. Select a server in the server list 31 | and hit the connect button. 32 | - Enjoy playing! 33 | 34 | ## Server installation 35 | Just launch the kissmp-server for your platform and you're ready to go. 36 | More detailed guide on server configuration can be found on this [wiki page](https://kissmp.online/docs/srv_hosting/hosting.html). 37 | 38 | 39 | ## Building 40 | First, download and install a [Rust toolchain](https://rustup.rs/) 41 | 42 | After, clone the repository 43 | ```sh 44 | git clone https://github.com/TheHellBox/KISS-multiplayer.git 45 | cd KISS-multiplayer 46 | ``` 47 | Now you are ready to build server and bridge. 48 | ### Server 49 | ```sh 50 | cd kissmp-server 51 | cargo run --release 52 | ``` 53 | or 54 | ```sh 55 | cargo run -p kissmp-server --release 56 | ``` 57 | ### Bridge 58 | ```sh 59 | cd kissmp-bridge 60 | cargo run --release 61 | ``` 62 | or 63 | ```sh 64 | cargo run -p kissmp-bridge --release 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/book/.nojekyll: -------------------------------------------------------------------------------- 1 | This file makes sure that Github Pages doesn't process mdBook's output. 2 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](introduction.md) 4 | - [Building](building.md) 5 | - [Server Hosting](srv_hosting/hosting.md) 6 | - [Installing Mods and Addons](srv_hosting/mods_and_addons.md) 7 | - [Troubleshooting](srv_hosting/troubleshooting.md) 8 | - [Server side Lua API](srv_lua/lua_api.md) 9 | - [Hooks](srv_lua/hooks.md) 10 | - [Vehicles](srv_lua/vehicles.md) 11 | - [Vehicle data](srv_lua/vehicle_data.md) 12 | - [Transform](srv_lua/transform.md) 13 | - [Connections](srv_lua/connection.md) 14 | - [Global functions](srv_lua/global_functions.md) 15 | - [Globals](srv_lua/globals.md) 16 | - [Examples](srv_lua/examples.md) 17 | - [Admin system](srv_lua/admin_system_example.md) 18 | -------------------------------------------------------------------------------- /docs/src/building.md: -------------------------------------------------------------------------------- 1 | # Building 2 | First, download and install [a Rust toolchain](https://rustup.rs/) 3 | 4 | After, clone the KissMP repository 5 | ```sh 6 | git clone https://github.com/TheHellBox/KISS-multiplayer.git 7 | cd KISS-multiplayer 8 | ``` 9 | Now you are ready to build the server and bridge. 10 | ## Server 11 | ```sh 12 | cd kissmp-server 13 | cargo run --release 14 | ``` 15 | or 16 | ```sh 17 | cargo run -p kissmp-server --release 18 | ``` 19 | ## Bridge 20 | ```sh 21 | cd kissmp-bridge 22 | cargo run --release 23 | ``` 24 | or 25 | ```sh 26 | cargo run -p kissmp-bridge --release 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Welcome to KissMP Documentation! 2 | It's currently in development, so expect it to be a little junky 3 | -------------------------------------------------------------------------------- /docs/src/srv_hosting/hosting.md: -------------------------------------------------------------------------------- 1 | # Hosting 2 | Hosting a server with KissMP is very easy. 3 | 4 | - 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. 5 | - Run the kissmp-server executable, it will generate a config file that you can edit. 6 | - Edit the config.json file to set the level, player limit, whether it's public, etc. 7 | - That's basically all there is to it. 8 | 9 | # How do I connect to my server? 10 | If your server is running on your own PC, connect using 127.0.0.1 as the address. Otherwise, follow the steps below. 11 | 12 | # How do others connect to my server? 13 | 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](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/)). 14 | 15 | If enabled in your config, your server will show up in the server list and others can just click the Connect button. Otherwise: 16 | - If you're not using any networking software like Hamachi, people connect to your server with your public IP address ([https://www.whatismyip.com](https://www.whatismyip.com/)). 17 | - If you're using networking software like Hamachi, use the IP address assigned to you by that software. 18 | 19 | # How do i change the level/map? 20 | To change what level the server is set on, simply specify your desired maps level path in your server configs `map` field. 21 | 22 | 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. 23 | 24 | If the map is modded, make sure to include it in your servers mods folder. See the instructions below on adding mods. 25 | # How do i add mods or addons to my server? 26 | See [Installing Mods and Addons](mods_and_addons.html). 27 | 28 | 29 | --- 30 | 31 | 32 | Having issues with setting up your server? Have a look at [Troubleshooting](troubleshooting.html) -------------------------------------------------------------------------------- /docs/src/srv_hosting/mods_and_addons.md: -------------------------------------------------------------------------------- 1 | # Installing Mods and Addons 2 | ## Mods 3 | Mods add additional content to the game and are downloaded for all players connecting to the server. 4 | A mod could for example add a new level or vehicle. 5 | 6 | #### Installation 7 | 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. 8 | 9 | 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.\ 10 | The `kissmp_mods` folder can be found in the same directory as your BeamNGs mods folder. 11 | 12 | ## Addons 13 | Addons are scripts that run on the server and are not downloaded to any players.\ 14 | With addons, servers are able to do all kinds of things (like gamemodes, commands, etc).\ 15 | 16 | If you would like to get started with creating Addons for KissMP, see [Server side Lua API](../srv_lua/lua_api.html).\ 17 | A community mantained collection of addons is available [here](https://github.com/AsciiJakob/Awesome-KissMP). 18 | 19 | #### Installation 20 | Just like with the `mods` folder, the `addons` folder is created automatically by your server.\ 21 | 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.\ 22 | KissMP addons use `main.lua` as their entrypoint and addons should follow the structure of:\ 23 | `/addons/ADDON_NAME/main.lua` -------------------------------------------------------------------------------- /docs/src/srv_hosting/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | ## I can connect to my server using its IP address, but it's not showing up on the server list! 3 | There are a few reasons as to why this may happen. 4 | - You may not have `show_in_server_list` set to `true` in your config. 5 | - Server is not up to date with the latest KissMP-server version. 6 | - Servers with inappropriate words in their name will automatically be blocked. 7 | - To avoid abuse, there is a limit of 10 servers per IP address. 8 | - Servers may not exceed the character limit of 64 characters for its name and 256 characters for its description. 9 | 10 | ## My friends can't to connect to my server! 11 | This is probably the result of incorrect port forwarding or firewall issues. Make sure to follow the port forwarding guide from [Server Hosting](hosting.html) 12 | 13 | ## I have another issue that is not listed here! 14 | If more help is needed, we are usually able to provide help at [our Discord server](https://discord.gg/ANPsDkeVVF). -------------------------------------------------------------------------------- /docs/src/srv_lua/admin_system_example.md: -------------------------------------------------------------------------------- 1 | Here's a simple admin system. 2 | It's probably not suitable for big servers, and only serves as an example, but it can still be used 3 | in its original form 4 | ```lua 5 | KSA = {} 6 | 7 | KSA.ban_list = {} 8 | KSA.player_roles = {} 9 | 10 | KSA.commands = { 11 | kick = { 12 | roles = {admin = true, superadmin = true}, 13 | exec = function(executor, args) 14 | if not args[1] then executor:sendChatMessage("No arguments provided") end 15 | for id, client in pairs(connections) do 16 | if client:getName() == args[1] then 17 | client:kick("You have been kicked. Reason: "..(args[2] or "No reason provided")) 18 | return 19 | end 20 | end 21 | end 22 | }, 23 | ban = { 24 | roles = {admin = true, superadmin = true}, 25 | exec = function(executor, args) 26 | if not args[1] then executor:sendChatMessage("No arguments provided") end 27 | for id, client in pairs(connections) do 28 | if client:getName() == args[1] then 29 | KSA.ban(client:getSecret(), client:getName(), client:getID(), tonumber(args[2]) or math.huge) 30 | return 31 | end 32 | end 33 | end 34 | }, 35 | promote = { 36 | roles = {superadmin = true}, 37 | exec = function(executor, args) 38 | if not args[1] then executor:sendChatMessage("No arguments provided") end 39 | for id, client in pairs(connections) do 40 | if client:getName() == args[1] then 41 | KSA.promote(client:getSecret(), args[2] or "user") 42 | return 43 | end 44 | end 45 | end 46 | } 47 | } 48 | 49 | -- Created by Dummiesman 50 | local function cmd_parse(cmd) 51 | local parts = {} 52 | local len = cmd:len() 53 | local escape_sequence_stack = 0 54 | local in_quotes = false 55 | 56 | local cur_part = "" 57 | for i=1,len,1 do 58 | local char = cmd:sub(i,i) 59 | if escape_sequence_stack > 0 then escape_sequence_stack = escape_sequence_stack + 1 end 60 | local in_escape_sequence = escape_sequence_stack > 0 61 | if char == "\\" then 62 | escape_sequence_stack = 1 63 | elseif char == " " and not in_quotes then 64 | table.insert(parts, cur_part) 65 | cur_part = "" 66 | elseif char == '"'and not in_escape_sequence then 67 | in_quotes = not in_quotes 68 | else 69 | cur_part = cur_part .. char 70 | end 71 | if escape_sequence_stack > 1 then escape_sequence_stack = 0 end 72 | end 73 | if cur_part:len() > 0 then 74 | table.insert(parts, cur_part) 75 | end 76 | return parts 77 | end 78 | 79 | local function load_roles() 80 | local file = io.open("./ksa_roles.json", "r") 81 | if not file then return end 82 | KSA.player_roles = decode_json(file:read("*a")) 83 | end 84 | 85 | local function save_roles() 86 | local file = io.open("./ksa_roles.json", "w") 87 | local content = encode_json_pretty(KSA.player_roles) 88 | if not content then return end 89 | file:write(content) 90 | end 91 | 92 | local function load_banlist() 93 | local file = io.open("./ksa_banlist.json", "r") 94 | if not file then return end 95 | KSA.ban_list = decode_json(file:read("*a")) 96 | end 97 | 98 | local function save_banlist() 99 | local file = io.open("./ksa_banlist.json", "w") 100 | local content = encode_json_pretty(KSA.ban_list) 101 | if not content then return end 102 | file:write(content) 103 | end 104 | 105 | function KSA.ban(secret, name, client_id, time) 106 | local time = time or math.huge() 107 | KSA.ban_list[secret] = { 108 | name = name, 109 | unban_time = os.time() + (time * 60) 110 | } 111 | connections[client_id]:kick("You've been banned on this server.") 112 | save_banlist() 113 | end 114 | 115 | function KSA.unban(secret) 116 | KSA.ban_list[secret] = nil 117 | save_banlist() 118 | end 119 | 120 | function KSA.promote(secret, new_role) 121 | KSA.player_roles[secret] = new_role 122 | save_roles() 123 | end 124 | 125 | hooks.register("OnPlayerConnected", "CheckBanList", function(client_id) 126 | local secret = connections[client_id]:getSecret() 127 | local ban = KSA.ban_list[secret] 128 | if not ban then return end 129 | local remaining = ban.unban_time - os.time() 130 | if remaining < 0 then 131 | KSA.unban(secret) 132 | return 133 | end 134 | connections[client_id]:kick("You've been banned on this server. Time remaining: "..tostring(remaining / 60).." min") 135 | end) 136 | 137 | hooks.register("OnStdIn", "KSA_Run_Lua", function(str) 138 | if string.sub(str, 1, 7) == "run_lua" then 139 | load(string.sub(str, 9, #str))() 140 | end 141 | end) 142 | 143 | hooks.register("OnStdIn", "KSA_Promote", function(str) 144 | if not string.sub(str, 1, 9) == "set_super" then return end 145 | local target = string.sub(str, 11, #str) 146 | print(target) 147 | for id, client in pairs(connections) do 148 | if client:getName() == target then 149 | KSA.promote(client:getSecret(), "superadmin") 150 | end 151 | end 152 | end) 153 | 154 | hooks.register("OnChat", "KSA_Process_Commands", function(client_id, str) 155 | if not string.sub(str, 1, 4) == "/ksa" then return end 156 | local args = cmd_parse(str, " ") 157 | table.remove(args, 1) 158 | local base = table.remove(args, 1) 159 | local executor = connections[client_id] 160 | local command = KSA.commands[base] 161 | if not command.roles[KSA.player_roles[executor:getSecret()] or "user"] then 162 | executor:sendChatMessage("KSA: You're not allowed to use this command") 163 | return 164 | end 165 | if not command then 166 | executor:sendChatMessage("KSA: Command not found") 167 | return 168 | end 169 | command.exec(executor, args) 170 | return "" 171 | end) 172 | 173 | load_roles() 174 | load_banlist() 175 | ``` 176 | -------------------------------------------------------------------------------- /docs/src/srv_lua/connection.md: -------------------------------------------------------------------------------- 1 | # Connections 2 | A **connection object** represents a player connected to the server. 3 | 4 | Connections are stored in the global table `connections` and a specific connection can be obtained using its client ID with `connections[client_id]`. 5 | 6 | **List of methods a connection object has:** 7 | - getID() 8 | - Returns: Integer ([Client ID](connection.html)) 9 | - getIpAddr() 10 | - Returns: String 11 | - getSecret() 12 | - 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! 13 | - Returns: String 14 | - getCurrentVehicle() 15 | - Returns: Integer ([Vehicle ID](vehicles.html)) 16 | - getName() 17 | - Returns: String 18 | - sendChatMessage(string message) 19 | - Returns: null 20 | - kick(string reason) 21 | - Returns: null 22 | - sendLua(string lua_command) 23 | - 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.\ 24 | For example `client:sendLua('ui_message("'..message..'")')` would be vulnerable if `message` is `") Evil code here--`.\ 25 | The [admin system example](admin_system_example.html) has an example of sanitization in the `cmd_parse` function. 26 | - Returns: null -------------------------------------------------------------------------------- /docs/src/srv_lua/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | ## Basic example of commands 3 | ```lua 4 | function string.startswith(input, start) 5 | return string.sub(input,1,string.len(start))==start 6 | end 7 | 8 | hooks.register("OnChat", "HomeCommand", function(client_id, message) 9 | local vehicle_id = connections[client_id]:getCurrentVehicle() 10 | if not vehicles[vehicle_id] then return end 11 | local vehicle = vehicles[vehicle_id] 12 | if message == "/home" then 13 | vehicle:setPositionRotation(0, 0, 0, 0, 0, 0, 1) 14 | end 15 | if message == "/reset" then 16 | vehicle:reset() 17 | end 18 | if message == "/remove" then 19 | vehicle:remove() 20 | end 21 | if message == "/kick_me" then 22 | connections[client_id]:kick("Kick reason") 23 | end 24 | if string.startswith(message, "/send_me_lua") then 25 | local message = message:gsub("%/send_me_lua", "") 26 | connections[client_id]:sendLua(message) 27 | end 28 | if string.startswith(message, "/send_me_msg") then 29 | local message = message:gsub("%/send_me_msg", "") 30 | connections[client_id]:sendChatMessage(message) 31 | end 32 | end) 33 | ``` 34 | ## Vote-kick system 35 | ```lua 36 | local vote = { 37 | victim = nil, 38 | votes = {}, 39 | end_time = 0 40 | } 41 | 42 | local function startswith(input, start) 43 | return string.sub(input,1,string.len(start))==start 44 | end 45 | 46 | local function count_players() 47 | local i = 0 48 | for _, _ in pairs(connections) do 49 | i = i + 1 50 | end 51 | return i 52 | end 53 | 54 | hooks.register("OnChat", "VoteKick", function(client_id, message) 55 | local initiator = connections[client_id] 56 | if startswith(message, "/votekick") then 57 | if not vote.victim then 58 | local victim = message:gsub("%/votekick ", "") 59 | for _, client in pairs(connections) do 60 | if victim == client:getName() then 61 | vote.victim = client:getID() 62 | vote.end_time = os.clock() + 30 63 | send_message_broadcast(initiator:getName().." has started a vote to kick "..client:getName()) 64 | send_message_broadcast("Type /vote to vote") 65 | local votes_needed = count_players() / 2 66 | send_message_broadcast(math.floor(votes_needed).." votes are needed") 67 | else 68 | initiator:sendChatMessage("No such player") 69 | end 70 | end 71 | else 72 | initiator:sendChatMessage("Wait until the current vote ends") 73 | end 74 | end 75 | if startswith(message, "/vote") then 76 | if not vote.votes[initiator:getID()] then 77 | vote.votes[initiator:getID()] = true 78 | else 79 | initiator:sendChatMessage("You have already voted!") 80 | end 81 | end 82 | end) 83 | 84 | hooks.register("Tick", "VoteTimer", function(client_id, message) 85 | if vote.victim and (os.clock() > vote.end_time) then 86 | local votes_count = 0 87 | for _, _ in pairs(vote.votes) do 88 | votes_count = votes_count + 1 89 | end 90 | if votes_count > (count_players() / 2) then 91 | local victim = connections[vote.victim] 92 | if victim then 93 | victim:kick("You have been kicked by vote results") 94 | send_message_broadcast(victim:getName().." has been kicked by vote results") 95 | end 96 | else 97 | send_message_broadcast("Vote has failed") 98 | end 99 | vote.victim = nil 100 | vote.votes = {} 101 | end 102 | end) 103 | ``` 104 | ## Vehicle list 105 | ```lua 106 | hooks.register("OnStdIn", "ListVehiclesCommand", function(input) 107 | if input == "/list_vehicles" then 108 | for vehicle_id, vehicle in pairs(vehicles) do 109 | local position = vehicle:getTransform():getPosition() 110 | print("Vehicle "..vehicle_id..": "..position[1]..", "..position[2]..", "..position[3]) 111 | end 112 | end 113 | end) 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/src/srv_lua/global_functions.md: -------------------------------------------------------------------------------- 1 | ## Global functions 2 | - send_message_broadcast(string) 3 | - encode_json(table) 4 | - encode_json_pretty(table) 5 | - decode_json(string) 6 | -------------------------------------------------------------------------------- /docs/src/srv_lua/globals.md: -------------------------------------------------------------------------------- 1 | ## Globals 2 | - SERVER_TICKRATE 3 | - SERVER_NAME 4 | - MAX_PLAYERS 5 | - MAX_VEHICLES_PER_CLIENT 6 | - MPSC_CHANNEL_SENDER 7 | - hooks 8 | - vehicles 9 | - connections 10 | -------------------------------------------------------------------------------- /docs/src/srv_lua/hooks.md: -------------------------------------------------------------------------------- 1 | ## Hooks 2 | You can register a hook by running 3 | ```lua 4 | hooks.register("HookName", "Subname", function(arguments) 5 | return value 6 | end) 7 | ``` 8 | Keep in mind that the subname has to be unique. 9 | 10 | **Default hooks include:** 11 | - OnChat(int client_id, string message) 12 | `returns string - modified message` 13 | 14 | - Tick() 15 | - OnStdIn(string input) 16 | - OnVehicleRemoved(vehicle_id, client_id) 17 | - OnVehicleSpawned(vehicle_id, client_id) 18 | - OnVehicleResetted(vehicle_id, client_id) 19 | - OnPlayerConnected(client_id) 20 | - OnPlayerDisconnected(client_id) 21 | -------------------------------------------------------------------------------- /docs/src/srv_lua/lua_api.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | **KissMP** server uses **lua** as its language for creating addons. 3 | 4 | Keep in mind that the server in KissMP doesn't do much, most of the stuff is done by clients. 5 | Server just passes some data between clients. 6 | 7 | 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. 8 | 9 | For this reason, lots of stuff can be done with the `connection:sendLua()` method. For example, the built in 10 | `vehicle:setPositionRotation` method uses sendLua as its backend.\ 11 | You can display UI, messages, modify input and change time just by sending small lua commands. 12 | 13 | # Creating an addon 14 | 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. 15 | 16 | Server also supports hot-reloading, so lua addons will reload automatically when saved without needing to restart the server. -------------------------------------------------------------------------------- /docs/src/srv_lua/transform.md: -------------------------------------------------------------------------------- 1 | # Transform 2 | A **transform object** holds information about a vehicles transform (position, rotation, etc) and can be obtained through the `getTransform()` method on a [vehicle object](vehicles.html). 3 | 4 | **Transform object has following methods:** 5 | - getPosition() 6 | - Returns: Table (Vector3) 7 | - getRotation() 8 | - Returns: Table (Quaternion) 9 | - getVelocity() 10 | - Returns: Table (Vector3) 11 | - getAngularVelocity() 12 | - Returns: Table (Vector3) -------------------------------------------------------------------------------- /docs/src/srv_lua/vehicle_data.md: -------------------------------------------------------------------------------- 1 | # Vehicle Data 2 | A **vehicle data object** holds information about a vehicle and can be obtained through the `getData()` method on a [vehicle object](vehicles.html). 3 | 4 | **List of methods available for vehicle data:** 5 | - getInGameID() 6 | - Returns: Integer 7 | - getID() 8 | - Returns: Integer ([Vehicle ID](vehicles.html)) 9 | - getColor() 10 | - Returns: Table 11 | - getPalete0() 12 | - Returns: Table 13 | - getPalete1() 14 | - Returns: Table 15 | - getPlate() 16 | - Returns: String 17 | - getName() 18 | - Returns: String 19 | - getOwner() 20 | - Returns: Integer ([Client ID](connection.html)) 21 | - getPartsConfig() 22 | - Returns: String (JSON) -------------------------------------------------------------------------------- /docs/src/srv_lua/vehicles.md: -------------------------------------------------------------------------------- 1 | # Vehicles 2 | 3 | A **vehicle object** represents a vehicle that was spawned by a client. 4 | 5 | Vehicle objects are stored in the global table `vehicles` and a specific vehicle can be obtained using its vehicle ID with `vehicles[vehicle_id]`. 6 | 7 | 8 | **Vehicle objects have the following methods:** 9 | - getTransform() 10 | - Returns: Table ([Transform](transform.html)) 11 | - getData() 12 | - Returns: Table ([Vehicle Data](vehicle_data.html)) 13 | - remove() 14 | - Returns: null 15 | - reset() 16 | - Returns: null 17 | - setPosition(x, y, z) 18 | - Sets vehicle position without reset 19 | - Returns: null 20 | - setPositionRotation(x, y, z, xr, yr, zr, w) 21 | - **Note:** Rotation is in quaternion form. 22 | - Returns: null 23 | - sendLua(string lua_command) 24 | - Returns: null -------------------------------------------------------------------------------- /kissmp-bridge/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /kissmp-bridge/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kissmp-bridge" 3 | version = "0.6.0" 4 | authors = ["hellbox"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | [features] 9 | default = ["discord-rpc-client"] 10 | 11 | [dependencies] 12 | shared = { path = "../shared" } 13 | kissmp-server = { path = "../kissmp-server" } 14 | bincode = "1.3" 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json="1.0" 17 | futures = "0.3.5" 18 | quinn = {version="0.8.5", features = ["tls-rustls"]} 19 | rustls = { version = "0.20.3", features = ["dangerous_configuration"] } 20 | # Held back due to rustls using webpki 0.21 21 | webpki = "0.21" 22 | anyhow = "1.0.32" 23 | reqwest = { version = "0.11", default-features = false, features=["rustls-tls"] } 24 | tiny_http="0.8" 25 | tokio-stream="0.1.5" 26 | tokio = { version = "1.4", features = ["time", "macros", "sync", "io-util", "net"] } 27 | discord-rpc-client = {version = "0.4", optional = true} 28 | percent-encoding = "2.1" 29 | audiopus = "0.2" 30 | rodio = "0.14" 31 | cpal = "0.13" 32 | fon = "0.5.0" 33 | log = "0.4" 34 | indoc = "1.0" 35 | -------------------------------------------------------------------------------- /kissmp-bridge/src/discord.rs: -------------------------------------------------------------------------------- 1 | pub async fn spawn_discord_rpc(discord_rx: std::sync::mpsc::Receiver) { 2 | //let discord_rx = tokio_stream::wrappers::ReceiverStream::new(discord_rx); 3 | std::thread::spawn(move || { 4 | let mut drpc_client = discord_rpc_client::Client::new(771278096627662928); 5 | drpc_client.start(); 6 | drpc_client 7 | .subscribe(discord_rpc_client::models::Event::ActivityJoin, |j| { 8 | j.secret("123456") 9 | }) 10 | .expect("Failed to subscribe to event"); 11 | //println!("test"); 12 | let mut state = crate::DiscordState { server_name: None }; 13 | loop { 14 | std::thread::sleep(std::time::Duration::from_millis(5000)); 15 | for new_state in discord_rx.try_recv() { 16 | state = new_state; 17 | } 18 | if state.server_name.is_none() { 19 | let _ = drpc_client.clear_activity(); 20 | continue; 21 | } 22 | let _ = drpc_client.set_activity(|activity| { 23 | activity 24 | .details(state.clone().server_name.unwrap()) 25 | //.state("[1/8]") 26 | .assets(|assets| assets.large_image("kissmp_logo")) 27 | //.secrets(|secrets| secrets.game("Test").join("127.0.0.1:3698")) 28 | }); 29 | } 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /kissmp-bridge/src/http_proxy.rs: -------------------------------------------------------------------------------- 1 | use percent_encoding::percent_decode_str; 2 | use serde::Deserialize; 3 | use std::net::Ipv4Addr; 4 | 5 | #[derive(Deserialize)] 6 | struct ServerHostData { 7 | name: String, 8 | max_players: u8, 9 | map: String, 10 | mods: Option>, 11 | port: u16, 12 | } 13 | 14 | pub async fn spawn_http_proxy(discord_tx: std::sync::mpsc::Sender) { 15 | // Master server proxy 16 | //println!("start"); 17 | let server = tiny_http::Server::http("0.0.0.0:3693").unwrap(); 18 | let mut destroyer: Option> = None; 19 | loop { 20 | for request in server.incoming_requests() { 21 | let addr = request.remote_addr(); 22 | if addr.ip() != Ipv4Addr::new(127, 0, 0, 1) { 23 | continue; 24 | } 25 | let mut url = request.url().to_string(); 26 | //println!("{:?}", url); 27 | url.remove(0); 28 | if url == "check" { 29 | let response = tiny_http::Response::from_string("ok"); 30 | request.respond(response).unwrap(); 31 | continue; 32 | } 33 | if url.starts_with("rich_presence") { 34 | let server_name_encoded = url.replace("rich_presence/", ""); 35 | let data = percent_decode_str(&server_name_encoded) 36 | .decode_utf8_lossy() 37 | .into_owned(); 38 | let server_name = { 39 | if data != "none" { 40 | Some(data) 41 | } else { 42 | None 43 | } 44 | }; 45 | let state = crate::DiscordState { server_name }; 46 | let _ = discord_tx.send(state); 47 | let response = tiny_http::Response::from_string("ok"); 48 | request.respond(response).unwrap(); 49 | continue; 50 | } 51 | if url.starts_with("host") { 52 | let data = url.replace("host/", ""); 53 | let data = percent_decode_str(&data).decode_utf8_lossy().into_owned(); 54 | if let Some(destroyer) = destroyer { 55 | let _ = destroyer.send(()); 56 | } 57 | let (destroyer_tx, destroyer_rx) = tokio::sync::oneshot::channel(); 58 | let (setup_result_tx, mut setup_result_rx) = tokio::sync::oneshot::channel(); 59 | destroyer = Some(destroyer_tx); 60 | std::thread::spawn(move || { 61 | let data: ServerHostData = serde_json::from_str(&data).unwrap(); 62 | let config = kissmp_server::config::Config { 63 | server_name: data.name, 64 | max_players: data.max_players, 65 | map: data.map, 66 | port: data.port, 67 | mods: data.mods, 68 | upnp_enabled: true, 69 | ..Default::default() 70 | }; 71 | let rt = tokio::runtime::Runtime::new().unwrap(); 72 | rt.block_on(async move { 73 | let server = kissmp_server::Server::from_config(config); 74 | server.run(false, destroyer_rx, Some(setup_result_tx)).await; 75 | }); 76 | }); 77 | // FIXME: Utilize setup response at some point. Like display dialog message on client with copy button instead of chat message 78 | loop { 79 | let result = setup_result_rx.try_recv(); 80 | if result.is_ok() { 81 | break; 82 | } 83 | } 84 | let response = tiny_http::Response::from_string("ok"); 85 | request.respond(response).unwrap(); 86 | continue; 87 | } 88 | if let Ok(response) = reqwest::get(&url).await { 89 | if let Ok(text) = response.text().await { 90 | let response = tiny_http::Response::from_string(text); 91 | request.respond(response).unwrap(); 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /kissmp-bridge/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod discord; 2 | pub mod http_proxy; 3 | pub mod voice_chat; 4 | 5 | use futures::stream::FuturesUnordered; 6 | use futures::StreamExt; 7 | use quinn::IdleTimeout; 8 | use rustls::{Certificate, ServerName}; 9 | use std::convert::TryFrom; 10 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; 11 | use std::sync::Arc; 12 | use std::time::SystemTime; 13 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, WriteHalf}; 14 | use tokio::net::{TcpListener, TcpStream}; 15 | #[macro_use] 16 | extern crate log; 17 | 18 | const SERVER_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120); 19 | const CONNECTED_BYTE: &[u8] = &[1]; 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct DiscordState { 23 | pub server_name: Option, 24 | } 25 | 26 | async fn read_pascal_bytes(stream: &mut R) -> Result, anyhow::Error> { 27 | let mut buffer = [0; 4]; 28 | stream.read_exact(&mut buffer).await?; 29 | let len = u32::from_le_bytes(buffer) as usize; 30 | let mut buffer = vec![0; len]; 31 | stream.read_exact(&mut buffer).await?; 32 | Ok(buffer) 33 | } 34 | 35 | async fn write_pascal_bytes( 36 | stream: &mut W, 37 | bytes: &mut Vec, 38 | ) -> Result<(), anyhow::Error> { 39 | let len = bytes.len() as u32; 40 | let mut data = Vec::with_capacity(len as usize + 4); 41 | data.append(&mut len.to_le_bytes().to_vec()); 42 | data.append(bytes); 43 | Ok(stream.write_all(&data).await?) 44 | } 45 | 46 | #[tokio::main] 47 | async fn main() { 48 | shared::init_logging(); 49 | 50 | let (discord_tx, discord_rx) = std::sync::mpsc::channel(); 51 | discord::spawn_discord_rpc(discord_rx).await; 52 | { 53 | let discord_tx = discord_tx.clone(); 54 | tokio::spawn(async move { 55 | http_proxy::spawn_http_proxy(discord_tx).await; 56 | }); 57 | } 58 | let bind_addr = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 7894)); 59 | let listener = TcpListener::bind(bind_addr).await.unwrap(); 60 | info!("Bridge is running!"); 61 | while let Ok((mut client_stream, _)) = listener.accept().await { 62 | info!("Attempting to connect to a server..."); 63 | 64 | let addr = { 65 | let address_string = 66 | String::from_utf8(read_pascal_bytes(&mut client_stream).await.unwrap()).unwrap(); 67 | 68 | let mut socket_addrs = match address_string.to_socket_addrs() { 69 | Ok(socket_addrs) => socket_addrs, 70 | Err(e) => { 71 | error!("Failed to parse address: {}", e); 72 | continue; 73 | } 74 | }; 75 | match socket_addrs.next() { 76 | Some(addr) => addr, 77 | None => { 78 | error!("Could not find address: {}", address_string); 79 | continue; 80 | } 81 | } 82 | }; 83 | 84 | info!("Connecting to {}...", addr); 85 | connect_to_server(addr, client_stream, discord_tx.clone()).await; 86 | } 87 | } 88 | 89 | async fn connect_to_server( 90 | addr: SocketAddr, 91 | client_stream: TcpStream, 92 | discord_tx: std::sync::mpsc::Sender, 93 | ) -> () { 94 | let endpoint = { 95 | let rustls_config = rustls::ClientConfig::builder() 96 | .with_safe_defaults() 97 | .with_custom_certificate_verifier(Arc::new(AcceptAnyCertificate)) 98 | .with_no_client_auth(); 99 | let mut client_cfg = quinn::ClientConfig::new(Arc::new(rustls_config)); 100 | 101 | let mut transport = quinn::TransportConfig::default(); 102 | transport.max_idle_timeout(Some(IdleTimeout::try_from(SERVER_IDLE_TIMEOUT).unwrap())); 103 | client_cfg.transport = std::sync::Arc::new(transport); 104 | 105 | let mut endpoint = quinn::Endpoint::client(addr).unwrap(); 106 | endpoint.set_default_client_config(client_cfg); 107 | endpoint 108 | }; 109 | 110 | let server_connection = match endpoint.connect(addr, "kissmp").unwrap().await { 111 | Ok(c) => c, 112 | Err(e) => { 113 | error!("Failed to connect to the server: {}", e); 114 | return; 115 | } 116 | }; 117 | 118 | let (client_stream_reader, mut client_stream_writer) = tokio::io::split(client_stream); 119 | 120 | let _ = client_stream_writer.write_all(CONNECTED_BYTE).await; 121 | 122 | let (client_event_sender, client_event_receiver) = 123 | tokio::sync::mpsc::unbounded_channel::<(bool, shared::ClientCommand)>(); 124 | let (server_commands_sender, server_commands_receiver) = 125 | tokio::sync::mpsc::channel::(256); 126 | let (vc_recording_sender, vc_recording_receiver) = std::sync::mpsc::channel(); 127 | let (vc_playback_sender, vc_playback_receiver) = std::sync::mpsc::channel(); 128 | 129 | // TODO: Use a struct that can hold either a JoinHandle or a bare future so 130 | // additional tasks that do not depend on using tokio::spawn can be added. 131 | let mut non_critical_tasks = FuturesUnordered::new(); 132 | 133 | match voice_chat::try_create_vc_playback_task(vc_playback_receiver) { 134 | Ok(handle) => { 135 | non_critical_tasks.push(handle); 136 | debug!("Playback OK") 137 | } 138 | Err(e) => { 139 | error!("Failed to set up voice chat playback: {}", e) 140 | } 141 | }; 142 | 143 | match voice_chat::try_create_vc_recording_task( 144 | client_event_sender.clone(), 145 | vc_recording_receiver, 146 | ) { 147 | Ok(handle) => { 148 | non_critical_tasks.push(handle); 149 | debug!("Recording OK") 150 | } 151 | Err(e) => { 152 | error!("Failed to set up voice chat recording: {}", e) 153 | } 154 | }; 155 | 156 | tokio::spawn(async move { 157 | debug!("Starting tasks"); 158 | match tokio::try_join!( 159 | async { 160 | while let Some(result) = non_critical_tasks.next().await { 161 | match result { 162 | Err(e) => warn!("Non-critical task failed: {}", e), 163 | Ok(Err(e)) => warn!("Non-critical task died with exception: {}", e), 164 | _ => (), 165 | } 166 | } 167 | Ok(()) 168 | }, 169 | client_outgoing(server_commands_receiver, client_stream_writer), 170 | client_incoming( 171 | server_connection.connection.clone(), 172 | vc_playback_sender.clone(), 173 | client_stream_reader, 174 | vc_recording_sender, 175 | client_event_sender 176 | ), 177 | server_outgoing(server_connection.connection.clone(), client_event_receiver), 178 | server_incoming( 179 | server_commands_sender, 180 | vc_playback_sender, 181 | server_connection 182 | ), 183 | ) { 184 | Ok(_) => debug!("Tasks completed successfully"), 185 | Err(e) => warn!("Tasks ended due to exception: {}", e), 186 | } 187 | discord_tx.send(DiscordState { server_name: None }).unwrap(); 188 | }); 189 | } 190 | 191 | fn server_command_to_client_bytes(command: shared::ServerCommand) -> Vec { 192 | match command { 193 | shared::ServerCommand::FilePart(name, data, chunk_n, file_size, data_left) => { 194 | let name_b = name.as_bytes(); 195 | let mut result = vec![0]; 196 | result.append(&mut (name_b.len() as u32).to_le_bytes().to_vec()); 197 | result.append(&mut name_b.to_vec()); 198 | result.append(&mut chunk_n.to_le_bytes().to_vec()); 199 | result.append(&mut file_size.to_le_bytes().to_vec()); 200 | result.append(&mut data_left.to_le_bytes().to_vec()); 201 | result.append(&mut data.clone()); 202 | result 203 | } 204 | shared::ServerCommand::VoiceChatPacket(_, _, _) => { 205 | panic!("Voice packets have to handled by the bridge itself.") 206 | } 207 | _ => { 208 | let json = serde_json::to_string(&command).unwrap(); 209 | //println!("{:?}", json); 210 | let mut data = json.into_bytes(); 211 | let mut result = vec![1]; 212 | result.append(&mut (data.len() as u32).to_le_bytes().to_vec()); 213 | result.append(&mut data); 214 | result 215 | } 216 | } 217 | } 218 | 219 | type AHResult = Result<(), anyhow::Error>; 220 | 221 | async fn client_outgoing( 222 | mut server_commands_receiver: tokio::sync::mpsc::Receiver, 223 | mut client_stream_writer: WriteHalf, 224 | ) -> AHResult { 225 | while let Some(server_command) = server_commands_receiver.recv().await { 226 | client_stream_writer 227 | .write_all(server_command_to_client_bytes(server_command).as_ref()) 228 | .await?; 229 | } 230 | debug!("Server outgoing closed"); 231 | Ok(()) 232 | } 233 | 234 | async fn server_incoming( 235 | server_commands_sender: tokio::sync::mpsc::Sender, 236 | vc_playback_sender: std::sync::mpsc::Sender, 237 | server_connection: quinn::NewConnection, 238 | ) -> AHResult { 239 | let mut reliable_commands = server_connection 240 | .uni_streams 241 | .map(|stream| async { Ok::<_, anyhow::Error>(read_pascal_bytes(&mut stream?).await?) }); 242 | 243 | let mut unreliable_commands = server_connection 244 | .datagrams 245 | .map(|data| async { Ok::<_, anyhow::Error>(data?.to_vec()) }) 246 | .buffer_unordered(1024); 247 | 248 | loop { 249 | let command_bytes = tokio::select! { 250 | Some(reliable_command) = reliable_commands.next() => { 251 | reliable_command.await? 252 | }, 253 | Some(unreliable_command) = unreliable_commands.next() => { 254 | unreliable_command? 255 | }, 256 | else => break 257 | }; 258 | let command = bincode::deserialize::(command_bytes.as_ref())?; 259 | match command { 260 | shared::ServerCommand::VoiceChatPacket(client, pos, data) => { 261 | let _ = vc_playback_sender.send(voice_chat::VoiceChatPlaybackEvent::Packet( 262 | client, pos, data, 263 | )); 264 | } 265 | _ => server_commands_sender.send(command).await?, 266 | }; 267 | } 268 | debug!("Server incoming closed"); 269 | Ok(()) 270 | } 271 | 272 | async fn client_incoming( 273 | server_stream: quinn::Connection, 274 | vc_playback_sender: std::sync::mpsc::Sender, 275 | mut client_stream_reader: tokio::io::ReadHalf, 276 | vc_recording_sender: std::sync::mpsc::Sender, 277 | client_event_sender: tokio::sync::mpsc::UnboundedSender<(bool, shared::ClientCommand)>, 278 | ) -> AHResult { 279 | let mut buffer = [0; 1]; 280 | while let Ok(_) = client_stream_reader.read_exact(&mut buffer).await { 281 | let reliable = buffer[0] == 1; 282 | let mut len_buf = [0; 4]; 283 | let _ = client_stream_reader.read_exact(&mut len_buf).await; 284 | let len = i32::from_le_bytes(len_buf) as usize; 285 | let mut data = vec![0; len]; 286 | let _ = client_stream_reader.read_exact(&mut data).await; 287 | let decoded = serde_json::from_slice::(&data); 288 | if let Ok(decoded) = decoded { 289 | match decoded { 290 | shared::ClientCommand::SpatialUpdate(left_ear, right_ear) => { 291 | let _ = vc_playback_sender.send( 292 | voice_chat::VoiceChatPlaybackEvent::PositionUpdate(left_ear, right_ear), 293 | ); 294 | } 295 | shared::ClientCommand::StartTalking => { 296 | let _ = vc_recording_sender.send(voice_chat::VoiceChatRecordingEvent::Start); 297 | } 298 | shared::ClientCommand::EndTalking => { 299 | let _ = vc_recording_sender.send(voice_chat::VoiceChatRecordingEvent::End); 300 | } 301 | _ => client_event_sender.send((reliable, decoded)).unwrap(), 302 | }; 303 | } else { 304 | error!("error decoding json {:?}", decoded); 305 | error!("{:?}", String::from_utf8(data)); 306 | } 307 | } 308 | info!("Connection with game is closed"); 309 | server_stream.close(0u32.into(), b"Client has left the game."); 310 | debug!("Client incoming closed"); 311 | Ok(()) 312 | } 313 | 314 | async fn server_outgoing( 315 | server_stream: quinn::Connection, 316 | mut client_event_receiver: tokio::sync::mpsc::UnboundedReceiver<(bool, shared::ClientCommand)>, 317 | ) -> AHResult { 318 | while let Some((reliable, client_command)) = client_event_receiver.recv().await { 319 | let mut data = bincode::serialize::(&client_command)?; 320 | if !reliable { 321 | server_stream.send_datagram(data.into())?; 322 | } else { 323 | write_pascal_bytes(&mut server_stream.open_uni().await?, &mut data).await?; 324 | } 325 | } 326 | debug!("Server outgoing closed"); 327 | Ok(()) 328 | } 329 | 330 | struct AcceptAnyCertificate; 331 | 332 | impl rustls::client::ServerCertVerifier for AcceptAnyCertificate { 333 | fn verify_server_cert( 334 | &self, 335 | _end_entity: &Certificate, 336 | _: &[Certificate], 337 | _: &ServerName, 338 | scts: &mut dyn Iterator, 339 | ocsp_response: &[u8], 340 | now: SystemTime, 341 | ) -> Result { 342 | Ok(rustls::client::ServerCertVerified::assertion()) 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /kissmp-bridge/src/voice_chat.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context}; 2 | use cpal::traits::HostTrait; 3 | use cpal::traits::StreamTrait; 4 | use indoc::formatdoc; 5 | use rodio::DeviceTrait; 6 | use tokio::task::JoinHandle; 7 | use std::format; 8 | use indoc::indoc; 9 | 10 | const DISTANCE_DIVIDER: f32 = 3.0; 11 | const SAMPLE_RATE: cpal::SampleRate = cpal::SampleRate(16000); 12 | const BUFFER_LEN: usize = 1920; 13 | const SAMPLE_FORMATS: &[cpal::SampleFormat] = &[ 14 | cpal::SampleFormat::I16, 15 | cpal::SampleFormat::U16, 16 | cpal::SampleFormat::F32, 17 | ]; 18 | 19 | #[derive(Debug)] 20 | pub enum VoiceChatPlaybackEvent { 21 | Packet(u32, [f32; 3], Vec), 22 | PositionUpdate([f32; 3], [f32; 3]), 23 | } 24 | 25 | pub enum VoiceChatRecordingEvent { 26 | Start, 27 | End, 28 | } 29 | 30 | fn find_supported_recording_configuration( 31 | streams: Vec 32 | ) -> Option { 33 | for channels in 1..5 { 34 | for sample_format in SAMPLE_FORMATS { 35 | for config_range in &streams { 36 | if config_range.channels() == channels && 37 | config_range.sample_format() == *sample_format 38 | { 39 | return Some(config_range.clone()) 40 | }; 41 | } 42 | } 43 | } 44 | None 45 | } 46 | 47 | fn configure_recording_device( 48 | device: &cpal::Device 49 | ) -> Result<(cpal::StreamConfig, cpal::SampleFormat), anyhow::Error> { 50 | let config_range = find_supported_recording_configuration( 51 | device.supported_input_configs()?.collect()) 52 | .ok_or_else(|| { 53 | let mut error_message = 54 | String::from("Recording device incompatible due to the \ 55 | parameters it offered:\n"); 56 | for cfg in device.supported_input_configs().unwrap() { 57 | error_message.push_str(formatdoc!(" 58 | \tChannels: {:?} 59 | \tSample Format: {:?} 60 | --- 61 | ", cfg.channels(), cfg.sample_format()).as_str()); 62 | } 63 | error_message.push_str("We support devices that offer below 5 \ 64 | channels and use signed 16 bit, unsigned 16 bit, or 32 bit \ 65 | floating point sample rates"); 66 | anyhow!(error_message) 67 | })?; 68 | 69 | let buffer_size = match config_range.buffer_size() { 70 | cpal::SupportedBufferSize::Range { min, .. } => { 71 | if BUFFER_LEN as u32 > *min { 72 | cpal::BufferSize::Fixed(BUFFER_LEN as u32) 73 | } else { 74 | cpal::BufferSize::Default 75 | } 76 | } 77 | _ => cpal::BufferSize::Default, 78 | }; 79 | let supported_config = if 80 | config_range.max_sample_rate() >= SAMPLE_RATE && 81 | config_range.min_sample_rate() <= SAMPLE_RATE 82 | { 83 | config_range.with_sample_rate(SAMPLE_RATE) 84 | } else { 85 | let sr = config_range.max_sample_rate(); 86 | config_range.with_sample_rate(sr) 87 | }; 88 | let mut config = supported_config.config(); 89 | config.buffer_size = buffer_size; 90 | Ok((config, supported_config.sample_format())) 91 | } 92 | 93 | pub fn try_create_vc_recording_task( 94 | sender: tokio::sync::mpsc::UnboundedSender<(bool, shared::ClientCommand)>, 95 | receiver: std::sync::mpsc::Receiver, 96 | ) -> Result>, anyhow::Error> { 97 | let device = cpal::default_host().default_input_device() 98 | .context("No default audio input device available for voice chat. \ 99 | Check your OS's settings and verify you have a device available.")?; 100 | info!("Using default audio input device: {}", device.name().unwrap()); 101 | let (config, sample_format) = configure_recording_device(&device)?; 102 | info!(indoc!(" 103 | Recording stream configured with the following settings: 104 | \tChannels: {:?} 105 | \tSample rate: {:?} 106 | \tBuffer size: {:?} 107 | Use it with a key bound in BeamNG.Drive"), 108 | config.channels, 109 | config.sample_rate, 110 | config.buffer_size 111 | ); 112 | 113 | let encoder = audiopus::coder::Encoder::new( 114 | audiopus::SampleRate::Hz16000, 115 | audiopus::Channels::Mono, 116 | audiopus::Application::Voip, 117 | )?; 118 | 119 | 120 | 121 | Ok(tokio::task::spawn_blocking(move || { 122 | let err_fn = move |err| { 123 | error!("an error occurred on stream: {}", err); 124 | }; 125 | let sample_rate = config.sample_rate; 126 | let channels = config.channels; 127 | let send = std::sync::Arc::new(std::sync::Mutex::new(false)); 128 | let buffer = std::sync::Arc::new(std::sync::Mutex::new(vec![])); 129 | let stream = { 130 | let send = send.clone(); 131 | let buffer = buffer.clone(); 132 | match sample_format { 133 | cpal::SampleFormat::F32 => device 134 | .build_input_stream( 135 | &config, 136 | move |data: &[f32], _: &_| { 137 | if !*send.lock().unwrap() { 138 | return; 139 | }; 140 | let samples: Vec = data 141 | .iter() 142 | .map(|x| cpal::Sample::to_i16(x)) 143 | .collect(); 144 | encode_and_send_samples( 145 | &mut buffer.lock().unwrap(), 146 | &samples, 147 | &sender, 148 | &encoder, 149 | channels, 150 | sample_rate, 151 | ); 152 | }, 153 | err_fn, 154 | ), 155 | cpal::SampleFormat::I16 => device 156 | .build_input_stream( 157 | &config, 158 | move |data: &[i16], _: &_| { 159 | if !*send.lock().unwrap() { 160 | return; 161 | }; 162 | encode_and_send_samples( 163 | &mut buffer.lock().unwrap(), 164 | &data, 165 | &sender, 166 | &encoder, 167 | channels, 168 | sample_rate, 169 | ); 170 | }, 171 | err_fn, 172 | ), 173 | cpal::SampleFormat::U16 => device 174 | .build_input_stream( 175 | &config, 176 | move |data: &[u16], _: &_| { 177 | if !*send.lock().unwrap() { 178 | return; 179 | }; 180 | let samples: Vec = data 181 | .iter() 182 | .map(|x| cpal::Sample::to_i16(x)) 183 | .collect(); 184 | encode_and_send_samples( 185 | &mut buffer.lock().unwrap(), 186 | &samples, 187 | &sender, 188 | &encoder, 189 | channels, 190 | sample_rate, 191 | ); 192 | }, 193 | err_fn, 194 | ), 195 | }? 196 | }; 197 | 198 | stream.play()?; 199 | 200 | while let Ok(event) = receiver.recv() { 201 | match event { 202 | VoiceChatRecordingEvent::Start => { 203 | let mut send = send.lock().unwrap(); 204 | *send = true; 205 | } 206 | VoiceChatRecordingEvent::End => { 207 | let mut send = send.lock().unwrap(); 208 | buffer.lock().unwrap().clear(); 209 | *send = false; 210 | } 211 | } 212 | } 213 | debug!("Recording closed"); 214 | Ok::<_, anyhow::Error>(()) 215 | })) 216 | } 217 | 218 | pub fn encode_and_send_samples( 219 | buffer: &mut Vec, 220 | samples: &[i16], 221 | sender: &tokio::sync::mpsc::UnboundedSender<(bool, shared::ClientCommand)>, 222 | encoder: &audiopus::coder::Encoder, 223 | channels: u16, 224 | sample_rate: cpal::SampleRate, 225 | ) { 226 | let mut data = { 227 | let data: Vec = samples.chunks(channels as usize) 228 | .map(|x| x[0]) 229 | .collect(); 230 | if sample_rate.0 != SAMPLE_RATE.0 { 231 | let audio = fon::Audio::::with_i16_buffer(sample_rate.0, data); 232 | let mut audio = fon::Audio::::with_stream(SAMPLE_RATE.0, &audio); 233 | audio.as_i16_slice().to_vec() 234 | } else { 235 | data 236 | } 237 | }; 238 | if buffer.len() < BUFFER_LEN { 239 | buffer.append(&mut data); 240 | if buffer.len() < BUFFER_LEN { 241 | return; 242 | } 243 | } 244 | let opus_out: &mut [u8; 512] = &mut [0; 512]; 245 | if let Ok(encoded) = 246 | encoder.encode(&buffer.drain(..BUFFER_LEN).collect::>(), opus_out) 247 | { 248 | sender 249 | .send(( 250 | false, 251 | shared::ClientCommand::VoiceChatPacket(opus_out[0..encoded].to_vec()), 252 | )) 253 | .unwrap(); 254 | } 255 | } 256 | 257 | pub fn try_create_vc_playback_task( 258 | receiver: std::sync::mpsc::Receiver 259 | ) -> Result>, anyhow::Error> { 260 | use rodio::Source; 261 | let mut decoder = audiopus::coder::Decoder::new( 262 | audiopus::SampleRate::Hz16000, 263 | audiopus::Channels::Mono)?; 264 | let device = cpal::default_host() 265 | .default_output_device() 266 | .context("Couldn't find a default device for playback. Check your OS's \ 267 | settings and verify you have a device available.")?; 268 | 269 | info!("Using default audio output device: {}", device.name().unwrap()); 270 | 271 | Ok(tokio::task::spawn_blocking(move || { 272 | let (_stream, stream_handle) = 273 | rodio::OutputStream::try_from_device(&device)?; 274 | let mut sinks = std::collections::HashMap::new(); 275 | while let Ok(event) = receiver.recv() { 276 | match event { 277 | VoiceChatPlaybackEvent::Packet(client, position, encoded) => { 278 | let (sink, updated_at) = { 279 | sinks.entry(client).or_insert_with(|| { 280 | let sink = rodio::SpatialSink::try_new( 281 | &stream_handle, 282 | position, 283 | [0.0, -1.0, 0.0], 284 | [0.0, 1.0, 0.0], 285 | ).unwrap(); 286 | sink.set_volume(2.0); 287 | sink.play(); 288 | (sink, std::time::Instant::now()) 289 | }) 290 | }; 291 | *updated_at = std::time::Instant::now(); 292 | let position = [ 293 | position[0] / DISTANCE_DIVIDER, 294 | position[1] / DISTANCE_DIVIDER, 295 | position[2] / DISTANCE_DIVIDER 296 | ]; 297 | sink.set_emitter_position(position); 298 | let mut samples: Vec = Vec::with_capacity(BUFFER_LEN); 299 | samples.resize(BUFFER_LEN, 0); 300 | let res = decoder 301 | .decode(Some(&encoded), &mut samples, false) 302 | .unwrap(); 303 | samples.resize(res, 0); 304 | let buf = rodio::buffer::SamplesBuffer::new(1, 16000, samples.as_slice()) 305 | .convert_samples::(); 306 | sink.append(buf); 307 | }, 308 | VoiceChatPlaybackEvent::PositionUpdate(left_ear, right_ear) => { 309 | sinks.retain(|_, (sink, updated_at)| { 310 | if updated_at.elapsed().as_secs() > 1 { 311 | false 312 | } else { 313 | let left_ear = [ 314 | left_ear[0] / DISTANCE_DIVIDER, 315 | left_ear[1] / DISTANCE_DIVIDER, 316 | left_ear[2] / DISTANCE_DIVIDER 317 | ]; 318 | let right_ear = [ 319 | right_ear[0] / DISTANCE_DIVIDER, 320 | right_ear[1] / DISTANCE_DIVIDER, 321 | right_ear[2] / DISTANCE_DIVIDER 322 | ]; 323 | sink.set_left_ear_position(left_ear); 324 | sink.set_right_ear_position(right_ear); 325 | true 326 | } 327 | }); 328 | } 329 | } 330 | } 331 | debug!("Playback closed."); 332 | Ok::<_, anyhow::Error>(()) 333 | })) 334 | } 335 | -------------------------------------------------------------------------------- /kissmp-master/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /kissmp-master/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kissmp-master" 3 | version = "0.6.0" 4 | authors = ["TheHellBox "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | shared = { path = "../shared" } 11 | #tide = "0.15.0" 12 | warp = "0.2" 13 | serde_json="1.0" 14 | censor = "0.1.1" 15 | tokio={version = "0.2", features = ["rt-threaded", "macros", "net"]} 16 | #async-std = { version = "1.8.0", features = ["attributes"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | -------------------------------------------------------------------------------- /kissmp-master/src/main.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use shared::{VERSION, VERSION_STR}; 3 | use std::collections::HashMap; 4 | use std::net::SocketAddr; 5 | use std::sync::{Arc, Mutex}; 6 | use warp::Filter; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone)] 9 | pub struct ServerInfo { 10 | name: String, 11 | player_count: u8, 12 | max_players: u8, 13 | description: String, 14 | map: String, 15 | port: u16, 16 | version: (u32, u32), 17 | #[serde(skip)] 18 | update_time: Option, 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug, Clone)] 22 | pub struct ServerList(HashMap); 23 | 24 | #[tokio::main] 25 | async fn main() { 26 | p2p_server().await; 27 | 28 | let server_list_r = Arc::new(Mutex::new(ServerList(HashMap::new()))); 29 | let addresses_r: Arc>>> = 30 | Arc::new(Mutex::new(HashMap::new())); 31 | 32 | let server_list = server_list_r.clone(); 33 | let addresses = addresses_r.clone(); 34 | let post = warp::post() 35 | .and(warp::addr::remote()) 36 | .and(warp::body::json()) 37 | .and(warp::path::end()) 38 | .map(move |addr: Option, server_info: ServerInfo| { 39 | let addr = { 40 | if let Some(addr) = addr { 41 | addr 42 | } else { 43 | return "err"; 44 | } 45 | }; 46 | let censor_standart = censor::Censor::Standard; 47 | let censor_sex = censor::Censor::Sex; 48 | let mut server_info: ServerInfo = server_info; 49 | if server_info.version != VERSION { 50 | return "Invalid server version"; 51 | } 52 | if server_info.description.len() > 256 || server_info.name.len() > 64 { 53 | return "Server descrition/name length is too big!"; 54 | } 55 | if censor_standart.check(&server_info.name) || censor_sex.check(&server_info.name) { 56 | return "Censor!"; 57 | } 58 | { 59 | let server_list = &mut *server_list.lock().unwrap(); 60 | let addresses = &mut *addresses.lock().unwrap(); 61 | if let Some(ports) = addresses.get_mut(&addr.ip()) { 62 | ports.insert(server_info.port, true); 63 | // Limit amount of servers per addr to avoid spam 64 | if ports.len() > 10 { 65 | return "Too many servers!"; 66 | } 67 | } else { 68 | addresses.insert(addr.ip(), HashMap::new()); 69 | addresses 70 | .get_mut(&addr.ip()) 71 | .unwrap() 72 | .insert(server_info.port, true); 73 | } 74 | let addr = SocketAddr::new(addr.ip(), server_info.port); 75 | server_info.update_time = Some(std::time::Instant::now()); 76 | server_list.0.insert(addr, server_info); 77 | } 78 | return "ok"; 79 | }); 80 | let server_list = server_list_r.clone(); 81 | let addresses = addresses_r.clone(); 82 | let ver = warp::path::param().map(move |ver: String| { 83 | if ver != VERSION_STR && ver != "latest" { 84 | return outdated_ver(); 85 | } 86 | let server_list = server_list.clone(); 87 | let addresses = addresses.clone(); 88 | { 89 | let server_list = &mut *server_list.lock().unwrap(); 90 | let addresses = &mut *addresses.lock().unwrap(); 91 | for (k, server) in server_list.0.clone() { 92 | if server.update_time.unwrap().elapsed().as_secs() > 10 { 93 | server_list.0.remove(&k); 94 | if let Some(ports) = addresses.get_mut(&k.ip()) { 95 | ports.remove(&k.port()); 96 | } 97 | } 98 | } 99 | } 100 | let response = { 101 | let server_list = &mut *server_list.lock().unwrap(); 102 | serde_json::to_string(&server_list).unwrap() 103 | }; 104 | response 105 | }); 106 | let outdated = warp::get().map(move || return outdated_ver()); 107 | let routes = post.or(ver).or(outdated); 108 | warp::serve(routes).run(([0, 0, 0, 0], 3692)).await; 109 | } 110 | 111 | async fn p2p_server() { 112 | tokio::spawn(async { 113 | let mut socket = tokio::net::UdpSocket::bind(( 114 | std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), 115 | 3691, 116 | )) 117 | .await 118 | .unwrap(); 119 | loop { 120 | let mut buf = [0; 16]; 121 | let result = socket.recv_from(&mut buf).await; 122 | if let Ok((_, src_addr)) = result { 123 | let _ = socket 124 | .send_to(src_addr.to_string().as_bytes(), src_addr) 125 | .await; 126 | } 127 | } 128 | }); 129 | } 130 | 131 | fn outdated_ver() -> String { 132 | let mut server_list = ServerList(HashMap::with_capacity(5)); 133 | for k in 0..5 { 134 | server_list.0.insert(SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), k), ServerInfo { 135 | name: "You're running an outdated version of KissMP. Please, consider updating to a newer version".to_string(), 136 | player_count: 0, 137 | max_players: 0, 138 | description: "You can find updated version of KissMP on a github releases page".to_string(), 139 | map: "Update to a newer version of KissMP".to_string(), 140 | port: 0, 141 | version: VERSION, 142 | update_time: None 143 | }); 144 | } 145 | serde_json::to_string(&server_list).unwrap() 146 | } 147 | -------------------------------------------------------------------------------- /kissmp-server/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "link-args=-rdynamic"] 3 | -------------------------------------------------------------------------------- /kissmp-server/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /mods 3 | -------------------------------------------------------------------------------- /kissmp-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kissmp-server" 3 | version = "0.6.0" 4 | authors = ["hellbox"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | shared = { path = "../shared" } 11 | rand = "0.7.3" 12 | serde_json="1.0" 13 | bincode="1.3.1" 14 | rmp="0.8.9" 15 | rmp-serde="0.14.4" 16 | futures = "0.3.13" 17 | quinn = {version="0.8.5", features = ["tls-rustls"]} 18 | rustls = { version = "0.20.3", default-features = false } 19 | anyhow = "1.0.32" 20 | rlua = "0.17.0" 21 | notify = "4.0.15" 22 | tokio-util = {version = "0.6.5", features = ["codec"]} 23 | serde = { version = "1.0", features = ["derive"] } 24 | reqwest = { version = "0.11.2", default-features = false, features=["rustls-tls"] } 25 | rcgen = { version = "0.8.2", default-features = false } 26 | tokio = { version = "1.4", features = ["rt-multi-thread", "time", "macros", "sync", "io-util", "io-std", "fs"] } 27 | tokio-stream = "0.1.5" 28 | dirs = "3.0" 29 | igd = { git = "https://github.com/stevefan1999-personal/rust-igd.git", rev = "c2d1f83" } 30 | ifcfg = "0.1.2" 31 | async-ctrlc = "1.2" 32 | ipnetwork = "0.18" 33 | log = "0.4" 34 | 35 | [target.'cfg(unix)'.dependencies] 36 | steamlocate = "1.0" 37 | -------------------------------------------------------------------------------- /kissmp-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Deserialize, Serialize)] 4 | #[serde(default)] 5 | pub struct Config { 6 | pub server_name: String, 7 | pub description: String, 8 | pub map: String, 9 | pub max_players: u8, 10 | pub tickrate: u8, 11 | pub port: u16, 12 | pub max_vehicles_per_client: u8, 13 | pub show_in_server_list: bool, 14 | pub upnp_enabled: bool, 15 | pub server_identifier: String, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub mods: Option>, 18 | } 19 | 20 | impl Default for Config { 21 | fn default() -> Self { 22 | Self { 23 | server_name: "Vanilla KissMP Server".to_string(), 24 | description: "Vanilla KissMP Server".to_string(), 25 | map: "/levels/smallgrid/info.json".to_string(), 26 | tickrate: 60, 27 | max_players: 8, 28 | max_vehicles_per_client: 3, 29 | port: 3698, 30 | show_in_server_list: false, 31 | upnp_enabled: false, 32 | server_identifier: rand_string(), 33 | mods: None, 34 | } 35 | } 36 | } 37 | 38 | impl Config { 39 | pub fn load(path: &std::path::Path) -> Self { 40 | if !path.exists() { 41 | create_default_config(); 42 | } 43 | let config_file = std::fs::File::open(path).unwrap(); 44 | let reader = std::io::BufReader::new(config_file); 45 | serde_json::from_reader(reader).unwrap() 46 | } 47 | } 48 | 49 | pub fn create_default_config() { 50 | use std::io::prelude::*; 51 | let mut config_file = std::fs::File::create("./config.json").unwrap(); 52 | let config = Config::default(); 53 | let config_str = serde_json::to_vec_pretty(&config).unwrap(); 54 | config_file.write_all(&config_str).unwrap(); 55 | } 56 | 57 | fn rand_string() -> String { 58 | (0..10) 59 | .map(|_| (0x20u8 + (rand::random::() * 96.0) as u8) as char) 60 | .collect() 61 | } 62 | -------------------------------------------------------------------------------- /kissmp-server/src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Server { 4 | pub async fn on_client_event(&mut self, client_id: u32, event: IncomingEvent) { 5 | use shared::ClientCommand::*; 6 | use IncomingEvent::*; 7 | match event { 8 | ClientConnected(connection) => { 9 | let player_name = connection.client_info_public.name.clone(); 10 | self.connections.insert(client_id, connection); 11 | // Kinda ugly, but idk how to deal with lifetimes otherwise 12 | let mut client_info_list = vec![]; 13 | for (_, connection) in self.connections.clone() { 14 | client_info_list.push(connection.client_info_public.clone()) 15 | } 16 | let connection = self.connections.get_mut(&client_id).unwrap(); 17 | if let Some(public_address) = &self.public_address { 18 | connection.send_chat_message( 19 | format!( 20 | "You're playing on a uPnP enabled server. Others can join you by the following address: \n{}.\nNo port forwarding is required", 21 | public_address 22 | ) 23 | ).await; 24 | } 25 | for (_, vehicle) in &self.vehicles { 26 | let _ = connection 27 | .ordered 28 | .send(ServerCommand::VehicleSpawn(vehicle.data.clone())) 29 | .await; 30 | } 31 | for info in client_info_list { 32 | let _ = connection 33 | .ordered 34 | .send(ServerCommand::PlayerInfoUpdate(info)) 35 | .await; 36 | } 37 | for (_, client) in &mut self.connections { 38 | client 39 | .send_chat_message(format!("Player {} has joined the server", player_name)) 40 | .await; 41 | } 42 | let _ = self.update_lua_connections(); 43 | self.lua.context(|lua_ctx| { 44 | let _ = crate::lua::run_hook::( 45 | lua_ctx, 46 | String::from("OnPlayerConnected"), 47 | client_id, 48 | ); 49 | }); 50 | } 51 | ConnectionLost => { 52 | let player_name = self 53 | .connections 54 | .get(&client_id) 55 | .unwrap() 56 | .client_info_public 57 | .name 58 | .clone(); 59 | self.connections 60 | .get_mut(&client_id) 61 | .unwrap() 62 | .conn 63 | .close(0u32.into(), b""); 64 | self.connections.remove(&client_id); 65 | if let Some(client_vehicles) = self.vehicle_ids.clone().get(&client_id) { 66 | for (_, id) in client_vehicles { 67 | self.remove_vehicle(*id, Some(client_id)).await; 68 | } 69 | } 70 | for (_, client) in &mut self.connections { 71 | client 72 | .send_chat_message(format!("Player {} has left the server", player_name)) 73 | .await; 74 | let _ = client 75 | .ordered 76 | .send(ServerCommand::PlayerDisconnected(client_id)) 77 | .await; 78 | } 79 | let _ = self.update_lua_connections(); 80 | self.lua.context(|lua_ctx| { 81 | let _ = crate::lua::run_hook::( 82 | lua_ctx, 83 | String::from("OnPlayerDisconnected"), 84 | client_id, 85 | ); 86 | }); 87 | info!("Client has disconnected from the server"); 88 | } 89 | ClientCommand(command) => { 90 | match command { 91 | Chat(initial_message) => { 92 | let mut initial_message = initial_message.clone(); 93 | initial_message.truncate(128); 94 | let mut message = initial_message.clone(); 95 | info!( 96 | "<{}> {}", 97 | self.connections 98 | .get(&client_id) 99 | .unwrap() 100 | .client_info_public 101 | .name, 102 | message 103 | ); 104 | self.lua.context(|lua_ctx| { 105 | let results = crate::lua::run_hook::<(u32, String), Option>( 106 | lua_ctx, 107 | String::from("OnChat"), 108 | (client_id, initial_message.clone()), 109 | ); 110 | for result in results { 111 | if let Some(result) = result { 112 | message = result; 113 | break; 114 | } 115 | } 116 | }); 117 | if message.len() > 0 { 118 | for (_, client) in &mut self.connections { 119 | client 120 | .send_player_chat_message(message.clone(), client_id) 121 | .await; 122 | } 123 | } 124 | } 125 | VehicleUpdate(data) => { 126 | if let Some(server_id) = 127 | self.get_server_id_from_game_id(client_id, data.vehicle_id) 128 | { 129 | if let Some(vehicle) = self.vehicles.get_mut(&server_id) { 130 | vehicle.data.position = data.transform.position; 131 | vehicle.data.rotation = data.transform.rotation; 132 | vehicle.transform = Some(data.transform); 133 | vehicle.electrics = Some(data.electrics); 134 | vehicle.gearbox = Some(data.gearbox); 135 | } 136 | } 137 | } 138 | VehicleData(data) => { 139 | // Remove old vehicle with the same ID 140 | if let Some(server_id) = 141 | self.get_server_id_from_game_id(client_id, data.in_game_id) 142 | { 143 | self.remove_vehicle(server_id, Some(client_id)).await; 144 | } 145 | if let Some(client_vehicles) = self.vehicle_ids.get(&client_id) { 146 | if (data.name != "unicycle") 147 | && (client_vehicles.len() as u8 >= self.max_vehicles_per_client) 148 | { 149 | return; 150 | } 151 | } 152 | self.spawn_vehicle(Some(client_id), data).await; 153 | } 154 | RemoveVehicle(id) => { 155 | if let Some(server_id) = self.get_server_id_from_game_id(client_id, id) { 156 | self.remove_vehicle(server_id, Some(client_id)).await; 157 | } 158 | } 159 | ResetVehicle(data) => { 160 | if let Some(server_id) = self.get_server_id_from_game_id(client_id, data.vehicle_id) { 161 | let mut data = data.clone(); 162 | data.vehicle_id = server_id; 163 | self.reset_vehicle(data, Some(client_id)).await; 164 | } 165 | } 166 | RequestMods(files) => { 167 | let paths = crate::list_mods(self.mods.clone()); 168 | for path in paths.unwrap().1 { 169 | if path.is_dir() { 170 | continue; 171 | } 172 | let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); 173 | let path = path.to_str().unwrap().to_string(); 174 | if !files.contains(&file_name) { 175 | continue; 176 | } 177 | let _ = self 178 | .connections 179 | .get_mut(&client_id) 180 | .unwrap() 181 | .ordered 182 | .send(ServerCommand::TransferFile(path)) 183 | .await; 184 | } 185 | } 186 | VehicleMetaUpdate(meta) => { 187 | if let Some(server_id) = 188 | self.get_server_id_from_game_id(client_id, meta.vehicle_id) 189 | { 190 | if let Some(vehicle) = self.vehicles.get_mut(&server_id) { 191 | vehicle.data.color = meta.colors_table[0]; 192 | vehicle.data.palete_0 = meta.colors_table[1]; 193 | vehicle.data.palete_1 = meta.colors_table[2]; 194 | vehicle.data.plate = meta.plate.clone(); 195 | let mut meta = meta.clone(); 196 | meta.vehicle_id = server_id; 197 | for (_, client) in &mut self.connections { 198 | let _ = client 199 | .ordered 200 | .send(ServerCommand::VehicleMetaUpdate(meta.clone())) 201 | .await; 202 | } 203 | } 204 | } 205 | } 206 | ElectricsUndefinedUpdate(vehicle_id, undefined_update) => { 207 | if let Some(server_id) = 208 | self.get_server_id_from_game_id(client_id, vehicle_id) 209 | { 210 | /* if let Some(vehicle) = self.vehicles.get_mut(&server_id) { 211 | for (key, value) in &undefined_update.diff { 212 | if let Some(electrics) = &mut vehicle.electrics { 213 | electrics.undefined.insert(key.clone(), *value); 214 | } 215 | } 216 | }*/ 217 | for (_, client) in &mut self.connections { 218 | let _ = client 219 | .ordered 220 | .send(ServerCommand::ElectricsUndefinedUpdate( 221 | server_id, 222 | undefined_update.clone(), 223 | )) 224 | .await; 225 | } 226 | } 227 | } 228 | Ping(ping) => { 229 | let connection = self.connections.get_mut(&client_id).unwrap(); 230 | connection.client_info_public.ping = ping as u32; 231 | let start = std::time::SystemTime::now(); 232 | let since_the_epoch = start.duration_since(std::time::UNIX_EPOCH).unwrap(); 233 | let data = bincode::serialize(&shared::ServerCommand::Pong( 234 | since_the_epoch.as_secs_f64(), 235 | )) 236 | .unwrap(); 237 | let _ = connection.conn.send_datagram(data.into()); 238 | } 239 | VehicleChanged(id) => { 240 | if let Some(server_id) = self.get_server_id_from_game_id(client_id, id) { 241 | self.set_current_vehicle(client_id, Some(server_id)).await; 242 | } 243 | } 244 | CouplerAttached(event) => { 245 | for (_, client) in &mut self.connections { 246 | let _ = client 247 | .ordered 248 | .send(ServerCommand::CouplerAttached(event.clone())) 249 | .await; 250 | } 251 | } 252 | CouplerDetached(event) => { 253 | for (_, client) in &mut self.connections { 254 | let _ = client 255 | .ordered 256 | .send(ServerCommand::CouplerDetached(event.clone())) 257 | .await; 258 | } 259 | } 260 | VoiceChatPacket(data) => { 261 | let connection = self.connections.get_mut(&client_id).unwrap(); 262 | let position = { 263 | if let Some(vehicle_id) = &connection.client_info_public.current_vehicle { 264 | if let Some(vehicle) = self.vehicles.get(vehicle_id) { 265 | vehicle.data.position 266 | } else { 267 | [0.0, 0.0, 0.0] 268 | } 269 | } else { 270 | [0.0, 0.0, 0.0] 271 | } 272 | }; 273 | let data = bincode::serialize(&shared::ServerCommand::VoiceChatPacket( 274 | client_id, position, data, 275 | )) 276 | .unwrap(); 277 | // TODO: Check for distane 278 | for (id, client) in &self.connections { 279 | if client_id == *id { 280 | continue; 281 | } 282 | let _ = client.conn.send_datagram(data.clone().into()); 283 | } 284 | } 285 | _ => {} 286 | } 287 | } 288 | } 289 | } 290 | } 291 | 292 | //fn _distance_sqrt(a: [f32; 3], b: [f32; 3]) -> f32 { 293 | // return ((b[0].powi(2) - a[0].powi(2)) + (b[1].powi(2) - a[1].powi(2)) + (b[2].powi(2) - a[2].powi(2))) 294 | //} 295 | -------------------------------------------------------------------------------- /kissmp-server/src/file_transfer.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use tokio::io::AsyncReadExt; 3 | 4 | const CHUNK_SIZE: usize = 65536; 5 | 6 | // FIXME 7 | pub async fn transfer_file( 8 | connection: quinn::Connection, 9 | path: &std::path::Path, 10 | ) -> anyhow::Result<()> { 11 | let mut file = tokio::fs::File::open(path).await?; 12 | let metadata = file.metadata().await?; 13 | let file_length = metadata.len() as u32; 14 | let file_name = path.file_name().unwrap().to_str().unwrap(); 15 | let mut buf = [0; CHUNK_SIZE]; 16 | let mut chunk_n = 0; 17 | while let Ok(n) = file.read(&mut buf).await { 18 | if n == 0 { 19 | break; 20 | } 21 | let mut stream = connection.open_uni().await?; 22 | send( 23 | &mut stream, 24 | &bincode::serialize(&shared::ServerCommand::FilePart( 25 | file_name.to_string(), 26 | buf[0..n].to_vec(), 27 | chunk_n, 28 | file_length, 29 | n as u32, 30 | )) 31 | .unwrap(), 32 | ) 33 | .await?; 34 | stream.finish(); 35 | chunk_n += 1; 36 | } 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /kissmp-server/src/incoming.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug)] 4 | pub enum IncomingEvent { 5 | ClientConnected(Connection), 6 | ConnectionLost, 7 | ClientCommand(shared::ClientCommand), 8 | } 9 | 10 | impl Server { 11 | pub async fn handle_incoming_data( 12 | id: u32, 13 | data: Vec, 14 | client_events_tx: &mut mpsc::Sender<(u32, IncomingEvent)>, 15 | ) -> anyhow::Result<()> { 16 | let client_command = bincode::deserialize::(&data)?; 17 | client_events_tx 18 | .send((id, IncomingEvent::ClientCommand(client_command))) 19 | .await?; 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kissmp-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use kissmp_server::*; 2 | use log::{info}; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | shared::init_logging(); 7 | 8 | info!("Gas, Gas, Gas!"); 9 | let path = std::path::Path::new("./mods/"); 10 | if !path.exists() { 11 | std::fs::create_dir(path).unwrap(); 12 | } 13 | let config = config::Config::load(std::path::Path::new("./config.json")); 14 | let server = Server::from_config(config); 15 | server.run(true, tokio::sync::oneshot::channel().1, None).await; 16 | std::process::exit(0); 17 | } 18 | -------------------------------------------------------------------------------- /kissmp-server/src/outgoing.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | impl Server { 4 | pub fn handle_outgoing_data(command: shared::ServerCommand) -> Vec { 5 | bincode::serialize(&command).unwrap() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /kissmp-server/src/server_vehicle.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Clone)] 4 | pub struct Vehicle { 5 | pub transform: Option, 6 | pub electrics: Option, 7 | pub gearbox: Option, 8 | pub data: VehicleData, 9 | } 10 | 11 | impl crate::Server { 12 | pub async fn remove_vehicle(&mut self, id: u32, client_id: Option) { 13 | if let Some(vehicle) = self.vehicles.get(&id) { 14 | if let Some(owner_id) = vehicle.data.owner { 15 | if let Some(client_vehicles) = self.vehicle_ids.get_mut(&owner_id) { 16 | client_vehicles.remove(&vehicle.data.in_game_id); 17 | if client_vehicles.len() == 0 { 18 | self.set_current_vehicle(owner_id, None).await; 19 | } 20 | } 21 | } 22 | } 23 | 24 | self.vehicles.remove(&id); 25 | for (cid, client) in &mut self.connections { 26 | if Some(*cid) == client_id { 27 | continue; 28 | } 29 | let _ = client.ordered.send(ServerCommand::RemoveVehicle(id)).await; 30 | } 31 | 32 | self.lua.context(|lua_ctx| { 33 | let _ = crate::lua::run_hook::<(u32, Option), ()>( 34 | lua_ctx, 35 | String::from("OnVehicleRemoved"), 36 | (id, client_id), 37 | ); 38 | }); 39 | } 40 | pub async fn reset_vehicle(&mut self, data: VehicleReset, client_id: Option) { 41 | for (cid, client) in &mut self.connections { 42 | if client_id.is_some() && *cid == client_id.unwrap() { 43 | continue; 44 | } 45 | let _ = client 46 | .ordered 47 | .send(ServerCommand::ResetVehicle(data.clone())) 48 | .await; 49 | } 50 | 51 | if let Some(vehicle) = self.vehicles.get_mut(&data.vehicle_id) { 52 | vehicle.data.position = data.position; 53 | vehicle.data.rotation = data.rotation; 54 | vehicle.transform = Some(Transform { 55 | position: data.position, 56 | rotation: data.rotation, 57 | angular_velocity: [0.0, 0.0, 0.0], 58 | velocity: [0.0, 0.0, 0.0] 59 | }); 60 | } 61 | 62 | let _ = self.update_lua_vehicles(); 63 | self.lua.context(|lua_ctx| { 64 | let _ = crate::lua::run_hook::<(u32, Option), ()>( 65 | lua_ctx, 66 | String::from("OnVehicleResetted"), 67 | (data.vehicle_id, client_id), 68 | ); 69 | }); 70 | } 71 | 72 | pub async fn set_current_vehicle(&mut self, client_id: u32, vehicle_id: Option) { 73 | if let Some(connection) = self.connections.get_mut(&client_id) { 74 | connection.client_info_public.current_vehicle = vehicle_id; 75 | let _ = self.update_lua_connections(); 76 | } 77 | } 78 | 79 | pub fn get_server_id_from_game_id(&self, client_id: u32, game_id: u32) -> Option { 80 | if let Some(client_vehicles) = self.vehicle_ids.get(&client_id) { 81 | if let Some(server_id) = client_vehicles.get(&game_id) { 82 | Some(*server_id) 83 | } else { 84 | None 85 | } 86 | } else { 87 | None 88 | } 89 | } 90 | 91 | pub async fn spawn_vehicle(&mut self, owner: Option, data: VehicleData) { 92 | let server_id = rand::random::() as u32; 93 | let mut data = data.clone(); 94 | data.server_id = server_id; 95 | data.owner = owner; 96 | for (_, client) in &mut self.connections { 97 | let _ = client 98 | .ordered 99 | .send(ServerCommand::VehicleSpawn(data.clone())) 100 | .await; 101 | } 102 | if let Some(owner) = owner { 103 | if self.vehicle_ids.get(&owner).is_none() { 104 | self.vehicle_ids 105 | .insert(owner, std::collections::HashMap::with_capacity(16)); 106 | } 107 | self.vehicle_ids 108 | .get_mut(&owner) 109 | .unwrap() 110 | .insert(data.in_game_id, server_id); 111 | } 112 | self.vehicles.insert( 113 | server_id, 114 | Vehicle { 115 | data, 116 | gearbox: None, 117 | electrics: None, 118 | transform: None, 119 | }, 120 | ); 121 | 122 | let _ = self.update_lua_vehicles(); 123 | if let Some(owner) = owner { 124 | self.set_current_vehicle(owner, Some(server_id)).await; 125 | self.lua.context(|lua_ctx| { 126 | let _ = crate::lua::run_hook::<(u32, u32), ()>( 127 | lua_ctx, 128 | String::from("OnVehicleSpawned"), 129 | (server_id, owner), 130 | ); 131 | }); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.6.0" 4 | authors = ["TheHellBox "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow="1.0" 11 | serde_json="1.0" 12 | bincode="1.3.1" 13 | rmp="0.8.9" 14 | rmp-serde="0.14.4" 15 | serde = { version = "1.0", features = ["derive"] } 16 | pretty_env_logger = "0.4" 17 | chrono = "0.4" 18 | log = "0.4" -------------------------------------------------------------------------------- /shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate pretty_env_logger; 2 | 3 | pub mod vehicle; 4 | use serde::{Deserialize, Serialize}; 5 | use vehicle::*; 6 | use std::io::Write; 7 | use chrono::Local; 8 | pub use log::{info, warn, error}; 9 | 10 | pub const VERSION: (u32, u32) = (0, 6); 11 | pub const VERSION_STR: &str = "0.6.0"; 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct ClientInfoPrivate { 15 | pub name: String, 16 | pub secret: String, 17 | pub steamid64: Option, 18 | pub client_version: (u32, u32), 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Debug, Clone)] 22 | pub struct ClientInfoPublic { 23 | pub name: String, 24 | pub id: u32, 25 | pub current_vehicle: Option, 26 | pub ping: u32, 27 | pub hide_nametag: bool, 28 | } 29 | 30 | #[derive(Serialize, Deserialize, Debug, Clone)] 31 | pub struct ServerInfo { 32 | pub name: String, 33 | pub player_count: u8, 34 | pub client_id: u32, 35 | pub map: String, 36 | pub tickrate: u8, 37 | pub max_vehicles_per_client: u8, 38 | pub mods: Vec<(String, u32)>, 39 | pub server_identifier: String, 40 | } 41 | 42 | impl ClientInfoPublic { 43 | pub fn new(id: u32) -> Self { 44 | Self { 45 | id, 46 | ..Default::default() 47 | } 48 | } 49 | } 50 | 51 | impl Default for ClientInfoPublic { 52 | fn default() -> Self { 53 | Self { 54 | name: String::from("Unknown"), 55 | id: 0, 56 | current_vehicle: None, 57 | ping: 0, 58 | hide_nametag: false, 59 | } 60 | } 61 | } 62 | 63 | #[derive(Debug, Serialize, Deserialize)] 64 | pub enum ClientCommand { 65 | ClientInfo(ClientInfoPrivate), 66 | VehicleUpdate(VehicleUpdate), 67 | VehicleData(VehicleData), 68 | GearboxUpdate(Gearbox), 69 | RemoveVehicle(u32), 70 | ResetVehicle(VehicleReset), 71 | Chat(String), 72 | RequestMods(Vec), 73 | VehicleMetaUpdate(VehicleMeta), 74 | VehicleChanged(u32), 75 | CouplerAttached(CouplerAttached), 76 | CouplerDetached(CouplerDetached), 77 | ElectricsUndefinedUpdate(u32, ElectricsUndefined), 78 | VoiceChatPacket(Vec), 79 | // Only used by bridge 80 | SpatialUpdate([f32; 3], [f32; 3]), 81 | // Only used by bridge 82 | StartTalking, 83 | // Only used by bridge 84 | EndTalking, 85 | Ping(u16), 86 | } 87 | 88 | #[derive(Debug, Serialize, Deserialize)] 89 | pub enum ServerCommand { 90 | VehicleUpdate(VehicleUpdate), 91 | VehicleSpawn(VehicleData), 92 | RemoveVehicle(u32), 93 | ResetVehicle(VehicleReset), 94 | Chat(String, Option), 95 | TransferFile(String), 96 | SendLua(String), 97 | PlayerInfoUpdate(ClientInfoPublic), 98 | VehicleMetaUpdate(VehicleMeta), 99 | PlayerDisconnected(u32), 100 | VehicleLuaCommand(u32, String), 101 | CouplerAttached(CouplerAttached), 102 | CouplerDetached(CouplerDetached), 103 | ElectricsUndefinedUpdate(u32, ElectricsUndefined), 104 | ServerInfo(ServerInfo), 105 | FilePart(String, Vec, u32, u32, u32), 106 | VoiceChatPacket(u32, [f32; 3], Vec), 107 | Pong(f64), 108 | } 109 | 110 | pub fn init_logging() 111 | { 112 | // pretty_env_logger doesn't appear to print anything without using 113 | // a filter in the builder. 114 | let filter = match std::env::var("RUST_LOG") 115 | { 116 | Ok(f) => f, 117 | Err(_e) => "info".to_owned() 118 | }; 119 | 120 | 121 | let _ = pretty_env_logger::formatted_builder(). 122 | parse_filters(&filter) 123 | .default_format() 124 | .format(|buf, record| { 125 | let level = { buf.default_styled_level(record.level()) }; 126 | let mut module_path = match record.module_path() 127 | { 128 | Some(path) => path, 129 | None => "unknown" 130 | }; 131 | 132 | // this removes anything past the root so the log stays clean (ex. kissmp_server::voice_Chat -> kissmp_server) 133 | let c_index = module_path.find(":"); 134 | if c_index.is_some() { 135 | module_path = &module_path[..c_index.unwrap()]; 136 | } 137 | 138 | writeln!(buf, "[{}] [{}] [{}]: {}", Local::now().format("%H:%M:%S%.3f"), module_path, format_args!("{:>5}", level), record.args()) 139 | }) 140 | .try_init(); 141 | } -------------------------------------------------------------------------------- /shared/src/vehicle/electrics.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 4 | pub struct ElectricsUndefined { 5 | pub diff: std::collections::HashMap, 6 | } 7 | 8 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 9 | pub struct Electrics { 10 | pub throttle_input: f32, 11 | pub brake_input: f32, 12 | pub clutch: f32, 13 | pub parkingbrake: f32, 14 | pub steering_input: f32, 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/vehicle/gearbox.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 4 | pub struct Gearbox { 5 | pub arcade: bool, 6 | pub lock_coef: f32, 7 | pub mode: Option, 8 | pub gear_indices: [i8; 2], 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/vehicle/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod electrics; 2 | pub mod gearbox; 3 | pub mod transform; 4 | pub mod vehicle_meta; 5 | 6 | pub use electrics::*; 7 | pub use gearbox::*; 8 | pub use transform::*; 9 | pub use vehicle_meta::*; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone)] 14 | pub struct VehicleReset { 15 | pub vehicle_id: u32, 16 | pub position: [f32; 3], 17 | pub rotation: [f32; 4], 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Debug, Clone)] 21 | pub struct VehicleData { 22 | pub parts_config: String, 23 | pub in_game_id: u32, 24 | pub color: [f32; 8], 25 | pub palete_0: [f32; 8], 26 | pub palete_1: [f32; 8], 27 | pub plate: Option, 28 | pub name: String, 29 | pub server_id: u32, 30 | pub owner: Option, 31 | pub position: [f32; 3], 32 | pub rotation: [f32; 4], 33 | } 34 | 35 | // A single packet that contains all of the vehicle updates. 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | pub struct VehicleUpdate { 38 | pub transform: Transform, 39 | pub electrics: Electrics, 40 | pub gearbox: Gearbox, 41 | pub vehicle_id: u32, 42 | pub generation: u64, 43 | pub sent_at: f64, 44 | } 45 | 46 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 47 | pub struct CouplerAttached { 48 | obj_a: u32, 49 | obj_b: u32, 50 | node_a_id: u32, 51 | node_b_id: u32, 52 | } 53 | 54 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 55 | pub struct CouplerDetached { 56 | obj_a: u32, 57 | obj_b: u32, 58 | node_a_id: u32, 59 | node_b_id: u32, 60 | } 61 | 62 | pub struct ServerSetupResult { 63 | pub addr: String, 64 | pub port: u16, 65 | pub is_upnp: bool, 66 | } 67 | -------------------------------------------------------------------------------- /shared/src/vehicle/transform.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub struct Transform { 5 | pub position: [f32; 3], 6 | pub rotation: [f32; 4], 7 | pub velocity: [f32; 3], 8 | pub angular_velocity: [f32; 3], 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/vehicle/vehicle_meta.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Deserialize, Serialize)] 4 | pub struct VehicleMeta { 5 | pub vehicle_id: u32, 6 | pub plate: Option, 7 | pub colors_table: [[f32; 8]; 3], 8 | } 9 | --------------------------------------------------------------------------------