├── .gitignore ├── LICENSE ├── README.md └── lua ├── menu ├── menu.lua ├── menu_plugins.lua └── plugin_bootstrapper │ ├── config_store.lua │ ├── lib │ ├── markdown.lua │ └── von.lua │ ├── load_shiz.lua │ ├── md_panel.lua │ ├── menu_button.lua │ ├── menup_command.lua │ ├── plugins_panel.lua │ ├── plugins_window.lua │ └── tooltip_delay.lua └── menu_plugins ├── gradient_bg.lua ├── loading_customizer.lua ├── manual_blacklist.lua ├── menu_dev.lua ├── mpr_update_check.lua └── pling.lua /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !README.md 4 | !LICENSE 5 | !/lua 6 | !/lua/menu_plugins 7 | !/lua/menu_plugins/gradient_bg.lua 8 | !/lua/menu_plugins/pling.lua 9 | !/lua/menu_plugins/mpr_update_check.lua 10 | !/lua/menu_plugins/menu_dev.lua 11 | !/lua/menu_plugins/loading_customizer.lua 12 | !/lua/menu_plugins/manual_blacklist.lua 13 | !/lua/menu 14 | !/lua/menu/menu.lua 15 | !/lua/menu/menu_plugins.lua 16 | !/lua/menu/plugin_bootstrapper 17 | !/lua/menu/plugin_bootstrapper/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 djsime1 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Menu Plugins Redux 4 | 5 |
6 |

7 | 👉 Garry's mod addons, for the main menu. 👈 8 |

9 |
10 |

✨ Features

11 | 20 |
21 |

📥 Installation

22 | 💭 You can click on each step for more details.

23 |
24 | 🌐 Recommended: Switch to the x64-86 beta branch. 25 | Why? Because it makes most things work smoother. It can also increase your games performance! 26 |
    27 |
  1. In Steam, open Garry's Mod properties (Located in the gear icon.)
  2. 28 |
  3. Go to the Betas tab (Click "Betas" on the left side)
  4. 29 |
  5. From the dropdown, select the option starting with "x86-64" (Not the one starting with "Chromium.")
  6. 30 |
31 |
32 |
33 | 1️⃣ Find your Garry's Mod installation directory and open it. 34 |
35 |
36 |
37 | 2️⃣ Download this repository and locate the lua directory. 38 |
39 | 40 | 41 | 42 | 43 | 46 | 49 | 50 |

Select a version

44 | Stable
45 |

47 | Beta
48 |

51 |
52 |
53 | 3️⃣ Drag and drop the lua directory into the garrysmod directory. 54 |
55 |
56 |
57 | 4️⃣ Overwrite files if prompted. 58 |
59 |
60 |
61 | 👍 Launch the game! 62 | If everything was successful, then MPR should now be installed and active!
63 | Assuming you're on the x86-64 branch, there should be a new button on the default menu toolbar named "Plugins."
64 | There should also be a fancy banner in your console. Running the command menup gui will open a GUI.
65 | More commands can be found by running the menup command. 66 |
67 |
68 | 🎆 Find more plugins. 69 | While MPR ships with a few plugins of its own, you can install any compatible plugin.
70 | Check out this page to find some more public plugins!
71 | To install them, drag and drop the plugin's Lua file into the menu_plugins folder in your garrysmod/lua directoy. 72 |
73 |
74 |

👨‍💻 Development & Contributing

75 | Currently, I'm working along this roadmap.
76 | For information on writing a Redux plugin, check out the wiki.
77 | Also look at the included plugins to understand how they are laid out.
78 | Looking to contribute to MPR? Pull requests are welcome!
79 | Need ideas? Grab some low hanging fruit by fixing one of the open issues, or progress the roadmap. 80 | -------------------------------------------------------------------------------- /lua/menu/menu.lua: -------------------------------------------------------------------------------- 1 | 2 | include( "mount/mount.lua" ) 3 | include( "getmaps.lua" ) 4 | include( "loading.lua" ) 5 | include( "menu_plugins.lua" ) 6 | include( "mainmenu.lua" ) 7 | include( "video.lua" ) 8 | include( "demo_to_video.lua" ) 9 | 10 | include( "menu_save.lua" ) 11 | include( "menu_demo.lua" ) 12 | include( "menu_addon.lua" ) 13 | include( "menu_dupe.lua" ) 14 | include( "errors.lua" ) 15 | include( "problems/problems.lua" ) 16 | 17 | include( "motionsensor.lua" ) 18 | include( "util.lua" ) -------------------------------------------------------------------------------- /lua/menu/menu_plugins.lua: -------------------------------------------------------------------------------- 1 | _G.menup = {} 2 | menup.version = "0.2.2" -- used to check for updates 3 | menup.source = "https://raw.githubusercontent.com/djsime1/menu-plugins-redux/main/lua/menu/menu_plugins.lua" -- link to a file with the version string above 4 | menup.changelog = [[ 5 | - Added menu button on 32-bit GMod (If this crashes, [open an issue!](https://github.com/djsime1/menu-plugins-redux/issues)) 6 | - Added current changelogs in about page. 7 | - Added initalization timer to plugins that auto-load. 8 | - Added `ShutDown` hook for when the player quits the game. 9 | - Added Custom Server Blacklist plugin. 10 | - Added Loading Screen Modifier plugin. 11 | - Changed error message when `menu_plugins` folder doesn't exist. 12 | - Changed config menu to fix an annoyance. 13 | - Updated "Find more" tab with new public repo link. 14 | - Updated the Update Checker to be slower (for a good reason). 15 | - Fixed Background Customizer in-game text. 16 | - Fixed Update Checker to handle HTTP errors. 17 | - Reduced the chance of menu button crashing on 64-bit GMod. 18 | 19 | *Previous changelog:* 20 | - Implimented PR #1875 from the garrysmod repo. (Tooltip delays) 21 | - Added helper text to all stock plugins. 22 | - Added tick mark to currently selected config items. 23 | - Added loading screen customizer. 24 | - Fixed grammar in Background Customizer and Pling. 25 | - Changed loading to gather all manifests before running the plugins for dependency checking. 26 | ]] 27 | 28 | local splash = [[ 29 | +-----------------------------------------------------------+ 30 | __ __ ___ _ 31 | | \/ |___ _ _ _ _ | _ \ |_ _ __ _(_)_ _ ___ 32 | | |\/| / -_) ' \ || | | _/ | || / _` | | ' \(_-< 33 | |_| |_\___|_||_\_,_| |_| |_|\_,_\__, |_|_||_/__/ 34 | |___/ 35 | ______ ______ _____ __ __ __ __ 36 | /\ == \ /\ ___\ /\ __-. /\ \/\ \ /\_\_\_\ 37 | \ \ __< \ \ __\ \ \ \/\ \ \ \ \_\ \ \/_/\_\/_ 38 | \ \_\ \_\ \ \_____\ \ \____- \ \_____\ /\_\/\_\ 39 | \/_/ /_/ \/_____/ \/____/ \/_____/ \/_/\/_/ 40 | 41 | +-----------------------------------------------------------+ 42 | ]] 43 | 44 | local l, c1, c2 = 0, Color(0, 195, 255):ToVector() , Color(255, 255, 28):ToVector() 45 | for i = 1, #splash do 46 | if splash[i] == "\n" then 47 | l = 0 48 | MsgC("\n") 49 | else 50 | l = l + 1 51 | local cvec = LerpVector(l / 61, c1, c2) -- if Color:ToVector exists in menu, then why not Vector:ToColor?? 52 | MsgC(Color(cvec.x * 255, cvec.y * 255, cvec.z * 255), splash[i]) 53 | end 54 | end 55 | 56 | MsgN() 57 | include("plugin_bootstrapper/tooltip_delay.lua") 58 | include("plugin_bootstrapper/md_panel.lua") 59 | include("plugin_bootstrapper/plugins_panel.lua") 60 | include("plugin_bootstrapper/plugins_window.lua") 61 | include("plugin_bootstrapper/menu_button.lua") 62 | include("plugin_bootstrapper/config_store.lua") 63 | include("plugin_bootstrapper/load_shiz.lua") 64 | include("plugin_bootstrapper/menup_command.lua") -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/config_store.lua: -------------------------------------------------------------------------------- 1 | local writeq = {} 2 | local delq = {} 3 | local von = include("lib/von.lua") 4 | menup.von = von 5 | 6 | if not sql.TableExists("menup_redux") then 7 | print("Creating SQL table.") 8 | sql.Query("CREATE TABLE IF NOT EXISTS menup_redux ( key TEXT NOT NULL PRIMARY KEY, value TEXT );") 9 | end 10 | 11 | local function dbget(key, default) 12 | return delq[key] and nil or writeq[key] or sql.QueryValue("SELECT value FROM menup_redux WHERE key = " .. SQLStr(key)) or default 13 | end 14 | 15 | local function dbset(key, value) 16 | writeq[key] = value 17 | end 18 | 19 | local function dbdel(key) 20 | delq[key] = true 21 | end 22 | 23 | local function dblist(query) 24 | return sql.Query("SELECT * FROM menup_redux WHERE key LIKE " .. SQLStr(query) .. "") 25 | end 26 | 27 | local function process() 28 | if table.IsEmpty(writeq) and table.IsEmpty(delq) then return end 29 | sql.Begin() 30 | 31 | for k, v in pairs(writeq) do 32 | sql.Query("INSERT OR REPLACE INTO menup_redux (key, value) VALUES ( " .. SQLStr(k) .. ", " .. SQLStr(v) .. " )") 33 | end 34 | 35 | for k, _ in pairs(delq) do 36 | sql.Query("DELETE FROM menup_redux WHERE key = " .. SQLStr(k)) 37 | end 38 | 39 | sql.Commit() 40 | table.Empty(writeq) 41 | table.Empty(delq) 42 | end 43 | 44 | menup.config = {} 45 | 46 | menup.config.set = function(id, key, value) 47 | local data = util.JSONToTable(dbget("data_" .. id, [[{"config": "{}", "store": ""}]])) 48 | 49 | if isstring(data.config) then 50 | data.config = util.JSONToTable(data.config) 51 | end 52 | 53 | local old = data.config[key] 54 | hook.Run("ConfigChange", id, key, value, old) 55 | data.config[key] = value 56 | dbset("data_" .. id, util.TableToJSON(data, false)) 57 | end 58 | 59 | menup.config.get = function(id, key, value) 60 | local data = util.JSONToTable(dbget("data_" .. id, [[{"config": "{}", "store": ""}]])) 61 | 62 | if isstring(data.config) then 63 | data.config = util.JSONToTable(data.config) 64 | end 65 | 66 | if key == nil then 67 | return data.config 68 | elseif data.config[key] == nil then 69 | return value 70 | else 71 | return data.config[key] 72 | end 73 | end 74 | 75 | menup.store = {} 76 | 77 | menup.store.set = function(id, str) 78 | local data = util.JSONToTable(dbget("data_" .. id, [[{"config": "{}", "store": ""}]])) 79 | data.store = str 80 | dbset("data_" .. id, util.TableToJSON(data, false)) 81 | end 82 | 83 | menup.store.get = function(id, default) 84 | local data = util.JSONToTable(dbget("data_" .. id, [[{"config": "{}", "store": ""}]])) 85 | 86 | return data.store or default 87 | end 88 | 89 | menup.options = {} -- compatiblity layer for menup.config 90 | 91 | function menup.options.addOption(id, key, default) 92 | id = "legacy." .. id 93 | local val = menup.config.get(id, key, default) 94 | menup.plugins[id].config[key] = val 95 | menup.config.set(id, key, val) 96 | end 97 | 98 | function menup.options.getOption(id, key, default) 99 | return menup.config.get("legacy." .. id, key, default) 100 | end 101 | 102 | function menup.options.setOption(id, key, value) 103 | menup.config.set("legacy." .. id, key, value) 104 | end 105 | 106 | function menup.options.getTable() 107 | local res = dblist("data_legacy.%") 108 | if not res then return {} end 109 | local out = {} 110 | 111 | for _, v in ipairs(res) do 112 | out[string.sub(v.key, 13)] = util.JSONToTable(v.value).config 113 | end 114 | 115 | return out 116 | end 117 | 118 | function menup.include(path) 119 | return include("menu_plugins/" .. path) 120 | end 121 | 122 | menup.db = {} -- please dont use this in a plugin 123 | menup.db.get = dbget 124 | menup.db.set = dbset 125 | menup.db.del = dbdel 126 | menup.db.list = dblist 127 | timer.Create("menup_db", 1, 0, process) -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/lib/markdown.lua: -------------------------------------------------------------------------------- 1 | -- Modified from https://github.com/mpeterv/markdown, MIT Licensed. 2 | -- Used to render README's and about/credits page. 3 | ---------------------------------------------------------------------- 4 | -- Utility functions 5 | ---------------------------------------------------------------------- 6 | -- Returns the result of mapping the values in table t through the function f 7 | local function map(t, f) 8 | local out = {} 9 | 10 | for k, v in pairs(t) do 11 | out[k] = f(v, k) 12 | end 13 | 14 | return out 15 | end 16 | 17 | -- Functional style if statement. (NOTE: no short circuit evaluation) 18 | local function iff(t, a, b) 19 | if t then 20 | return a 21 | else 22 | return b 23 | end 24 | end 25 | 26 | -- Splits the text into an array of separate lines. 27 | local function split(text, sep) 28 | sep = sep or "\n" 29 | local lines = {} 30 | local pos = 1 31 | 32 | while true do 33 | local b, e = text:find(sep, pos) 34 | 35 | if not b then 36 | table.insert(lines, text:sub(pos)) 37 | break 38 | end 39 | 40 | table.insert(lines, text:sub(pos, b - 1)) 41 | pos = e + 1 42 | end 43 | 44 | return lines 45 | end 46 | 47 | -- Converts tabs to spaces 48 | local function detab(text) 49 | local tab_width = 4 50 | 51 | local function rep(match) 52 | local spaces = -match:len() 53 | 54 | while spaces < 1 do 55 | spaces = spaces + tab_width 56 | end 57 | 58 | return match .. string.rep(" ", spaces) 59 | end 60 | 61 | text = text:gsub("([^\n]-)\t", rep) 62 | 63 | return text 64 | end 65 | 66 | -- Applies string.find for every pattern in the list and returns the first match 67 | local function find_first(s, patterns, index) 68 | local res = {} 69 | 70 | for _, p in ipairs(patterns) do 71 | local match = {s:find(p, index)} 72 | 73 | if #match > 0 and (#res == 0 or match[1] < res[1]) then 74 | res = match 75 | end 76 | end 77 | 78 | return unpack(res) 79 | end 80 | 81 | -- If a replacement array is specified, the range [start, stop] in the array is replaced 82 | -- with the replacement array and the resulting array is returned. Without a replacement 83 | -- array the section of the array between start and stop is returned. 84 | local function splice(array, start, stop, replacement) 85 | if replacement then 86 | local n = stop - start + 1 87 | 88 | while n > 0 do 89 | table.remove(array, start) 90 | n = n - 1 91 | end 92 | 93 | for _, v in ipairs(replacement) do 94 | table.insert(array, start, v) 95 | end 96 | 97 | return array 98 | else 99 | local res = {} 100 | 101 | for i = start, stop do 102 | table.insert(res, array[i]) 103 | end 104 | 105 | return res 106 | end 107 | end 108 | 109 | -- Outdents the text one step. 110 | local function outdent(text) 111 | text = "\n" .. text 112 | text = text:gsub("\n ? ? ?", "\n") 113 | text = text:sub(2) 114 | 115 | return text 116 | end 117 | 118 | -- Indents the text one step. 119 | local function indent(text) 120 | text = text:gsub("\n", "\n ") 121 | 122 | return text 123 | end 124 | 125 | -- Does a simple tokenization of html data. Returns the data as a list of tokens. 126 | -- Each token is a table with a type field (which is either "tag" or "text") and 127 | -- a text field (which contains the original token data). 128 | local function tokenize_html(html) 129 | local tokens = {} 130 | local pos = 1 131 | 132 | while true do 133 | local start = find_first(html, {"", start) 155 | elseif html:match("^<%?", start) then 156 | _, stop = html:find("?>", start) 157 | else 158 | _, stop = html:find("%b<>", start) 159 | end 160 | 161 | if not stop then 162 | -- error("Could not match html tag " .. html:sub(start,start+30)) 163 | table.insert(tokens, { 164 | type = "text", 165 | text = html:sub(start, start) 166 | }) 167 | 168 | pos = start + 1 169 | else 170 | table.insert(tokens, { 171 | type = "tag", 172 | text = html:sub(start, stop) 173 | }) 174 | 175 | pos = stop + 1 176 | end 177 | end 178 | 179 | return tokens 180 | end 181 | 182 | ---------------------------------------------------------------------- 183 | -- Hash 184 | ---------------------------------------------------------------------- 185 | -- This is used to "hash" data into alphanumeric strings that are unique 186 | -- in the document. (Note that this is not cryptographic hash, the hash 187 | -- function is not one-way.) The hash procedure is used to protect parts 188 | -- of the document from further processing. 189 | local HASH = { 190 | -- Has the hash been inited. 191 | inited = false, 192 | -- The unique string prepended to all hash values. This is to ensure -- that hash values do not accidently coincide with an actual existing -- string in the document. 193 | identifier = "", 194 | -- Counter that counts up for each new hash instance. 195 | counter = 0, 196 | -- Hash table. 197 | table = {} 198 | } 199 | 200 | -- Inits hashing. Creates a hash_identifier that doesn't occur anywhere 201 | -- in the text. 202 | local function init_hash(text) 203 | HASH.inited = true 204 | HASH.identifier = "" 205 | HASH.counter = 0 206 | HASH.table = {} 207 | local s = "HASH" 208 | local counter = 0 209 | local id 210 | 211 | while true do 212 | id = s .. counter 213 | if not text:find(id, 1, true) then break end 214 | counter = counter + 1 215 | end 216 | 217 | HASH.identifier = id 218 | end 219 | 220 | -- Returns the hashed value for s. 221 | local function hash(s) 222 | assert(HASH.inited) 223 | 224 | if not HASH.table[s] then 225 | HASH.counter = HASH.counter + 1 226 | local id = HASH.identifier .. HASH.counter .. "X" 227 | HASH.table[s] = id 228 | end 229 | 230 | return HASH.table[s] 231 | end 232 | 233 | ---------------------------------------------------------------------- 234 | -- Protection 235 | ---------------------------------------------------------------------- 236 | -- The protection module is used to "protect" parts of a document 237 | -- so that they are not modified by subsequent processing steps. 238 | -- Protected parts are saved in a table for later unprotection 239 | -- Protection data 240 | local PD = { 241 | -- Saved blocks that have been converted 242 | blocks = {}, 243 | -- Block level tags that will be protected 244 | tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset", "iframe", "math", "ins", "del"} 245 | } 246 | 247 | -- Pattern for matching a block tag that begins and ends in the leftmost 248 | -- column and may contain indented subtags, i.e. 249 | --
250 | -- A nested block. 251 | --
252 | -- Nested data. 253 | --
254 | --
255 | local function block_pattern(tag) 256 | return "\n<" .. tag .. ".-\n[ \t]*\n" 257 | end 258 | 259 | -- Pattern for matching a block tag that begins and ends with a newline 260 | local function line_pattern(tag) 261 | return "\n<" .. tag .. ".-[ \t]*\n" 262 | end 263 | 264 | -- Protects the range of characters from start to stop in the text and 265 | -- returns the protected string. 266 | local function protect_range(text, start, stop) 267 | local s = text:sub(start, stop) 268 | local h = hash(s) 269 | PD.blocks[h] = s 270 | text = text:sub(1, start) .. h .. text:sub(stop) 271 | 272 | return text 273 | end 274 | 275 | -- Protect every part of the text that matches any of the patterns. The first 276 | -- matching pattern is protected first, etc. 277 | local function protect_matches(text, patterns) 278 | while true do 279 | local start, stop = find_first(text, patterns) 280 | if not start then break end 281 | text = protect_range(text, start, stop) 282 | end 283 | 284 | return text 285 | end 286 | 287 | -- Protects blocklevel tags in the specified text 288 | local function protect(text) 289 | -- First protect potentially nested block tags 290 | text = protect_matches(text, map(PD.tags, block_pattern)) 291 | -- Then protect block tags at the line level. 292 | text = protect_matches(text, map(PD.tags, line_pattern)) 293 | 294 | -- Protect
and comment tags 295 | text = protect_matches(text, {"\n]->[ \t]*\n"}) 296 | 297 | text = protect_matches(text, {"\n[ \t]*\n"}) 298 | 299 | return text 300 | end 301 | 302 | -- Returns true if the string s is a hash resulting from protection 303 | local function is_protected(s) 304 | return PD.blocks[s] 305 | end 306 | 307 | -- Unprotects the specified text by expanding all the nonces 308 | local function unprotect(text) 309 | for k, v in pairs(PD.blocks) do 310 | v = v:gsub("%%", "%%%%") 311 | text = text:gsub(k, v) 312 | end 313 | 314 | return text 315 | end 316 | 317 | ---------------------------------------------------------------------- 318 | -- Block transform 319 | ---------------------------------------------------------------------- 320 | -- The block transform functions transform the text on the block level. 321 | -- They work with the text as an array of lines rather than as individual 322 | -- characters. 323 | -- Returns true if the line is a ruler of (char) characters. 324 | -- The line must contain at least three char characters and contain only spaces and 325 | -- char characters. 326 | local function is_ruler_of(line, char) 327 | if not line:match("^[ %" .. char .. "]*$") then return false end 328 | if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end 329 | 330 | return true 331 | end 332 | 333 | -- Identifies the block level formatting present in the line 334 | local function classify(line) 335 | local info = { 336 | line = line, 337 | text = line 338 | } 339 | 340 | if line:match("^ ") then 341 | info.type = "indented" 342 | info.outdented = line:sub(5) 343 | 344 | return info 345 | end 346 | 347 | for _, c in ipairs({'*', '-', '_', '='}) do 348 | if is_ruler_of(line, c) then 349 | info.type = "ruler" 350 | info.ruler_char = c 351 | 352 | return info 353 | end 354 | end 355 | 356 | if line == "" then 357 | info.type = "blank" 358 | 359 | return info 360 | end 361 | 362 | if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then 363 | local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") 364 | info.type = "header" 365 | info.level = m1:len() 366 | info.text = m2 367 | 368 | return info 369 | end 370 | 371 | if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then 372 | local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") 373 | info.type = "list_item" 374 | info.list_type = "numeric" 375 | info.number = 0 + number 376 | info.text = text 377 | 378 | return info 379 | end 380 | 381 | if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then 382 | local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") 383 | info.type = "list_item" 384 | info.list_type = "bullet" 385 | info.bullet = bullet 386 | info.text = text 387 | 388 | return info 389 | end 390 | 391 | if line:match("^>[ \t]?(.*)") then 392 | info.type = "blockquote" 393 | info.text = line:match("^>[ \t]?(.*)") 394 | 395 | return info 396 | end 397 | 398 | if is_protected(line) then 399 | info.type = "raw" 400 | info.html = unprotect(line) 401 | 402 | return info 403 | end 404 | 405 | info.type = "normal" 406 | 407 | return info 408 | end 409 | 410 | -- Find headers constisting of a normal line followed by a ruler and converts them to 411 | -- header entries. 412 | local function headers(array) 413 | local i = 1 414 | 415 | while i <= #array - 1 do 416 | if array[i].type == "normal" and array[i + 1].type == "ruler" and (array[i + 1].ruler_char == "-" or array[i + 1].ruler_char == "=") then 417 | local info = { 418 | line = array[i].line 419 | } 420 | 421 | info.text = info.line 422 | info.type = "header" 423 | info.level = iff(array[i + 1].ruler_char == "=", 1, 2) 424 | table.remove(array, i + 1) 425 | array[i] = info 426 | end 427 | 428 | i = i + 1 429 | end 430 | 431 | return array 432 | end 433 | 434 | -- Forward declarations 435 | local block_transform, span_transform, encode_code 436 | 437 | -- Convert lines to html code 438 | local function blocks_to_html(lines, no_paragraphs) 439 | local out = {} 440 | local i = 1 441 | 442 | while i <= #lines do 443 | local line = lines[i] 444 | 445 | if line.type == "ruler" then 446 | table.insert(out, "
") 447 | elseif line.type == "raw" then 448 | table.insert(out, line.html) 449 | elseif line.type == "normal" then 450 | local s = line.line 451 | 452 | while i + 1 <= #lines and lines[i + 1].type == "normal" do 453 | i = i + 1 454 | s = s .. "\n" .. lines[i].line 455 | end 456 | 457 | if no_paragraphs then 458 | table.insert(out, span_transform(s)) 459 | else 460 | table.insert(out, "

" .. span_transform(s) .. "

") 461 | end 462 | elseif line.type == "header" then 463 | local s = "" .. span_transform(line.text) .. "" 464 | table.insert(out, s) 465 | else 466 | table.insert(out, line.line) 467 | end 468 | 469 | i = i + 1 470 | end 471 | 472 | return out 473 | end 474 | 475 | -- Find list blocks and convert them to protected data blocks 476 | local function lists(array, sublist) 477 | local function process_list(arr) 478 | local function any_blanks(arr) 479 | for i = 1, #arr do 480 | if arr[i].type == "blank" then return true end 481 | end 482 | 483 | return false 484 | end 485 | 486 | local function split_list_items(arr) 487 | local acc = {arr[1]} 488 | 489 | local res = {} 490 | 491 | for i = 2, #arr do 492 | if arr[i].type == "list_item" then 493 | table.insert(res, acc) 494 | 495 | acc = {arr[i]} 496 | else 497 | table.insert(acc, arr[i]) 498 | end 499 | end 500 | 501 | table.insert(res, acc) 502 | 503 | return res 504 | end 505 | 506 | local function process_list_item(lines, block) 507 | while lines[#lines].type == "blank" do 508 | table.remove(lines) 509 | end 510 | 511 | local itemtext = lines[1].text 512 | 513 | for i = 2, #lines do 514 | itemtext = itemtext .. "\n" .. outdent(lines[i].line) 515 | end 516 | 517 | if block then 518 | itemtext = block_transform(itemtext, true) 519 | 520 | if not itemtext:find("
") then
 521 |                     itemtext = indent(itemtext)
 522 |                 end
 523 | 
 524 |                 return "    
  • " .. itemtext .. "
  • " 525 | else 526 | local lines = split(itemtext) 527 | lines = map(lines, classify) 528 | lines = lists(lines, true) 529 | lines = blocks_to_html(lines, true) 530 | itemtext = table.concat(lines, "\n") 531 | 532 | if not itemtext:find("
    ") then
     533 |                     itemtext = indent(itemtext)
     534 |                 end
     535 | 
     536 |                 return "    
  • " .. itemtext .. "
  • " 537 | end 538 | end 539 | 540 | local block_list = any_blanks(arr) 541 | local items = split_list_items(arr) 542 | local out = "" 543 | 544 | for _, item in ipairs(items) do 545 | out = out .. process_list_item(item, block_list) .. "\n" 546 | end 547 | 548 | if arr[1].list_type == "numeric" then 549 | return "
      \n" .. out .. "
    " 550 | else 551 | return "
      \n" .. out .. "
    " 552 | end 553 | end 554 | 555 | -- Finds the range of lines composing the first list in the array. A list 556 | -- starts with (^ list_item) or (blank list_item) and ends with 557 | -- (blank* $) or (blank normal). 558 | -- 559 | -- A sublist can start with just (list_item) does not need a blank... 560 | local function find_list(array, sublist) 561 | local function find_list_start(array, sublist) 562 | if array[1].type == "list_item" then return 1 end 563 | 564 | if sublist then 565 | for i = 1, #array do 566 | if array[i].type == "list_item" then return i end 567 | end 568 | else 569 | for i = 1, #array - 1 do 570 | if array[i].type == "blank" and array[i + 1].type == "list_item" then return i + 1 end 571 | end 572 | end 573 | 574 | return nil 575 | end 576 | 577 | local function find_list_end(array, start) 578 | local pos = #array 579 | 580 | for i = start, #array - 1 do 581 | if array[i].type == "blank" and array[i + 1].type ~= "list_item" and array[i + 1].type ~= "indented" and array[i + 1].type ~= "blank" then 582 | pos = i - 1 583 | break 584 | end 585 | end 586 | 587 | while pos > start and array[pos].type == "blank" do 588 | pos = pos - 1 589 | end 590 | 591 | return pos 592 | end 593 | 594 | local start = find_list_start(array, sublist) 595 | if not start then return nil end 596 | 597 | return start, find_list_end(array, start) 598 | end 599 | 600 | while true do 601 | local start, stop = find_list(array, sublist) 602 | if not start then break end 603 | local text = process_list(splice(array, start, stop)) 604 | 605 | local info = { 606 | line = text, 607 | type = "raw", 608 | html = text 609 | } 610 | 611 | array = splice(array, start, stop, {info}) 612 | end 613 | 614 | -- Convert any remaining list items to normal 615 | for _, line in ipairs(array) do 616 | if line.type == "list_item" then 617 | line.type = "normal" 618 | end 619 | end 620 | 621 | return array 622 | end 623 | 624 | -- Find and convert blockquote markers. 625 | local function blockquotes(lines) 626 | local function find_blockquote(lines) 627 | local start 628 | 629 | for i, line in ipairs(lines) do 630 | if line.type == "blockquote" then 631 | start = i 632 | break 633 | end 634 | end 635 | 636 | if not start then return nil end 637 | local stop = #lines 638 | 639 | for i = start + 1, #lines do 640 | if lines[i].type == "blank" or lines[i].type == "blockquote" then 641 | elseif lines[i].type == "normal" then 642 | if lines[i - 1].type == "blank" then 643 | stop = i - 1 644 | break 645 | end 646 | else 647 | stop = i - 1 648 | break 649 | end 650 | end 651 | 652 | while lines[stop].type == "blank" do 653 | stop = stop - 1 654 | end 655 | 656 | return start, stop 657 | end 658 | 659 | local function process_blockquote(lines) 660 | local raw = lines[1].text 661 | 662 | for i = 2, #lines do 663 | raw = raw .. "\n" .. lines[i].text 664 | end 665 | 666 | local bt = block_transform(raw) 667 | 668 | if not bt:find("
    ") then
     669 |             bt = indent(bt)
     670 |         end
     671 | 
     672 |         return "
    \n " .. bt .. "\n
    " 673 | end 674 | 675 | while true do 676 | local start, stop = find_blockquote(lines) 677 | if not start then break end 678 | local text = process_blockquote(splice(lines, start, stop)) 679 | 680 | local info = { 681 | line = text, 682 | type = "raw", 683 | html = text 684 | } 685 | 686 | lines = splice(lines, start, stop, {info}) 687 | end 688 | 689 | return lines 690 | end 691 | 692 | -- Find and convert codeblocks. 693 | local function codeblocks(lines) 694 | local function find_codeblock(lines) 695 | local start 696 | 697 | for i, line in ipairs(lines) do 698 | if line.type == "indented" then 699 | start = i 700 | break 701 | end 702 | end 703 | 704 | if not start then return nil end 705 | local stop = #lines 706 | 707 | for i = start + 1, #lines do 708 | if lines[i].type ~= "indented" and lines[i].type ~= "blank" then 709 | stop = i - 1 710 | break 711 | end 712 | end 713 | 714 | while lines[stop].type == "blank" do 715 | stop = stop - 1 716 | end 717 | 718 | return start, stop 719 | end 720 | 721 | local function process_codeblock(lines) 722 | local raw = detab(encode_code(outdent(lines[1].line))) 723 | 724 | for i = 2, #lines do 725 | raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) 726 | end 727 | 728 | return "
    " .. raw .. "\n
    " 729 | end 730 | 731 | while true do 732 | local start, stop = find_codeblock(lines) 733 | if not start then break end 734 | local text = process_codeblock(splice(lines, start, stop)) 735 | 736 | local info = { 737 | line = text, 738 | type = "raw", 739 | html = text 740 | } 741 | 742 | lines = splice(lines, start, stop, {info}) 743 | end 744 | 745 | return lines 746 | end 747 | 748 | -- Perform all the block level transforms 749 | function block_transform(text, sublist) 750 | local lines = split(text) 751 | lines = map(lines, classify) 752 | lines = headers(lines) 753 | lines = lists(lines, sublist) 754 | lines = codeblocks(lines) 755 | lines = blockquotes(lines) 756 | lines = blocks_to_html(lines) 757 | local text = table.concat(lines, "\n") 758 | 759 | return text 760 | end 761 | 762 | ---------------------------------------------------------------------- 763 | -- Span transform 764 | ---------------------------------------------------------------------- 765 | -- Functions for transforming the text at the span level. 766 | -- These characters may need to be escaped because they have a special 767 | -- meaning in markdown. 768 | local escape_chars = "'\\`*_{}[]()>#+-.!'" 769 | local escape_table = {} 770 | 771 | local function init_escape_table() 772 | escape_table = {} 773 | 774 | for i = 1, #escape_chars do 775 | local c = escape_chars:sub(i, i) 776 | escape_table[c] = hash(c) 777 | end 778 | end 779 | 780 | -- Adds a new escape to the escape table. 781 | local function add_escape(text) 782 | if not escape_table[text] then 783 | escape_table[text] = hash(text) 784 | end 785 | 786 | return escape_table[text] 787 | end 788 | 789 | -- Encode backspace-escaped characters in the markdown source. 790 | local function encode_backslash_escapes(t) 791 | for i = 1, escape_chars:len() do 792 | local c = escape_chars:sub(i, i) 793 | t = t:gsub("\\%" .. c, escape_table[c]) 794 | end 795 | 796 | return t 797 | end 798 | 799 | -- Escape characters that should not be disturbed by markdown. 800 | local function escape_special_chars(text) 801 | local tokens = tokenize_html(text) 802 | local out = "" 803 | 804 | for _, token in ipairs(tokens) do 805 | local t = token.text 806 | 807 | if token.type == "tag" then 808 | -- In tags, encode * and _ so they don't conflict with their use in markdown. 809 | t = t:gsub("%*", escape_table["*"]) 810 | t = t:gsub("%_", escape_table["_"]) 811 | else 812 | t = encode_backslash_escapes(t) 813 | end 814 | 815 | out = out .. t 816 | end 817 | 818 | return out 819 | end 820 | 821 | -- Unescape characters that have been encoded. 822 | local function unescape_special_chars(t) 823 | local tin = t 824 | 825 | for k, v in pairs(escape_table) do 826 | k = k:gsub("%%", "%%%%") 827 | t = t:gsub(v, k) 828 | end 829 | 830 | if t ~= tin then 831 | t = unescape_special_chars(t) 832 | end 833 | 834 | return t 835 | end 836 | 837 | -- Encode/escape certain characters inside Markdown code runs. 838 | -- The point is that in code, these characters are literals, 839 | -- and lose their special Markdown meanings. 840 | function encode_code(s) 841 | s = s:gsub("%&", "&") 842 | s = s:gsub("<", "<") 843 | s = s:gsub(">", ">") 844 | 845 | for k, v in pairs(escape_table) do 846 | s = s:gsub("%" .. k, v) 847 | end 848 | 849 | return s 850 | end 851 | 852 | -- Handle backtick blocks. 853 | local function code_spans(s) 854 | s = s:gsub("\\\\", escape_table["\\"]) 855 | s = s:gsub("\\`", escape_table["`"]) 856 | local pos = 1 857 | 858 | while true do 859 | local start, stop = s:find("`+", pos) 860 | if not start then return s end 861 | local count = stop - start + 1 862 | -- Find a matching numbert of backticks 863 | local estart, estop = s:find(string.rep("`", count), stop + 1) 864 | local brstart = s:find("\n", stop + 1) 865 | 866 | if estart and (not brstart or estart < brstart) then 867 | local code = s:sub(stop + 1, estart - 1) 868 | code = code:gsub("^[ \t]+", "") 869 | code = code:gsub("[ \t]+$", "") 870 | code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) 871 | code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) 872 | code = "" .. encode_code(code) .. "" 873 | code = add_escape(code) 874 | s = s:sub(1, start - 1) .. code .. s:sub(estop + 1) 875 | pos = start + code:len() 876 | else 877 | pos = stop + 1 878 | end 879 | end 880 | 881 | return s 882 | end 883 | 884 | -- Encode alt text... enodes &, and ". 885 | local function encode_alt(s) 886 | if not s then return s end 887 | s = s:gsub('&', '&') 888 | s = s:gsub('"', '"') 889 | s = s:gsub('<', '<') 890 | 891 | return s 892 | end 893 | 894 | -- Forward declaration for link_db as returned by strip_link_definitions. 895 | local link_database 896 | 897 | -- Handle image references 898 | local function images(text) 899 | local function reference_link(alt, id) 900 | alt = encode_alt(alt:match("%b[]"):sub(2, -2)) 901 | id = id:match("%[(.*)%]"):lower() 902 | 903 | if id == "" then 904 | id = text:lower() 905 | end 906 | 907 | link_database[id] = link_database[id] or {} 908 | if not link_database[id].url then return nil end 909 | local url = link_database[id].url or id 910 | url = encode_alt(url) 911 | local title = encode_alt(link_database[id].title) 912 | 913 | if title then 914 | title = " title=\"" .. title .. "\"" 915 | else 916 | title = "" 917 | end 918 | 919 | return add_escape('' .. alt .. '") 920 | end 921 | 922 | local function inline_link(alt, link) 923 | alt = encode_alt(alt:match("%b[]"):sub(2, -2)) 924 | local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") 925 | url = url or link:match("%(?%)") 926 | url = encode_alt(url) 927 | title = encode_alt(title) 928 | 929 | if title then 930 | return add_escape('' .. alt .. '') 931 | else 932 | return add_escape('' .. alt .. '') 933 | end 934 | end 935 | 936 | text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) 937 | text = text:gsub("!(%b[])(%b())", inline_link) 938 | 939 | return text 940 | end 941 | 942 | -- Handle anchor references 943 | local function anchors(text) 944 | local function reference_link(text, id) 945 | text = text:match("%b[]"):sub(2, -2) 946 | id = id:match("%b[]"):sub(2, -2):lower() 947 | 948 | if id == "" then 949 | id = text:lower() 950 | end 951 | 952 | link_database[id] = link_database[id] or {} 953 | if not link_database[id].url then return nil end 954 | local url = link_database[id].url or id 955 | url = encode_alt(url) 956 | local title = encode_alt(link_database[id].title) 957 | 958 | if title then 959 | title = " title=\"" .. title .. "\"" 960 | else 961 | title = "" 962 | end 963 | 964 | return add_escape("") .. text .. add_escape("") 965 | end 966 | 967 | local function inline_link(text, link) 968 | text = text:match("%b[]"):sub(2, -2) 969 | local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") 970 | title = encode_alt(title) 971 | url = url or link:match("%(?%)") or "" 972 | url = encode_alt(url) 973 | 974 | if title then 975 | return add_escape("") .. text .. "" 976 | else 977 | return add_escape("") .. text .. add_escape("") 978 | end 979 | end 980 | 981 | text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) 982 | text = text:gsub("(%b[])(%b())", inline_link) 983 | 984 | return text 985 | end 986 | 987 | -- Handle auto links, i.e. . 988 | local function auto_links(text) 989 | local function link(s) 990 | return add_escape("") .. s .. "" 991 | end 992 | 993 | -- Encode chars as a mix of dec and hex entitites to (perhaps) fool 994 | -- spambots. 995 | local function encode_email_address(s) 996 | -- Use a deterministic encoding to make unit testing possible. 997 | -- Code 45% hex, 45% dec, 10% plain. 998 | local hex = { 999 | code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, 1000 | count = 1, 1001 | rate = 0.45 1002 | } 1003 | 1004 | local dec = { 1005 | code = function(c) return "&#" .. c:byte() .. ";" end, 1006 | count = 0, 1007 | rate = 0.45 1008 | } 1009 | 1010 | local plain = { 1011 | code = function(c) return c end, 1012 | count = 0, 1013 | rate = 0.1 1014 | } 1015 | 1016 | local codes = {hex, dec, plain} 1017 | 1018 | local function swap(t, k1, k2) 1019 | local temp = t[k2] 1020 | t[k2] = t[k1] 1021 | t[k1] = temp 1022 | end 1023 | 1024 | local out = "" 1025 | 1026 | for i = 1, s:len() do 1027 | for _, code in ipairs(codes) do 1028 | code.count = code.count + code.rate 1029 | end 1030 | 1031 | if codes[1].count < codes[2].count then 1032 | swap(codes, 1, 2) 1033 | end 1034 | 1035 | if codes[2].count < codes[3].count then 1036 | swap(codes, 2, 3) 1037 | end 1038 | 1039 | if codes[1].count < codes[2].count then 1040 | swap(codes, 1, 2) 1041 | end 1042 | 1043 | local code = codes[1] 1044 | local c = s:sub(i, i) 1045 | 1046 | -- Force encoding of "@" to make email address more invisible. 1047 | if c == "@" and code == plain then 1048 | code = codes[2] 1049 | end 1050 | 1051 | out = out .. code.code(c) 1052 | code.count = code.count - 1 1053 | end 1054 | 1055 | return out 1056 | end 1057 | 1058 | local function mail(s) 1059 | s = unescape_special_chars(s) 1060 | local address = encode_email_address("mailto:" .. s) 1061 | local text = encode_email_address(s) 1062 | 1063 | return add_escape("") .. text .. "" 1064 | end 1065 | 1066 | -- links 1067 | text = text:gsub("<(https?:[^'\">%s]+)>", link) 1068 | text = text:gsub("<(ftp:[^'\">%s]+)>", link) 1069 | -- mail 1070 | text = text:gsub("%s]+)>", mail) 1071 | text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) 1072 | 1073 | return text 1074 | end 1075 | 1076 | -- Encode free standing amps (&) and angles (<)... note that this does not 1077 | -- encode free >. 1078 | local function amps_and_angles(s) 1079 | -- encode amps not part of &..; expression 1080 | local pos = 1 1081 | 1082 | while true do 1083 | local amp = s:find("&", pos) 1084 | if not amp then break end 1085 | local semi = s:find(";", amp + 1) 1086 | local stop = s:find("[ \t\n&]", amp + 1) 1087 | 1088 | if not semi or (stop and stop < semi) or (semi - amp) > 15 then 1089 | s = s:sub(1, amp - 1) .. "&" .. s:sub(amp + 1) 1090 | pos = amp + 1 1091 | else 1092 | pos = amp + 1 1093 | end 1094 | end 1095 | 1096 | -- encode naked <'s 1097 | s = s:gsub("<([^a-zA-Z/?$!])", "<%1") 1098 | s = s:gsub("<$", "<") 1099 | -- what about >, nothing done in the original markdown source to handle them 1100 | 1101 | return s 1102 | end 1103 | 1104 | -- Handles emphasis markers (* and _) in the text. 1105 | local function emphasis(text) 1106 | for _, s in ipairs{"%*%*", "%_%_"} do 1107 | text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") 1108 | text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") 1109 | end 1110 | 1111 | for _, s in ipairs{"%*", "%_"} do 1112 | text = text:gsub(s .. "([^%s_])" .. s, "%1") 1113 | text = text:gsub(s .. "([^%s_])" .. s, "%1") 1114 | text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") 1115 | text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") 1116 | end 1117 | 1118 | return text 1119 | end 1120 | 1121 | -- Handles line break markers in the text. 1122 | local function line_breaks(text) 1123 | return text:gsub(" +\n", "
    \n") 1124 | end 1125 | 1126 | -- Perform all span level transforms. 1127 | function span_transform(text) 1128 | text = code_spans(text) 1129 | text = escape_special_chars(text) 1130 | text = images(text) 1131 | text = anchors(text) 1132 | text = auto_links(text) 1133 | text = amps_and_angles(text) 1134 | text = emphasis(text) 1135 | text = line_breaks(text) 1136 | 1137 | return text 1138 | end 1139 | 1140 | ---------------------------------------------------------------------- 1141 | -- Markdown 1142 | ---------------------------------------------------------------------- 1143 | -- Cleanup the text by normalizing some possible variations to make further 1144 | -- processing easier. 1145 | local function cleanup(text) 1146 | -- Standardize line endings 1147 | text = text:gsub("\r\n", "\n") -- DOS to UNIX 1148 | text = text:gsub("\r", "\n") -- Mac to UNIX 1149 | -- Convert all tabs to spaces 1150 | text = detab(text) 1151 | 1152 | -- Strip lines with only spaces and tabs 1153 | while true do 1154 | local subs 1155 | text, subs = text:gsub("\n[ \t]+\n", "\n\n") 1156 | if subs == 0 then break end 1157 | end 1158 | 1159 | return "\n" .. text .. "\n" 1160 | end 1161 | 1162 | -- Strips link definitions from the text and stores the data in a lookup table. 1163 | local function strip_link_definitions(text) 1164 | local linkdb = {} 1165 | 1166 | local function link_def(id, url, title) 1167 | id = id:match("%[(.+)%]"):lower() 1168 | linkdb[id] = linkdb[id] or {} 1169 | linkdb[id].url = url or linkdb[id].url 1170 | linkdb[id].title = title or linkdb[id].title 1171 | 1172 | return "" 1173 | end 1174 | 1175 | local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" 1176 | local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" 1177 | local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" 1178 | local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" 1179 | text = text:gsub(def_title1, link_def) 1180 | text = text:gsub(def_title2, link_def) 1181 | text = text:gsub(def_title3, link_def) 1182 | text = text:gsub(def_no_title, link_def) 1183 | 1184 | return text, linkdb 1185 | end 1186 | 1187 | -- Main markdown processing function 1188 | local function markdown(text) 1189 | init_hash(text) 1190 | init_escape_table() 1191 | text = cleanup(text) 1192 | text = protect(text) 1193 | text, link_database = strip_link_definitions(text) 1194 | text = block_transform(text) 1195 | text = unescape_special_chars(text) 1196 | 1197 | return text 1198 | end 1199 | 1200 | ---------------------------------------------------------------------- 1201 | -- End of module 1202 | ---------------------------------------------------------------------- 1203 | 1204 | return markdown -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/lib/von.lua: -------------------------------------------------------------------------------- 1 | --[[ vON 1.3.4 2 | 3 | Copyright 2012-2014 Alexandru-Mihai Maftei 4 | aka Vercas 5 | 6 | GitHub Repository: 7 | https://github.com/vercas/vON 8 | 9 | You may use this for any purpose as long as: 10 | - You don't remove this copyright notice. 11 | - You don't claim this to be your own. 12 | - You properly credit the author (Vercas) if you publish your work based on (and/or using) this. 13 | 14 | If you modify the code for any purpose, the above obligations still apply. 15 | If you make any interesting modifications, try forking the GitHub repository instead. 16 | 17 | Instead of copying this code over for sharing, rather use the link: 18 | https://github.com/vercas/vON/blob/master/von.lua 19 | 20 | The author may not be held responsible for any damage or losses directly or indirectly caused by 21 | the use of vON. 22 | 23 | If you disagree with the above, don't use the code. 24 | 25 | ----------------------------------------------------------------------------------------------------------------------------- 26 | 27 | Thanks to the following people for their contribution: 28 | - Divran Suggested improvements for making the code quicker. 29 | Suggested an excellent new way of deserializing strings. 30 | Lead me to finding an extreme flaw in string parsing. 31 | - pennerlord Provided some performance tests to help me improve the code. 32 | - Chessnut Reported bug with handling of nil values when deserializing array components. 33 | 34 | - People who contributed on the GitHub repository by reporting bugs, posting fixes, etc. 35 | 36 | ----------------------------------------------------------------------------------------------------------------------------- 37 | 38 | The vanilla types supported in this release of vON are: 39 | - table 40 | - number 41 | - boolean 42 | - string 43 | - nil 44 | 45 | The Garry's Mod-specific types supported in this release are: 46 | - Vector 47 | - Angle 48 | + Entities: 49 | - Entity 50 | - Vehicle 51 | - Weapon 52 | - NPC 53 | - Player 54 | - NextBot 55 | 56 | These are the types one would normally serialize. 57 | 58 | ----------------------------------------------------------------------------------------------------------------------------- 59 | 60 | New in this version: 61 | - Fixed addition of extra entity types. I messed up really badly. 62 | --]] 63 | 64 | 65 | 66 | local _deserialize, _serialize, _d_meta, _s_meta, d_findVariable, s_anyVariable 67 | local sub, gsub, find, insert, concat, error, tonumber, tostring, type, next = string.sub, string.gsub, string.find, table.insert, table.concat, error, tonumber, tostring, type, next 68 | 69 | 70 | 71 | --[[ This section contains localized functions which (de)serialize 72 | variables according to the types found. ]] 73 | 74 | 75 | 76 | -- This is kept away from the table for speed. 77 | function d_findVariable(s, i, len, lastType, jobstate) 78 | local i, c, typeRead, val = i or 1 79 | 80 | -- Keep looping through the string. 81 | while true do 82 | -- Stop at the end. Throw an error. This function MUST NOT meet the end! 83 | if i > len then 84 | error("vON: Reached end of string, cannot form proper variable.") 85 | end 86 | 87 | -- Cache the character. Nobody wants to look for the same character ten times. 88 | c = sub(s, i, i) 89 | 90 | -- If it just read a type definition, then a variable HAS to come after it. 91 | if typeRead then 92 | -- Attempt to deserialize a variable of the freshly read type. 93 | val, i = _deserialize[lastType](s, i, len, false, jobstate) 94 | -- Return the value read, the index of the last processed character, and the type of the last read variable. 95 | return val, i, lastType 96 | 97 | -- @ means nil. It should not even appear in the output string of the serializer. Nils are useless to store. 98 | elseif c == "@" then 99 | return nil, i, lastType 100 | 101 | -- $ means a table reference will follow - a number basically. 102 | elseif c == "$" then 103 | lastType = "table_reference" 104 | typeRead = true 105 | 106 | -- n means a number will follow. Base 10... :C 107 | elseif c == "n" then 108 | lastType = "number" 109 | typeRead = true 110 | 111 | -- b means boolean flags. 112 | elseif c == "b" then 113 | lastType = "boolean" 114 | typeRead = true 115 | 116 | -- ' means the start of a string. 117 | elseif c == "'" then 118 | lastType = "string" 119 | typeRead = true 120 | 121 | -- " means the start of a string prior to version 1.2.0. 122 | elseif c == "\"" then 123 | lastType = "oldstring" 124 | typeRead = true 125 | 126 | -- { means the start of a table! 127 | elseif c == "{" then 128 | lastType = "table" 129 | typeRead = true 130 | 131 | 132 | --[[ Garry's Mod types go here ]] 133 | 134 | -- e means an entity ID will follow. 135 | elseif c == "e" then 136 | lastType = "Entity" 137 | typeRead = true 138 | --[[ 139 | -- c means a vehicle ID will follow. 140 | elseif c == "c" then 141 | lastType = "Vehicle" 142 | typeRead = true 143 | 144 | -- w means a weapon entity ID will follow. 145 | elseif c == "w" then 146 | lastType = "Weapon" 147 | typeRead = true 148 | 149 | -- x means a NPC ID will follow. 150 | elseif c == "x" then 151 | lastType = "NPC" 152 | typeRead = true 153 | --]] 154 | -- p means a player ID will follow. 155 | -- Kept for backwards compatibility. 156 | elseif c == "p" then 157 | lastType = "Entity" 158 | typeRead = true 159 | 160 | -- v means a vector will follow. 3 numbers. 161 | elseif c == "v" then 162 | lastType = "Vector" 163 | typeRead = true 164 | 165 | -- a means an Euler angle will follow. 3 numbers. 166 | elseif c == "a" then 167 | lastType = "Angle" 168 | typeRead = true 169 | 170 | --[[ Garry's Mod types end here ]] 171 | 172 | 173 | -- If no type has been found, attempt to deserialize the last type read. 174 | elseif lastType then 175 | val, i = _deserialize[lastType](s, i, len, false, jobstate) 176 | return val, i, lastType 177 | 178 | -- This will occur if the very first character in the vON code is wrong. 179 | else 180 | error("vON: Malformed data... Can't find a proper type definition. Char#" .. i .. ":" .. c) 181 | end 182 | 183 | -- Move the pointer one step forward. 184 | i = i + 1 185 | end 186 | end 187 | 188 | -- This is kept away from the table for speed. 189 | -- Yeah, ton of parameters. 190 | function s_anyVariable(data, lastType, isNumeric, isKey, isLast, jobstate) 191 | local tp = type(data) 192 | 193 | if jobstate[1] and jobstate[2][data] then 194 | tp = "table_reference" 195 | end 196 | 197 | -- Basically, if the type changes. 198 | if lastType ~= tp then 199 | -- Remember the new type. Caching the type is useless. 200 | lastType = tp 201 | 202 | if _serialize[lastType] then 203 | -- Return the serialized data and the (new) last type. 204 | -- The second argument, which is true now, means that the data type was just changed. 205 | return _serialize[lastType](data, true, isNumeric, isKey, isLast, false, jobstate), lastType 206 | else 207 | error("vON: No serializer defined for type \"" .. lastType .. "\"!") 208 | end 209 | end 210 | 211 | -- Otherwise, simply serialize the data. 212 | return _serialize[lastType](data, false, isNumeric, isKey, isLast, false, jobstate), lastType 213 | end 214 | 215 | 216 | 217 | --[[ This section contains the tables with the functions necessary 218 | for decoding basic Lua data types. ]] 219 | 220 | 221 | 222 | _deserialize = { 223 | -- Well, tables are very loose... 224 | -- The first table doesn't have to begin and end with { and }. 225 | ["table"] = function(s, i, len, unnecessaryEnd, jobstate) 226 | local ret, numeric, i, c, lastType, val, ind, expectValue, key = {}, true, i or 1, nil, nil, nil, 1 227 | -- Locals, locals, locals, locals, locals, locals, locals, locals and locals. 228 | 229 | if sub(s, i, i) == "#" then 230 | local e = find(s, "#", i + 2, true) 231 | 232 | if e then 233 | local id = tonumber(sub(s, i + 1, e - 1)) 234 | 235 | if id then 236 | if jobstate[1][id] and not jobstate[2] then 237 | error("vON: There already is a table of reference #" .. id .. "! Missing an option maybe?") 238 | end 239 | 240 | jobstate[1][id] = ret 241 | 242 | i = e + 1 243 | else 244 | error("vON: Malformed table! Reference ID starting at char #" .. i .. " doesn't contain a number!") 245 | end 246 | else 247 | error("vON: Malformed table! Cannot find end of reference ID start at char #" .. i .. "!") 248 | end 249 | end 250 | 251 | -- Keep looping. 252 | while true do 253 | -- Until it meets the end. 254 | if i > len then 255 | -- Yeah, if the end is unnecessary, it won't spit an error. The main chunk doesn't require an end, for example. 256 | if unnecessaryEnd then 257 | return ret, i 258 | 259 | -- Otherwise, the data has to be damaged. 260 | else 261 | error("vON: Reached end of string, incomplete table definition.") 262 | end 263 | end 264 | 265 | -- Cache the character. 266 | c = sub(s, i, i) 267 | --print(i, "table char:", c, tostring(unnecessaryEnd)) 268 | 269 | -- If it's the end of a table definition, return. 270 | if c == "}" then 271 | return ret, i 272 | 273 | -- If it's the component separator, switch to key:value pairs. 274 | elseif c == "~" then 275 | numeric = false 276 | 277 | elseif c == ";" then 278 | -- Lol, nothing! 279 | -- Remenant from numbers, for faster parsing. 280 | 281 | -- OK, now, if it's on the numeric component, simply add everything encountered. 282 | elseif numeric then 283 | -- Find a variable and it's value 284 | val, i, lastType = d_findVariable(s, i, len, lastType, jobstate) 285 | -- Add it to the table. 286 | ret[ind] = val 287 | 288 | ind = ind + 1 289 | 290 | -- Otherwise, if it's the key:value component... 291 | else 292 | -- If a value is expected... 293 | if expectValue then 294 | -- Read it. 295 | val, i, lastType = d_findVariable(s, i, len, lastType, jobstate) 296 | -- Add it? 297 | ret[key] = val 298 | -- Clean up. 299 | expectValue, key = false, nil 300 | 301 | -- If it's the separator... 302 | elseif c == ":" then 303 | -- Expect a value next. 304 | expectValue = true 305 | 306 | -- But, if there's a key read already... 307 | elseif key then 308 | -- Then this is malformed. 309 | error("vON: Malformed table... Two keys declared successively? Char#" .. i .. ":" .. c) 310 | 311 | -- Otherwise the key will be read. 312 | else 313 | -- I love multi-return and multi-assignement. 314 | key, i, lastType = d_findVariable(s, i, len, lastType, jobstate) 315 | end 316 | end 317 | 318 | i = i + 1 319 | end 320 | 321 | return nil, i 322 | end, 323 | 324 | -- Just a number which points to a table. 325 | ["table_reference"] = function(s, i, len, unnecessaryEnd, jobstate) 326 | local i, a = i or 1 327 | -- Locals, locals, locals, locals 328 | 329 | a = find(s, "[;:}~]", i) 330 | 331 | if a then 332 | local n = tonumber(sub(s, i, a - 1)) 333 | 334 | if n then 335 | return jobstate[1][n] or error("vON: Table reference does not point to a (yet) known table!"), a - 1 336 | else 337 | error("vON: Table reference definition does not contain a valid number!") 338 | end 339 | end 340 | 341 | -- Using %D breaks identification of negative numbers. :( 342 | 343 | error("vON: Number definition started... Found no end.") 344 | end, 345 | 346 | 347 | -- Numbers are weakly defined. 348 | -- The declaration is not very explicit. It'll do it's best to parse the number. 349 | -- Has various endings: \n, }, ~, : and ;, some of which will force the table deserializer to go one char backwards. 350 | ["number"] = function(s, i, len, unnecessaryEnd, jobstate) 351 | local i, a = i or 1 352 | -- Locals, locals, locals, locals 353 | 354 | a = find(s, "[;:}~]", i) 355 | 356 | if a then 357 | return tonumber(sub(s, i, a - 1)) or error("vON: Number definition does not contain a valid number!"), a - 1 358 | end 359 | 360 | -- Using %D breaks identification of negative numbers. :( 361 | 362 | error("vON: Number definition started... Found no end.") 363 | end, 364 | 365 | 366 | -- A boolean is A SINGLE CHARACTER, either 1 for true or 0 for false. 367 | -- Any other attempt at boolean declaration will result in a failure. 368 | ["boolean"] = function(s, i, len, unnecessaryEnd, jobstate) 369 | local c = sub(s,i,i) 370 | -- Only one character is needed. 371 | 372 | -- If it's 1, then it's true 373 | if c == "1" then 374 | return true, i 375 | 376 | -- If it's 0, then it's false. 377 | elseif c == "0" then 378 | return false, i 379 | end 380 | 381 | -- Any other supposely "boolean" is just a sign of malformed data. 382 | error("vON: Invalid value on boolean type... Char#" .. i .. ": " .. c) 383 | end, 384 | 385 | 386 | -- Strings prior to 1.2.0 387 | ["oldstring"] = function(s, i, len, unnecessaryEnd, jobstate) 388 | local res, i, a = "", i or 1 389 | -- Locals, locals, locals, locals 390 | 391 | while true do 392 | a = find(s, "\"", i, true) 393 | 394 | if a then 395 | if sub(s, a - 1, a - 1) == "\\" then 396 | res = res .. sub(s, i, a - 2) .. "\"" 397 | i = a + 1 398 | else 399 | return res .. sub(s, i, a - 2), a 400 | end 401 | else 402 | error("vON: Old string definition started... Found no end.") 403 | end 404 | end 405 | end, 406 | 407 | -- Strings after 1.2.0 408 | ["string"] = function(s, i, len, unnecessaryEnd, jobstate) 409 | local res, i, a = "", i or 1 410 | -- Locals, locals, locals, locals 411 | 412 | while true do 413 | a = find(s, "\"", i, true) 414 | 415 | if a then 416 | if sub(s, a - 1, a - 1) == "\\" then 417 | res = res .. sub(s, i, a - 2) .. "\"" 418 | i = a + 1 419 | else 420 | return res .. sub(s, i, a - 1), a 421 | end 422 | else 423 | error("vON: String definition started... Found no end.") 424 | end 425 | end 426 | end, 427 | } 428 | 429 | 430 | 431 | _serialize = { 432 | -- Uh. Nothing to comment. 433 | -- Ton of parameters. 434 | -- Makes stuff faster than simply passing it around in locals. 435 | -- table.concat works better than normal concatenations WITH LARGE-ISH STRINGS ONLY. 436 | ["table"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 437 | --print(string.format("data: %s; mustInitiate: %s; isKey: %s; isLast: %s; nice: %s; indent: %s; first: %s", tostring(data), tostring(mustInitiate), tostring(isKey), tostring(isLast), tostring(nice), tostring(indent), tostring(first))) 438 | 439 | local result, keyvals, len, keyvalsLen, keyvalsProgress, val, lastType, newIndent, indentString = {}, {}, #data, 0, 0 440 | -- Locals, locals, locals, locals, locals, locals, locals, locals, locals and locals. 441 | 442 | -- First thing to be done is separate the numeric and key:value components of the given table in two tables. 443 | -- pairs(data) is slower than next, data as far as my tests tell me. 444 | for k, v in next, data do 445 | -- Skip the numeric keyz. 446 | if type(k) ~= "number" or k < 1 or k > len or (k % 1 ~= 0) then -- k % 1 == 0 is, as proven by personal benchmarks, 447 | keyvals[#keyvals + 1] = k -- the quickest way to check if a number is an integer. 448 | end -- k % 1 ~= 0 is the fastest way to check if a number 449 | end -- is NOT an integer. > is proven slower. 450 | 451 | keyvalsLen = #keyvals 452 | 453 | -- Main chunk - no initial character. 454 | if not first then 455 | result[#result + 1] = "{" 456 | end 457 | 458 | if jobstate[1] and jobstate[1][data] then 459 | if jobstate[2][data] then 460 | error("vON: Table #" .. jobstate[1][data] .. " written twice..?") 461 | end 462 | 463 | result[#result + 1] = "#" 464 | result[#result + 1] = jobstate[1][data] 465 | result[#result + 1] = "#" 466 | 467 | jobstate[2][data] = true 468 | end 469 | 470 | -- Add numeric values. 471 | if len > 0 then 472 | for i = 1, len do 473 | val, lastType = s_anyVariable(data[i], lastType, true, false, i == len and not first, jobstate) 474 | result[#result + 1] = val 475 | end 476 | end 477 | 478 | -- If there are key:value pairs. 479 | if keyvalsLen > 0 then 480 | -- Insert delimiter. 481 | result[#result + 1] = "~" 482 | 483 | -- Insert key:value pairs. 484 | for _i = 1, keyvalsLen do 485 | keyvalsProgress = keyvalsProgress + 1 486 | 487 | val, lastType = s_anyVariable(keyvals[_i], lastType, false, true, false, jobstate) 488 | 489 | result[#result + 1] = val..":" 490 | 491 | val, lastType = s_anyVariable(data[keyvals[_i]], lastType, false, false, keyvalsProgress == keyvalsLen and not first, jobstate) 492 | 493 | result[#result + 1] = val 494 | end 495 | end 496 | 497 | -- Main chunk needs no ending character. 498 | if not first then 499 | result[#result + 1] = "}" 500 | end 501 | 502 | return concat(result) 503 | end, 504 | 505 | -- Number which points to table. 506 | ["table_reference"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 507 | data = jobstate[1][data] 508 | 509 | -- If a number hasn't been written before, add the type prefix. 510 | if mustInitiate then 511 | if isKey or isLast then 512 | return "$"..data 513 | else 514 | return "$"..data..";" 515 | end 516 | end 517 | 518 | if isKey or isLast then 519 | return data 520 | else 521 | return data..";" 522 | end 523 | end, 524 | 525 | 526 | -- Normal concatenations is a lot faster with small strings than table.concat 527 | -- Also, not so branched-ish. 528 | ["number"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 529 | -- If a number hasn't been written before, add the type prefix. 530 | if mustInitiate then 531 | if isKey or isLast then 532 | return "n"..data 533 | else 534 | return "n"..data..";" 535 | end 536 | end 537 | 538 | if isKey or isLast then 539 | return data 540 | else 541 | return data..";" 542 | end 543 | end, 544 | 545 | 546 | -- I hope gsub is fast enough. 547 | ["string"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 548 | if sub(data, #data, #data) == "\\" then -- Hah, old strings fix this best. 549 | return "\"" .. gsub(data, "\"", "\\\"") .. "v\"" 550 | end 551 | 552 | return "'" .. gsub(data, "\"", "\\\"") .. "\"" 553 | end, 554 | 555 | 556 | -- Fastest. 557 | ["boolean"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 558 | -- Prefix if we must. 559 | if mustInitiate then 560 | if data then 561 | return "b1" 562 | else 563 | return "b0" 564 | end 565 | end 566 | 567 | if data then 568 | return "1" 569 | else 570 | return "0" 571 | end 572 | end, 573 | 574 | 575 | -- Fastest. 576 | ["nil"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 577 | return "@" 578 | end, 579 | } 580 | 581 | 582 | 583 | --[[ This section handles additions necessary for Garry's Mod. ]] 584 | 585 | 586 | 587 | if gmod then -- Luckily, a specific table named after the game is present in Garry's Mod. 588 | local Entity = Entity 589 | 590 | 591 | 592 | local extra_deserialize = { 593 | -- Entities are stored simply by the ID. They're meant to be transfered, not stored anyway. 594 | -- Exactly like a number definition, except it begins with "e". 595 | ["Entity"] = function(s, i, len, unnecessaryEnd, jobstate) 596 | local i, a = i or 1 597 | -- Locals, locals, locals, locals 598 | 599 | a = find(s, "[;:}~]", i) 600 | 601 | if a then 602 | return Entity(tonumber(sub(s, i, a - 1))), a - 1 603 | end 604 | 605 | error("vON: Entity ID definition started... Found no end.") 606 | end, 607 | 608 | 609 | -- A pair of 3 numbers separated by a comma (,). 610 | ["Vector"] = function(s, i, len, unnecessaryEnd, jobstate) 611 | local i, a, x, y, z = i or 1 612 | -- Locals, locals, locals, locals 613 | 614 | a = find(s, ",", i) 615 | 616 | if a then 617 | x = tonumber(sub(s, i, a - 1)) 618 | i = a + 1 619 | end 620 | 621 | a = find(s, ",", i) 622 | 623 | if a then 624 | y = tonumber(sub(s, i, a - 1)) 625 | i = a + 1 626 | end 627 | 628 | a = find(s, "[;:}~]", i) 629 | 630 | if a then 631 | z = tonumber(sub(s, i, a - 1)) 632 | end 633 | 634 | if x and y and z then 635 | return Vector(x, y, z), a - 1 636 | end 637 | 638 | error("vON: Vector definition started... Found no end.") 639 | end, 640 | 641 | 642 | -- A pair of 3 numbers separated by a comma (,). 643 | ["Angle"] = function(s, i, len, unnecessaryEnd, jobstate) 644 | local i, a, p, y, r = i or 1 645 | -- Locals, locals, locals, locals 646 | 647 | a = find(s, ",", i) 648 | 649 | if a then 650 | p = tonumber(sub(s, i, a - 1)) 651 | i = a + 1 652 | end 653 | 654 | a = find(s, ",", i) 655 | 656 | if a then 657 | y = tonumber(sub(s, i, a - 1)) 658 | i = a + 1 659 | end 660 | 661 | a = find(s, "[;:}~]", i) 662 | 663 | if a then 664 | r = tonumber(sub(s, i, a - 1)) 665 | end 666 | 667 | if p and y and r then 668 | return Angle(p, y, r), a - 1 669 | end 670 | 671 | error("vON: Angle definition started... Found no end.") 672 | end, 673 | } 674 | 675 | local extra_serialize = { 676 | -- Same as numbers, except they start with "e" instead of "n". 677 | ["Entity"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 678 | data = data:EntIndex() 679 | 680 | if mustInitiate then 681 | if isKey or isLast then 682 | return "e"..data 683 | else 684 | return "e"..data..";" 685 | end 686 | end 687 | 688 | if isKey or isLast then 689 | return data 690 | else 691 | return data..";" 692 | end 693 | end, 694 | 695 | 696 | -- 3 numbers separated by a comma. 697 | ["Vector"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 698 | if mustInitiate then 699 | if isKey or isLast then 700 | return "v"..data.x..","..data.y..","..data.z 701 | else 702 | return "v"..data.x..","..data.y..","..data.z..";" 703 | end 704 | end 705 | 706 | if isKey or isLast then 707 | return data.x..","..data.y..","..data.z 708 | else 709 | return data.x..","..data.y..","..data.z..";" 710 | end 711 | end, 712 | 713 | 714 | -- 3 numbers separated by a comma. 715 | ["Angle"] = function(data, mustInitiate, isNumeric, isKey, isLast, first, jobstate) 716 | if mustInitiate then 717 | if isKey or isLast then 718 | return "a"..data.p..","..data.y..","..data.r 719 | else 720 | return "a"..data.p..","..data.y..","..data.r..";" 721 | end 722 | end 723 | 724 | if isKey or isLast then 725 | return data.p..","..data.y..","..data.r 726 | else 727 | return data.p..","..data.y..","..data.r..";" 728 | end 729 | end, 730 | } 731 | 732 | for k, v in pairs(extra_serialize) do 733 | _serialize[k] = v 734 | end 735 | 736 | for k, v in pairs(extra_deserialize) do 737 | _deserialize[k] = v 738 | end 739 | 740 | local extraEntityTypes = { "Vehicle", "Weapon", "NPC", "Player", "NextBot" } 741 | 742 | for i = 1, #extraEntityTypes do 743 | _serialize[extraEntityTypes[i]] = _serialize.Entity 744 | end 745 | end 746 | 747 | 748 | 749 | --[[ This section exposes the functions of the library. ]] 750 | 751 | 752 | 753 | local function checkTableForRecursion(tab, checked, assoc) 754 | local id = checked.ID 755 | 756 | if not checked[tab] and not assoc[tab] then 757 | assoc[tab] = id 758 | checked.ID = id + 1 759 | else 760 | checked[tab] = true 761 | end 762 | 763 | for k, v in pairs(tab) do 764 | if type(k) == "table" and not checked[k] then 765 | checkTableForRecursion(k, checked, assoc) 766 | end 767 | 768 | if type(v) == "table" and not checked[v] then 769 | checkTableForRecursion(v, checked, assoc) 770 | end 771 | end 772 | end 773 | 774 | 775 | 776 | local _s_table = _serialize.table 777 | local _d_table = _deserialize.table 778 | 779 | _d_meta = { 780 | __call = function(self, str, allowIdRewriting) 781 | if type(str) == "string" then 782 | return _d_table(str, nil, #str, true, {{}, allowIdRewriting}) 783 | end 784 | 785 | error("vON: You must deserialize a string, not a "..type(str)) 786 | end 787 | } 788 | _s_meta = { 789 | __call = function(self, data, checkRecursion) 790 | if type(data) == "table" then 791 | if checkRecursion then 792 | local assoc, checked = {}, {ID = 1} 793 | 794 | checkTableForRecursion(data, checked, assoc) 795 | 796 | return _s_table(data, nil, nil, nil, nil, true, {assoc, {}}) 797 | end 798 | 799 | return _s_table(data, nil, nil, nil, nil, true, {false}) 800 | end 801 | 802 | error("vON: You must serialize a table, not a "..type(data)) 803 | end 804 | } 805 | 806 | 807 | 808 | von = { 809 | version = "1.3.4", 810 | versionNumber = 1003004, -- Reserving 3 digits per version component. 811 | 812 | deserialize = setmetatable(_deserialize,_d_meta), 813 | serialize = setmetatable(_serialize,_s_meta) 814 | } 815 | 816 | 817 | 818 | return von 819 | -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/load_shiz.lua: -------------------------------------------------------------------------------- 1 | -- call me crazy for how i do this 2 | menup.plugins = {} 3 | menup.control = {} 4 | local meta, env, manifest = {}, {}, nil 5 | local shouldload = util.JSONToTable(menup.db.get("enabled", "{}")) 6 | 7 | local manifest_default = { 8 | author = "Unknown", 9 | description = "Legacy addon.", 10 | version = "Legacy", 11 | config = {}, 12 | undo = function() end 13 | } 14 | 15 | -- local config_default = { 16 | -- bool = false, 17 | -- int = 0, 18 | -- float = 0, 19 | -- range = 0, 20 | -- string = "", 21 | -- select = "", 22 | -- } 23 | function meta:__call() 24 | end 25 | 26 | setmetatable(menup, meta) 27 | 28 | function env.menup(perhaps) 29 | manifest = perhaps 30 | end 31 | 32 | function menup.control.preload(func) 33 | setfenv(func, env) 34 | pcall(func) 35 | setfenv(func, _G) 36 | 37 | -- new plugin 38 | if istable(manifest) then 39 | manifest.config = manifest.config or {} 40 | manifest.legacy = false 41 | manifest.func = func 42 | else -- legacy plugin 43 | manifest = table.Copy(manifest_default) 44 | manifest.id = "legacy.unknown" 45 | manifest.name = "Unknown" 46 | manifest.config = {} 47 | manifest.legacy = true 48 | manifest.func = func 49 | end 50 | 51 | local temp = table.Copy(manifest) 52 | manifest = nil 53 | 54 | return temp 55 | end 56 | 57 | function menup.control.load(fileorfunc, name) 58 | if isfunction(fileorfunc) and not isstring(name) then 59 | error("Calling load with a function MUST also supply a name!") 60 | end 61 | 62 | if isstring(fileorfunc) then 63 | name = name or string.StripExtension(string.Right(fileorfunc, #fileorfunc - #string.GetPathFromFilename(fileorfunc))) 64 | local script = file.Read(fileorfunc, "GAME") 65 | 66 | if not script then 67 | error("File " .. fileorfunc .. " doesn't exist!\n(Is the path based from garrysmod?)") 68 | end 69 | 70 | fileorfunc = CompileString(script, fileorfunc, false) 71 | 72 | if isstring(fileorfunc) then 73 | error("Error loading plugin " .. name .. ":\n" .. fileorfunc) 74 | end 75 | end 76 | 77 | local temp = menup.control.preload(fileorfunc) 78 | 79 | if temp.legacy then 80 | temp.id = "legacy." .. name 81 | temp.name = name 82 | end 83 | 84 | hook.Run("PluginLoaded", temp) 85 | 86 | return temp 87 | end 88 | 89 | function menup.control.run(id) 90 | local manifest = menup.plugins[id] 91 | 92 | if not manifest.enabled then 93 | ErrorNoHalt("Running plugin " .. id .. " despite it being disabled.") 94 | end 95 | 96 | hook.Run("PluginRun", manifest) 97 | local success, result = pcall(manifest.func) 98 | 99 | if not success then 100 | ErrorNoHaltWithStack("Error running plugin " .. id .. ":\n" .. result) 101 | elseif isfunction(result) then 102 | manifest.undo = result 103 | else 104 | manifest.undo = nil -- function() end 105 | end 106 | 107 | return success 108 | end 109 | 110 | function menup.control.undo(id) 111 | local manifest = menup.plugins[id] 112 | 113 | if isfunction(manifest.undo) then 114 | hook.Run("PluginUndo", manifest) 115 | local success, result = pcall(manifest.undo) 116 | 117 | if not success then 118 | ErrorNoHaltWithStack("Error undoing plugin " .. id .. ":\n" .. result) 119 | 120 | return false 121 | else 122 | return result ~= nil and result or true 123 | end 124 | else 125 | return false 126 | end 127 | end 128 | 129 | function menup.control.shouldload(id, enabled) 130 | local shouldload = util.JSONToTable(menup.db.get("enabled", "{}")) 131 | enabled = enabled ~= nil and enabled or menup.plugins[id].enabled 132 | shouldload[id] = enabled 133 | menup.db.set("enabled", util.TableToJSON(shouldload, false)) 134 | end 135 | 136 | function menup.control.enable(id, save) 137 | local manifest = menup.plugins[id] 138 | 139 | if save == nil then 140 | save = true 141 | else 142 | save = false 143 | end 144 | 145 | if not istable(manifest) then 146 | error("Attempted to enable unregistered plugin " .. id .. ".") 147 | end 148 | 149 | manifest.enabled = true 150 | hook.Run("PluginEnabled", manifest) 151 | 152 | if save then 153 | menup.control.shouldload(id, true) 154 | end 155 | 156 | return menup.control.run(id) 157 | end 158 | 159 | function menup.control.disable(id, save) 160 | local manifest = menup.plugins[id] 161 | 162 | if save == nil then 163 | save = true 164 | else 165 | save = false 166 | end 167 | 168 | if not istable(manifest) then 169 | error("Attempted to disable unregistered plugin " .. id .. ".") 170 | end 171 | 172 | manifest.enabled = false 173 | hook.Run("PluginDisabled", manifest) 174 | 175 | if save then 176 | menup.control.shouldload(id, false) 177 | end 178 | 179 | return menup.control.undo(id) 180 | end 181 | 182 | if not file.IsDir("lua/menu_plugins", "GAME") then 183 | Derma_Message("You are missing the menu_plugins folder in your garrysmod lua directory! Please create the folder, then restart the game.", "Menu Plugins Redux", "OK") 184 | 185 | return error("Missing menu_plugins folder in garrysmod/lua!!") 186 | end 187 | 188 | local files = file.Find("lua/menu_plugins/*.lua", "GAME") 189 | local status = util.JSONToTable(menup.db.get("loadstatus", [[{"loading": "done", "blame": ""}]])) 190 | local start = SysTime() 191 | 192 | -- Experimental crash prevention, comment out to enable. 193 | status = {loading = "done"} 194 | 195 | local function LoadManifests() 196 | for _, v in ipairs(files) do 197 | if v == status.blame then continue end 198 | 199 | menup.db.set("loadstatus", util.TableToJSON({ 200 | loading = "manifest", 201 | blame = v 202 | }, false)) 203 | 204 | local res, ret = pcall(menup.control.load, "lua/menu_plugins/" .. v) 205 | if not res then continue end 206 | ret.file = v 207 | menup.plugins[ret.id] = ret 208 | end 209 | end 210 | 211 | local function LoadPlugins() 212 | for k, v in pairs(menup.plugins) do 213 | if v.file == status.blame then continue end 214 | menup.db.set("loadstatus", util.TableToJSON({ 215 | loading = "plugin", 216 | blame = v.file 217 | }, false)) 218 | if shouldload[k] == true then 219 | MsgC(Color(166, 166, 166), "| ", Color(255, 255, 255), string.format("%s (%s) is ", k, v.file), Color(0, 230, 118), "enabled.\n") 220 | local lstart = SysTime() 221 | menup.control.enable(k, false) 222 | v.initalization = math.Round((SysTime() - start) * 1000, 2) 223 | else 224 | MsgC(Color(166, 166, 166), "| ", Color(255, 255, 255), string.format("%s (%s) is ", k, v.file), Color(255, 23, 68), "disabled.\n") 225 | v.enabled = false 226 | shouldload[k] = false 227 | end 228 | end 229 | end 230 | 231 | local function LoadFull() 232 | MsgC(Color(166, 166, 166), "+ ", Color(41, 121, 255), "[MPR]", Color(255, 255, 255), " Now loading plugins...\n") 233 | start = SysTime() 234 | LoadManifests() 235 | LoadPlugins() 236 | menup.db.set("loadstatus", util.TableToJSON({ 237 | loading = "done", 238 | blame = "" 239 | }, false)) 240 | menup.db.set("enabled", util.TableToJSON(shouldload, false)) 241 | MsgC(Color(166, 166, 166), "+ ", Color(255, 255, 255), "Done! Plugins loaded in ", Color(41, 121, 255), tostring(math.Round((SysTime() - start) * 1000, 2)), Color(255, 255, 255), " milliseconds.\n") 242 | end 243 | 244 | if status.loading ~= "done" then 245 | Derma_Query(string.format("Last time plugins were loaded, the game crashed. The crash was likely caused by the following plugin: %s. What would you like to do?", status.blame), "Menu Plugins Redux", 246 | "Continue as normal", function() 247 | status.blame = "" 248 | LoadFull() 249 | end, "Disable " .. status.blame, function() 250 | shouldload[status.blame] = false 251 | LoadFull() 252 | end, "Don't load anything", function() 253 | end) 254 | return 255 | else 256 | LoadFull() 257 | end -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/md_panel.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | local markdown = include("lib/markdown.lua") 3 | 4 | function PANEL:Init() 5 | self.hashes = {} 6 | self.head = [[ 7 | 8 | 11 | 14 | 15 | ]] 16 | self:SetAllowLua(true) 17 | end 18 | 19 | function PANEL:OnDocumentReady() 20 | -- self:AddFunction("lua", "Callback", function(hash) 21 | -- if self.hashes[hash] ~= nil then 22 | -- self.hashes[hash]() 23 | -- else 24 | -- print("Unknown hash: " .. hash) 25 | -- end 26 | -- end) 27 | self:AddFunction("lua", "Open", function(url) 28 | gui.OpenURL(url) 29 | end) 30 | end 31 | 32 | function PANEL:SetBody(txt) 33 | self.body = txt 34 | self:SetHTML(self.head .. "\n" .. txt .. "\n") 35 | end 36 | 37 | function PANEL:SetMarkdown(txt) 38 | self.md = txt 39 | self:SetBody(markdown(txt)) 40 | end 41 | 42 | -- function PANEL:AddHash(title, callback) 43 | -- self.hashes[title] = callback 44 | -- end 45 | vgui.Register("MarkdownPanel", PANEL, "DHTML") 46 | menup.markdown = markdown -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/menu_button.lua: -------------------------------------------------------------------------------- 1 | menup.drawer = {} 2 | menup.drawer.buttons = {} 3 | 4 | function menup.drawer.add(id, title, cb, icon) 5 | menup.drawer.buttons[id] = {title, cb, icon} 6 | end 7 | 8 | function menup.drawer.del(id) 9 | menup.drawer.buttons[id] = nil 10 | end 11 | 12 | function menup.drawer.open(x, y) 13 | local dm = DermaMenu() 14 | 15 | for k, v in SortedPairs(menup.drawer.buttons) do 16 | local btn = dm:AddOption(v[1], v[2]) 17 | 18 | if v[3] then 19 | btn:SetIcon(v[3]) 20 | end 21 | end 22 | 23 | dm:AddSpacer() 24 | dm:AddOption("Manage plugins", ShowPluginsWindow):SetIcon("icon16/plugin_edit.png") 25 | 26 | if x and y then 27 | dm:Open(x, y) 28 | else 29 | dm:Open() 30 | end 31 | end 32 | 33 | menup.RGUIC = RunGameUICommand 34 | 35 | function RunGameUICommand(...) 36 | local args = {...} 37 | if string.lower(args[1]) == "quit" then 38 | hook.Run("ShutDown") 39 | timer.Simple(0, function() 40 | menup.RGUIC("quit") 41 | end) 42 | return 43 | end 44 | menup.RGUIC(...) 45 | end 46 | 47 | hook.Add("DrawOverlay", "menup_button", function() 48 | hook.Remove("DrawOverlay", "menup_button") 49 | pcall(hook.Run, "MenuVGUIReady") 50 | 51 | if IsValid(pnlMainMenu) and IsValid(pnlMainMenu.HTML) and vgui.GetControlTable("MainMenuPanel") then 52 | print("Pretty sure this is the default menu, injecting button!") 53 | pnlMainMenu.HTML:Call([[ 54 | var navright = document.getElementById("NavBar").getElementsByClassName("right")[0]; 55 | var container = document.createElement("span"); 56 | container.setAttribute("id", "PluginsButton") 57 | navright.appendChild(container); 58 | container.innerHTML = "
  • Plugins
  • " 59 | ]]) 60 | else 61 | print("Custom menu detected, open plugins window by running menu_plugins.") 62 | end 63 | end) 64 | 65 | -- concommand.Add("menup_drawer", menup.drawer.open) -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/menup_command.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | menup list 3 | menup enable 4 | menup disable 5 | menup gui 6 | menup drawer 7 | menup reload 8 | menup load 9 | ]] 10 | local function menup_list(filter) 11 | filter = table.concat(filter, " ", 2) 12 | MsgC(Color(41, 121, 255), "Loaded menu plugins:\n") 13 | 14 | for k, v in pairs(menup.plugins) do 15 | local p = menup.plugins[k] 16 | if filter and not (string.find(k, filter) or string.find(p.name, filter)) then continue end 17 | MsgC(Color(166, 166, 166), " - ", Color(255, 255, 255), string.format("%s (%s)", p.name, k), Color(166, 166, 166), " :: ") 18 | 19 | if p.enabled then 20 | MsgC(Color(0, 230, 118), "Enabled.\n") 21 | else 22 | MsgC(Color(255, 23, 68), "Disabled.\n") 23 | end 24 | end 25 | end 26 | 27 | local function menup_enable(id) 28 | id = table.concat(id, " ", 2) 29 | local p = menup.plugins[id] 30 | 31 | if not p then 32 | MsgC(Color(255, 234, 0), "Plugin not found.\n") 33 | elseif p.enabled then 34 | MsgC(Color(255, 234, 0), "Plugin already enabled.\n") 35 | else 36 | menup.control.enable(id) 37 | MsgC(Color(41, 121, 255), p.name, Color(255, 255, 255), " is now ", Color(0, 230, 118), "enabled.\n") 38 | end 39 | end 40 | 41 | local function menup_disable(id) 42 | id = table.concat(id, " ", 2) 43 | local p = menup.plugins[id] 44 | 45 | if not p then 46 | MsgC(Color(255, 234, 0), "Plugin not found.\n") 47 | elseif not p.enabled then 48 | MsgC(Color(255, 234, 0), "Plugin already disabled.\n") 49 | else 50 | menup.control.disable(id) 51 | MsgC(Color(41, 121, 255), p.name, Color(255, 255, 255), " is now ", Color(255, 23, 68), "disabled.\n") 52 | end 53 | end 54 | 55 | local function menup_restart(confirm) 56 | confirm = confirm[2] 57 | 58 | if confirm == "confirm" then 59 | MsgC(Color(41, 121, 255), "Restarting MPR...\n") 60 | 61 | for k, v in pairs(menup.plugins) do 62 | if v.enabled then 63 | menup.control.disable(k, false) 64 | end 65 | end 66 | 67 | CloseDermaMenus() 68 | 69 | if IsValid(PluginsWindow) then 70 | PluginsWindow:Close() 71 | end 72 | 73 | RunGameUICommand = menup.RGUIC 74 | 75 | table.Empty(menup) 76 | 77 | if pnlMainMenu and pnlMainMenu.HTML and vgui.GetControlTable("MainMenuPanel") then 78 | pnlMainMenu.HTML:Call([[document.getElementById("PluginsButton").remove();]]) 79 | end 80 | 81 | include("menu/menu_plugins.lua") 82 | elseif confirm == "risky" then 83 | include("menu/menu_plugins.lua") 84 | else 85 | MsgC(Color(255, 234, 0), "Hold up a second! ", Color(255, 255, 255), "Things can break if you do this. To proceed, run 'menup restart confirm'\n") 86 | end 87 | end 88 | 89 | local function menup_load(stuff) 90 | stuff = stuff[2] 91 | if not stuff then 92 | MsgC(Color(255, 255, 255), "Please specify a file name or plugin ID to (re)load.\n") 93 | elseif file.Exists("lua/menu_plugins/" .. stuff, "GAME") then 94 | MsgC(Color(255, 255, 255), "Loading file ", Color(41, 121, 255), stuff, Color(255, 255, 255), " as a plugin...\n") 95 | local manifest = menup.control.load("lua/menu_plugins/" .. stuff) 96 | menup.control.run(manifest.id) 97 | elseif menup.plugins[stuff] then 98 | local p = menup.plugins[stuff] 99 | if not p.file then MsgC(Color(255, 234, 0), "The plugin has no file assicaited with it.\n") return end 100 | menup_load({"bazinga", p.file}) 101 | else 102 | MsgC(Color(255, 234, 0), "Could not find a file or plugin ID with that query.\n") 103 | end 104 | end 105 | 106 | local function menup_command(_, _, args, argstr) 107 | local cmds = { 108 | list = menup_list, 109 | enable = menup_enable, 110 | disable = menup_disable, 111 | gui = ShowPluginsWindow, 112 | drawer = menup.drawer.open, 113 | restart = menup_restart, 114 | load = menup_load, 115 | } 116 | 117 | local sub = args[1] 118 | 119 | if not sub or not cmds[sub] then 120 | MsgC(Color(41, 121, 255), "Available commands:\n") 121 | 122 | for k, _ in SortedPairs(cmds) do 123 | MsgC(Color(255, 255, 255), " - " .. k .. "\n") 124 | end 125 | else 126 | cmds[sub](args) 127 | end 128 | end 129 | 130 | concommand.Add("menup", menup_command) -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/plugins_panel.lua: -------------------------------------------------------------------------------- 1 | local InfoPanel = table.Copy(vgui.GetControlTable("DPanel")) 2 | local color_gray = Color(222, 222, 222) 3 | local html_queue = {} 4 | 5 | local function LegacyConfig(v) 6 | local dm = DermaMenu() 7 | 8 | for i, _ in pairs(v.config) do 9 | local x = menup.config.get(v.id, i) 10 | local cv = dm:AddOption(i) 11 | 12 | cv.DoClick = function() 13 | Derma_StringRequest("Change option", v.name .. "." .. i .. " = " .. tostring(x), tostring(x), function(txt) 14 | menup.config.set(v.id, i, tonumber(txt) == nil and txt or tonumber(txt)) 15 | end) 16 | end 17 | 18 | if x == "true" or x == 1 or x == true then 19 | cv:SetIcon("icon16/tick.png") 20 | elseif x == "false" or x == 0 or x == false then 21 | cv:SetIcon("icon16/cross.png") 22 | else 23 | cv:SetIcon("icon16/pencil.png") 24 | end 25 | end 26 | 27 | dm:Open() 28 | end 29 | 30 | local cfpnls = { 31 | bool = function(id, key, data) 32 | local val = menup.config.get(id, key, isbool(data[3]) and data[3] or false) 33 | local root = vgui.Create("DPanel") 34 | local label = root:Add("DLabel") 35 | local cb = root:Add("DCheckBox") 36 | cb:Dock(RIGHT) 37 | cb:SetWide(15) 38 | cb:SetChecked(val) 39 | label:Dock(FILL) 40 | label:SetText(data[1]) 41 | label:SetTextColor(Color(0, 0, 0)) 42 | 43 | cb.OnChange = function(pnl, newval) 44 | hook.Run("UserConfigChange", id, key, newval, val) 45 | menup.config.set(id, key, newval) 46 | val = newval 47 | end 48 | 49 | return root 50 | end, 51 | int = function(id, key, data) 52 | local val = menup.config.get(id, key, isnumber(data[3]) and data[3] or 0) 53 | local root = vgui.Create("DPanel") 54 | local label = root:Add("DLabel") 55 | local wang = root:Add("DNumberWang") 56 | wang:Dock(RIGHT) 57 | wang:SetWide(96) 58 | wang:SetDecimals(0) 59 | wang:SetMin(-math.huge) 60 | wang:SetMax(math.huge) 61 | wang:SetValue(val) 62 | label:Dock(FILL) 63 | label:SetText(data[1]) 64 | label:SetTextColor(Color(0, 0, 0)) 65 | 66 | wang.OnValueChanged = function(pnl, newval) 67 | if string.match(newval, "%D") then 68 | wang:SetText(tostring(val)) 69 | else 70 | hook.Run("UserConfigChange", id, key, newval, val) 71 | menup.config.set(id, key, newval) 72 | val = newval 73 | end 74 | end 75 | 76 | return root 77 | end, 78 | float = function(id, key, data) 79 | local val = menup.config.get(id, key, isnumber(data[3]) and data[3] or 0) 80 | local root = vgui.Create("DPanel") 81 | local label = root:Add("DLabel") 82 | local wang = root:Add("DNumberWang") 83 | wang:Dock(RIGHT) 84 | wang:SetWide(96) 85 | wang:SetMin(-math.huge) 86 | wang:SetMax(math.huge) 87 | wang:SetValue(val) 88 | label:Dock(FILL) 89 | label:SetText(data[1]) 90 | label:SetTextColor(Color(0, 0, 0)) 91 | 92 | wang.OnValueChanged = function(pnl, newval) 93 | hook.Run("UserConfigChange", id, key, newval, val) 94 | menup.config.set(id, key, newval) 95 | val = newval 96 | end 97 | 98 | return root 99 | end, 100 | range = function(id, key, data) 101 | local min, max, default = data[3][1], data[3][2], data[3][3] 102 | min = min ~= nil and min or 0 103 | max = max ~= nil and max or 100 104 | local val = menup.config.get(id, key, isnumber(default) and default or 0) 105 | local root = vgui.Create("DPanel") 106 | local slider = root:Add("DNumSlider") 107 | slider:Dock(FILL) 108 | slider:SetDecimals(3) 109 | slider:SetMinMax(min, max) 110 | slider:SetValue(val) 111 | slider:SetText(data[1]) 112 | slider:SetDark(true) 113 | 114 | slider.OnValueChanged = function(pnl, newval) 115 | hook.Run("UserConfigChange", id, key, newval, val) 116 | menup.config.set(id, key, newval) 117 | val = newval 118 | end 119 | 120 | return root 121 | end, 122 | string = function(id, key, data) 123 | local val = menup.config.get(id, key, isstring(data[3]) and data[3] or "") 124 | local root = vgui.Create("DPanel") 125 | local label = root:Add("DLabel") 126 | local tbox = root:Add("DTextEntry") 127 | root:SetTall(48) 128 | tbox:Dock(BOTTOM) 129 | tbox:SetText(val) 130 | tbox:SetPlaceholderText(isstring(data[3]) and data[3]) 131 | label:Dock(FILL) 132 | label:SetText(data[1]) 133 | label:SetTextColor(Color(0, 0, 0)) 134 | 135 | tbox.OnLoseFocus = function(pnl) 136 | local newval = pnl:GetText() 137 | hook.Run("UserConfigChange", id, key, newval, val) 138 | menup.config.set(id, key, newval) 139 | val = newval 140 | end 141 | 142 | return root 143 | end, 144 | select = function(id, key, data) 145 | local val = menup.config.get(id, key, 1) 146 | local root = vgui.Create("DPanel") 147 | local label = root:Add("DLabel") 148 | local combo = root:Add("DComboBox") 149 | root:SetTall(48) 150 | combo:Dock(BOTTOM) 151 | combo:SetSortItems(false) 152 | 153 | for k, txt in ipairs(data[3]) do 154 | if txt == "" then 155 | combo:AddSpacer() 156 | else 157 | combo:AddChoice(txt) 158 | end 159 | end 160 | 161 | combo:ChooseOptionID(val) 162 | label:Dock(FILL) 163 | label:SetText(data[1]) 164 | label:SetTextColor(Color(0, 0, 0)) 165 | 166 | combo.OnMenuOpened = function(pnl, dm) 167 | dm:GetChild(val):SetChecked(true) 168 | end 169 | 170 | combo.OnSelect = function(pnl, newval) 171 | hook.Run("UserConfigChange", id, key, newval, val) 172 | menup.config.set(id, key, newval) 173 | val = newval 174 | end 175 | 176 | return root 177 | end, 178 | color = function(id, key, data) 179 | local val = menup.config.get(id, key, istable(data[3]) and Color(data[3][1], data[3][2], data[3][3], data[3][4] or 255) or Color(255, 255, 255, 255)) 180 | local root = vgui.Create("DPanel") 181 | local label = root:Add("DLabel") 182 | local preview = root:Add("DColorButton") 183 | preview:Dock(RIGHT) 184 | preview:SetWide(48) 185 | preview:SetColor(val, true) 186 | label:Dock(FILL) 187 | label:SetText(data[1]) 188 | label:SetTextColor(Color(0, 0, 0)) 189 | local picker 190 | 191 | preview.DoClick = function() 192 | if IsValid(picker) then 193 | picker:Remove() 194 | end 195 | 196 | picker = vgui.Create("DPanel") 197 | picker:SetSize(272, 296) 198 | local sx, sy = preview:LocalToScreen(24, 0) 199 | picker:SetPos(sx - 128, sy - 296) 200 | 201 | picker.Paint = function(s, w, h) 202 | picker:GetSkin().tex.Tab_Control(0, 0, w, h) 203 | end 204 | 205 | local cc = picker:Add("DColorMixer") 206 | cc:SetColor(val) 207 | 208 | -- why do i have to do this? 209 | timer.Simple(0, function() 210 | cc:SetColor(val) 211 | end) 212 | 213 | cc:SetPos(8, 8) 214 | cc:SetSize(256, 250) 215 | local detail = cc.WangsPanel:Add("DColorButton") 216 | detail:Dock(FILL) 217 | detail:DockMargin(0, 4, 0, 0) 218 | detail:SetColor(val) 219 | 220 | cc.ValueChanged = function(_, newval) 221 | detail:SetColor(Color(newval.r, newval.g, newval.b, newval.a)) 222 | end 223 | 224 | local ok = picker:Add("DButton") 225 | ok:SetPos(94, 264) 226 | ok:SetSize(170, 24) 227 | ok:SetText("Save") 228 | ok:SetIcon("icon16/tick.png") 229 | 230 | ok.DoClick = function() 231 | local newval = cc:GetColor() 232 | preview:SetColor(newval, true) 233 | hook.Run("UserConfigChange", id, key, newval, val) 234 | menup.config.set(id, key, newval) 235 | val = newval 236 | picker:Remove() 237 | end 238 | 239 | local cancel = picker:Add("DButton") 240 | cancel:SetPos(8, 264) 241 | cancel:SetSize(86, 24) 242 | cancel:SetText("Cancel") 243 | cancel:SetIcon("icon16/cross.png") 244 | 245 | cancel.DoClick = function() 246 | picker:Remove() 247 | end 248 | 249 | picker:MakePopup() 250 | end 251 | 252 | return root 253 | end, 254 | keybind = function(id, key, data) 255 | if vgui.GetControlTable("DBinder") == nil then 256 | include("vgui/dbinder.lua") -- not included in menu realm by default 257 | end 258 | 259 | local val = menup.config.get(id, key, isnumber(data[3]) and data[3] or 0) 260 | local root = vgui.Create("DPanel") 261 | local label = root:Add("DLabel") 262 | local binder = root:Add("DBinder") 263 | binder:Dock(RIGHT) 264 | binder:SetWide(96) 265 | binder:SetValue(val) 266 | label:Dock(FILL) 267 | label:SetText(data[1]) 268 | label:SetTextColor(Color(0, 0, 0)) 269 | 270 | binder.OnChange = function(pnl, newval) 271 | hook.Run("UserConfigChange", id, key, newval, val) 272 | menup.config.set(id, key, newval) 273 | val = newval 274 | end 275 | 276 | return root 277 | end, 278 | file = function(id, key, data) 279 | if vgui.GetControlTable("DFileBrowser") == nil then 280 | include("vgui/dfilebrowser.lua") -- not included in menu realm by default 281 | include("vgui/dhorizontaldivider.lua") 282 | end 283 | 284 | -- base, match, default 285 | local val = menup.config.get(id, key, isstring(data[3][3]) and data[3][3] or nil) 286 | local base = isstring(data[3][1]) and data[3][1] or "" 287 | local match = isstring(data[3][2]) and data[3][2] or "*" 288 | local root = vgui.Create("DPanel") 289 | local label = root:Add("DLabel") 290 | local preview = root:Add("DButton") 291 | root:SetTall(48) 292 | preview:Dock(BOTTOM) 293 | preview:SetText(isstring(val) and val or "No file selected") 294 | label:Dock(FILL) 295 | label:SetText(data[1]) 296 | label:SetTextColor(Color(0, 0, 0)) 297 | local frame 298 | 299 | preview.DoClick = function() 300 | if IsValid(frame) then 301 | frame:Remove() 302 | end 303 | 304 | frame = vgui.Create("DFrame") 305 | frame:SetSize(512, 384) 306 | frame:Center() 307 | frame:SetTitle("Select file") 308 | frame:SetSizable(true) 309 | frame:SetScreenLock(true) 310 | frame:NoClipping(true) 311 | local bgcol = Color(0, 0, 0, 128) 312 | local message = "Double-click to select" 313 | 314 | frame.PaintOver = function(s, w, h) 315 | draw.RoundedBoxEx(16, 8, h, w - 16, 28, bgcol, false, false, true, true) 316 | surface.SetFont("Trebuchet24") 317 | local tw = surface.GetTextSize(message) 318 | surface.SetTextColor(color_white) -- where does this even come from lmao 319 | surface.SetTextPos(w / 2 - tw / 2, h) 320 | surface.DrawText(message) 321 | end 322 | 323 | local browser = frame:Add("DFileBrowser") 324 | browser:Dock(FILL) 325 | browser:SetOpen(true) 326 | browser:SetPath("GAME") 327 | browser:SetBaseFolder(base) 328 | browser:SetCurrentFolder(base) 329 | browser:SetSearch(match) 330 | 331 | browser.OnDoubleClick = function(_, newval) 332 | newval = newval:sub(#base + 2) 333 | preview:SetText(newval) 334 | hook.Run("UserConfigChange", id, key, newval, val) 335 | menup.config.set(id, key, newval) 336 | val = newval 337 | frame:Close() 338 | end 339 | 340 | frame:MakePopup() 341 | end 342 | 343 | return root 344 | end, 345 | stack = function(id, key, data) 346 | local val = menup.config.get(id, key, data[3]) 347 | local root = vgui.Create("DPanel") 348 | local label = root:Add("DLabel") 349 | local combo = root:Add("DComboBox") 350 | root:SetTall(48) 351 | combo:Dock(BOTTOM) 352 | combo:SetSortItems(false) 353 | -- function combo.DropButton.GetExpanded(s) 354 | -- return combo:IsMenuOpen() 355 | -- end 356 | -- Derma_Hook(combo.DropButton, "Paint", "Paint", "ExpandButton") 357 | local enabled = 0 358 | local total = 0 359 | 360 | for k, v in SortedPairs(val) do 361 | combo:AddChoice(k) 362 | 363 | if v then 364 | enabled = enabled + 1 365 | end 366 | 367 | total = total + 1 368 | end 369 | 370 | combo:SetText(enabled .. "/" .. total .. " selected") 371 | label:Dock(FILL) 372 | label:SetText(data[1]) 373 | label:SetTextColor(Color(0, 0, 0)) 374 | 375 | combo.OnMenuOpened = function(_, pnl) 376 | enabled = 0 377 | pnl:SetDrawColumn(true) 378 | 379 | for i = 1, pnl:ChildCount() do 380 | local child = pnl:GetChild(i) 381 | local check = val[child:GetText()] 382 | 383 | if check then 384 | child:SetChecked(true) 385 | enabled = enabled + 1 386 | end 387 | end 388 | 389 | combo:SetText(enabled .. "/" .. total .. " selected") 390 | end 391 | 392 | combo.OnSelect = function(pnl, _, txt) 393 | local oldval = table.Copy(val) 394 | val[txt] = not val[txt] 395 | 396 | timer.Simple(0, function() 397 | combo:OpenMenu() 398 | end) 399 | 400 | hook.Run("UserConfigChange", id, key, val, oldval) 401 | menup.config.set(id, key, val) 402 | end 403 | 404 | return root 405 | end, 406 | sort = function(id, key, data) 407 | local val = menup.config.get(id, key, data[3]) 408 | local root = vgui.Create("DPanel") 409 | local label = root:Add("DLabel") 410 | local combo = root:Add("DComboBox") 411 | root:SetTall(48) 412 | combo:Dock(BOTTOM) 413 | combo:SetSortItems(false) 414 | 415 | function combo.DropButton.GetExpanded(s) 416 | return combo:IsMenuOpen() 417 | end 418 | 419 | Derma_Hook(combo.DropButton, "Paint", "Paint", "ExpandButton") 420 | label:Dock(FILL) 421 | label:SetText(data[1]) 422 | label:SetTextColor(Color(0, 0, 0)) 423 | combo:SetText(table.concat(val, ", ")) 424 | 425 | combo.DoClick = function(_, force) 426 | if IsValid(combo.Menu) then 427 | combo.Menu:Remove() 428 | if not force then return end 429 | end 430 | 431 | val = menup.config.get(id, key, data[3]) 432 | local dm = DermaMenu(false, combo) 433 | combo.Menu = dm 434 | local ll = dm:Add("DListLayout") 435 | ll:MakeDroppable("menup." .. id .. "." .. key) 436 | ll.children = {} 437 | dm:AddPanel(ll) 438 | 439 | for k, v in ipairs(val) do 440 | local pan = ll:Add("DPanel") 441 | local txt = pan:Add("DLabel") 442 | pan.pos = k 443 | pan.val = v 444 | ll.children[k] = pan 445 | pan.GetChecked = function() end 446 | 447 | pan.Paint = function(_, w, h) 448 | draw.RoundedBox(0, 1, 0, 22, 22, color_gray) 449 | draw.RoundedBox(0, 22, 0, w - 23, 22, (pan.pos % 2 == 0) and color_gray or color_white) 450 | derma.SkinHook("Paint", "MenuOption", pan, w, h) 451 | 452 | if pan:IsHovered() and not dragndrop.IsDragging() then 453 | pan:GetSkin().tex.Input.Slider.V.Normal(3, 3, 15, 16) 454 | else 455 | surface.SetFont("DermaDefaultBold") 456 | surface.SetTextColor(Color(0, 0, 0)) 457 | surface.SetTextPos(4, 4) 458 | surface.DrawText(pan.pos .. ":") 459 | end 460 | end 461 | 462 | pan:Dock(TOP) 463 | pan:SetTall(22) 464 | txt:Dock(RIGHT) 465 | txt:SetWide(combo:GetWide() - 28) 466 | txt:SetDark(true) 467 | txt:SetText(v) 468 | end 469 | 470 | ll.OnModified = function(_, refresh) 471 | timer.Simple(0, function() 472 | if not IsValid(ll) then return end 473 | local newval = {} 474 | 475 | for k, v in ipairs(ll.children) do 476 | if not IsValid(v) then continue end 477 | local pan = v 478 | local pos = pan:GetY() / 22 + 1 479 | pan.pos = pos 480 | newval[pos] = pan.val 481 | end 482 | 483 | combo:SetText(table.concat(newval, ", ")) 484 | hook.Run("UserConfigChange", id, key, newval, val) 485 | menup.config.set(id, key, newval) 486 | val = newval 487 | 488 | if refresh then 489 | combo:DoClick(true):MakePopup() 490 | end 491 | end) 492 | end 493 | 494 | local x, y = combo:LocalToScreen(0, combo:GetTall()) 495 | dm:SetMinimumWidth(combo:GetWide()) 496 | dm:Open(x, y, false, combo) 497 | end 498 | 499 | return root 500 | end, 501 | -- this was so painful 502 | list = function(id, key, data) 503 | local val = menup.config.get(id, key, data[3]) 504 | local root = vgui.Create("DPanel") 505 | local label = root:Add("DLabel") 506 | local combo = root:Add("DComboBox") 507 | root:SetTall(48) 508 | combo:Dock(BOTTOM) 509 | combo:SetSortItems(false) 510 | 511 | function combo.DropButton.GetExpanded(s) 512 | return combo:IsMenuOpen() 513 | end 514 | 515 | Derma_Hook(combo.DropButton, "Paint", "Paint", "ExpandButton") 516 | label:Dock(FILL) 517 | label:SetText(data[1]) 518 | label:SetTextColor(Color(0, 0, 0)) 519 | combo:SetText(table.concat(val, ", ")) 520 | 521 | combo.DoClick = function(_, force) 522 | if IsValid(combo.Menu) then 523 | combo.Menu:Remove() 524 | if not force then return end 525 | end 526 | 527 | val = menup.config.get(id, key, data[3]) 528 | local dm = DermaMenu(false, combo) 529 | combo.Menu = dm 530 | local add = dm:Add("DButton") 531 | local ll = dm:Add("DListLayout") 532 | add:Dock(TOP) 533 | add:SetTall(22) 534 | add:SetText("Add") 535 | add:SetIcon("icon16/add.png") 536 | 537 | add.DoClick = function() 538 | local newval = table.Copy(val) 539 | table.insert(newval, 1, "") 540 | menup.config.set(id, key, newval) 541 | hook.Run("UserConfigChange", id, key, newval, val) 542 | local newdm = combo:DoClick(true) 543 | newdm.first:RequestFocus() 544 | end 545 | 546 | ll:MakeDroppable("menup." .. id .. "." .. key) 547 | ll.children = {} 548 | dm:AddPanel(ll) 549 | 550 | for k, v in ipairs(val) do 551 | local pan = ll:Add("DPanel") 552 | local ep = pan:Add("EditablePanel") 553 | local del = pan:Add("DButton") 554 | local txt = ep:Add("DTextEntry") 555 | 556 | if k == 1 then 557 | dm.first = txt 558 | end 559 | 560 | pan.pos = k 561 | pan.val = v 562 | ll.children[k] = pan 563 | pan.GetChecked = function() end 564 | 565 | pan.Paint = function(_, w, h) 566 | draw.RoundedBox(0, 1, 0, 22, h, color_gray) 567 | draw.RoundedBox(0, 22, 0, w - 23, h, (pan.pos % 2 == 0) and color_gray or color_white) 568 | 569 | if (pan:IsHovered() or pan:IsChildHovered() or txt:IsEditing()) and not dragndrop.IsDragging() then 570 | pan:GetSkin().tex.Input.Slider.V.Hover(3, 3, 15, 16) 571 | txt:SetPaintBackground(true) 572 | del:SetVisible(true) 573 | else 574 | surface.SetFont("DermaDefaultBold") 575 | surface.SetTextColor(Color(0, 0, 0)) 576 | surface.SetTextPos(4, 4) 577 | surface.DrawText(pan.pos .. ":") 578 | txt:SetPaintBackground(false) 579 | del:SetVisible(false) 580 | end 581 | end 582 | 583 | del.DoClick = function() 584 | pan:Remove() 585 | ll:OnModified() 586 | end 587 | 588 | txt.OnGetFocus = function() 589 | txt:MakePopup() 590 | txt:SetDrawOnTop(true) 591 | txt:SetPos(pan:LocalToScreen(22, 0)) 592 | end 593 | 594 | txt.OnLoseFocus = function() 595 | -- focus isnt automatically removed and the panel bugs out 596 | -- this is the nuclear option :) 597 | pan.val = txt:GetValue() 598 | ll:OnModified(true) 599 | end 600 | 601 | txt.OnKeyCode = function(_, kc) 602 | if kc == KEY_ENTER or kc == KEY_TAB then 603 | txt:OnLoseFocus() 604 | end 605 | end 606 | 607 | pan:Dock(TOP) 608 | pan:SetTall(22) 609 | del:SetPos(combo:GetWide() - 22, 0) 610 | del:SetSize(22, 22) 611 | del:SetText("") 612 | del:SetIcon("icon16/cancel.png") 613 | del:Hide() 614 | ep:SetPos(22, 0) 615 | ep:SetSize(combo:GetWide() - 43, 22) 616 | ep:SetPaintBackgroundEnabled(false) 617 | txt:SetPos(0, 0) 618 | txt:SetSize(ep:GetSize()) 619 | txt:SetText(pan.val) 620 | txt:SetPlaceholderText("(Empty)") 621 | txt:SetPaintBackground(false) 622 | txt:SetEnterAllowed(true) 623 | txt:SetMultiline(false) 624 | pan:InvalidateLayout(false) 625 | end 626 | 627 | ll.OnModified = function(_, refresh) 628 | timer.Simple(0, function() 629 | if not IsValid(ll) then return end 630 | local newval = {} 631 | 632 | for k, v in ipairs(ll.children) do 633 | if not IsValid(v) then continue end 634 | local pan = v 635 | local pos = pan:GetY() / 22 + 1 636 | pan.pos = pos 637 | newval[pos] = pan.val 638 | end 639 | 640 | combo:SetText(table.concat(newval, ", ")) 641 | hook.Run("UserConfigChange", id, key, newval, val) 642 | menup.config.set(id, key, newval) 643 | val = newval 644 | 645 | if refresh then 646 | combo:DoClick(true):MakePopup() 647 | end 648 | end) 649 | end 650 | 651 | local x, y = combo:LocalToScreen(0, combo:GetTall()) 652 | dm:SetMinimumWidth(combo:GetWide()) 653 | dm:Open(x, y, false, combo) 654 | 655 | return dm 656 | end 657 | 658 | return root 659 | end, 660 | } 661 | 662 | -- this was even more painful 663 | function InfoPanel:Init() 664 | self:SetTall(512) 665 | self:SetPaintBackground(false) 666 | local controls = self:Add("DPanel") 667 | controls:SetPaintBackground(false) 668 | controls:Dock(TOP) 669 | controls:SetTall(36) 670 | local toggle = controls:Add("DButton") 671 | local alt = controls:Add("DButton") 672 | local md = self:Add("DPanel") 673 | local cp = self:Add("DScrollPanel") 674 | md:SetPos(0, 32) 675 | md:SetTall(512) 676 | md:SetPaintBackground(false) 677 | cp:SetPos(self:GetWide(), 32) 678 | cp:SetTall(512) 679 | self.controls = controls 680 | self.toggle = toggle 681 | self.alt = alt 682 | self.md = md 683 | self.cp = cp 684 | self.scroll = 0 685 | self.target = 0 686 | end 687 | 688 | function InfoPanel:Think() 689 | local w = self.controls:GetWide() 690 | local h = self:GetParent():GetParent():GetParent():GetTall() - 56 -- info collapse list sheet frame 691 | self.scroll = Lerp(FrameTime() * 10, self.scroll, self.target) 692 | local s = self.scroll 693 | self.toggle:SetWide(w / 2) 694 | self.alt:SetWide(w / 2) 695 | self.md:SetSize(w, h) 696 | self.cp:SetSize(w, h) 697 | self.alt:SetPos(w / 2, 0) 698 | self.md:SetPos(-w * s, 32) 699 | self.cp:SetPos((1 - s) * w, 32) 700 | end 701 | 702 | function InfoPanel:SetEnabled(state) 703 | self.target = 0 704 | local manifest = self.manifest 705 | menup.control[state and "enable" or "disable"](manifest.id) 706 | self:GetParent().toggle:SetChecked(state) 707 | self:Load(manifest) 708 | end 709 | 710 | function InfoPanel:BuildConfig(manifest) 711 | self.cp:Clear() 712 | 713 | -- name type param desc 714 | for k, v in SortedPairs(manifest.config) do 715 | if isfunction(cfpnls[v[2]]) then 716 | local pnl = cfpnls[v[2]](manifest.id, k, v) 717 | self.cp:AddItem(pnl) 718 | pnl:Dock(TOP) 719 | pnl:DockPadding(4, 4, 4, 4) 720 | pnl:DockMargin(0, 2, 0, 2) 721 | 722 | if isstring(v[4]) then 723 | pnl:SetTooltip(v[4]) 724 | pnl:SetTooltipDelay(0) -- https://github.com/Facepunch/garrysmod/pull/1875 please 725 | end 726 | else 727 | print(manifest.id .. " has unknown config type \"" .. v[2] .. "\" for key \"" .. k .. "\"!") 728 | end 729 | end 730 | 731 | local apply = self.cp:Add("DButton") 732 | apply:Dock(TOP) 733 | apply:DockPadding(4, 4, 4, 4) 734 | apply:DockMargin(32, 8, 32, 0) 735 | apply:SetTall(32) 736 | apply:SetText("Apply settings") 737 | apply:SetIcon("icon16/disk.png") 738 | 739 | apply.DoClick = function() 740 | hook.Run("ConfigApply", manifest.id) 741 | end 742 | end 743 | 744 | function InfoPanel:Load(manifest) 745 | self.manifest = manifest 746 | local info = string.format([[ 747 | ## %s 748 | %s 749 | ## 750 | *Author* : %s 751 | *Version* : %s 752 | *ID* : `%s` 753 | *File* : `%s` 754 | ]], manifest.name, manifest.description, manifest.author, manifest.version, manifest.id, manifest.file) 755 | 756 | if manifest.source then 757 | info = info .. string.format("*Source* : [%s](%s) \n", manifest.source:match("^https?://([^/]+)"), manifest.source) 758 | end 759 | 760 | if manifest.initalization then 761 | info = info .. string.format("*Initalization* : %s ms \n", tostring(manifest.initalization)) 762 | end 763 | 764 | if self.md:ChildCount() == 0 then 765 | -- hopefully this fixes the crashing issue 766 | table.insert(html_queue, {self.md, info}) 767 | end 768 | 769 | if manifest.enabled then 770 | self.toggle:SetText("Disable") 771 | self.toggle:SetIcon("icon16/delete.png") 772 | self.alt:SetText("Config") 773 | self.alt:SetIcon("icon16/cog.png") 774 | self.alt:SetEnabled(not table.IsEmpty(manifest.config)) 775 | else 776 | self.toggle:SetText("Enable") 777 | self.toggle:SetIcon("icon16/add.png") 778 | self.alt:SetText("Reset") 779 | self.alt:SetIcon("icon16/control_repeat.png") 780 | self.alt:SetEnabled(true) 781 | end 782 | 783 | self.toggle.DoClick = function(pnl) 784 | self:SetEnabled(not manifest.enabled) 785 | end 786 | 787 | self.alt.DoClick = function(pnl) 788 | -- goto config 789 | if manifest.enabled and self.target == 0 then 790 | self:BuildConfig(manifest) 791 | self.target = 1 792 | self.alt:SetText("Description") 793 | self.alt:SetIcon("icon16/text_dropcaps.png") 794 | elseif manifest.enabled and self.target == 1 then 795 | -- goto description 796 | self.target = 0 797 | self.alt:SetText("Config") 798 | self.alt:SetIcon("icon16/cog.png") 799 | else -- reset 800 | Derma_Query("Are you sure you want to reset this plugins config & store?", "Confirmation", "Yes", function() 801 | hook.Run("PluginReset", manifest) 802 | menup.db.del("data_" .. manifest.id) 803 | end, "No") 804 | end 805 | end 806 | end 807 | 808 | local PANEL = {} 809 | 810 | function PANEL:Init() 811 | local new, legacy = {}, {} 812 | local lcollapse 813 | self.plugins = {} 814 | self:SetPaintBackground(false) 815 | 816 | for _, v in SortedPairsByMemberValue(menup.plugins, "name") do 817 | if v.legacy then 818 | table.insert(legacy, v) 819 | else 820 | table.insert(new, v) 821 | end 822 | end 823 | 824 | for _, v in ipairs(new) do 825 | local collapse = self:Add(" " .. v.name) 826 | local toggle = collapse.Header:Add("DCheckBox") 827 | toggle:SetPos(2, 2) 828 | toggle:SetChecked(v.enabled) 829 | local info = vgui.CreateFromTable(InfoPanel, collapse) 830 | collapse:SetContents(info) 831 | collapse:SetExpanded(false) 832 | 833 | function collapse.OnToggle(me, state) 834 | if not state then return end 835 | 836 | -- info.target = 0 837 | for _, c in pairs(self:GetChildren()[1]:GetChildren()) do 838 | if c ~= me then 839 | c:DoExpansion(false) 840 | end 841 | end 842 | 843 | timer.Simple(me:GetAnimTime(), function() 844 | self:ScrollToChild(collapse) 845 | end) 846 | end 847 | 848 | function toggle:OnChange(state) 849 | info:SetEnabled(state) 850 | end 851 | 852 | info:Load(v) 853 | collapse.toggle = toggle 854 | collapse.info = info 855 | self.plugins[v.id] = collapse 856 | end 857 | 858 | if not table.IsEmpty(legacy) then 859 | lcollapse = self:Add("Legacy plugins") 860 | lcollapse:SetExpanded(false) 861 | 862 | lcollapse.OnToggle = function(me, state) 863 | if not state then return end 864 | 865 | for _, c in pairs(self:GetChildren()[1]:GetChildren()) do 866 | if c ~= me then 867 | c:DoExpansion(false) 868 | end 869 | end 870 | 871 | timer.Simple(me:GetAnimTime(), function() 872 | self:ScrollToChild(lcollapse) 873 | end) 874 | end 875 | 876 | for _, v in ipairs(legacy) do 877 | local btn = lcollapse:Add(v.name) 878 | btn:SetTall(22) 879 | btn:SetEnabled(false) 880 | btn:SetCursor("arrow") 881 | 882 | btn.Paint = function(pnl, w, h) 883 | draw.NoTexture() 884 | surface.SetDrawColor(255, 255, 255) 885 | surface.DrawRect(0, 0, w, h) 886 | derma.SkinHook("Paint", "CategoryButton", pnl, w, h) 887 | end 888 | 889 | local alt = btn:Add("DButton") 890 | alt:Dock(RIGHT) 891 | alt:SetWide(22) 892 | alt:SetText("") 893 | alt:SetIcon("icon16/cog.png") 894 | local toggle = btn:Add("DButton") 895 | toggle:Dock(RIGHT) 896 | toggle:SetWide(22) 897 | toggle:SetText("") 898 | 899 | alt.DoClick = function() 900 | LegacyConfig(v) 901 | end 902 | 903 | toggle.DoClick = function() 904 | local state = not v.enabled 905 | menup.control[state and "enable" or "disable"](v.id) 906 | alt:SetEnabled(v.enabled and not table.IsEmpty(v.config)) 907 | toggle:SetIcon(v.enabled and "icon16/lightbulb.png" or "icon16/lightbulb_off.png") 908 | end 909 | 910 | alt:SetEnabled(v.enabled and not table.IsEmpty(v.config)) 911 | toggle:SetIcon(v.enabled and "icon16/lightbulb.png" or "icon16/lightbulb_off.png") 912 | end 913 | end 914 | end 915 | 916 | function PANEL:Paint() 917 | end 918 | 919 | hook.Add("DrawOverlay", "MPR_HTML", function() 920 | if #html_queue > 0 then 921 | local data = table.remove(html_queue, 1) 922 | local md = data[1]:Add("MarkdownPanel") 923 | md:SetMarkdown(data[2]) 924 | md:Dock(FILL) 925 | end 926 | end) 927 | 928 | vgui.Register("PluginsPanel", PANEL, "DCategoryList") -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/plugins_window.lua: -------------------------------------------------------------------------------- 1 | local PANEL = {} 2 | 3 | function PANEL:Init() 4 | self:SetSize(400, 600) 5 | --self:SetSize(ScrW() * 0.4, ScrH() * 0.6) 6 | self:Center() 7 | self:SetMinimumSize(400, 600) 8 | self:DockPadding(5, 3, 5, 3) 9 | self:SetTitle("") 10 | self:SetDraggable(true) 11 | self:SetScreenLock(true) 12 | self:SetSizable(true) 13 | local tabs = vgui.Create("DPropertySheet", self) 14 | tabs:Dock(FILL) 15 | tabs:SetFadeTime(0) 16 | self.tabs = tabs 17 | local plist = self:Add("PluginsPanel") 18 | tabs:AddSheet("Installed plugins", plist, "icon16/box.png") 19 | self.plist = plist 20 | local find = vgui.Create("DPanel") 21 | tabs:AddSheet("Find more", find, "icon16/add.png") 22 | local openrepo = find:Add("DButton") 23 | openrepo:Dock(BOTTOM) 24 | openrepo:SetText("Open repository") 25 | openrepo:SetIcon("icon16/folder_go.png") 26 | openrepo.DoClick = function() 27 | gui.OpenURL("https://github.com/djsime1/redux-plugins/") 28 | end 29 | local fm = find:Add("MarkdownPanel") 30 | fm:Dock(FILL) 31 | fm:SetMarkdown([[# Sorta W.I.P. 32 | Eventually this screen will automagically list all avilable plugins from the public repository. 33 | However, that hasn't been set up yet. 34 | Click the button at the bottom of this window to open the repository URL and browse the plugins. 35 | ]]) 36 | find:SetPaintBackground(false) 37 | self.find = find 38 | local about = self:Add("MarkdownPanel") 39 | tabs:AddSheet("About/Credits", about, "icon16/information.png") 40 | about:SetMarkdown(([[# Menu Plugins *Redux* 41 | Version %s : : [GitHub](https://github.com/djsime1/menu-plugins-redux) 42 | 43 | ## About 44 | This modification was written to enable the usage of menu plugins. 45 | They're like addons, but for the main/pause menu. 46 | The Redux version extends the existing Menu Plugins framework while retaining compatibility with existing scripts. 47 | Want to make your own menu plugin? Check out [the wiki](https://github.com/djsime1/menu-plugins-redux/wiki). 48 | 49 | ## Changelog 50 | %s 51 | 52 | ## Credits 53 | - *[djsime1](https://github.com/djsime1)* : Lead author of this mess. 54 | - *[GLua team](https://github.com/glua)* : Original menu plugins. 55 | - *[mpeterv](https://github.com/mpeterv)* : markdown.lua. 56 | - *[markdowncss](https://github.com/markdowncss)* : Modest CSS. 57 | - *[vercas](https://github.com/vercas)* : vON. 58 | - *[Garry](https://garry.tv)* : Obligatory thanks. 59 | - *[You](https://steamcommunity.com/my)* : For being epic **<3** 60 | 61 | ## Licenses 62 | [MPR is licensesd under the MIT license](https://github.com/djsime1/menu-plugins-redux/blob/dev/LICENSE). 63 | In addition, the following licenses apply to libraries/code used within MPR: 64 | - [markdown.lua](https://github.com/mpeterv/markdown) : [MIT license](https://github.com/mpeterv/markdown/blob/master/LICENSE). 65 | - [Modest CSS](https://github.com/markdowncss/modest) : [MIT license](https://github.com/markdowncss/modest/blob/master/LICENSE). 66 | - [vON](https://github.com/vercas/vON) :[Read here](https://github.com/vercas/vON/blob/master/von.lua#L1:L23). 67 | ]]):format(menup.version, menup.changelog)) 68 | self.about = about 69 | self.btnClose:MoveToFront() 70 | self.btnMaxim:Hide() 71 | self.btnMinim:Hide() 72 | end 73 | 74 | function PANEL:Paint(w, h) 75 | end 76 | 77 | vgui.Register("PluginsWindow", PANEL, "DFrame") 78 | 79 | function ShowPluginsWindow() 80 | PluginsWindow = IsValid(PluginsWindow) and PluginsWindow or vgui.Create("PluginsWindow") 81 | PluginsWindow:Center() 82 | PluginsWindow:SetZPos(9001) 83 | PluginsWindow:MakePopup() 84 | end 85 | 86 | function DoMenuButton() 87 | if table.Count(menup.drawer.buttons) == 0 then ShowPluginsWindow() else menup.drawer.open() end 88 | end 89 | 90 | -- concommand.Add("menu_plugins", ShowPluginsWindow) -------------------------------------------------------------------------------- /lua/menu/plugin_bootstrapper/tooltip_delay.lua: -------------------------------------------------------------------------------- 1 | -- https://github.com/Facepunch/garrysmod/pull/1875 2 | local pmeta = FindMetaTable("Panel") 3 | local ttmeta = vgui.GetControlTable("DTooltip") 4 | local tooltip_delay = GetConVar("tooltip_delay") 5 | 6 | function pmeta:GetTooltipPanel() 7 | return self.pnlTooltipPanel 8 | end 9 | 10 | function pmeta:SetTooltipDelay(delay) 11 | self.numTooltipDelay = delay 12 | end 13 | 14 | function ttmeta:OpenForPanel(panel) 15 | self.TargetPanel = panel 16 | self.OpenDelay = isnumber(panel.numTooltipDelay) and panel.numTooltipDelay or tooltip_delay:GetFloat() 17 | self:PositionTooltip() 18 | -- Use the parent panel's skin 19 | self:SetSkin(panel:GetSkin().Name) 20 | 21 | if (self.OpenDelay > 0) then 22 | self:SetVisible(false) 23 | 24 | timer.Simple(self.OpenDelay, function() 25 | if (not IsValid(self)) then return end 26 | if (not IsValid(panel)) then return end 27 | self:PositionTooltip() 28 | self:SetVisible(true) 29 | end) 30 | end 31 | end -------------------------------------------------------------------------------- /lua/menu_plugins/gradient_bg.lua: -------------------------------------------------------------------------------- 1 | -- alphabetically sorted 2 | local CONFIG = { 3 | afade = { 4 | "Gradient speed", "range", {.1, 3, 1}, 5 | "Speed at which colors change" 6 | }, 7 | btext = {"Background texts", "list", {}, "One is selected at random."}, 8 | cschmoove = { 9 | "Schmoove speed", "range", {.5, 4, 1}, 10 | "Speed at which text waves." 11 | }, 12 | dsize = {"Font size", "int", 0, "0 for automatic."}, 13 | eingame = {"Show text ingame", "bool", false, "Should text be visible on the pause menu during gameplay?"}, 14 | fcolor = { 15 | "In-game color", "color", {255, 255, 255, 160}, 16 | "Only works if the above option is enabled." 17 | }, 18 | } 19 | 20 | local MANIFEST = { 21 | id = "djsime1.gradient_bg", 22 | author = "djsime1", 23 | name = "Background customizer", 24 | description = "Allows you to change how your menu background looks.", 25 | version = "1.6", 26 | config = CONFIG 27 | } 28 | 29 | menup(MANIFEST) 30 | local grad = Material("gui/gradient", "nocull smooth") 31 | local r1, r2, r3, r4 = math.random(0, 359), math.random(0, 359), math.random(0, 359), math.random(0, 359) 32 | local fade = 1 33 | local OldDrawBackground = DrawBackground 34 | 35 | local function reload() 36 | local fspeed = menup.config.get(MANIFEST.id, "afade", 1) 37 | local bgtxt = menup.config.get(MANIFEST.id, "btext", {}) 38 | local tspeed = menup.config.get(MANIFEST.id, "cschmoove", 1) 39 | local size = menup.config.get(MANIFEST.id, "dsize", 0) 40 | local doingame = menup.config.get(MANIFEST.id, "eingame", false) 41 | local ingamecolor = menup.config.get(MANIFEST.id, "fcolor", Color(255, 255, 255, 160)) 42 | 43 | if isstring(bgtxt) then 44 | menup.config.set(MANIFEST.id, "btext", {bgtxt}) 45 | else 46 | bgtxt = table.Random(bgtxt) or "" 47 | end 48 | 49 | size = size > 0 and size or 96 * (ScrH() / 480) 50 | 51 | surface.CreateFont("GradientText", { 52 | font = "Coolvetica", 53 | size = size, 54 | weight = 400 55 | }) 56 | 57 | surface.SetFont("GradientText") 58 | local bgtxtx, bgtxty = surface.GetTextSize(bgtxt) 59 | 60 | function DrawBackground() 61 | local w, h = ScrW(), ScrH() 62 | local t = SysTime() * fspeed 63 | 64 | if not IsInGame() then 65 | surface.SetTextColor(0, 0, 0, 96) 66 | surface.SetDrawColor(0, 0, 0) 67 | surface.DrawRect(-1, -1, w + 2, h + 2) 68 | fade = 1 69 | else 70 | surface.SetTextColor(ingamecolor.r, ingamecolor.g, ingamecolor.b, ingamecolor.a) 71 | fade = 0 72 | end 73 | 74 | if IsInGame() and not doingame then return end 75 | surface.SetMaterial(grad) 76 | surface.SetAlphaMultiplier(1 * fade) 77 | surface.SetDrawColor(HSVToColor(t * 20 + r1, 1, .9)) 78 | surface.DrawTexturedRectRotated(w / 2, h / 2, w + 2, h + 2, 0) 79 | surface.SetAlphaMultiplier(0.75 * fade) 80 | surface.SetDrawColor(HSVToColor(t * 15 + r2, 1, .9)) 81 | surface.DrawTexturedRectRotated(w / 2, h / 2, h + 2, w + 2, 90) 82 | surface.SetAlphaMultiplier(0.50 * fade) 83 | surface.SetDrawColor(HSVToColor(t * 10 + r3, 1, .9)) 84 | surface.DrawTexturedRectRotated(w / 2, h / 2, w + 2, h + 2, 180) 85 | surface.SetAlphaMultiplier(0.25 * fade) 86 | surface.SetDrawColor(HSVToColor(t * 5 + r4, 1, .9)) 87 | surface.DrawTexturedRectRotated(w / 2, h / 2, h + 2, w + 2, 270) 88 | surface.SetAlphaMultiplier(1) 89 | t = SysTime() * tspeed 90 | surface.SetFont("GradientText") 91 | local x = (w / 2) - (bgtxtx / 2) 92 | 93 | for i = 1, #bgtxt do 94 | surface.SetTextPos(x, (h / 2) - (bgtxty / 2) + (math.sin(math.rad((i * 20) - (t * 60))) * 32)) 95 | surface.DrawText(bgtxt[i]) 96 | x = x + surface.GetTextSize(bgtxt[i]) 97 | end 98 | end 99 | end 100 | 101 | hook.Add("ConfigApply", MANIFEST.id, function(id) 102 | if id == MANIFEST.id then 103 | reload() 104 | end 105 | end) 106 | 107 | if IsValid(pnlMainMenu) then 108 | reload() 109 | else 110 | hook.Add("MenuVGUIReady", MANIFEST.id, function() 111 | OldDrawBackground = DrawBackground 112 | reload() 113 | end) 114 | end 115 | 116 | return function() 117 | DrawBackground = OldDrawBackground 118 | end -------------------------------------------------------------------------------- /lua/menu_plugins/loading_customizer.lua: -------------------------------------------------------------------------------- 1 | local CONFIG = { 2 | amode = { 3 | "Loadingscreen type", "select", {"Use server loading screen", "Use GMod's loading screen", "Use custom URL"} 4 | }, 5 | burl = {"Custom URL", "string", "", "Set first option to 'Use custom URL'."}, 6 | csteamid = {"Override SteamID64", "string", "", "Leave blank to use your own."}, 7 | dsound = {"Disable sounds", "bool", false, "*Attempts* to prevent sounds from playing."}, 8 | } 9 | 10 | local MANIFEST = { 11 | id = "djsime1.loading_customizer", 12 | author = "djsime1 & Meepen", 13 | name = "Loading screen modifier", 14 | description = "Allows you to tweak the loading screen. \nCode adapted from [Meepen's Loading Screen sound remover](https://forum.facepunch.com/t/loading-screen-sound-remover-clientside/194462) with permission.", 15 | version = "1.0", 16 | config = CONFIG 17 | } 18 | 19 | menup(MANIFEST) 20 | local oldGD = GameDetails 21 | 22 | function GameDetails(name, url, mapname, maxply, steamid, gamemode) 23 | url = ({ 24 | url, 25 | GetDefaultLoadingHTML(), 26 | menup.config.get(MANIFEST.id, "burl", GetDefaultLoadingHTML()) 27 | })[menup.config.get(MANIFEST.id, "amode", 1)] 28 | 29 | steamid = menup.config.get(MANIFEST.id, "csteamid", "") == "" and steamid or menup.config.get(MANIFEST.id, "csteamid", steamid) 30 | 31 | return oldGD(name, url, mapname, maxply, steamid, gamemode) 32 | end 33 | 34 | -- The following is adapted from https://forum.facepunch.com/t/loading-screen-sound-remover-clientside/194462 with permission from Meepen. 35 | local loadp 36 | local old_showurl 37 | local javascript = [[ 38 | var amount = 0; 39 | function DeleteAll(name) { 40 | var all = document.getElementsByTagName(name); 41 | for(var i = 0; i < all.length; i++) { 42 | amount = amount + 1; 43 | all*.parentElement.removeChild(all*); 44 | } 45 | } 46 | DeleteAll("iframe"); 47 | DeleteAll("audio"); 48 | DeleteAll("source"); 49 | ]] 50 | local overwrite = [[ 51 | var old = document.createElement; 52 | document.createElement = function(tagname) { 53 | tagname = tagname.toLowerCase(); 54 | if(tagname === "iframe" || tagname === "audio" || tagname === "source") { 55 | return; // sorry i am too lazy to redirect this to our stuff 56 | } 57 | return old(tagname); 58 | }; 59 | ]] 60 | local hasoverwritten = false 61 | 62 | local function ThinkLoad() 63 | if not IsValid(loadp) then 64 | hook.Remove("Think", MANIFEST.id) 65 | 66 | return 67 | end 68 | 69 | if not IsValid(loadp.HTML) then return end -- wait for it 70 | 71 | if hasoverwritten == false then 72 | loadp.HTML:RunJavascript(overwrite) 73 | hasoverwritten = true 74 | end 75 | 76 | if loadp.HTML:IsLoading() then return end -- wait for the load! 77 | loadp.HTML:RunJavascript(javascript) 78 | hook.Remove("Think", MANIFEST.id) 79 | end 80 | 81 | local function ApplyMute() 82 | if isfunction(GetLoadPanel) and ispanel(GetLoadPanel()) then 83 | loadp = GetLoadPanel() 84 | old_showurl = loadp.ShowURL 85 | 86 | function loadp:ShowURL(a, b, c, d, e, f) 87 | local ret = old_showurl(self, a, b, c, d, e, f) 88 | hasoverwritten = false 89 | hook.Add("Think", MANIFEST.id, ThinkLoad) 90 | 91 | return ret 92 | end 93 | 94 | print("Now muting loading screen.") 95 | else 96 | print("Unable to find loading panel, sounds will not be muted.") 97 | end 98 | end 99 | 100 | hook.Add("MenuVGUIReady", MANIFEST.id, function() 101 | if menup.config.get(MANIFEST.id, "dsound", false) then 102 | ApplyMute() 103 | end 104 | end) 105 | 106 | hook.Add("ConfigApply", MANIFEST.id, function(id) 107 | if id ~= MANIFEST.id then return end 108 | 109 | if menup.config.get(MANIFEST.id, "dsound", false) then 110 | ApplyMute() 111 | else 112 | if ispanel(loadp) then 113 | loadp.ShowURL = old_showurl 114 | end 115 | end 116 | end) 117 | 118 | return function() 119 | GameDetails = oldGD 120 | 121 | if ispanel(loadp) then 122 | loadp.ShowURL = old_showurl 123 | end 124 | 125 | hook.Remove("Think", MANIFEST.id) 126 | end -------------------------------------------------------------------------------- /lua/menu_plugins/manual_blacklist.lua: -------------------------------------------------------------------------------- 1 | local CONFIG = { 2 | bl = {"Blacklist filters", "list", {"123.456.789.000", "gm:Really bad gamemode", "map:rp_2spooky4you"}, "Things to blacklist."}, 3 | usefp = {"Add default blacklist", "bool", true, "You should probably keep this enabled."} 4 | } 5 | 6 | local MANIFEST = { 7 | id = "djsime1.custom_blacklist", 8 | author = "djsime1", 9 | name = "Custom server blacklist", 10 | description = [[Modify the server blacklist. Supports the following prefixes: 11 | - "map:" Block maps containing this phrase. 12 | - "desc:" Block servers with this description. 13 | - "host:" Block servers with this hostname. 14 | - "gm:" Block gamemodes with this phrase. 15 | No prefix means blacklist an IP.]], 16 | version = "1.0", 17 | config = CONFIG 18 | } 19 | 20 | menup(MANIFEST) 21 | 22 | local oldAPI = GetAPIManifest 23 | 24 | function GetAPIManifest(cb) 25 | oldAPI(function(json) 26 | local data = util.JSONToTable(json) 27 | local custom = menup.config.get(MANIFEST.id, "bl", {}) 28 | if menup.config.get(MANIFEST.id, "usefp") then 29 | table.Add(data.Servers.Banned, custom) 30 | else 31 | data.Servers.Banned = custom 32 | end 33 | cb(util.TableToJSON(data)) 34 | end) 35 | end 36 | 37 | if IsValid(pnlMainMenu) and not menup.config.get(MANIFEST.id, "seenpre", false) then 38 | Derma_Query("Custom blacklist only works if it's enabled when you launch the game. Please restart for your settings to take effect.", "Custom server blacklist", 39 | "Don't remind me", function() menup.config.set(MANIFEST.id, "seenpre", true) end, 40 | "Acknowledge this once") 41 | elseif not IsValid(pnlMainMenu) then 42 | print("Custom server blacklist is now active for this session.") 43 | end 44 | 45 | return function() 46 | GetAPIManifest = oldAPI 47 | if not menup.config.get(MANIFEST.id, "seenpost", false) then 48 | Derma_Query("Restart your game to remove custom filters.", "Custom server blacklist", 49 | "Don't remind me", function() menup.config.set(MANIFEST.id, "seenpost", true) end, 50 | "Acknowledge this once") 51 | end 52 | end -------------------------------------------------------------------------------- /lua/menu_plugins/menu_dev.lua: -------------------------------------------------------------------------------- 1 | -- I don't recall where I got this script from, forgive me if it's yours. 2 | 3 | local function lua_run_menu(_, _, _, code) 4 | local func = CompileString(code, "", false) 5 | 6 | if isstring(func) then 7 | Msg"Invalid syntax> " 8 | print(func) 9 | 10 | return 11 | end 12 | 13 | MsgN("> ", code) 14 | 15 | xpcall(func, function(err) 16 | print(debug.traceback(err)) 17 | end) 18 | end 19 | 20 | concommand.Add("lua_run_menu", lua_run_menu) 21 | 22 | local function FindInTable(tab, find, parents, depth) 23 | depth = depth or 0 24 | parents = parents or "" 25 | if (not istable(tab)) then return end 26 | if (depth > 3) then return end 27 | depth = depth + 1 28 | 29 | for k, v in pairs(tab) do 30 | if (type(k) == "string") then 31 | if (k and k:lower():find(find:lower())) then 32 | Msg("\t", parents, k, " - (", type(v), " - ", v, ")\n") 33 | end 34 | 35 | -- Recurse 36 | if (istable(v) and k ~= "_R" and k ~= "_E" and k ~= "_G" and k ~= "_M" and k ~= "_LOADED" and k ~= "__index") then 37 | local NewParents = parents .. k .. "." 38 | FindInTable(v, find, NewParents, depth) 39 | end 40 | end 41 | end 42 | end 43 | 44 | local function Find(ply, command, arguments) 45 | if (IsValid(ply) and ply:IsPlayer() and not ply:IsAdmin()) then return end 46 | if (not arguments[1]) then return end 47 | Msg("Finding '", arguments[1], "':\n\n") 48 | FindInTable(_G, arguments[1]) 49 | FindInTable(debug.getregistry(), arguments[1]) 50 | Msg("\n\n") 51 | end 52 | 53 | concommand.Add("lua_find_menu", Find, nil, "", {FCVAR_DONTRECORD}) 54 | 55 | function ReloadMenu() 56 | if IsValid(pnlMainMenu) then 57 | pnlMainMenu:Remove() 58 | end 59 | 60 | include("menu/menu.lua") 61 | end 62 | 63 | -- concommand.Add("menu_reload", ReloadMenu) -------------------------------------------------------------------------------- /lua/menu_plugins/mpr_update_check.lua: -------------------------------------------------------------------------------- 1 | local CONFIG = { 2 | channel = { 3 | "MPR Update channel", "select", {"Stable", "Beta"}, 4 | "Beta has more frequent updates, but may contain bugs." 5 | }, 6 | plugins = {"Check for plugin updates", "bool", true, "Only works on plugins with sources."} 7 | } 8 | 9 | local MANIFEST = { 10 | id = "djsime1.update_check", 11 | author = "djsime1", 12 | name = "Update checker", 13 | description = "Notifies when a new version of MPR and/or plugins are available.", 14 | version = "1.5", 15 | config = CONFIG, 16 | } 17 | 18 | menup(MANIFEST) 19 | 20 | local function vnum(version) 21 | local sum, pieces = 0, {} 22 | 23 | for s in version:gmatch("%d+") do 24 | table.insert(pieces, tonumber(s)) 25 | end 26 | 27 | for i = 1, #pieces do 28 | sum = sum + pieces[i] * math.pow(100, #pieces - i) 29 | end 30 | 31 | return sum 32 | end 33 | 34 | local function mprcheck(url, cb) 35 | http.Fetch(url, function(body, _, _, code) 36 | if code ~= 200 then 37 | print("MPR update check failed. (HTTP Code " .. code .. ")") 38 | cb(false) 39 | return 40 | end 41 | 42 | local ver, changelog = body:match("menup%.version = \"(%d+%.%d+%.%d+)\""), body:match("menup%.changelog = %[%[\n?(.-)\n?]]") or "No changelog." 43 | 44 | if not ver then 45 | print("MPR update check failed. (Couldn't find version string)") 46 | cb(false) 47 | 48 | return 49 | end 50 | 51 | local new, cur = vnum(ver), vnum(menup.version) 52 | 53 | if new > cur then 54 | cb(true, {"Menu Plugins Redux", menup.version, ver, "https://github.com/djsime1/menu-plugins-redux", changelog}) 55 | else 56 | print("You are running the latest version of MPR.") 57 | cb(false) 58 | end 59 | end, function(err) 60 | print("MPR update check failed. (" .. err .. ")") 61 | cb(false) 62 | end) 63 | end 64 | 65 | local function pcheck(id, cb) 66 | local plugin = menup.plugins[id] 67 | 68 | http.Fetch(plugin.source, function(body, _, _, code) 69 | 70 | if code ~= 200 then 71 | print(id .. " update check failed. (HTTP Code " .. code .. ")") 72 | cb(false) 73 | 74 | return 75 | end 76 | 77 | local func = CompileString(body, "Update check", false) 78 | 79 | if isstring(func) then 80 | print(id .. " update check failed. (" .. func .. ")") 81 | cb(false) 82 | return 83 | end 84 | 85 | local manifest = menup.control.preload(func) 86 | local ver = manifest.version 87 | 88 | if not ver then 89 | print(id .. " update check failed. (Couldn't find version string)") 90 | 91 | return 92 | end 93 | 94 | local new, cur = vnum(ver), vnum(plugin.version) 95 | 96 | if new > cur then 97 | cb(true, {plugin.name, plugin.version, ver, plugin.source, manifest.changelog}) 98 | else 99 | print("You are running the latest version of " .. id) 100 | cb(false) 101 | end 102 | end, function(err) 103 | print(id .. " update check failed. (" .. err .. ")") 104 | cb(false) 105 | end) 106 | end 107 | 108 | local function popup(updates) 109 | local updatelist, changelogs = "", "" 110 | 111 | for _, v in ipairs(updates) do 112 | updatelist = updatelist .. string.format("- [%s](%s) (%s -> %s) \n", v[1], v[4], v[2], v[3]) 113 | 114 | if v[5] then 115 | changelogs = changelogs .. string.format("### [%s](%s)\n%s \n", v[1], v[4], v[5]) 116 | end 117 | end 118 | 119 | local frame = vgui.Create("DFrame") 120 | frame:SetSize(800, 600) 121 | frame:Center() 122 | frame:SetTitle("Updates available!") 123 | local silence = frame:Add("DButton") 124 | silence:SetText("Got it, don't remind me for...") 125 | silence:SetIcon("icon16/bell_delete.png") 126 | silence:Dock(BOTTOM) 127 | 128 | silence.DoClick = function() 129 | local times = { 130 | ["6 hours"] = 21600, 131 | ["12 hours"] = 43200, 132 | ["1 day"] = 86400, 133 | ["3 days"] = 172800, 134 | ["A week"] = 604800, 135 | } 136 | 137 | local dm = DermaMenu() 138 | 139 | for k, v in pairs(times) do 140 | dm:AddOption(k, function() 141 | menup.config.set(MANIFEST.id, "silence", os.time() + v) 142 | frame:Close() 143 | end) 144 | end 145 | 146 | dm:Open() 147 | end 148 | 149 | local md = frame:Add("MarkdownPanel") 150 | md:SetMarkdown(string.format([[# Updates avilable! 151 | *Updates for the following are avilable:* 152 | %s 153 | ## Changelogs: 154 | %s]], updatelist, changelogs)) 155 | md:Dock(FILL) 156 | frame:MakePopup() 157 | end 158 | 159 | local function check() 160 | local branch = ({"main", "dev"})[menup.config.get(MANIFEST.id, "channel", 1)] 161 | 162 | local url = string.format("https://raw.githubusercontent.com/djsime1/menu-plugins-redux/%s/lua/menu/menu_plugins.lua", branch) 163 | local expecting = 1 164 | local queue = {} 165 | local updates = {} -- {name, current, new, source, changelog} 166 | RealFrameTime = FrameTime -- RFT doesn't exist in menu realm and is needed for notifications 167 | notification.AddProgress(MANIFEST.id, "Checking for updates...", 0) 168 | 169 | local function cb(success, data) 170 | if success then 171 | table.insert(updates, data) 172 | end 173 | 174 | notification.AddProgress(MANIFEST.id, "Checking for updates...", 1 - (#queue / expecting)) 175 | 176 | if #queue == 0 then 177 | notification.Kill(MANIFEST.id) 178 | 179 | if #updates ~= 0 then 180 | notification.AddLegacy("There are " .. #updates .. " updates available!", NOTIFY_UNDO, 5) 181 | popup(updates) 182 | else 183 | notification.AddLegacy("You are all up to date.", NOTIFY_GENERIC, 5) 184 | end 185 | else 186 | pcheck(table.remove(queue), cb) 187 | end 188 | end 189 | 190 | if menup.config.get(MANIFEST.id, "plugins", true) then 191 | for k, v in pairs(menup.plugins) do 192 | if not v.legacy and v.source and v.version then 193 | table.insert(queue, k) 194 | expecting = expecting + 1 195 | end 196 | end 197 | end 198 | 199 | mprcheck(url, cb) 200 | end 201 | 202 | local didrun = false 203 | 204 | hook.Add("MenuVGUIReady", MANIFEST.id, function() 205 | if menup.config.get(MANIFEST.id, "silence", 0) < os.time() then 206 | check() 207 | didrun = true 208 | end 209 | end) 210 | 211 | if IsValid(pnlMainMenu) and not didrun and menup.config.get(MANIFEST.id, "silence", 0) < os.time() then 212 | check() 213 | end 214 | 215 | hook.Add("ConfigApply", MANIFEST.id, function(id) 216 | if id == MANIFEST.id then 217 | check() 218 | end 219 | end) -------------------------------------------------------------------------------- /lua/menu_plugins/pling.lua: -------------------------------------------------------------------------------- 1 | local CONFIG = { 2 | sound = {"Sound path", "file", {"sound", "*", "garrysmod/content_downloaded.wav"}, "Sound to play when a pling is triggered."}, 3 | asound = {"Alert with sound", "bool", true, "Play a sound when a pling is triggered?"}, 4 | aflash = {"Alert with taskbar flash", "bool", true, "Flash the taskbar icon when a pling is triggered? (Windows only)"}, 5 | onlaunch = { 6 | "Notify on launch...", "select", {"When menu is loaded", "When workshop is complete", "Never"}, "When you launch Garry's Mod." 7 | }, 8 | onjoin = { 9 | "Notify on join... ", "select", {"When spawned in server", "When lua started", "When FPS stabilizes", "Never"}, "When you join a server." 10 | }, 11 | } 12 | 13 | local MANIFEST = { 14 | id = "djsime1.pling", 15 | author = "djsime1", 16 | name = "Pling!", 17 | description = "Allows you to be notified when GMod finishes loading, or when you fully load into a server.", 18 | version = "1.3", 19 | config = CONFIG 20 | } 21 | 22 | menup(MANIFEST) 23 | 24 | local function apply() 25 | local soundfile = menup.config.get(MANIFEST.id, "sound", "garrysmod/content_downloaded.wav") 26 | local asound = menup.config.get(MANIFEST.id, "asound", true) 27 | local aflash = menup.config.get(MANIFEST.id, "aflash", true) 28 | local onlaunch = menup.config.get(MANIFEST.id, "onlaunch", 1) 29 | local onjoin = menup.config.get(MANIFEST.id, "onjoin", 1) 30 | 31 | local function alert() 32 | if asound then 33 | surface.PlaySound(soundfile) 34 | end 35 | 36 | if aflash then 37 | system.FlashWindow() 38 | end 39 | 40 | print("Pling!") 41 | end 42 | 43 | hook.Remove("MenuStart", MANIFEST.id) 44 | hook.Remove("WorkshopEnd", MANIFEST.id) 45 | hook.Remove("CaptureVideo", MANIFEST.id) 46 | timer.Remove(MANIFEST.id) 47 | 48 | local launchfuncs = { 49 | function() -- When menu is loaded 50 | hook.Add("MenuStart", MANIFEST.id, function() 51 | alert() 52 | end) 53 | end, 54 | function() -- When workshop is complete 55 | hook.Add("WorkshopEnd", MANIFEST.id, function() 56 | if IsInGame() then return end 57 | timer.Simple(1, alert) 58 | end) 59 | end, 60 | function() end -- Never 61 | } 62 | 63 | local joinfuncs = { 64 | function() -- When spawned in server 65 | local wasingame = false 66 | 67 | timer.Create(MANIFEST.id, 1, 0, function() 68 | if IsInGame() == true and wasingame == false then 69 | alert() 70 | end 71 | wasingame = IsInGame() 72 | end) 73 | end, 74 | function() -- When lua started 75 | local loadstatus = "" 76 | 77 | hook.Add("CaptureVideo", MANIFEST.id, function() 78 | if GetLoadStatus() == "Lua Started!" and loadstatus ~= "Lua Started!" then -- TODO: Localize 79 | alert() 80 | end 81 | 82 | loadstatus = GetLoadStatus() 83 | end) 84 | end, 85 | function() -- When FPS stabilizes 86 | local wasingame = false 87 | 88 | timer.Create(MANIFEST.id, 1, 0, function() 89 | if IsInGame() == true and wasingame == false then 90 | local fps = {} 91 | 92 | hook.Add("CaptureVideo", MANIFEST.id, function() 93 | if not IsInGame() then return end 94 | local sum = 0 95 | table.insert(fps, 1, 1 / FrameTime()) 96 | fps[30] = nil 97 | 98 | for i = 1, #fps do 99 | sum = sum + fps[i] 100 | end 101 | 102 | if #fps < 20 then return end 103 | 104 | if math.abs((sum / 30) - (1 / FrameTime())) <= 5 then 105 | hook.Remove("CaptureVideo", MANIFEST.id) 106 | alert() 107 | end 108 | end) 109 | end 110 | 111 | wasingame = IsInGame() 112 | end) 113 | end, 114 | function() end -- Never 115 | } 116 | 117 | launchfuncs[onlaunch]() 118 | joinfuncs[onjoin]() 119 | end 120 | 121 | apply() 122 | 123 | hook.Add("ConfigApply", "GradientBackgroundReload", function(id) 124 | if id == MANIFEST.id then 125 | apply() 126 | surface.PlaySound(menup.config.get(MANIFEST.id, "sound", "garrysmod/content_downloaded.wav")) 127 | end 128 | end) 129 | 130 | return function() 131 | hook.Remove("MenuStart", MANIFEST.id) 132 | hook.Remove("WorkshopEnd", MANIFEST.id) 133 | hook.Remove("CaptureVideo", MANIFEST.id) 134 | timer.Remove(MANIFEST.id) 135 | end --------------------------------------------------------------------------------