├── LICENSE ├── README.md ├── client.lua ├── dot.bmp ├── meta.xml ├── server.lua └── status.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marek Kulik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootloader 2 | 3 | A resource for server bootloading and resource keep-alive for a Multi Theft Auto server. 4 | 5 | Bootloading in the context of a MTA:SA server means this resource will start pre-configured resources in an undefined order. Furthermore, this bootloader will also keep pre-configured resources alive by checking the resource state with a timer - this behavior can be disabled. 6 | 7 | The act of enabling or disabling a resource in the bootloader is logged by the server. 8 | 9 | This resource is production-ready and can be used as-is. 10 | 11 | ## Installation 12 | 13 | ### Summary 14 | 15 | 1. Upload to your server's **resources** directory 16 | 2. Refresh your server with `/refresh` command 17 | 3. Grant ACL permissions with `/aclrequest allow bootloader all` 18 | 4. Add `command.bootloader` to **Admin** ACL permission list 19 | 5. Add bootloader to **mtaserver.conf** resource startup list 20 | 21 | ### How to install this resource 22 | 23 | Place this resource in your server's **resources** directory to install it. 24 | 25 | If your MTA:SA server is already running, you must execute the `/refresh` command (either as administrator or in the server console - without the slash prefix). 26 | 27 | This resource has two ACL requests and it will __NOT__ work without these permissions, since they're essential to the functionality. This resource needs access to **startResource** and **stopResource** functions to start and stop other resources, respectively. You can grant these permissions with the command: `/aclrequest allow bootloader all` 28 | 29 | To allow administrators to open the GUI configuration panel, they must be granted the permission to use the ACL right `command.bootloader`. You can do this either by manually editing your ACL.xml __AND__ executing `/reloadacl` on the server or by adding the right to the **Admin** ACL through the admin panel **Resources** tab. 30 | 31 | This resource is meant to be put into your **mtaserver.conf** like this: 32 | ```xml 33 | 34 | 35 | 36 | 37 | 38 | ``` 39 | 40 | You should move the **bootloader** to the end of the resource list, if you encounter any issues. You should also mark the bootloader as `protected="1"` to prevent any user from stopping the resource. 41 | 42 | ## Configuration 43 | 44 | The bootloader exposes a few resource-level settings, which you can set manually by editing **settings.xml** (not recommended) or by opening the resource configuration through the admin panel **Resources** tab. 45 | 46 | | Setting | Description | 47 | | ---------------- | -------------------------------------------------- | 48 | | \*keepAlive | Periodically start checked, but stopped, resources | 49 | | \*showGamemodes | Display gamemode resources in the GUI | 50 | | \*showMaps | Display map resources in the GUI | 51 | | \*showRaceAddons | Display race addon resources in the GUI | 52 | 53 | **NOTE:** The bootloader stores the list of enabled resources in the server's **settings.xml** file. It uses the key `@resources` and the value is a list of enabled resource names concatenated by a comma `,` character. 54 | 55 | ## Usage 56 | 57 | You open the bootloader configuration window with the `/bootloader` command. You can easily bind this command to any key (example: `/bind f4 bootloader`). 58 | 59 | The configuration panel will receive the resource list in batches from the server (indicated by the loading spinner in the window title) to avoid hanging either the server or client. You can use the panel before the loading finishes. 60 | 61 | Every line in the resource list of the bootloader GUI represents a single resource on the server. The circle indicates whether the resource is running (circle is green) or stopped (circle is gray). The checkbox indicates whether the resource is enabled for bootloading and keep-alive. 62 | 63 | ![A line in the resource list of the bootloader window](.github/bootloader-line.png) 64 | 65 | To find a resource in the resource list, you can apply several filters through the filter editbox in the configuration panel. The filter text is separated by whitespace and every *token* must match for a resource to appear. 66 | 67 | You can filter by... 68 | 69 | - resource name 70 | - resource type with `#type` (corresponds to the `` meta.xml value). 71 | - resource state with `~on` or `~off` (read: is the resource running?) 72 | - enabled state with `@on` or `@off` (read: resource is checked) 73 | 74 | Any filter can be prefixed with a `!` to filter out results. By default, a filter includes a match in the resource list. 75 | 76 | ![The filter editbox in the bootloader window](.github/bootloader-filter.png) 77 | ![An example filter text](.github/bootloader-filter-filled.png) 78 | 79 | **NOTE:** By enabling a resource, the server will automatically start the resource. 80 | By disabling a resource, the server will automatically stop the resource. 81 | 82 | ## Screenshots 83 | 84 | ![The bootloader configuration window](.github/bootloader.png) 85 | 86 | ![The bootloader configuration window with a filter to match 'misc' type resources](.github/bootloader-misc.png) 87 | 88 | ![The bootloader configuration window with a filter to match all running resources without 'race' in the name](.github/bootloader-no-race.png) 89 | 90 | ## License 91 | 92 | Licensed under the [MIT](LICENSE) license. 93 | -------------------------------------------------------------------------------- /client.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- 2 | -- Access control and session management 3 | -------------------------------------------------------------------------------- 4 | function onToggleConfigurationPanel(enabled) 5 | if enabled then 6 | openBootloaderWindow() 7 | requestBootloaderResourceDataList() 8 | else 9 | closeBootloaderWindow() 10 | end 11 | end 12 | addEvent("Bootloader.toggleConfigurationPanel", true) 13 | addEventHandler("Bootloader.toggleConfigurationPanel", resourceRoot, onToggleConfigurationPanel, false) 14 | 15 | -------------------------------------------------------------------------------- 16 | -- Data stream events 17 | -------------------------------------------------------------------------------- 18 | function requestBootloaderResourceDataList() 19 | preBootloaderResourceDataListRequest() 20 | triggerServerEvent("BootloaderClient.requestResourceDataList", resourceRoot) 21 | end 22 | 23 | function handleBootloaderResourceDataListBatch(resourceDataBatch) 24 | processBootloaderResourceDataBatch(resourceDataBatch) 25 | end 26 | addEvent("Bootloader.resourceDataListBatch", true) 27 | addEventHandler("Bootloader.resourceDataListBatch", resourceRoot, handleBootloaderResourceDataListBatch, false) 28 | 29 | function handleBootloaderResourceDataListComplete() 30 | completeBootloaderResourceDataBatch() 31 | end 32 | addEvent("Bootloader.resourceDataListComplete", true) 33 | addEventHandler("Bootloader.resourceDataListComplete", resourceRoot, handleBootloaderResourceDataListComplete, false) 34 | 35 | function requestBootloaderResourceToggle(resourceName, enabled) 36 | triggerServerEvent("BootloaderClient.toggleBootloaderResource", resourceRoot, resourceName, enabled) 37 | end 38 | 39 | function handleBootloaderResourceDataResponse(resourceName, isEnabled, isRunning) 40 | processBootloaderResourceData(resourceName, isEnabled, isRunning) 41 | end 42 | addEvent("Bootloader.resourceDataResponse", true) 43 | addEventHandler("Bootloader.resourceDataResponse", resourceRoot, handleBootloaderResourceDataResponse, false) 44 | 45 | -------------------------------------------------------------------------------- 46 | -- Graphical User Interface 47 | -------------------------------------------------------------------------------- 48 | local gui = {} 49 | local minimumWindowWidth = 400 50 | local minimumWindowHeight = 400 51 | local windowTitle = "Bootloader Configuration" 52 | 53 | function openBootloaderWindow() 54 | if gui.window ~= nil then 55 | return 56 | end 57 | 58 | gui.width = 600 59 | gui.height = 600 60 | gui.rows = {} 61 | gui.checkboxes = {} 62 | gui.resources = {} 63 | 64 | local innerWidth = gui.width - 20 65 | local screenWidth, screenHeight = guiGetScreenSize() 66 | local posX = (screenWidth - gui.width) / 2 67 | local posY = (screenHeight - gui.height) / 2 68 | gui.window = guiCreateWindow(posX, posY, gui.width, gui.height, windowTitle, false) 69 | addEventHandler("onClientGUISize", gui.window, onBootloaderWindowResize, false) 70 | 71 | local function createHeader(x, y, text) 72 | local label = guiCreateLabel(x, y, 100, 15, text, false, gui.window) 73 | guiLabelSetColor(label, 160, 160, 192) 74 | return label 75 | end 76 | 77 | gui.headers = {} 78 | gui.headers[1] = createHeader(20, 25, "State") 79 | gui.headers[2] = createHeader(75, 25, "Name") 80 | gui.headers[3] = createHeader(210, 25, "Description") 81 | 82 | local function createBackgroundBorder(x, y, width, height) 83 | local image = guiCreateStaticImage(x, y, width, height, "dot.bmp", false, gui.window) 84 | guiStaticImageSetColor(image, 160, 160, 190, 255) 85 | guiSetEnabled(image, false) 86 | guiForceSetAlpha(image, 1.0) 87 | return image 88 | end 89 | 90 | gui.background = guiCreateStaticImage(10, 42, innerWidth, gui.height - 84, "dot.bmp", false, gui.window) 91 | guiStaticImageSetVerticalBackground(gui.background, 40, 40, 70, 200, 0, 0, 0, 200) 92 | guiSetEnabled(gui.background, false) 93 | guiForceSetAlpha(gui.background, 1.0) 94 | 95 | gui.backgroundborder_top = createBackgroundBorder(10, 42, innerWidth, 1) 96 | gui.backgroundborder_bottom = createBackgroundBorder(10, gui.height - 42, innerWidth, 1) 97 | gui.backgroundborder_left = createBackgroundBorder(10, 42, 1, gui.height - 84) 98 | gui.backgroundborder_right = createBackgroundBorder(10 + innerWidth, 42, 1, gui.height - 84) 99 | 100 | local bottomY = gui.height - 35 101 | 102 | gui.filters = {} 103 | gui.filter_label = guiCreateLabel(10, bottomY + 5, 30, 25, "Filter:", false, gui.window) 104 | gui.filter_edit = guiCreateEdit(50, bottomY, gui.width - 260, 25, "", false, gui.window) 105 | gui.filter_help = guiCreateLabel(10, 5, gui.width - 260, 25, "Type: #script | State: ~on, ~off | Enabled: @on, @off", false, gui.filter_edit) 106 | guiLabelSetColor(gui.filter_help, 60, 60, 60) 107 | guiSetEnabled(gui.filter_help, false) 108 | addEventHandler("onClientGUIChanged", gui.filter_edit, onBootloaderFilterChanged, false) 109 | 110 | gui.refresh = guiCreateButton(gui.width - 200, bottomY, 90, 25, "Refresh", false, gui.window) 111 | addEventHandler("onClientGUIClick", gui.refresh, onBootloaderRefreshButtonClick, false) 112 | 113 | gui.close = guiCreateButton(gui.width - 100, bottomY, 90, 25, "Close", false, gui.window) 114 | addEventHandler("onClientGUIClick", gui.close, onBootloaderCloseButtonClick, false) 115 | 116 | showCursor(true) 117 | guiSetInputEnabled(true) 118 | end 119 | 120 | function closeBootloaderWindow() 121 | destroyElement(gui.window) 122 | guiSetInputEnabled(false) 123 | showCursor(false) 124 | gui = {} 125 | end 126 | 127 | function onBootloaderWindowResize() 128 | gui.width, gui.height = guiGetSize(source, false) 129 | 130 | if gui.width < minimumWindowWidth or gui.height < minimumWindowWidth then 131 | gui.width = math.max(minimumWindowWidth, gui.width) 132 | gui.height = math.max(minimumWindowWidth, gui.height) 133 | return guiSetSize(source, gui.width, gui.height, false) 134 | end 135 | 136 | if gui.scrollpane then 137 | guiSetSize(gui.scrollpane, gui.width - 35, gui.height - 100, false) 138 | end 139 | 140 | local innerWidth = gui.width - 20 141 | 142 | guiSetSize(gui.background, innerWidth, gui.height - 84, false) 143 | 144 | guiSetSize(gui.backgroundborder_top, innerWidth, 1, false) 145 | guiSetSize(gui.backgroundborder_bottom, innerWidth, 1, false) 146 | guiSetSize(gui.backgroundborder_left, 1, gui.height - 84, false) 147 | guiSetSize(gui.backgroundborder_right, 1, gui.height - 84, false) 148 | 149 | guiSetPosition(gui.backgroundborder_bottom, 10, gui.height - 42, false) 150 | guiSetPosition(gui.backgroundborder_right, 10 + innerWidth, 42, false) 151 | 152 | local bottomY = gui.height - 35 153 | 154 | guiSetSize(gui.filter_edit, gui.width - 260, 25, false) 155 | 156 | guiSetPosition(gui.filter_label, 10, bottomY + 5, false) 157 | guiSetPosition(gui.filter_edit, 50, bottomY, false) 158 | guiSetPosition(gui.refresh, gui.width - 200, bottomY, false) 159 | guiSetPosition(gui.close, gui.width - 100, bottomY, false) 160 | end 161 | 162 | function onBootloaderFilterChanged() 163 | local filter = guiGetText(gui.filter_edit) or "" 164 | local filters = split(utf8.fold(filter), " ") 165 | local length = 0 166 | gui.filters = {} 167 | 168 | if filters then 169 | for i = 1, #filters do 170 | local filter = { 171 | text = utf8.trim(filters[i]), 172 | negation = false, 173 | } 174 | 175 | if utf8.sub(filter.text, 1, 1) == "!" then 176 | filter.negation = true 177 | filter.text = utf8.sub(filter.text, 2) 178 | end 179 | 180 | if filter.text ~= "" then 181 | length = length + 1 182 | gui.filters[length] = filter 183 | end 184 | end 185 | end 186 | 187 | guiSetVisible(gui.filter_help, filter == "") 188 | updateBootloaderScrollpaneLayout() 189 | end 190 | 191 | function onBootloaderCloseButtonClick() 192 | closeBootloaderWindow() 193 | triggerServerEvent("BootloaderClient.closePanel", resourceRoot) 194 | end 195 | 196 | function onBootloaderRefreshButtonClick() 197 | requestBootloaderResourceDataList() 198 | end 199 | 200 | function onBootloaderScrollpaneClick() 201 | if getElementType(source) == "gui-checkbox" then 202 | local row = gui.checkboxes[source] 203 | 204 | if row ~= nil then 205 | local selected = guiCheckBoxGetSelected(source) 206 | guiSetEnabled(source, false) 207 | requestBootloaderResourceToggle(row.data.name, selected) 208 | end 209 | end 210 | end 211 | 212 | local spinnersText = { 213 | "⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱", 214 | } 215 | 216 | local spinnersLength = #spinnersText 217 | 218 | function updateBootloaderWindowSpinner() 219 | gui.spinner = ((gui.spinner + 1) % spinnersLength) + 1 220 | guiSetText(gui.window, ("%s - Loading %s"):format(windowTitle, spinnersText[gui.spinner])) 221 | end 222 | 223 | function preBootloaderResourceDataListRequest() 224 | if gui.window == nil then 225 | return 226 | end 227 | 228 | clearBootloaderScrollpane() 229 | 230 | gui.spinner = 0 231 | updateBootloaderWindowSpinner() 232 | end 233 | 234 | function processBootloaderResourceDataBatch(resourceDataBatch) 235 | if gui.window == nil then 236 | return 237 | end 238 | 239 | if gui.scrollpane == nil then 240 | createBootloaderScrollpane() 241 | end 242 | 243 | createBootloaderResourceRows(resourceDataBatch) 244 | sortBootloaderResourceRows() 245 | updateBootloaderScrollpaneLayout() 246 | 247 | updateBootloaderWindowSpinner() 248 | end 249 | 250 | function completeBootloaderResourceDataBatch() 251 | if gui.window == nil then 252 | return 253 | end 254 | 255 | gui.spinner = nil 256 | guiSetText(gui.window, windowTitle) 257 | end 258 | 259 | function createBootloaderScrollpane() 260 | gui.scrollpane = guiCreateScrollPane(20, 50, gui.width - 35, gui.height - 100, false, gui.window) 261 | gui.scrollpadding = guiCreateLabel(0, 0, 1, 1, "", false, gui.scrollpane) 262 | addEventHandler("onClientGUIClick", gui.scrollpane, onBootloaderScrollpaneClick) 263 | guiSetProperty(gui.scrollpane, "VertStepSize", 0.05) 264 | guiForceSetAlpha(gui.scrollpane, 1.0) 265 | end 266 | 267 | function clearBootloaderScrollpane() 268 | if gui.scrollpane then 269 | destroyElement(gui.scrollpane) 270 | gui.scrollpane = nil 271 | gui.rows = {} 272 | gui.checkboxes = {} 273 | gui.resources = {} 274 | end 275 | end 276 | 277 | function generateResourceDataFilter(data) 278 | return utf8.fold(table.concat({ 279 | data.name, 280 | "#"..(data.type or "none"), 281 | (data.running and "~on" or "~off"), 282 | (data.enabled and "@on" or "@off"), 283 | }, " ")) 284 | end 285 | 286 | function createBootloaderResourceRows(resourceDataBatch) 287 | local rowsLength = #gui.rows 288 | 289 | for i = 1, #resourceDataBatch do 290 | local data = resourceDataBatch[i] 291 | 292 | -- Trim string values 293 | for key, value in pairs(data) do 294 | if type(value) == "string" then 295 | value = utf8.trim(value) 296 | 297 | if value == "" then 298 | data[key] = false 299 | else 300 | data[key] = value 301 | end 302 | end 303 | end 304 | 305 | -- Create table data for row 306 | local row = { 307 | data = data, 308 | filter = generateResourceDataFilter(data), 309 | sortableValue = utf8.fold(data.name), 310 | widths = {}, 311 | gui = {}, 312 | } 313 | 314 | -- Create row gui 315 | row.gui.status = guiCreateStaticImage(0, 0, 20, 20, "status.png", false, gui.scrollpane) 316 | guiSetEnabled(row.gui.status, false) 317 | 318 | if data.running then 319 | guiStaticImageSetColor(row.gui.status, 100, 255, 100, 255) 320 | else 321 | guiStaticImageSetColor(row.gui.status, 100, 100, 100, 255) 322 | end 323 | 324 | row.gui.checkbox = guiCreateCheckBox(0, 0, 0, 20, "", data.enabled, false, gui.scrollpane) 325 | 326 | row.gui.name = guiCreateLabel(0, 0, 0, 20, data.name, false, gui.scrollpane) 327 | guiSetEnabled(row.gui.name, false) 328 | 329 | row.gui.description = guiCreateLabel(0, 0, 0, 20, data.description or "-", false, gui.scrollpane) 330 | guiSetEnabled(row.gui.description, false) 331 | 332 | -- Calculate column widths 333 | row.widths.name = guiLabelGetTextExtent(row.gui.name) 334 | guiSetSize(row.gui.name, row.widths.name, 20, false) 335 | 336 | row.widths.description = guiLabelGetTextExtent(row.gui.description) 337 | guiSetSize(row.gui.description, row.widths.description, 20, false) 338 | 339 | -- Store row references 340 | gui.rows[i + rowsLength] = row 341 | gui.checkboxes[row.gui.checkbox] = row 342 | gui.resources[data.name] = row 343 | end 344 | end 345 | 346 | function sortBootloaderResourceRows() 347 | table.sort(gui.rows, function (rowA, rowB) 348 | return rowA.sortableValue < rowB.sortableValue 349 | end) 350 | end 351 | 352 | function updateBootloaderScrollpaneLayout() 353 | local visible_length = 0 354 | 355 | local column_width = { 356 | [1] = 55, 357 | [2] = 100, 358 | [3] = 200, 359 | } 360 | 361 | for i = 1, #gui.rows do 362 | local row = gui.rows[i] 363 | row.visible = true 364 | 365 | if gui.filters[1] then 366 | for i = 1, #gui.filters do 367 | local filter = gui.filters[i] 368 | local matched = (utf8.find(row.filter, filter.text, 1, true) ~= nil) 369 | 370 | if filter.negation then 371 | matched = not matched 372 | end 373 | 374 | row.visible = matched 375 | 376 | if not row.visible then 377 | break 378 | end 379 | end 380 | end 381 | 382 | if row.visible then 383 | visible_length = visible_length + 1 384 | 385 | local r, g, b = 255, 255, 100 386 | 387 | if (visible_length % 2) == 0 then 388 | r, g, b = 255, 255, 170 389 | end 390 | 391 | row.rgb = { r, g, b } 392 | 393 | if not row.data.running then 394 | r = r * 0.5 395 | g = g * 0.5 396 | b = b * 0.5 397 | end 398 | 399 | guiLabelSetColor(row.gui.name, r, g, b) 400 | guiLabelSetColor(row.gui.description, r, g, b) 401 | 402 | column_width[2] = math.max(column_width[2], row.widths.name) 403 | column_width[3] = math.max(column_width[3], row.widths.description) 404 | 405 | -- Apply the real width to the labels 406 | guiSetSize(row.gui.name, row.widths.name, 20, false) 407 | guiSetSize(row.gui.description, row.widths.description, 20, false) 408 | 409 | guiSetSize(row.gui.status, 20, 20, false) 410 | else 411 | -- Reset the size of the labels because the scrollbars inside the scrollpane 412 | -- include invisible items in the scrollbar-visibility calculation 413 | guiSetSize(row.gui.name, 0, 0, false) 414 | guiSetSize(row.gui.description, 0, 0, false) 415 | end 416 | 417 | for name, element in pairs(row.gui) do 418 | guiSetVisible(element, row.visible) 419 | 420 | if not row.visible then 421 | guiSetPosition(element, 0, 0, false) 422 | end 423 | end 424 | end 425 | 426 | local column_position = {} 427 | column_position[1] = 0 428 | column_position[2] = column_position[1] + column_width[1] 429 | column_position[3] = 10 + column_position[2] + column_width[2] 430 | 431 | for i = 1, 3 do 432 | local header = gui.headers[i] 433 | local px, py = guiGetPosition(header, false) 434 | guiSetPosition(header, column_position[i] + 20, py, false) 435 | end 436 | 437 | if visible_length == 0 then 438 | guiSetPosition(gui.scrollpadding, 0, 0, false) 439 | return 440 | end 441 | 442 | local y = 0 443 | 444 | for i = 1, #gui.rows do 445 | local row = gui.rows[i] 446 | 447 | if row.visible then 448 | guiSetPosition(row.gui.status, 0, y, false) 449 | guiSetPosition(row.gui.checkbox, 30, y, false) 450 | guiSetPosition(row.gui.name, column_position[2], y, false) 451 | guiSetPosition(row.gui.description, column_position[3], y, false) 452 | 453 | guiSetSize(row.gui.checkbox, column_width[2] + 25, 20, false) 454 | 455 | y = y + 20 456 | end 457 | end 458 | 459 | local paddingX = 25 + column_position[3] + column_width[3] 460 | local paddingY = y + 20 461 | guiSetPosition(gui.scrollpadding, paddingX, paddingY, false) 462 | end 463 | 464 | function processBootloaderResourceData(resourceName, isEnabled, isRunning) 465 | if gui.window == nil then 466 | return 467 | end 468 | 469 | local row = gui.resources[resourceName] 470 | 471 | if row == nil then 472 | return 473 | end 474 | 475 | updateBootloaderResourceRow(row, isEnabled, isRunning) 476 | end 477 | 478 | function updateBootloaderResourceRow(row, isEnabled, isRunning) 479 | row.data.enabled = isEnabled 480 | row.data.running = isRunning 481 | row.filter = generateResourceDataFilter(row.data) 482 | guiCheckBoxSetSelected(row.gui.checkbox, isEnabled) 483 | guiSetEnabled(row.gui.checkbox, true) 484 | 485 | local r, g, b = unpack(row.rgb) 486 | 487 | if isRunning then 488 | guiStaticImageSetColor(row.gui.status, 100, 255, 100, 255) 489 | else 490 | guiStaticImageSetColor(row.gui.status, 100, 100, 100, 255) 491 | 492 | r = r * 0.5 493 | g = g * 0.5 494 | b = b * 0.5 495 | end 496 | 497 | guiLabelSetColor(row.gui.name, r, g, b) 498 | guiLabelSetColor(row.gui.description, r, g, b) 499 | end 500 | 501 | -------------------------------------------------------------------------------- 502 | -- Utility and extension functions 503 | -------------------------------------------------------------------------------- 504 | function utf8.trim(self) 505 | assert(type(self) == "string", "expected string at argument 1, got ".. type(self)) 506 | local from = utf8.match(self, "^%s*()") 507 | return from > utf8.len(self) and "" or utf8.match(self, ".*%S", from) 508 | end 509 | 510 | function guiStaticImageSetColor(guiElement, r, g, b, a) 511 | local c = ("%02X%02X%02X%02X"):format(a, r, g, b) 512 | local value = ("tl:%s tr:%s bl:%s br:%s"):format(c, c, c, c) 513 | guiSetProperty(guiElement, "ImageColours", value) 514 | end 515 | 516 | function guiStaticImageSetVerticalBackground(guiElement, r1, g1, b1, a1, r2, g2, b2, a2) 517 | local c1 = ("%02X%02X%02X%02X"):format(a1, r1, g1, b1) 518 | local c2 = ("%02X%02X%02X%02X"):format(a2, r2, g2, b2) 519 | local value = ("tl:%s tr:%s bl:%s br:%s"):format(c1, c1, c2, c2) 520 | guiSetProperty(guiElement, "ImageColours", value) 521 | end 522 | 523 | function guiForceSetAlpha(element, alpha) 524 | guiSetProperty(element, "InheritsAlpha", "False") 525 | guiSetAlpha(element, alpha) 526 | end 527 | -------------------------------------------------------------------------------- /dot.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/botder/mtasa-bootloader/4d386d7a36e0865e92ff289ce88ebac50ee9ed4f/dot.bmp -------------------------------------------------------------------------------- /meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |